Forum Archive

Odd behavior with gesture recognizers

mcriley821

I'm doing a project with ui and objc_util (typical). I'm getting this strange behavior when overriding a ui.TextView's gestures; the gesture's view is somehow re-overriding the gestures.
The simplest code example I can muster is as follows:

from objc_util import *
import ui


UITap = ObjCClass("UITapGestureRecognizer")


class Node (ui.View):
    class TextViewDelegate (object):
        def textview_did_change(self, tv):
            if '\n' == tv.text[-1]:
                tv.text = tv.text[:-1]
                tv.end_editing()

    def __init__(self, *args, **kwargs):
        ui.View.__init__(self, *args, **kwargs)
        self.frame = (0, 0, *(ui.get_screen_size()))
        tv = ui.TextView(
            name="node_label",
            frame=(100, 100, 50, 50),
            bg_color="blue",
            delegate=self.TextViewDelegate()
        )
        # this is where the interesting bit starts
        tvObj = tv.objc_instance
        # remove all the gestures of tv
        for gesture in tvObj.gestureRecognizers():
            tvObj.removeGestureRecognizer_(gesture)

        # for a new gesture, we need a target and action
        # how to make self a target? not really sure
        # maybe making a "blank" target with the method 
        # would be enough? since self carries?
        # I tried making a target of self, but crashes
        target = create_objc_class(
            "self",
            methods=[self.handleTap_]
        ).alloc().init()
        # now we have a target and action
        # let's make the actual gesture
        doubletap = UITap.alloc().initWithTarget_action_(
            target,
            "handleTap:"
        )
        # make into an actual doubletap, and add to view
        doubletap.setNumberOfTapsRequired_(2)
        tvObj.addGestureRecognizer_(doubletap)

        # add the tv subview with a single gesture: doubletap
        # can confirm only one gesture by uncommenting below
        #print(self.objc_instance.gestureRecognizers())  # None
        #print(tvObj.gestureRecognizers())  # doubletap
        self.add_subview(tv)

# Now, without @static_method, everything is fine up until
# the below function is called -> results in TypeError of passing
# 4 args to a 3 arg function, since the first arg is self. However,
# with @static_method, we have to do some round-about trickery
# to do what we want. 
    @static_method
    def handleTap_(_slf, _cmd, _gesture):
        gesture = ObjCInstance(_gesture)
        view = gesture.view()
# More interesting stuff happens now. On the first call of handleTap_,
# the next line prints only doubletap. On next calls, all the gestures 
# have been reset
        print(view.gestureRecognizers())
# we can only start editing now by becoming the first responder,
# since we can't access self
        view.becomeFirstResponder()

# I assume here that the call to becomeFirstResponder instantiates
# a new TextView object somehow, yet this new object still contains
# a doubletap gesture. Re-tapping the TextView in the UI will start
# editing - no double tapping needed. 


if __name__ == "__main__":
    w, h = ui.get_screen_size()
    view = Node(bg_color="white")
    view.present()

What can I do to make self my target? Or how can I pass self to my method? Can I create a wrapper for the @static_method wrapper and pass self still? I'm stuck on what to do, since any direction I go seems to be a dead-end:

  • make self into a target = crash
  • @static_method = no reference to the true self instance
  • no @static_method = TypeError
  • can't use global variables, since I hope to have ~10 of these Nodes at a time

Also, I'd prefer to not use a TextField since they have that awful bounding box. I also think this issue would carry to a TextField anyhow.

Any ideas are greatly appreciated! I'm stumped!

JonB

Have you seen @mikael's pythonista-gestures repo? It takes out a lot of the guesswork about creating GestureRecognizers.

https://github.com/mikaelho/pythonista-gestures

Target needs to be an instance of an ObjC class, which must be retained someplace, and which has a selector with whatever your action name is. I dont think you are retaining target -- try self.target=target. Don't name your class self, you will just be confused....

@mikael's package makes it all very pythonic and easy, and let's you implement the other methods to set priority,etc.

JonB

By the way, a way around the issue with static method is just to make the method an inline function within init, prior to defining your class. That way you have access to self within the scope. But defining an ObjC class within init is probably not the best approach, as you will end up with dozens of ObjCClass's that are one time use.

mcriley821

@JonB I’ve seen @mikael’s repo, but based on personal reasons I’d rather not use it. I want to understand how things are working instead of importing.

Anyhow, inlining the method did not fix the issue. Making tv a self attribute, and changing the method to:

def handleTap_(_slf, _cmd, _gesture):
    self.tv.begin_editing()

After the first double tap, a single tap suffices to start editing again. I also retained target (self.target).

cvp

@mcriley821 said:

I'd prefer to not use a TextField since they have that awful bounding box.

To avoid it:

ObjCInstance(tf).textField().setBorderStyle_(0)

Same as Pythonista, sorry

tf.bordered = False
mcriley821

@cvp TextField is even odder, having no gestureRecognizers. It probably has a view hierarchy with a TextView. Maybe I need to remove gestures from all underlying views of my TextView?

cvp

@mcriley821 sorry but I'm not able to help, I only was reacting about the bounding box. I hope you will find some help here.

mikael

@mcriley821, random thoughts:

  • I seem to remember reading and experiencing that some complex views like UITextView actively reinstate their gesture recognizers if you remove them.
  • Thus I try to avoid messing with them if possible.
  • As @JonB said, prioritizing gestures may help, but even that is iffy with these classes that have 10+ very specialized gesture handlers.

All that said, I tried this straightforward implementation for reference:

import ui
import gestures as g


class Node(ui.View):

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.tv = ui.TextView(
            frame=self.bounds,
            flex='WH'
        )
        self.add_subview(self.tv)

        g.doubletap(self.tv, self.doubletap)        

    def doubletap(self, data):
        self.tv.text += 'doubletap\n'

node = Node()

node.present('fullscreen')

It behaves roughly as expected: doubletap handler fires consistently, but with some selection effects as TextView tries to do its thing. Prioritization did not seem to so much to help.

Regarding getting to self, here are some approaches I have used, in the order of increasing preference:

  • Global lookup dict to map the _self the handler receives to the Python "self"
  • Storing the reference to Python self on the objc object passed to the objc handler
  • Creating the objc handler function in the closure of the "self" Python object

gestures has used all of these at some point or the other, and they have all worked.

Current implementation creates an ObjC delegate class (which is an ObjCInstance) and then inserts a number of Python methods and attributes on the instance of it, allowing the ObjC handler method to just ObjCInstance(_self) to get the instance that has all the Python attributes available. The gotcha here is that since ObjCInstance is a proxy, I need to set all the Python attributes in a __new__ call - after that, any attempts to add attributes
fail as ObjCInstance tries to find them on the ObjC instance.

Hope some of this helps. Happy to dig deeper, depending on which approach you end up pursuing.

mcriley821

@mikael @JonB

I think I figured out a solution that will work. Since the doubletap gesture was recognizing double-taps and gain access to self by inlining the objc method, I realized that’s all I really needed. I can initialize the TextView with editable=False, enable editing inside the objc method, and start editing. Then in the textview’s delegate, when editing is done, disable editing!

Thank you guys for the help!

Here’s the code if anyone is interested:

from objc_util import *
import ui

UITap = ObjCClass('UITapGestureRecognizer')

class Node (ui.View):
    class TextViewDelegate (object):
        def textview_did_change(self, tv):
            try:
                if '\n' == tv.text[-1]:
                    tv.text = tv.text[:-1]
                    tv.end_editing()
            except IndexError:
                pass

        def textview_did_end_editing(self, tv):
            # do whatever with the text
            tv.editable = False

    def __init__(self, *args, **kwargs):
        ui.View.__init__(self, *args, **kwargs)
        self.frame = (0,0,*(ui.get_screen_size()))
        self.tv = ui.TextView(
            name='node_label',
            frame=(100, 100, 50, 50),
            delegate=self.TextViewDelegate(),
            bg_color='#b9b9ff',
            editable=False
        )

        tvObj = self.tv.objc_instance

        def handleTap_(_slf, _cmd, _gesture):
            self.tv.editable = True
            self.tv.begin_editing()

        self.target = create_objc_class(
            'fake_target',
            methods=[handleTap_]
            ).new()

        self.doubletap = UITap.alloc().initWithTarget_action_(
            self.target,
            'handleTap:'
        )
        self.doubletap.setNumberOfTapsRequired_(2)
        tvObj.addGestureRecognizer_(
            self.doubletap
        )
        self.add_subview(self.tv)

    def will_close(self):
        self.doubletap.release()
        self.target.release()

if __name__ == '__main__':
    w, h = ui.get_screen_size()
    view = Node(bg_color='white')
    view.present()

mikael

@mcriley821, thanks, using editable to effectively disable the one-tap gestures is a good trick, have to keep it in mind.