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.


    Traceback using Gestures module

    Pythonista
    custom-view gesture objc
    3
    25
    14402
    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.
    • shinyformica
      shinyformica last edited by shinyformica

      @JonB, here's a very simple example which exhibits the behavior. Of note: I'm running in python2.7, so I have a local copy of the Gestures.py module which has this import line commented out:

      #from types import SimpleNamespace

      since that's a python3 module. Other than that, and the debug printout in the simplify() function:

      def simplify(func, gr, other_gr):
            print "in simplify:",ObjCInstance,gr,other_gr
            gr_o = ObjCInstance(gr)
            other_gr_o = ObjCInstance(other_gr)
            if (gr_o.view() != other_gr_o.view()):
              return False
            gr_name = gr_o._get_objc_classname()
            other_gr_name = other_gr_o._get_objc_classname()
            return func(gr_name, other_gr_name)
      

      there's no changes to the module at all, but it is in the same directory as the following code, so it can be imported locally:

      import ui
      
      class MyView(ui.View):
          def __init__(self, *args, **kws):
              ui.View.__init__(self, *args, **kws)
              self._color = (1,0,0,1)
      
              import Gestures
              self._gr = Gestures.Gestures(retain_global_reference=False)
              self._gr.add_swipe(self, self._handleSwipe,
                                          direction=[Gestures.Gestures.LEFT,
                                                      Gestures.Gestures.RIGHT])
      
          def draw(self):
              ui.set_color(self._color)
              ui.fill_rect(0, 0, self.width, self.height)
      
          def _handleSwipe(self, data):
              print "swiped!"
              print data
              self._color = (0,1,0,1)
              self.set_needs_display()
      
      def run():
          v = MyView()
          v.present('full_screen')
      
      if __name__ == '__main__':
          run()
      

      To reproduce, I just run once, swipe to have it do its thing, hit the "X" to exit the script, then immediately run again and try to swipe...the swipe happens, but I get this traceback:

      Traceback (most recent call last):
        File "_ctypes/callbacks.c", line 315, in 'calling callback function'
        File "/private/var/mobile/Library/Mobile Documents/iCloud~com~omz-software~Pythonista3/Documents/Gestures.py", line 155, in gestureRecognizer_shouldRequireFailureOfGestureRecognizer_
          return self.objc_should_require_failure(self.fail_other, gr, other_gr)
        File "/private/var/mobile/Library/Mobile Documents/iCloud~com~omz-software~Pythonista3/Documents/Gestures.py", line 158, in objc_should_require_failure_default
          return simplify(func, gr, other_gr)
        File "/private/var/mobile/Library/Mobile Documents/iCloud~com~omz-software~Pythonista3/Documents/Gestures.py", line 134, in simplify
          gr_o = ObjCInstance(gr)
      TypeError: 'NoneType' object is not callable
      
      1 Reply Last reply Reply Quote 0
      • shinyformica
        shinyformica last edited by

        @JonB So, modifying the Gestures.py module ever so slightly allowed it to work without a traceback, based off what you said about having to import modules in the place where they're used, I just went ahead and changed that "simplify()" function to:

        def simplify(func, gr, other_gr):
              import objc_util
              print "in simplify..."
              print "objc_util:",objc_util
              print "ObjCInstance:",objc_util.ObjCInstance
              print "gr,gr_other:",gr,other_gr
        
              gr_o = objc_util.ObjCInstance(gr)
              other_gr_o = objc_util.ObjCInstance(other_gr)
              if (gr_o.view() != other_gr_o.view()):
                return False
              gr_name = gr_o._get_objc_classname()
              other_gr_name = other_gr_o._get_objc_classname()
              return func(gr_name, other_gr_name)
        

        and voila, it suddenly works fine. I'm still not sure how exactly the ObjCInstance is getting set to None, and by whom, but by importing objc_util and using the definition contained within, I'm ok. Must be some consequence of callbacks from objc and the pythonista framework's way of loading and unloading objects when scripts are run.

        That example above still stands as a way to see this oddness in action.

        mikael 1 Reply Last reply Reply Quote 0
        • mikael
          mikael @shinyformica last edited by

          @shinyformica, for reference, I tested your code on both 2.7 and 3.6, restarting several times, and unfortunately I cannot reproduce the issue. This is on iPhone X.

          Also, I am curious why you set retain_global_reference to False?

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

            @mikael that is really odd...on my iPad Air it happens every time I run/end/run-again. On my iPhone 7, it does not.
            Setting "retain_global_reference" to True or False appears not to make a difference, I guess since I'm holding a reference to the Gestures instance in my View instance.

            I would love to know if @JonB or anyone else can reproduce this? Seems strange it would only be my iPad that sees the issue.

            Regardless, simply importing objc_util inside simplify() fixed it for me, so I'm ok with a tiny change like that. The module is fantastic @mikael.

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

              @mikael @JonB, or anyone else, instead of starting a new thread...thought I'd add this here, since it might be related to the previous issue.

              Another Gestures thing I've just had come up: I have multiple custom Views which each handle gestures. This has been working fine, up till now, when I needed one of these views to allow simultaneously handling two gestures. It seems like the first View to create a Gestures instance is the one that has
              recognize_simultaneously, fail, and fail_other
              functions called, no matter if each view independently creates its own Gestures instance.

              Here's a moderately simple example:

              import ui
              import Gestures
              
              class GestureViewA(ui.View):
                  def __init__(self, *args, **kws):
                      ui.View.__init__(self, *args, **kws)
                      self._color = (1,0,0,1)
                      self.gestures = Gestures_test.Gestures()
                      self.gestures.recognize_simultaneously = self._recognizeSimultaneously
                      self.gestures.add_swipe(self, self._handleSwipe,
                                                  direction=[Gestures_test.Gestures.LEFT,
                                                              Gestures_test.Gestures.RIGHT])
              
                  def draw(self):
                      ui.set_color(self._color)
                      ui.fill_rect(0, 0, self.width, self.height)
              
                  def _recognizeSimultaneously(self, gr, other_gr):
                      print "view A: recognize simultaneously:",gr,other_gr
                      return False
              
                  def _handleSwipe(self, data):
                      print "swipe view A!"
                      self._color = (0,1,0,1)
                      self.set_needs_display()
              
              class GestureViewB(ui.View):
                  def __init__(self, *args, **kws):
                      ui.View.__init__(self, *args, **kws)
                      self._color = (0,0,1,1)
                      self.gestures = Gestures_test.Gestures()
                      self.gestures.recognize_simultaneously = self._recognizeSimultaneously
                      self.gestures.add_swipe(self, self._handleSwipe,
                                                  direction=[Gestures_test.Gestures.LEFT,
                                                              Gestures_test.Gestures.RIGHT])
                      self.gestures.add_pan(self, self._handlePan)
              
                  def draw(self):
                      ui.set_color(self._color)
                      ui.fill_rect(0, 0, self.width, self.height)
              
                  def _recognizeSimultaneously(self, gr, other_gr):
                      print "view B: recognize simultaneously:",gr,other_gr
                      return False
              
                  def _handleSwipe(self, data):
                      print "swipe view B!"
                      self._color = (1,0,1,1)
                      self.set_needs_display()
              
                  def _handlePan(self, data):
                      print "pan view B!"
                      self._color = (1,1,1,1)
                      self.set_needs_display()
              
              mainView = ui.View()
              
              viewA = GestureViewA()
              viewA.x = 0
              viewA.y = 0
              viewA.width = mainView.width
              viewA.height = 100
              viewA.flex = "WHB"
              mainView.add_subview(viewA)
              
              viewB = GestureViewB()
              viewB.x = 0
              viewB.y = mainView.height/2
              viewB.width = mainView.width
              viewB.height = 100
              viewB.flex = "WHT"
              mainView.add_subview(viewB)
              
              mainView.present('full_screen')
              

              What I think should happen in the above code:

              • When viewA is swiped its _handleSwipe() is called, and there's no need for its _recognizeSimultaneously() to be called, since there's only one gesture registered.
              • When viewB is either swiped or panned, I would expect one of its _handleSwipe() or _handlePan() to be called, and also to see its _recognizeSimultaneously() called, since there's two gestures registered.

              What I see when I run it is that viewA is having its _recognizeSimultaneously called for viewB's gestures. Which doesn't seem right.

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

                Also, I've tried having GestureViewB have a function replace:

                Gestures.objc_should_recognize_simultaneously
                

                with its own function to see if it was something happening in simplify(), but same result, only the method on GestureViewA gets called. This makes me believe that it's actually when the ObjC class is created that the first object to do so "wins" and has its methods registered:

                try:
                            PythonistaGestureDelegate = ObjCClass('PythonistaGestureDelegate')
                        except:
                            PythonistaGestureDelegate = create_objc_class('PythonistaGestureDelegate',
                            superclass=NSObject,
                            methods=[
                              #gestureRecognizer_shouldReceiveTouch_,
                              gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_,
                              gestureRecognizer_shouldRequireFailureOfGestureRecognizer_,
                              gestureRecognizer_shouldBeRequiredToFailByGestureRecognizer_],
                            classmethods=[],
                            protocols=['UIGestureRecognizerDelegate'],
                            debug=True)
                self._delegate = PythonistaGestureDelegate.new()
                

                so even though I'm getting a new instance of the PythonistaGestureDelegate objective-C object, the underlying functions registered with the original definition are pointing at my new instance's methods, or are being called with the first instance as "self", or something to that effect.

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

                  And...doing it again...(I should really test my theories before posting)...if I simply force a new ObjC class definition to be created for the gesture delegate for each different view class, by modifying Gestures.py slightly so it takes a "name" in its init():

                  try:
                      PythonistaGestureDelegate = ObjCClass('PythonistaGestureDelegate_{0}'.format(name))
                  except:
                      PythonistaGestureDelegate = create_objc_class('PythonistaGestureDelegate_{0}'.format(name),
                  

                  this works, in that it now does what I expect and calls the methods defined on the specific view class which the Gestures object was created in. But my understanding of what's happening on the Objective-C side of things is not clear here.

                  mikael 1 Reply Last reply Reply Quote 0
                  • mikael
                    mikael @shinyformica last edited by

                    @shinyformica, I think the issue is related to the way I have used closure to access self in the ObjC delegate functions. Maybe we could find another way to connect to the Python instance.

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

                      @mikael yeah, that's definitely the reason, in some fashion. The Objective-C side of things is definitely holding tightly to the Gestures object instance that "self" refers to at the time the delegate ObjCClass is defined. I had to modify it even further to get it to work from one run of the script to another (without killing and restarting Pythonista in between) because it was still holding the reference to the first instance of my custom View class that created the ObjCClass from one run to the next, and thus passing the wrong gesture recognizer instances to the simultaneous gesture and fail/fail_other calls.

                      Not knowing much about how this works under the hood, I don't know how to prevent it, but I'll look into it a bit more...perhaps it would be possible to hold or pass the reference to the Gestures object as data, and use some generic way to call the appropriate method on that object, instead of holding the method instance.

                      mikael 1 Reply Last reply Reply Quote 0
                      • mikael
                        mikael @shinyformica last edited by mikael

                        @shinyformica, latest version on Github now uses a class-level lookup to find the right instance when calling delegate functions. Could you please give it a try?

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

                          @mikael just got to try this this morning. Unfortunately, while it seems like it would work, I once again encounter the strange issue of module-level things becoming undefined when the script is run a second time, from way back at the start of this thread. In this case it is the actual Gestures class which is suddenly None when the code tries to call Gestures.get_self(cls, key).

                          Again, this only happens the second time the script runs, and every time thereafter. First time through, things work as expected. I actually managed a solution of my own, which isn't ideal: I append the id of the instance of the Gestures object to the name of the delegate class being created by the call to objc_util.create_objc_class() so each Gestures instance gets a unique delegate class and instance of that class, which will be sure to have self as a reference to that particular Gestures instance. This appears to solve the problem, though it means I'll have a new ObjC class definition for each Gesture instance, which in my case means one for each control that gets instantiated...and I don't know the performance or memory implications of that.

                          I really wish I knew why I was having that strange "undefinition" issue, since your solution only creates one class, and registers the instances of the Gesture object, it seems much more efficient. I'll see if I can figure out a way to do something similar to your way of finding a reference to the Gestures instance without holding an explicit self reference, without triggering my other problem.

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

                            @mikael a little more fiddling around, and looking at how other people have used objc_util and ObjCInstance's, it seems I can just stash a reference to the Gestures object instance for my control in the actual delegate instance created for that control:

                            self._delegate = PythonistaGestureDelegate.new()
                            self._delegate._gestures = self
                            

                            and then I can safely access that reference in the objc callbacks:

                            def gestureRecognizer_shouldRecognizeSimultaneouslyWithGestureRecognizer_(_self, _sel, gr, other_gr):
                                import objc_util
                                delegate_instance = objc_util.ObjCInstance(_self)
                                gestures_instance = delegate_instance._gestures
                                return gestures_instance.objc_should_recognize_simultaneously(self.recognize_simultaneously, gr, other_gr)
                            

                            so now only one actual delegate class definition is created in objective-c, and the delegate instances for each control are all separately able to access the Gestures instance they use. I'll have to make sure I break the cyclic reference between the gesture delegate instance and the Gestures instance, but I can do that via a weakref or explicitly removing the cycle when I get rid of the control.

                            mikael 1 Reply Last reply Reply Quote 0
                            • mikael
                              mikael @shinyformica last edited by

                              @shinyformica, thanks, I will implement this. Does this option suffer from the vanishing Gestures class?

                              I will also check this with an iPad.

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

                                @mikael thankfully it does not suffer from that strange vanishing-definitions thing. Though I am now locally importing objc_util anyplace I try to use ObjCInstance() or ObjCClass(), which appears to prevent it from showing up and causing trouble.

                                I tested the above technique using weakref to hold the reference to the Gestures object and it worked fine, so that will prevent a cyclic-reference:

                                self._delegate = PythonistaGestureDelegate.new()
                                
                                import weakref
                                self._delegate._gestures = weakref.ref(self)
                                
                                mikael 1 Reply Last reply Reply Quote 0
                                • JonB
                                  JonB last edited by

                                  hey, have you copies objc_util and made edits? objc_util should actually never be cleared after it is loaded, so unless you are manually del'ing sys.modules['objc_util'] it should not be getting cleared out from under you. But, if you had a shadow copy on your path, it would.

                                  import objc_util
                                  print(objc_util.__file__)
                                  

                                  i have an older version of gestures, but i also could not reproduce your issues

                                  1 Reply Last reply Reply Quote 0
                                  • mikael
                                    mikael @shinyformica last edited by

                                    @shinyformica, thanks. I will include the weakref version.

                                    I was also wondering if you had some globals-clearing code somewhere. Remembering some previous threads where the goal was to make Pythonista act like a fresh interpreter starting every time you run a script.

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