User specified custom key combination for a global hotkey May 13 2015

Over a year ago, I covered global hotkeys in a tutorial on this very blog. However, handling a global hotkey combo is only half of the story - I'm sure in most apps you've seen that provide that kind of functionality, the app also allows the user to override the default, and specify their own key combination to use instead. How do they do it? We're going to build on the app we already created in that tutorial (the code is here) to find out.

DDHotKey, revisited

The library we used in the last tutorial on global hotkeys, that provided a nice and neat wrapper around the old Carbon APIs for hotkeys, also provides a ready-to-use UI component for user input of hotkeys! The only thing we need to figure out is how we integrate with it, and how we store the users chosen key combination for use between app sessions. Let's start with the UI.

We're going to add the DDHotKeyTextField to the window, so at the end of our buildWindow method, we'll set it up like this:

size = @mainWindow.frame.size
field_size = [150, 30]
@hot_key_field = DDHotKeyTextField.alloc.initWithFrame(
    [(size.width / 2.0) - (field_size[0] / 2.0), (size.height / 2.0) - (field_size[1] / 2.0)],
@hot_key_field.delegate = self


Nothing too complex there - we're centering the field in our window, setting the AppDelegate to be the delegate for the field, and adding it to our window.

Next up, we need a method to register our hotkeys. This will serve two purposes - it's what we'll call when we detect a new hotkey set using our DDHotKeyTextField, and we'll also switch the initial hotkey registration for the app to use it too, for consistency:

def registerHotKey(keyCode, modifierFlags) center = DDHotKeyCenter.sharedHotKeyCenter center.unregisterAllHotKeys center.registerHotKeyWithKeyCode( keyCode, modifierFlags: modifierFlags, target: self, action: 'handleHotkey:', object: nil) end

This should be fairly simple to follow based on how we were registering hotkeys before - basically we're passing in the key code and modifier flags that describes the hotkey combo; we're grabbing the hotkey center, this time firstly unregistering all existing hotkeys (remember this will be called when changing the hotkey so we need to remove any existing combo), and then registering the hotkey pointing to our handle hotkey method, handleHotKey, for when it is triggered.

Let's change the initial app setup to use this method to make sure it's working. In applicationDidFinishLaunching, we'll replace our registerHotKeyWithKeyCode call to the DDHotKeyCenter with a call to our new registerHotKey method:

@hot_key_field.hotKey = self.registerHotKey(KVK_ANSI_H, (NSCommandKeyMask | NSAlternateKeyMask))

Then we need to hook into when the DDHotKeyTextField is done being edited, so we can re-register the hotkey. We've already set the delegate for the field to the AppDelegate, so now all we need to do is define the following method:

def controlTextDidEndEditing(notification)
  self.registerHotKey(@hot_key_field.hotKey.keyCode, @hot_key_field.hotKey.modifierFlags) if @hot_key_field.hotKey

This will get called when editing is finished on that field (i.e. when enter is pressed), and it simply calls registerHotKey with the current key code and modifier flags for the field hotkey, so long as it exists.

If you fire up the app now, you should see that a default hotkey combo is set, the field allows that to be changed, and when it is changed, the app responds as expected on that new hotkey, instead of the old one.

Hotkey field



You'll also notice though that shutting down the app and firing it back up restores the default combo, and any user customization is lost. We need to save the key code and modifier flags for any hotkey selected, so that we can use that when the app is restarted, and so if the user has changed it, it stays changed. We're going to use NSUserDefaults to store the key code and modifier flags. First things first then, when a hotkey is registered, let's store the values. Amend the top of registerHotKey to the following:

def registerHotKey(keyCode, modifierFlags)
  NSUserDefaults.standardUserDefaults['keyCode'] = keyCode
  NSUserDefaults.standardUserDefaults['modifierFlags'] = modifierFlags

The key code and modifier flags is all we need to describe the hotkey combo, so we'll stash those. Next up, let's add some helper methods for retrieving those values - and we'll move our defaults into those methods too, for the first run of the app where no values will be set:

def userKeyCode
  NSUserDefaults.standardUserDefaults['keyCode'] || KVK_ANSI_H

def userModifierFlags
  NSUserDefaults.standardUserDefaults['modifierFlags'] || (NSCommandKeyMask | NSAlternateKeyMask)

So we're looking up our persisted values, falling back to the same defaults for the key code and modifier flags that we were using before. Now, in applicationDidFinishLaunching, we can put into place the final piece of the puzzle. Instead of using our default combo there on every app startup, we'll refer to these helper methods:

@hot_key_field.hotKey = self.registerHotKey(self.userKeyCode, self.userModifierFlags)

This means that when the app is run for the first time, it'll fall back on the defaults provided within those helper methods still, but once values are persisted and saved, it'll use those instead.

Try it out now, fire up the app, change the key combo from cmd+alt+h to something else, press enter, restart the app, and then test out your new custom key combo for hiding and showing the app, persisted between app sessions. Power to the users!