Forum Archive

Possible to subclass UIView and redefine keyCommands property?

ywangd

The goal is to support modifier keys from external physical keyboard. According to the apple dev document, it requires the custom view to redefine the keyCommands property.

Is this something possible with the beta ctypes and objc_utils modules?

Any help is appreciated!

omz

Yes, it's possible, I can post an example here when the next beta is out (some things in the next build make this a little easier).

ywangd

Wow awesome! I posted the question but was prepared to be told No. Can't wait to see the example. This new beta is getting really exciting! Thanks @omz

omz

As promised, here's a minimal demo of a view that defines a hardware keyboard shortcut – note that this will only work with the latest beta (#160023).

from objc_util import *
from random import random
import ui

UIKeyCommand = ObjCClass('UIKeyCommand')

def keyCommands(_self, _cmd):
    cmd_key_flag = (1<<20)
    key_command = UIKeyCommand.keyCommandWithInput_modifierFlags_action_('R', cmd_key_flag, 'keyCommandAction')
    commands = ns([key_command])
    return commands.ptr

def canBecomeFirstResponder(_self, _cmd):
    return True

def keyCommandAction(_self, _cmd):
    self = ObjCInstance(_self)
    r, g, b = random(), random(), random()
    self.setBackgroundColor_(UIColor.colorWithRed_green_blue_alpha_(r, g, b, 1))

KeyCommandsView = create_objc_class('KeyCommandsView', UIView, [keyCommands, canBecomeFirstResponder, keyCommandAction])

@on_main_thread
def main():
    main_view = ui.View(frame=(0, 0, 400, 400))
    main_view.name = 'Key Commands Demo'

    v = KeyCommandsView.alloc().initWithFrame_(((0, 0), (400, 400)))
    v.setBackgroundColor_(UIColor.lightGrayColor())
    v.becomeFirstResponder()
    ObjCInstance(main_view).addSubview_(v)

    label = ui.Label(frame=(0, 0, 400, 400))
    label.alignment = ui.ALIGN_CENTER
    label.text = 'Press Cmd+R on an external keyboard to change the background color.'
    label.number_of_lines = 0
    main_view.add_subview(label)

    main_view.present('sheet')

if __name__ == '__main__':
    main()
ywangd

@omz Thanks for the sample. It works like magic! Now my homework is to implement this into the existing TextView!

ywangd

@omz Is it possible to find out the key and modifier information inside the action function, i.e. inside keyCommandAction(_self, _cmd) how can I tell which key is pressed? This is useful when a single action function is used to handle multiple keyboard shortcuts.

I tried to check _cmd, but calling ObjCInstance(_cmd) crashed the app.

omz

@ywangd Yes, that's possible. Below is a modified version of the example I posted that prints the actual key command to the console. Because this only makes sense with multiple commands, I've also added a simple Cmd+B shortcut to make the background color blue (Cmd+R still chooses a random color).

The most important change is that the action now takes a "sender" argument. _cmd is a hidden argument that is passed to every ObjC method, it doesn't have anything to do with the key command here – it's the selector that was used to send the message, and you only need this in very rare cases, if ever.

# coding: utf-8
from objc_util import *
from random import random
import ui

UIKeyCommand = ObjCClass('UIKeyCommand')

modifiers = {(1<<17): 'Shift', (1<<18): 'Ctrl', (1<<19): 'Alt', (1<<20): 'Cmd', (1<<21): 'NumPad'}

def keyCommands(_self, _cmd):
    cmd_key_flag = (1<<20)
    key_command_r = UIKeyCommand.keyCommandWithInput_modifierFlags_action_('R', cmd_key_flag, 'keyCommandAction:')
    key_command_b = UIKeyCommand.keyCommandWithInput_modifierFlags_action_('B', cmd_key_flag, 'keyCommandAction:')
    commands = ns([key_command_r, key_command_b])
    return commands.ptr

def canBecomeFirstResponder(_self, _cmd):
    return True

def keyCommandAction_(_self, _cmd, _sender):
    self = ObjCInstance(_self)
    key_cmd = ObjCInstance(_sender)
    flags = key_cmd.modifierFlags()
    modifier_str = ' + '.join(modifiers[m] for m in modifiers.keys() if (m & flags))
    key_input = key_cmd.input()
    print 'Input: "%s" Modifiers: %s' % (key_input, modifier_str)
    if str(key_input) == 'R':
        r, g, b = random(), random(), random()
    else:
        r, g, b = 0.0, 0.0, 1.0
    self.setBackgroundColor_(UIColor.colorWithRed_green_blue_alpha_(r, g, b, 1))

KeyCommandsView = create_objc_class('KeyCommandsView', UIView, [keyCommands, canBecomeFirstResponder, keyCommandAction_])

@on_main_thread
def main():
    main_view = ui.View(frame=(0, 0, 400, 400))
    main_view.name = 'Key Commands Demo'

    v = KeyCommandsView.alloc().initWithFrame_(((0, 0), (400, 400)))
    v.setBackgroundColor_(UIColor.lightGrayColor())
    v.becomeFirstResponder()
    ObjCInstance(main_view).addSubview_(v)

    label = ui.Label(frame=(0, 0, 400, 400))
    label.alignment = ui.ALIGN_CENTER
    label.text = 'Press Cmd+R on an external keyboard to change the background color. Cmd+B makes the background blue.'
    label.number_of_lines = 0
    main_view.add_subview(label)

    main_view.present('sheet')

if __name__ == '__main__':
    main()
ywangd

Thanks @omz Works perfectly!

mikael

@omz: Simple learning question, please - why is it important in this case to run the main() on_main_thread?

omz

@mikael Basically, everything that involves UIKit has to run on the main thread. From Apple's documentation:

For the most part, use UIKit classes only from your app’s main thread. This is particularly true for classes derived from UIResponder or that involve manipulating your app’s user interface in any way.

In this example, main() is the only function that is actually decorated with on_main_thread, but the callbacks (keyCommands, canBecomeFirstResponder) actually also run on the main thread because they're called by UIKit.

When you use the ui module, this is handled automatically (internally, a lot of things are dispatched to the main thread, without you having to declare it), but as objc_util is more low-level, this has to be done explicitly.

omz

Btw, in case you want to create keyboard shortcuts involving the arrow keys, you can use these special strings for the input argument:

  • 'UIKeyInputLeftArrow'
  • 'UIKeyInputRightArrow'
  • 'UIKeyInputUpArrow'
  • 'UIKeyInputDownArrow'

There's also 'UIKeyInputEscape', but I wouldn't use that because not all iPad keyboards actually have an esc key.

mikael

@omz: Thanks. If I am reading this right, I need to go back and decorate some gesture-adding functions.