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.
Odd behavior with gesture recognizers
-
I'm doing a project with
ui
andobjc_util
(typical). I'm getting this strange behavior when overriding aui.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 passself
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 trueself
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!
- make
-
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.
-
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. -
@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).
-
@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
-
@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?
-
@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.
-
@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.
-
I think I figured out a solution that will work. Since the
doubletap
gesture was recognizing double-taps and gain access toself
by inlining the objc method, I realized that’s all I really needed. I can initialize the TextView witheditable=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()
-
@mcriley821, thanks, using
editable
to effectively disable the one-tap gestures is a good trick, have to keep it in mind.