omz:forum

    • Register
    • Login
    • Search
    • Recent
    • Popular

    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

    Pythonista
    ui.textview objcutil
    4
    10
    3081
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • mcriley821
      mcriley821 last edited by 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!

      cvp 1 Reply Last reply Reply Quote 0
      • JonB
        JonB last edited by 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.

        1 Reply Last reply Reply Quote 0
        • JonB
          JonB last edited by

          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 1 Reply Last reply Reply Quote 0
          • mcriley821
            mcriley821 @JonB last edited by

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

            1 Reply Last reply Reply Quote 0
            • cvp
              cvp @mcriley821 last edited by 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 1 Reply Last reply Reply Quote 0
              • mcriley821
                mcriley821 @cvp last edited by

                @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 mikael 2 Replies Last reply Reply Quote 0
                • cvp
                  cvp @mcriley821 last edited by

                  @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.

                  1 Reply Last reply Reply Quote 1
                  • mikael
                    mikael @mcriley821 last edited by

                    @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 1 Reply Last reply Reply Quote 0
                    • mcriley821
                      mcriley821 @mikael last edited by

                      @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 1 Reply Last reply Reply Quote 0
                      • mikael
                        mikael @mcriley821 last edited by

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

                        1 Reply Last reply Reply Quote 1
                        • First post
                          Last post
                        Powered by NodeBB Forums | Contributors