← Blog

Working with Key Events

· James

Almost all apps on your Mac will support keyboard shortcuts to help users be more productive. Adding keyboard shortcuts to your app is as easy as adding a Key Equivalent to a button or menu item in your Nib. This can be found in the Attributes Inspector pane in Xcode. You can also detect key down events on your NSResponder subclasses by implementing the following:

- (void)keyDown:(NSEvent *)event
{
	if (([NSEvent modifierFlags] & NSCommandKeyMask) == NSCommandKeyMask && [event keyCode] == kVK_ANSI_G) {
		// CMD + G
		return;
	}
	if ([event keyCode] == kVK_ANSI_G) {
		// G
		return;
	}
	[super keyDown:event];
}

There are two issues with the above. Firstly, order is important. Of the two statements, if we had the ([event keyCode] == kVK_ANSI_G) statement first then it would still resolve as true if the user held the Command key or not. Secondly, even with the statements in the correct order, if the user held the Shift key in addition to the Command key, the statement would still resolve as true. There is a better way to test for key events, but before I go in to that I’d like to discuss a couple of points about the key masks.

Key masks are usually unsigned integers with a single bit set, usually achieved by shifting 1 by a number making sure you do not shift more bits than the unsigned integer type can contain. By applying a bitwise AND between any unsigned integer and such a mask will return either zero or the mask.

NSUInteger foo = 101;
NSUInteger bar = 145;
NSUInteger mask = 1 << 4; // 16

In base 2:

    0110 0101 // foo
AND 0001 0000 // mask
  = 0000 0000

    1001 0001 // bar
AND 0001 0000 // mask
  = 0001 0000

As can be seen above, a bitwise AND between two unsigned integers will always produce:

  1. Zero if they have no bit in common
  2. An unsigned integer greater than zero if there is one bit in common

A BOOL in Objective-C is typedef to a char, though in the more modern ARM64 runtime this is no longer the case.

// iOS 7.0 > usr/include > objc > objc.h

/// Type to represent a boolean value.
#if !defined(OBJC_HIDE_64) && TARGET_OS_IPHONE && __LP64__
typedef bool BOOL;
#else
typedef signed char BOOL; 
// BOOL is explicitly signed so @encode(BOOL) == "c" rather than "C" 
// even if -funsigned-char is used.
#endif

When BOOL is typdef to a char we can demonstrate an issue:

#include <stdio.h>

int main()
{
	int foo = 1456;
	int mask = 1 << 10;
	
	char firstCheck = ((foo & mask) == mask);
	char secondCheck = (foo & mask);
	
	printf("%d\n", firstCheck); // 1
	printf("%d\n", secondCheck); // 0
}

The reason the second check prints zero is that the mask, 1024, can be represented in base 2 as 0100 0000 0000. Since a char is only 8 bits, when casted to a char the four less significant bits (the ones to the right) will be kept and the rest discarded, leaving zero. By adding the bitwise comparison, we make sure that we are indeed comparing unsigned integers and the result, with the one bit able to fit in to a char.

This might not be an issue when used as the expression in an if statement but can definitely be a problem when used in a char / BOOL variable as demonstrated above.

Another issue can arise if a mask is used that doesn’t have any bit set.

typedef NS_ENUM(NSUInteger, RMCustomMask) {
	RMCustomMaskNone = 0,
	RMCustomMaskSome = 1 << 0,
	RMCustomMaskAll = 1 << 1,
};

NSUInteger foo = 0;

BOOL firstCheck = (foo & RMCustomMaskNone); // FALSE
BOOL secondCheck = ((foo & RMCustomMaskNone) == RMCustomMaskNone); // TRUE

In the example above the first check will be false and the second true. The reason the first check has an incorrect result is that foo is indeed matching the mask but the result is zero.

    0000 0000 // foo
AND 0000 0000 // RMCustomMaskNone
  = 0000 0000

Putting bitwise operations to one side, let’s get back to the better way of determining a key combination. You can instead customise an NSButton and message it to determine whether the event can be performed. Let’s see how:

- (void)keyDown:(NSEvent *)event
{
	NSButton *button = [[NSButton alloc] init];
	
	[button setKeyEquivalentModifierMask:NSCommandKeyMask];
	[button setKeyEquivalent:@"g"];
	if ([button performKeyEquivalent:event]) {
		// CMD + G
		return;
	}
	
	[button setKeyEquivalentModifierMask:(NSCommandKeyMask | NSAlternateKeyMask)];
	[button setKeyEquivalent:@"G"];
	if ([button performKeyEquivalent:event]) {
		// CMD + ALT + SHIFT + G
		return;
	}
	
	[super keyDown:event];
}

As you can see in the example the button has its keyEquivalentModifierMask and keyEquivalent set and is then asked whether it can perform the key equivalent with the event. Here the order of the if statements doesn’t make any difference compared to the earlier example. Also to quote from the docs regarding setKeyEquivalentModifierMask “The only mask bits relevant in button key-equivalent modifier masks are NSControlKeyMask, NSAlternateKeyMask, and NSCommandKeyMask.” As you can see in the example above, for a Shift key you use an uppercase key equivalent character.

I hope this has been helpful look in to key events and some insightful bitwise operations. If you have any questions, feel free to contact me on Twitter.

Download

Confirm your email and we'll send your code. Unsubscribe any time. Privacy policy