Forum Archive

Touch Events outside of bounds

mcriley821

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)

cvp

@mcriley821 you could try this one

JonB

@mcriley821
Not sure if this is what you are after, but
try setting clips_to_bounds to false on the view that contains the tableviee. When you do that, you can have subviews which are outside of the parent's bounds, yet still get drawn. Useful for things like keyboard popups, or dropdowns, etc.

T

JonB

So, I think the options are:

1) change the frame size when the box is activated. That's what polymerchm is doing in his code.

2) 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 .

cvp

Why no use of did_select delegate of ui.TableView?

JonB

Iirc, listdatasource.action is called by did_select, it you don't otherwise override it.

mcriley821

@JonB @cvp
Thank you for the suggestions! I ended up going the easy way and requiring to pass in the master view. Kept the TableView hidden on the master (and thus un-touchable) until the combobox is touched.