Hello,
I’m trying to make a custom UI view that allows the user to select an option from a drop down box (like a custom ComboBox).
Everything seems to be working okay, until I actually try to select a row in the TableView. I know it has to do with the way that iOS does hit-testing for views, but I can’t think of an elegant way to get around this. The only thing I can think of is to customize the root view also (but I want to avoid that so I can simply import it to work with any view ootb).
Essentially, the root view receives the location of the touch, and it then asks its subviews if they contain that touch in their bounds. The custom view then returns False since the touch isn’t in its bounds, although visually the touch is on the TableView.
Apologies for any formatting confusion/differences (I’m on iPhone), and for any mistakes/issues I’m not completely finished yet!
Here’s the code:
import ui
"""ComboBox is a ui View class that allows a
string to be chosen from a list of choices by
tapping on the dropbox button."""
class ComboBox (ui.View):
"""Initializing of ComboBox:
Accepts all kwargs of ui.View, as well as:
font -> Chosen string font
choices -> List of choice strings
selected_index -> Initial choice index
padding -> Internal padding around the label and dropdown button
button_tint -> Color of the dropbox button"""
def __init__(self, *args, **kwargs):
self.font = kwargs.pop(
'font', ('<System>', 20))
self.text_color = kwargs.pop(
'text_color', "black")
self.choices = kwargs.pop(
'choices', [""])
self.selected_index = kwargs.pop(
'selected_index', 0)
self.padding = kwargs.pop(
'padding', 3)
self.button_tint = kwargs.pop(
'button_tint', "grey")
kwargs.setdefault('border_width', 1.0)
kwargs.setdefault(
'border_color', '#e5e5e5')
kwargs.setdefault('corner_radius', 3.5)
ui.View.__init__(self, *args, **kwargs)
# button for the dropbox
_x = self.width - self._padding - 30
_h = self.height - self._padding * 2
self.drop_button = ui.Button(
bg_color=self.bg_color,
frame=(_x, self._padding,
self.height, _h),
image=ui.Image('iow:arrow_down_b_24'),
tint_color=self.button_tint,
action=self.dropbox_should_open
)
# label for selected item
# default to item 0
_w = self.width - self.drop_button.width - self._padding * 2
self.selected_label = ui.Label(
bg_color=self.bg_color,
alignment=ui.ALIGN_CENTER,
font=self.font,
text=self.choices[self.selected_index],
frame=(self._padding, self._padding,
_w, _h),
text_color=self.text_color
)
# dropbox
_row_h = ui.measure_string(
self.choices[0], font=self.font)[1]
_row_h += self._padding
_h *= 5 if len(self.choices) > 5 else len(self.choices)
self.dropbox = ui.TableView(
bg_color=self.bg_color,
row_height=_row_h,
seperator_color=self.border_color,
data_source=self._data_source,
selected_row=-1,
allows_selection=True,
frame=(self._padding, self.height - 1,
self.selected_label.width,
_h),
border_color=self.border_color,
border_width=self.border_width,
corner_radius=self.corner_radius,
hidden=True,
touch_enabled=True
)
# draw tableview although out of bounds
obj = self.objc_instance
obj.setClipsToBounds_(False)
# add subviews
self.add_subview(self.selected_label)
self.add_subview(self.drop_button)
self.add_subview(self.dropbox)
@property
def selected_index(self):
return self._selected_index
@selected_index.setter
def selected_index(self, value):
if value < len(self.choices):
self._selected_index = value
if hasattr(self, 'selected_label'):
self.selected_label.text = self.choices[value]
self.set_needs_display()
@property
def selected_text(self):
return self.choices[self._selected_index]
@property
def padding(self):
return self._padding
@padding.setter
def padding(self, value):
if value < self.height / 2:
self._padding = value
self.set_needs_display()
@property
def choices(self):
return self._data_source.items
@choices.setter
def choices(self, value):
if type(value) is list and len(value) > 0:
ds = ui.ListDataSource(value)
ds.delete_enabled = False
ds.move_enabled = False
ds.font=self.font
ds.action=self.index_changed
ds.text_color=self.text_color
self._data_source = ds
def layout(self):
# selected label layout
self.selected_label.width = self.width - self.height - self._padding * 2
self.selected_label.height = self.height - self._padding * 2
# drop button layout
self.drop_button.x = self.width - self.height
self.drop_button.width = self.height - self._padding
self.drop_button.height = self.height - self._padding * 2
# dropbox layout
self.dropbox.width = self.selected_label.width
self.dropbox.y = self.height
_h = ui.measure_string(
self.choices[0], font=self.font)[1]
_h += self._padding
_h *= 5 if len(self.choices) > 5 else len(self.choices)
self.dropbox.height = _h
def touch_began(self, touch):
print(touch)
if self._touch_in_frame(touch, self.selected_label.frame) and touch.phase == "began":
if self.dropbox.hidden:
self.dropbox_should_open(None)
else:
self.dropbox_should_close(None)
@staticmethod
def _touch_in_frame(touch, frame):
x, y, w, h = frame
xmin, xmax = x, x + w
ymin, ymax = y, y + h
x, y = touch.location
if xmin < x < xmax:
if ymin < y < ymax:
return True
return False
def draw(self):
# draw the splitter border
p = ui.Path()
p.move_to(
self.selected_label.width + self._padding * 1.5, 0)
p.line_to(
self.selected_label.width + self._padding * 1.5,
self.height)
p.line_width = self.border_width
ui.set_color(self.border_color)
p.stroke()
def dropbox_should_open(self, sender):
# animate drop box
if sender:
sender.action = self.dropbox_should_close
ui.animate(self.do_dropbox)
def do_dropbox(self):
self.dropbox.hidden = not self.dropbox.hidden
def dropbox_should_close(self, sender):
if sender:
sender.action = self.dropbox_should_open
ui.animate(self.do_dropbox)
def index_changed(self, sender):
new_index = sender.selected_row
if new_index != self.selected_index and new_index != -1:
self.selected_index = new_index
if __name__ == "__main__":
root = ui.View(bg_color="white")
combo = ComboBox(bg_color="white",
choices=['test', 'test2'],
corner_radius=3.5,
)
combo.frame = (50, 50, 275, 40)
root.add_subview(combo)
root.present('sheet', hide_title_bar=True)