Help calling "addTarget:action:forControlEvents:" from objc_util
I've encountered a situation where I will need to know which control in my UI is currently being interacted with. That's easy enough with custom Views I write myself, but I also need to know when the user is fiddling with a slider or button, so I thought I could maybe connect up a method which will be called whenever a touch event occurs on a real UIControl via:
Using the appropriate control events from here:
But I'm having trouble figuring out how to convert the call to that function into the proper objcutil code in python.
I have my, say, Slider control, and can get the actual objc instance of it, but I'm not clear on the right way to add a python function or method as the selector for the action to that addTarget_action_forControlEvents_() call, or how to access the UIControlEvents enum values.
Any help from more experienced objc_util users would be appreciated. Or if there's a simpler/better way to be notified when one of the wrapped native controls is being interacted with.
@shinyformica, for the action, here’s a trick used in omz MapView example and subsequently in Gestures:
Create and store a reference to a ui.Button. Set the button’s
actionas the Python function you want called when the control fires. Then call the method like this:
view.addTarget_action_forControlEvents_(button, sel('invokeAction:'), 8)
... using the example control event constant from the previous post.
@mikael I see...that's where I wasn't understanding things. So you can't just somehow convert a python function or bound method into an object valid for use as a selector? It has to be something that can be called from within objective-c already as a selector.
I'll give this a try. Thanks.
@shinyformica, need to get some guru input on whether it would be possible directly. I just assumed that omz would have known.
Searching through the forum archives...I think this may be the best example I've seen:
I'll see if I can muddle through how this works...seems like it does what I am asking about.
@shinyformica, thanks! Combine that with setting the real Python target object as a property of
target, and we are done.
See if I will get around to removing the ui.Buttons from Gestures.
Hrm...I'm still doing something wrong...
I tried to hook this up just to a simple ui.Button, so I define a function to be called when the control gets a touch event, and turn that into a target objc object so the function can be called as a selector:
def _controlTouchEvent(_self, _cmd, sender): print "control touch event:",sender ControlTouchEventTarget = objc_util.create_objc_class("ControlTouchEventTarget",methods=[_controlTouchEvent]) target = ControlTouchEventTarget.new().autorelease()
Then I try to hook it up to the ui.Button by getting the actual objc instance of the button and then calling "addTarget_action_forControlEvents_" like so:
instance = objc_util.ObjCInstance(button) instance.addTarget_action_forControlEvents_(target, "_controlTouchEvent", 0x00000FFF)
But I get the following error:
AttributeError: No method found for selector "addTarget:action:forControlEvents:"
And, inspecting a ui.Button objc instance in the console, it doesn't appear to have that method...in fact it doesn't appear to have any of the UIButton-specific methods...is there something I have to do to get the actual "UIButton" instance to get this selector? I'm not entirely clear what I'm getting back from objc_util.ObjCInstance(button) on a button created via ui.Button().
Ah...wait...I see, the result of:
is an SUIButton instance, which is a view that contains an actual UIButton instance underneath! So I need to pull out that subview if I want to manipulate the actual UIButton? Is there an accessor for that on the wrapper instance, or should I just get instance.subviews()?
Ok, that gets me closer...now it seems to be able to call "addTarget_action_forControlEvents_()" fine, but when it actually attempts to call the "_controlTouchEvent()" selector on the ControlTouchEventTarget object I created, it crashes and the objc exception details look like:
Fatal Python error: Aborted Thread 0x000000016f13b000 (most recent call first): ------------------------------------------------------------------------ Objective-C exception details: NSInvalidArgumentException: -[ControlTouchEventTarget _controlTouchEvent]: unrecognized selector sent to instance 0x1c4014450 Stack trace: 0 CoreFoundation 0x00000001817b6da4 <redacted> + 252 1 libobjc.A.dylib 0x00000001809705ec objc_exception_throw + 56 2 CoreFoundation 0x00000001817c4098 <redacted> + 0 3 CoreFoundation 0x00000001817bc5c8 <redacted> + 1380 4 CoreFoundation 0x00000001816a241c _CF_forwarding_prep_0 + 92 5 UIKit 0x000000018b52564c <redacted> + 96 6 UIKit 0x000000018b646870 <redacted> + 80 7 UIKit 0x000000018b52b700 <redacted> + 440 8 UIKit 0x000000018b66022c <redacted> + 308 9 UIKit 0x000000018b5a87c8 <redacted> + 1892 10 UIKit 0x000000018b59d890 <redacted> + 3160 11 UIKit 0x000000018b59c1d0 <redacted> + 340 12 UIKit 0x000000018bd7dd1c <redacted> + 2340 13 UIKit 0x000000018bd802c8 <redacted> + 4744 14 UIKit 0x000000018bd79368 <redacted> + 152 15 CoreFoundation 0x000000018175f404 <redacted> + 24 16 CoreFoundation 0x000000018175ec2c <redacted> + 276 17 CoreFoundation 0x000000018175c79c <redacted> + 1204 18 CoreFoundation 0x000000018167cda8 CFRunLoopRunSpecific + 552 19 GraphicsServices 0x0000000183662020 GSEventRunModal + 100 20 UIKit 0x000000018b69c758 UIApplicationMain + 236 21 Pythonista3 0x00000001010d1c4c Pythonista3 + 253004 22 libdyld.dylib 0x000000018110dfc0 <redacted> + 4 End of exception details.
Any idea why it would not be able to see the selector for that function on the ControlTouchEventTarget object? Is my naming invalid somehow?
Any help on this would be appreciated...I keep getting the crash with "unrecognized selector sent to instance". I've tried changing the name to get rid of the underscore, and providing it in the "addTarget_action_forControlEvents()" call as:
objcinstance.addTarget_action_forControlEvents_(target, "controlTouchEvent", ...)
objcinstance.addTarget_action_forControlEvents_(target, objc_util.sel("controlTouchEvent"), ...)
objcinstance.addTarget_action_forControlEvents_(target, objc_util.sel("controlTouchEvent:"), ...)
but none of those work...somehow I'm not providing a valid selector name, even though the example here:
seems to show the way to do it...and I'm not seeing how what I'm doing is substantially different.
So I see part of the problem: I was defining the target function "controlTouchEvent" inside a class definition, which unsurprisingly messed up the selector name...if I define the function globally, it works.
Therefore I have two questions:
- What do I need to provide as the selector name for a function defined within a class (not a python instance method, just a function defined within a class definition) as in:
class Thing(object): def __init__(self, button, ...): def targetFunction(_self, _cmd): print "target function" TargetClass = objc_util.create_objc_class("TargetClass", methods=[targetFunction]) self.target = TargetClass.new().autorelease() objcbutton = objc_util.ObjCInstance(button) objcbutton = objcbutton.subviews() objcbutton.addTarget_action_forControlEvents_(self.target, "targetFunction", 0x00000FFF)
The above is what I was doing, but it was giving me the "No method found for selector" error every time.
Doing it this way works:
def targetFunction(_self, _cmd): print "target function" TargetClass = objc_util.create_objc_class("TargetClass", methods=[targetFunction]) target = TargetClass.new().autorelease() class Thing(object): def __init__(self, button, ...): objcbutton = objc_util.ObjCInstance(button) objcbutton = objcbutton.subviews() objcbutton.addTarget_action_forControlEvents_(target, "targetFunction", 0x00000FFF)
So how do you reference a selector defined within a class? Or is that just not possible?
- There appear to be three overloaded versions of the action method which can be provided to "addTarget_action_forControlEvents":
- (IBAction)doSomething; - (IBAction)doSomething:(id)sender; - (IBAction)doSomething:(id)sender forEvent:(UIEvent*)event;
But it seems like I can only provide the first definition, the one which takes no arguments. If I try to supply a function which takes a "sender" argument or both "sender" and "event", I get a traceback saying that the function only takes 2 arguments. Does the selector name change when it takes more arguments than the first two, i.e. doSomething_sender or doSomething_sender_event?
Well...I think I was bitten by the same "changes in objective-c remain across script runs" issue that I've gotten bitten by before. If I force quit and restart Pythonista, the function-defined-within-class works as expected. And for the second issue: the selector must be named to match the parameters...so for the third one, with both sender and event:
Name the target function:
def controlTouchEvent_sender_event(_self, _cmd, sender, event):
and then in the actual call to establish the target selector:
objcinstance.addTarget_action_forControlEvents_(self._target, "controlTouchEvent:sender:event", 0x00000FFF)
So it seems I muddled through it after all.
@shinyformica, thank you for a very interesting and educational series of posts – they read a bit like a detective novel.
Being an objc muddler myself, I am wondering why your method and selector do not need to have the trailing underscore/colon.
@mikael ...I...don't know. It seems possible the objc_util module is forgiving about that kind of thing when providing a selector name string. I didn't even fully grasp how the parameters needed to be defined or named in my function...the objective-c definition looks like:
- (IBAction)doSomething:(id)sender forEvent:(UIEvent*)event;
so I just assumed it would need to have a slot for a "sender" and "event" parameter, along with the default "_self" and "_cmd" parameters, and voila.
Some of this still feels a bit like magic and rainbows to me...
Being an objc muddler myself, I am wondering why your method and selector do not need to have the trailing underscore/colon.
Objective-C's requirements about where to put colons are only enforced at compile time (they are a part of the language syntax). At runtime it doesn't matter if the colons match up with the method arguments (at that point everything has been translated to regular C function pointer calls), so the runtime never checks the selector names in any way. It's similar to how Python lets you say
setattr(obj, "funny attribute name!", 42), even though you can't use
funny attribute name!as an attribute in your source code.
The underscores in the function name do matter to
create_objc_class, it counts the number of underscores in the name to figure out how many parameters the method has. So even though
controlTouchEvent_sender_eventis not really a correct Objective-C method name, it has two underscores, which matches the number of arguments that the Python function takes (in addition to
_cmd), so the call works in the end.
I didn't even fully grasp how the parameters needed to be defined or named in my function...
The example method (
(IBAction)doSomething:(id)sender forEvent:(UIEvent *)event) would translate to
doSomething:forEvent:as a selector, or
doSomething_event_as a Python function name. Only the word to the left of each colon is part of the selector. The word to the right of each parameter type is the internal parameter name - this only matters if you're implementing the method in Objective-C source code, you can ignore it when translating the method name to
In this case it doesn't matter though what exactly the method name is. You only need to make sure that the underscore/colon count in your function name matches how many arguments the function takes, otherwise
objc_utilwon't call your function with the correct number of arguments.
@dgelessus thanks for the very informative response!
out of curiosity, what is the use case here? Another option might be to have a transparent overlay, which implements touch_moved, etc, then uses the objc hitTest:withEvent: on the view underneath, which then returns the objc view that wants to handle the touch. Then you simply forward the event onto it, doing whatever other logging or hijacking you wanted.
@JonB yeah, I thought of doing something of that nature...but that would require some other complicated handling that doing things this way removes. I'm not really hijacking the touch events, but I am keeping track of what control is currently being interacted with for other reasons. If there is a more general, global, robust way of determining the currently active control, meaning the control or controls which the user is currently manipulating, then I definitely would want to do that instead of this.
I also needed to make this more-or-less transparent to whether the control is a custom ui.View or a standard wrapped UIKit control, from an outside API perspective.
This way does work, but I'm always completely open to better ideas.
I guess I'm trying to understand the end goal. This is for some sort of debugging or logging purpose? A prank keylogger app?
Anything that has an python action or delegate includes a sender argument that gives you access to the item being touched, so you could use a decorator on those methods which handles the logging...
One approach would be to add a gesture recognizer on the application keyWindow, that does not cancel touch events.
The use case is a little complicated, and very task-specific:
- app is constantly listening for incoming messages
- manipulating controls in the view sends outgoing messages
- some incoming messages will try to set the value of a control being manipulated
- this leads to "fighting" between the user and the system
- knowing which control or controls are being interacted with, we can filter the incoming messages so they don't attempt to set controls actively being manipulated, effectively "debouncing" the signal
I like the idea of a transparent layer...but I wanted to be more specific about what I considered "manipulating" the control...with this, I can say exactly what I consider to be user interaction for these purposes.
Anyway, I am using the sender argument coming from the addTarget_action_forControlEvents() call, which is how I'm keying which controls are marked active. For my purposes it is working.Unfortunately, it seems like the various Pythonista-wrapped UIControls have different ways, or sometimes no way, of getting access to the actual UIControl instance they wrap, which makes it a little complicated to call the addTarget_ objectiveC method to install the monitor.