Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Touch Events outside of bounds
-
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)
-
@mcriley821 you could try this one
-
This post is deleted! -
So, I think the options are:
-
change the frame size when the box is activated. That's what polymerchm is doing in his code.
-
see https://stackoverflow.com/questions/5432995/interaction-beyond-bounds-of-uiview
It would be possible to create a custom UIView class, I think, where you override pointInside_withEvent_ (I think that's how it would translate to objc_util).
Or you can swizzle that method on SUIView or whatever it is called . -
-
Why no use of did_select delegate of ui.TableView?
-
Iirc, listdatasource.action is called by did_select, it you don't otherwise override it.
-