aboutsummaryrefslogtreecommitdiffstats
path: root/Framework
diff options
context:
space:
mode:
authorTomáš Znamenáček2015-01-08 12:00:53 +0100
committerTomáš Znamenáček2015-01-08 12:00:53 +0100
commit9b919cba51e4cd11b0c4424930d6c18a1baec73c (patch)
treea8107774609d5f4263f7b79749d93e6c6ff2642d /Framework
parenta3a459b4e4e47bf18dccd5dc7f315389346e3d6c (diff)
parentea69d5939511f61a7082ba1e8ff46d247862a3fa (diff)
downloadMASShortcut-9b919cba51e4cd11b0c4424930d6c18a1baec73c.tar.bz2
Merge pull request #53 from zoul/2.0-candidate
Thank you very much!
Diffstat (limited to 'Framework')
-rw-r--r--Framework/Info.plist24
-rw-r--r--Framework/MASDictionaryTransformer.h19
-rw-r--r--Framework/MASDictionaryTransformer.m51
-rw-r--r--Framework/MASDictionaryTransformerTests.m32
-rw-r--r--Framework/MASHotKey.h12
-rw-r--r--Framework/MASHotKey.m44
-rw-r--r--Framework/MASKeyCodes.h42
-rw-r--r--Framework/MASShortcut.h70
-rw-r--r--Framework/MASShortcut.m241
-rw-r--r--Framework/MASShortcutBinder.h67
-rw-r--r--Framework/MASShortcutBinder.m114
-rw-r--r--Framework/MASShortcutBinderTests.m98
-rw-r--r--Framework/MASShortcutMonitor.h27
-rw-r--r--Framework/MASShortcutMonitor.m101
-rw-r--r--Framework/MASShortcutTests.m26
-rw-r--r--Framework/MASShortcutValidator.h15
-rw-r--r--Framework/MASShortcutValidator.m111
-rw-r--r--Framework/MASShortcutView+Bindings.h25
-rw-r--r--Framework/MASShortcutView+Bindings.m50
-rw-r--r--Framework/MASShortcutView.h24
-rw-r--r--Framework/MASShortcutView.m511
-rw-r--r--Framework/Prefix.pch2
-rw-r--r--Framework/Shortcut.h7
23 files changed, 1713 insertions, 0 deletions
diff --git a/Framework/Info.plist b/Framework/Info.plist
new file mode 100644
index 0000000..91a62a8
--- /dev/null
+++ b/Framework/Info.plist
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.github.shpakovski.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>2.0.0</string>
+ <key>CFBundleVersion</key>
+ <string>2.0.0</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2014–2015 Vadim Shpakovski. All rights reserved.</string>
+</dict>
+</plist>
diff --git a/Framework/MASDictionaryTransformer.h b/Framework/MASDictionaryTransformer.h
new file mode 100644
index 0000000..eced1bb
--- /dev/null
+++ b/Framework/MASDictionaryTransformer.h
@@ -0,0 +1,19 @@
+extern NSString *const MASDictionaryTransformerName;
+
+/**
+ @brief Converts shortcuts for storage in user defaults.
+
+ User defaults can’t stored custom types directly, they have to
+ be serialized to @p NSData or some other supported type like an
+ @p NSDictionary. In Cocoa Bindings, the conversion can be done
+ using value transformers like this one.
+
+ There’s a built-in transformer (@p NSKeyedUnarchiveFromDataTransformerName)
+ that converts any @p NSCoding types to @p NSData, but with shortcuts
+ it makes sense to use a dictionary instead – the defaults look better
+ when inspected with the @p defaults command-line utility and the
+ format is compatible with an older sortcut library called Shortcut
+ Recorder.
+*/
+@interface MASDictionaryTransformer : NSValueTransformer
+@end
diff --git a/Framework/MASDictionaryTransformer.m b/Framework/MASDictionaryTransformer.m
new file mode 100644
index 0000000..9e4c82b
--- /dev/null
+++ b/Framework/MASDictionaryTransformer.m
@@ -0,0 +1,51 @@
+#import "MASDictionaryTransformer.h"
+#import "MASShortcut.h"
+
+NSString *const MASDictionaryTransformerName = @"MASDictionaryTransformer";
+
+static NSString *const MASKeyCodeKey = @"keyCode";
+static NSString *const MASModifierFlagsKey = @"modifierFlags";
+
+@implementation MASDictionaryTransformer
+
++ (BOOL) allowsReverseTransformation
+{
+ return YES;
+}
+
+// Storing nil values as an empty dictionary lets us differ between
+// “not available, use default value” and “explicitly set to none”.
+// See http://stackoverflow.com/questions/5540760 for details.
+- (NSDictionary*) reverseTransformedValue: (MASShortcut*) shortcut
+{
+ if (shortcut == nil) {
+ return [NSDictionary dictionary];
+ } else {
+ return @{
+ MASKeyCodeKey: @([shortcut keyCode]),
+ MASModifierFlagsKey: @([shortcut modifierFlags])
+ };
+ }
+}
+
+- (MASShortcut*) transformedValue: (NSDictionary*) dictionary
+{
+ // We have to be defensive here as the value may come from user defaults.
+ if (![dictionary isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+
+ id keyCodeBox = [dictionary objectForKey:MASKeyCodeKey];
+ id modifierFlagsBox = [dictionary objectForKey:MASModifierFlagsKey];
+
+ SEL integerValue = @selector(integerValue);
+ if (![keyCodeBox respondsToSelector:integerValue] || ![modifierFlagsBox respondsToSelector:integerValue]) {
+ return nil;
+ }
+
+ return [MASShortcut
+ shortcutWithKeyCode:[keyCodeBox integerValue]
+ modifierFlags:[modifierFlagsBox integerValue]];
+}
+
+@end
diff --git a/Framework/MASDictionaryTransformerTests.m b/Framework/MASDictionaryTransformerTests.m
new file mode 100644
index 0000000..48e11f3
--- /dev/null
+++ b/Framework/MASDictionaryTransformerTests.m
@@ -0,0 +1,32 @@
+@interface MASDictionaryTransformerTests : XCTestCase
+@end
+
+@implementation MASDictionaryTransformerTests
+
+- (void) testErrorHandling
+{
+ MASDictionaryTransformer *transformer = [MASDictionaryTransformer new];
+ XCTAssertNil([transformer transformedValue:nil],
+ @"Decoding a shortcut from a nil dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:(id)@"foo"],
+ @"Decoding a shortcut from a invalid-type dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{}],
+ @"Decoding a shortcut from an empty dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{@"keyCode":@"foo"}],
+ @"Decoding a shortcut from a wrong-typed dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{@"keyCode":@1}],
+ @"Decoding a shortcut from an incomplete dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{@"modifierFlags":@1}],
+ @"Decoding a shortcut from an incomplete dictionary returns nil.");
+}
+
+- (void) testNilRepresentation
+{
+ MASDictionaryTransformer *transformer = [MASDictionaryTransformer new];
+ XCTAssertEqualObjects([transformer reverseTransformedValue:nil], [NSDictionary dictionary],
+ @"Store nil values as an empty dictionary.");
+ XCTAssertNil([transformer transformedValue:[NSDictionary dictionary]],
+ @"Load empty dictionary as nil.");
+}
+
+@end
diff --git a/Framework/MASHotKey.h b/Framework/MASHotKey.h
new file mode 100644
index 0000000..1d267e4
--- /dev/null
+++ b/Framework/MASHotKey.h
@@ -0,0 +1,12 @@
+#import "MASShortcut.h"
+
+extern FourCharCode const MASHotKeySignature;
+
+@interface MASHotKey : NSObject
+
+@property(readonly) UInt32 carbonID;
+@property(copy) dispatch_block_t action;
+
++ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut;
+
+@end
diff --git a/Framework/MASHotKey.m b/Framework/MASHotKey.m
new file mode 100644
index 0000000..7886440
--- /dev/null
+++ b/Framework/MASHotKey.m
@@ -0,0 +1,44 @@
+#import "MASHotKey.h"
+
+FourCharCode const MASHotKeySignature = 'MASS';
+
+@interface MASHotKey ()
+@property(assign) EventHotKeyRef hotKeyRef;
+@property(assign) UInt32 carbonID;
+@end
+
+@implementation MASHotKey
+
+- (instancetype) initWithShortcut: (MASShortcut*) shortcut
+{
+ self = [super init];
+
+ static UInt32 CarbonHotKeyID = 0;
+
+ _carbonID = ++CarbonHotKeyID;
+ EventHotKeyID hotKeyID = { .signature = MASHotKeySignature, .id = _carbonID };
+
+ OSStatus status = RegisterEventHotKey([shortcut carbonKeyCode], [shortcut carbonFlags],
+ hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &_hotKeyRef);
+
+ if (status != noErr) {
+ return nil;
+ }
+
+ return self;
+}
+
++ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut
+{
+ return [[self alloc] initWithShortcut:shortcut];
+}
+
+- (void) dealloc
+{
+ if (_hotKeyRef) {
+ UnregisterEventHotKey(_hotKeyRef);
+ _hotKeyRef = NULL;
+ }
+}
+
+@end
diff --git a/Framework/MASKeyCodes.h b/Framework/MASKeyCodes.h
new file mode 100644
index 0000000..8c1ce06
--- /dev/null
+++ b/Framework/MASKeyCodes.h
@@ -0,0 +1,42 @@
+#import <Carbon/Carbon.h>
+
+// These glyphs are missed in Carbon.h
+enum {
+ kMASShortcutGlyphEject = 0x23CF,
+ kMASShortcutGlyphClear = 0x2715,
+ kMASShortcutGlyphDeleteLeft = 0x232B,
+ kMASShortcutGlyphDeleteRight = 0x2326,
+ kMASShortcutGlyphLeftArrow = 0x2190,
+ kMASShortcutGlyphRightArrow = 0x2192,
+ kMASShortcutGlyphUpArrow = 0x2191,
+ kMASShortcutGlyphDownArrow = 0x2193,
+ kMASShortcutGlyphEscape = 0x238B,
+ kMASShortcutGlyphHelp = 0x003F,
+ kMASShortcutGlyphPageDown = 0x21DF,
+ kMASShortcutGlyphPageUp = 0x21DE,
+ kMASShortcutGlyphTabRight = 0x21E5,
+ kMASShortcutGlyphReturn = 0x2305,
+ kMASShortcutGlyphReturnR2L = 0x21A9,
+ kMASShortcutGlyphPadClear = 0x2327,
+ kMASShortcutGlyphNorthwestArrow = 0x2196,
+ kMASShortcutGlyphSoutheastArrow = 0x2198,
+} MASShortcutGlyph;
+
+NS_INLINE NSString* NSStringFromMASKeyCode(unsigned short ch)
+{
+ return [NSString stringWithFormat:@"%C", ch];
+}
+
+NS_INLINE NSUInteger MASPickCocoaModifiers(NSUInteger flags)
+{
+ return (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask));
+}
+
+NS_INLINE UInt32 MASCarbonModifiersFromCocoaModifiers(NSUInteger cocoaFlags)
+{
+ return
+ (cocoaFlags & NSCommandKeyMask ? cmdKey : 0)
+ | (cocoaFlags & NSAlternateKeyMask ? optionKey : 0)
+ | (cocoaFlags & NSControlKeyMask ? controlKey : 0)
+ | (cocoaFlags & NSShiftKeyMask ? shiftKey : 0);
+}
diff --git a/Framework/MASShortcut.h b/Framework/MASShortcut.h
new file mode 100644
index 0000000..3e1bedf
--- /dev/null
+++ b/Framework/MASShortcut.h
@@ -0,0 +1,70 @@
+#import "MASKeyCodes.h"
+
+/**
+ @brief A model class to hold a key combination.
+
+ This class just represents a combination of keys. It does not care if
+ the combination is valid or can be used as a hotkey, it doesn’t watch
+ the input system for the shortcut appearance, nor it does access user
+ defaults.
+*/
+@interface MASShortcut : NSObject <NSSecureCoding, NSCopying>
+
+/**
+ @brief The virtual key code for the keyboard key.
+
+ @Hardware independent, same as in NSEvent. Events.h in the HIToolbox
+ framework for a complete list, or Command-click this symbol: kVK_ANSI_A.
+*/
+@property (nonatomic, readonly) NSUInteger keyCode;
+
+/**
+ @brief Cocoa keyboard modifier flags.
+
+ Same as in NSEvent: NSCommandKeyMask, NSAlternateKeyMask, etc.
+*/
+@property (nonatomic, readonly) NSUInteger modifierFlags;
+
+/**
+ @brief Same as @p keyCode, just a different type.
+*/
+@property (nonatomic, readonly) UInt32 carbonKeyCode;
+
+/**
+ @brief Carbon modifier flags.
+
+ A bit sum of @p cmdKey, @p optionKey, etc.
+*/
+@property (nonatomic, readonly) UInt32 carbonFlags;
+
+/**
+ @brief A string representing the “key” part of a shortcut, like the “5” in “⌘5”.
+*/
+@property (nonatomic, readonly) NSString *keyCodeString;
+
+/**
+ @brief A key-code string used in key equivalent matching.
+
+ For precise meaning of “key equivalents” see the @p keyEquivalent
+ property of @p NSMenuItem. Here the string is used to support shortcut
+ validation (“is the shortcut already taken in this menu?”) and
+ for display in @p NSMenu.
+*/
+@property (nonatomic, readonly) NSString *keyCodeStringForKeyEquivalent;
+
+/**
+ @brief A string representing the shortcut modifiers, like the “⌘” in “⌘5”.
+*/
+@property (nonatomic, readonly) NSString *modifierFlagsString;
+
+- (instancetype)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
++ (instancetype)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
+
+/**
+ @brief Creates a new shortcut from an NSEvent object.
+
+ This is just a convenience initializer that reads the key code and modifiers from an NSEvent.
+*/
++ (instancetype)shortcutWithEvent:(NSEvent *)anEvent;
+
+@end
diff --git a/Framework/MASShortcut.m b/Framework/MASShortcut.m
new file mode 100644
index 0000000..e6fa63d
--- /dev/null
+++ b/Framework/MASShortcut.m
@@ -0,0 +1,241 @@
+#import "MASShortcut.h"
+
+static NSString *const MASShortcutKeyCode = @"KeyCode";
+static NSString *const MASShortcutModifierFlags = @"ModifierFlags";
+
+@implementation MASShortcut
+
+#pragma mark Initialization
+
+- (instancetype)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
+{
+ self = [super init];
+ if (self) {
+ _keyCode = code;
+ _modifierFlags = MASPickCocoaModifiers(flags);
+ }
+ return self;
+}
+
++ (instancetype)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
+{
+ return [[self alloc] initWithKeyCode:code modifierFlags:flags];
+}
+
++ (instancetype)shortcutWithEvent:(NSEvent *)event
+{
+ return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags];
+}
+
+#pragma mark Shortcut Accessors
+
+- (UInt32)carbonKeyCode
+{
+ return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode);
+}
+
+- (UInt32)carbonFlags
+{
+ return MASCarbonModifiersFromCocoaModifiers(self.modifierFlags);
+}
+
+- (NSString *)description
+{
+ return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString];
+}
+
+- (NSString *)keyCodeStringForKeyEquivalent
+{
+ NSString *keyCodeString = self.keyCodeString;
+ if (keyCodeString.length > 1) {
+ switch (self.keyCode) {
+ case kVK_F1: return NSStringFromMASKeyCode(0xF704);
+ case kVK_F2: return NSStringFromMASKeyCode(0xF705);
+ case kVK_F3: return NSStringFromMASKeyCode(0xF706);
+ case kVK_F4: return NSStringFromMASKeyCode(0xF707);
+ case kVK_F5: return NSStringFromMASKeyCode(0xF708);
+ case kVK_F6: return NSStringFromMASKeyCode(0xF709);
+ case kVK_F7: return NSStringFromMASKeyCode(0xF70a);
+ case kVK_F8: return NSStringFromMASKeyCode(0xF70b);
+ case kVK_F9: return NSStringFromMASKeyCode(0xF70c);
+ case kVK_F10: return NSStringFromMASKeyCode(0xF70d);
+ case kVK_F11: return NSStringFromMASKeyCode(0xF70e);
+ case kVK_F12: return NSStringFromMASKeyCode(0xF70f);
+ // From this point down I am guessing F13 etc come sequentially, I don't have a keyboard to test.
+ case kVK_F13: return NSStringFromMASKeyCode(0xF710);
+ case kVK_F14: return NSStringFromMASKeyCode(0xF711);
+ case kVK_F15: return NSStringFromMASKeyCode(0xF712);
+ case kVK_F16: return NSStringFromMASKeyCode(0xF713);
+ case kVK_F17: return NSStringFromMASKeyCode(0xF714);
+ case kVK_F18: return NSStringFromMASKeyCode(0xF715);
+ case kVK_F19: return NSStringFromMASKeyCode(0xF716);
+ case kVK_Space: return NSStringFromMASKeyCode(0x20);
+ default: return @"";
+ }
+ }
+ return keyCodeString.lowercaseString;
+}
+
+- (NSString *)keyCodeString
+{
+ // Some key codes don't have an equivalent
+ switch (self.keyCode) {
+ case NSNotFound: return @"";
+ case kVK_F1: return @"F1";
+ case kVK_F2: return @"F2";
+ case kVK_F3: return @"F3";
+ case kVK_F4: return @"F4";
+ case kVK_F5: return @"F5";
+ case kVK_F6: return @"F6";
+ case kVK_F7: return @"F7";
+ case kVK_F8: return @"F8";
+ case kVK_F9: return @"F9";
+ case kVK_F10: return @"F10";
+ case kVK_F11: return @"F11";
+ case kVK_F12: return @"F12";
+ case kVK_F13: return @"F13";
+ case kVK_F14: return @"F14";
+ case kVK_F15: return @"F15";
+ case kVK_F16: return @"F16";
+ case kVK_F17: return @"F17";
+ case kVK_F18: return @"F18";
+ case kVK_F19: return @"F19";
+ case kVK_Space: return NSLocalizedString(@"Space", @"Shortcut glyph name for SPACE key");
+ case kVK_Escape: return NSStringFromMASKeyCode(kMASShortcutGlyphEscape);
+ case kVK_Delete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteLeft);
+ case kVK_ForwardDelete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteRight);
+ case kVK_LeftArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphLeftArrow);
+ case kVK_RightArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphRightArrow);
+ case kVK_UpArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphUpArrow);
+ case kVK_DownArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphDownArrow);
+ case kVK_Help: return NSStringFromMASKeyCode(kMASShortcutGlyphHelp);
+ case kVK_PageUp: return NSStringFromMASKeyCode(kMASShortcutGlyphPageUp);
+ case kVK_PageDown: return NSStringFromMASKeyCode(kMASShortcutGlyphPageDown);
+ case kVK_Tab: return NSStringFromMASKeyCode(kMASShortcutGlyphTabRight);
+ case kVK_Return: return NSStringFromMASKeyCode(kMASShortcutGlyphReturnR2L);
+
+ // Keypad
+ case kVK_ANSI_Keypad0: return @"0";
+ case kVK_ANSI_Keypad1: return @"1";
+ case kVK_ANSI_Keypad2: return @"2";
+ case kVK_ANSI_Keypad3: return @"3";
+ case kVK_ANSI_Keypad4: return @"4";
+ case kVK_ANSI_Keypad5: return @"5";
+ case kVK_ANSI_Keypad6: return @"6";
+ case kVK_ANSI_Keypad7: return @"7";
+ case kVK_ANSI_Keypad8: return @"8";
+ case kVK_ANSI_Keypad9: return @"9";
+ case kVK_ANSI_KeypadDecimal: return @".";
+ case kVK_ANSI_KeypadMultiply: return @"*";
+ case kVK_ANSI_KeypadPlus: return @"+";
+ case kVK_ANSI_KeypadClear: return NSStringFromMASKeyCode(kMASShortcutGlyphPadClear);
+ case kVK_ANSI_KeypadDivide: return @"/";
+ case kVK_ANSI_KeypadEnter: return NSStringFromMASKeyCode(kMASShortcutGlyphReturn);
+ case kVK_ANSI_KeypadMinus: return @"–";
+ case kVK_ANSI_KeypadEquals: return @"=";
+
+ // Hardcode
+ case 119: return NSStringFromMASKeyCode(kMASShortcutGlyphSoutheastArrow);
+ case 115: return NSStringFromMASKeyCode(kMASShortcutGlyphNorthwestArrow);
+ }
+
+ // Everything else should be printable so look it up in the current keyboard
+ OSStatus error = noErr;
+ NSString *keystroke = nil;
+ TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
+ if (inputSource) {
+ CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
+ if (layoutDataRef) {
+ UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef);
+ UniCharCount length = 0;
+ UniChar chars[256] = { 0 };
+ UInt32 deadKeyState = 0;
+ error = UCKeyTranslate(layoutData, (UInt16)self.keyCode, kUCKeyActionDisplay, 0, // No modifiers
+ LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState,
+ sizeof(chars) / sizeof(UniChar), &length, chars);
+ keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @"");
+ }
+ CFRelease(inputSource);
+ }
+
+ // Validate keystroke
+ if (keystroke.length) {
+ static NSMutableCharacterSet *validChars = nil;
+ if (validChars == nil) {
+ validChars = [[NSMutableCharacterSet alloc] init];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
+ }
+ for (NSUInteger i = 0, length = keystroke.length; i < length; i++) {
+ if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) {
+ keystroke = @"";
+ break;
+ }
+ }
+ }
+
+ // Finally, we've got a shortcut!
+ return keystroke.uppercaseString;
+}
+
+- (NSString *)modifierFlagsString
+{
+ unichar chars[4];
+ NSUInteger count = 0;
+ // These are in the same order as the menu manager shows them
+ if (self.modifierFlags & NSControlKeyMask) chars[count++] = kControlUnicode;
+ if (self.modifierFlags & NSAlternateKeyMask) chars[count++] = kOptionUnicode;
+ if (self.modifierFlags & NSShiftKeyMask) chars[count++] = kShiftUnicode;
+ if (self.modifierFlags & NSCommandKeyMask) chars[count++] = kCommandUnicode;
+ return (count ? [NSString stringWithCharacters:chars length:count] : @"");
+}
+
+#pragma mark NSObject
+
+- (BOOL) isEqual: (MASShortcut*) object
+{
+ return [object isKindOfClass:[self class]]
+ && (object.keyCode == self.keyCode)
+ && (object.modifierFlags == self.modifierFlags);
+}
+
+- (NSUInteger) hash
+{
+ return self.keyCode + self.modifierFlags;
+}
+
+#pragma mark NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)coder
+{
+ [coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:MASShortcutKeyCode];
+ [coder encodeInteger:(NSInteger)self.modifierFlags forKey:MASShortcutModifierFlags];
+}
+
+- (instancetype)initWithCoder:(NSCoder *)decoder
+{
+ self = [super init];
+ if (self) {
+ NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode];
+ _keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
+ _modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags];
+ }
+ return self;
+}
+
+#pragma mark NSSecureCoding
+
++ (BOOL)supportsSecureCoding
+{
+ return YES;
+}
+
+#pragma mark NSCopying
+
+- (instancetype) copyWithZone:(NSZone *)zone
+{
+ return [[self class] shortcutWithKeyCode:_keyCode modifierFlags:_modifierFlags];
+}
+
+@end
diff --git a/Framework/MASShortcutBinder.h b/Framework/MASShortcutBinder.h
new file mode 100644
index 0000000..1592e90
--- /dev/null
+++ b/Framework/MASShortcutBinder.h
@@ -0,0 +1,67 @@
+#import "MASShortcutMonitor.h"
+
+/**
+ @brief Binds actions to user defaults keys.
+
+ If you store shortcuts in user defaults (for example by binding
+ a @p MASShortcutView to user defaults), you can use this class to
+ connect an action directly to a user defaults key. If the shortcut
+ stored under the key changes, the action will get automatically
+ updated to the new one.
+
+ This class is mostly a wrapper around a @p MASShortcutMonitor. It
+ watches the changes in user defaults and updates the shortcut monitor
+ accordingly with the new shortcuts.
+*/
+@interface MASShortcutBinder : NSObject
+
+/**
+ @brief A convenience shared instance.
+
+ You may use it so that you don’t have to manage an instance by hand,
+ but it’s perfectly fine to allocate and use a separate instance instead.
+*/
++ (instancetype) sharedBinder;
+
+/**
+ @brief The underlying shortcut monitor.
+*/
+@property(strong) MASShortcutMonitor *shortcutMonitor;
+
+/**
+ @brief Binding options customizing the access to user defaults.
+
+ As an example, you can use @p NSValueTransformerNameBindingOption to customize
+ the storage format used for the shortcuts. By default the shortcuts are converted
+ from @p NSData (@p NSKeyedUnarchiveFromDataTransformerName). Note that if the
+ binder is to work with @p MASShortcutView, both object have to use the same storage
+ format.
+*/
+@property(copy) NSDictionary *bindingOptions;
+
+/**
+ @brief Binds given action to a shortcut stored under the given defaults key.
+
+ In other words, no matter what shortcut you store under the given key,
+ pressing it will always trigger the given action.
+*/
+- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action;
+
+/**
+ @brief Disconnect the binding between user defaults and action.
+
+ In other words, the shortcut stored under the given key will no longer trigger an action.
+*/
+- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName;
+
+/**
+ @brief Register default shortcuts in user defaults.
+
+ This is a convenience frontent to [NSUserDefaults registerDefaults].
+ The dictionary should contain a map of user defaults’ keys to appropriate
+ keyboard shortcuts. The shortcuts will be transformed according to
+ @p bindingOptions and registered using @p registerDefaults.
+*/
+- (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts;
+
+@end
diff --git a/Framework/MASShortcutBinder.m b/Framework/MASShortcutBinder.m
new file mode 100644
index 0000000..bf85c41
--- /dev/null
+++ b/Framework/MASShortcutBinder.m
@@ -0,0 +1,114 @@
+#import "MASShortcutBinder.h"
+#import "MASShortcut.h"
+
+@interface MASShortcutBinder ()
+@property(strong) NSMutableDictionary *actions;
+@property(strong) NSMutableDictionary *shortcuts;
+@end
+
+@implementation MASShortcutBinder
+
+#pragma mark Initialization
+
+- (id) init
+{
+ self = [super init];
+ [self setActions:[NSMutableDictionary dictionary]];
+ [self setShortcuts:[NSMutableDictionary dictionary]];
+ [self setShortcutMonitor:[MASShortcutMonitor sharedMonitor]];
+ [self setBindingOptions:@{NSValueTransformerNameBindingOption: NSKeyedUnarchiveFromDataTransformerName}];
+ return self;
+}
+
+- (void) dealloc
+{
+ for (NSString *bindingName in [_actions allKeys]) {
+ [self unbind:bindingName];
+ }
+}
+
++ (instancetype) sharedBinder
+{
+ static dispatch_once_t once;
+ static MASShortcutBinder *sharedInstance;
+ dispatch_once(&once, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+#pragma mark Registration
+
+- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action
+{
+ [_actions setObject:[action copy] forKey:defaultsKeyName];
+ [self bind:defaultsKeyName toObject:[NSUserDefaultsController sharedUserDefaultsController]
+ withKeyPath:[@"values." stringByAppendingString:defaultsKeyName] options:_bindingOptions];
+}
+
+- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName
+{
+ [_shortcutMonitor unregisterShortcut:[_shortcuts objectForKey:defaultsKeyName]];
+ [_shortcuts removeObjectForKey:defaultsKeyName];
+ [_actions removeObjectForKey:defaultsKeyName];
+ [self unbind:defaultsKeyName];
+}
+
+- (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts
+{
+ NSValueTransformer *transformer = [_bindingOptions valueForKey:NSValueTransformerBindingOption];
+ if (transformer == nil) {
+ NSString *transformerName = [_bindingOptions valueForKey:NSValueTransformerNameBindingOption];
+ if (transformerName) {
+ transformer = [NSValueTransformer valueTransformerForName:transformerName];
+ }
+ }
+
+ NSAssert(transformer != nil, @"Can’t register default shortcuts without a transformer.");
+
+ [defaultShortcuts enumerateKeysAndObjectsUsingBlock:^(NSString *defaultsKey, MASShortcut *shortcut, BOOL *stop) {
+ id value = [transformer reverseTransformedValue:shortcut];
+ [[NSUserDefaults standardUserDefaults] registerDefaults:@{defaultsKey:value}];
+ }];
+}
+
+#pragma mark Bindings
+
+- (BOOL) isRegisteredAction: (NSString*) name
+{
+ return !![_actions objectForKey:name];
+}
+
+- (id) valueForUndefinedKey: (NSString*) key
+{
+ return [self isRegisteredAction:key] ?
+ [_shortcuts objectForKey:key] :
+ [super valueForUndefinedKey:key];
+}
+
+- (void) setValue: (id) value forUndefinedKey: (NSString*) key
+{
+ if (![self isRegisteredAction:key]) {
+ [super setValue:value forUndefinedKey:key];
+ return;
+ }
+
+ MASShortcut *newShortcut = value;
+ MASShortcut *currentShortcut = [_shortcuts objectForKey:key];
+
+ // Unbind previous shortcut if any
+ if (currentShortcut != nil) {
+ [_shortcutMonitor unregisterShortcut:currentShortcut];
+ }
+
+ // Just deleting the old shortcut
+ if (newShortcut == nil) {
+ return;
+ }
+
+ // Bind new shortcut
+ [_shortcuts setObject:newShortcut forKey:key];
+ [_shortcutMonitor registerShortcut:newShortcut withAction:[_actions objectForKey:key]];
+}
+
+@end
diff --git a/Framework/MASShortcutBinderTests.m b/Framework/MASShortcutBinderTests.m
new file mode 100644
index 0000000..9f90a94
--- /dev/null
+++ b/Framework/MASShortcutBinderTests.m
@@ -0,0 +1,98 @@
+static NSString *const SampleDefaultsKey = @"sampleShortcut";
+
+@interface MASShortcutBinderTests : XCTestCase
+@property(strong) MASShortcutBinder *binder;
+@property(strong) MASShortcutMonitor *monitor;
+@property(strong) NSUserDefaults *defaults;
+@end
+
+@implementation MASShortcutBinderTests
+
+- (void) setUp
+{
+ [super setUp];
+ [self setBinder:[[MASShortcutBinder alloc] init]];
+ [self setMonitor:[_binder shortcutMonitor]];
+ [self setDefaults:[[NSUserDefaults alloc] init]];
+ [_defaults removeObjectForKey:SampleDefaultsKey];
+}
+
+- (void) tearDown
+{
+ [_monitor unregisterAllShortcuts];
+ [self setMonitor:nil];
+ [self setDefaults:nil];
+ [self setBinder:nil];
+ [super tearDown];
+}
+
+- (void) testInitialValueReading
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Pass the initial shortcut from defaults to shortcut monitor.");
+}
+
+- (void) testValueChangeReading
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Pass the shortcut from defaults to shortcut monitor after defaults change.");
+}
+
+- (void) testValueClearing
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_defaults removeObjectForKey:SampleDefaultsKey];
+ XCTAssertFalse([_monitor isShortcutRegistered:shortcut],
+ @"Unregister shortcut from monitor after value is cleared from defaults.");
+}
+
+- (void) testBindingRemoval
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_binder breakBindingWithDefaultsKey:SampleDefaultsKey];
+ XCTAssertFalse([_monitor isShortcutRegistered:shortcut],
+ @"Unregister shortcut from monitor after binding was removed.");
+}
+
+- (void) testRebinding
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_binder breakBindingWithDefaultsKey:SampleDefaultsKey];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Bind after unbinding.");
+}
+
+- (void) testTransformerDeserialization
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576];
+ NSDictionary *storedShortcut = @{@"keyCode": @5, @"modifierFlags": @1048576};
+ [_defaults setObject:storedShortcut forKey:SampleDefaultsKey];
+ [_binder setBindingOptions:@{NSValueTransformerBindingOption:[MASDictionaryTransformer new]}];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Deserialize shortcut from user defaults using a custom transformer.");
+}
+
+- (void) testDefaultShortcuts
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576];
+ [_binder registerDefaultShortcuts:@{SampleDefaultsKey: shortcut}];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Bind shortcut using a default value.");
+}
+
+@end
diff --git a/Framework/MASShortcutMonitor.h b/Framework/MASShortcutMonitor.h
new file mode 100644
index 0000000..609686a
--- /dev/null
+++ b/Framework/MASShortcutMonitor.h
@@ -0,0 +1,27 @@
+#import "MASShortcut.h"
+
+/**
+ @brief Executes action when a shortcut is pressed.
+
+ There can only be one instance of this class, otherwise things
+ will probably not work. (There’s a Carbon event handler inside
+ and there can only be one Carbon event handler of a given type.)
+*/
+@interface MASShortcutMonitor : NSObject
+
+- (instancetype) init __unavailable;
++ (instancetype) sharedMonitor;
+
+/**
+ @brief Register a shortcut along with an action.
+
+ Attempting to insert an already registered shortcut probably won’t work.
+ It may burn your house or cut your fingers. You have been warned.
+*/
+- (void) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action;
+- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut;
+
+- (void) unregisterShortcut: (MASShortcut*) shortcut;
+- (void) unregisterAllShortcuts;
+
+@end
diff --git a/Framework/MASShortcutMonitor.m b/Framework/MASShortcutMonitor.m
new file mode 100644
index 0000000..cb89ce1
--- /dev/null
+++ b/Framework/MASShortcutMonitor.m
@@ -0,0 +1,101 @@
+#import "MASShortcutMonitor.h"
+#import "MASHotKey.h"
+
+@interface MASShortcutMonitor ()
+@property(assign) EventHandlerRef eventHandlerRef;
+@property(strong) NSMutableDictionary *hotKeys;
+@end
+
+static OSStatus MASCarbonEventCallback(EventHandlerCallRef, EventRef, void*);
+
+@implementation MASShortcutMonitor
+
+#pragma mark Initialization
+
+- (instancetype) init
+{
+ self = [super init];
+ [self setHotKeys:[NSMutableDictionary dictionary]];
+ EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed };
+ OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), MASCarbonEventCallback,
+ 1, &hotKeyPressedSpec, (__bridge void*)self, &_eventHandlerRef);
+ if (status != noErr) {
+ return nil;
+ }
+ return self;
+}
+
+- (void) dealloc
+{
+ if (_eventHandlerRef) {
+ RemoveEventHandler(_eventHandlerRef);
+ _eventHandlerRef = NULL;
+ }
+}
+
++ (instancetype) sharedMonitor
+{
+ static dispatch_once_t once;
+ static MASShortcutMonitor *sharedInstance;
+ dispatch_once(&once, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+#pragma mark Registration
+
+- (void) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action
+{
+ MASHotKey *hotKey = [MASHotKey registeredHotKeyWithShortcut:shortcut];
+ [hotKey setAction:action];
+ [_hotKeys setObject:hotKey forKey:shortcut];
+}
+
+- (void) unregisterShortcut: (MASShortcut*) shortcut
+{
+ [_hotKeys removeObjectForKey:shortcut];
+}
+
+- (void) unregisterAllShortcuts
+{
+ [_hotKeys removeAllObjects];
+}
+
+- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut
+{
+ return !![_hotKeys objectForKey:shortcut];
+}
+
+#pragma mark Event Handling
+
+- (void) handleEvent: (EventRef) event
+{
+ if (GetEventClass(event) != kEventClassKeyboard) {
+ return;
+ }
+
+ EventHotKeyID hotKeyID;
+ OSStatus status = GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID);
+ if (status != noErr || hotKeyID.signature != MASHotKeySignature) {
+ return;
+ }
+
+ [_hotKeys enumerateKeysAndObjectsUsingBlock:^(MASShortcut *shortcut, MASHotKey *hotKey, BOOL *stop) {
+ if (hotKeyID.id == [hotKey carbonID]) {
+ if ([hotKey action]) {
+ dispatch_async(dispatch_get_main_queue(), [hotKey action]);
+ }
+ *stop = YES;
+ }
+ }];
+}
+
+@end
+
+static OSStatus MASCarbonEventCallback(EventHandlerCallRef _, EventRef event, void *context)
+{
+ MASShortcutMonitor *dispatcher = (__bridge id)context;
+ [dispatcher handleEvent:event];
+ return noErr;
+}
diff --git a/Framework/MASShortcutTests.m b/Framework/MASShortcutTests.m
new file mode 100644
index 0000000..28eab56
--- /dev/null
+++ b/Framework/MASShortcutTests.m
@@ -0,0 +1,26 @@
+@interface MASShortcutTests : XCTestCase
+@end
+
+@implementation MASShortcutTests
+
+- (void) testEquality
+{
+ MASShortcut *keyA = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSControlKeyMask];
+ MASShortcut *keyB = [MASShortcut shortcutWithKeyCode:2 modifierFlags:NSControlKeyMask];
+ MASShortcut *keyC = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSAlternateKeyMask];
+ MASShortcut *keyD = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSControlKeyMask];
+ XCTAssertTrue([keyA isEqual:keyA], @"Shortcut is equal to itself.");
+ XCTAssertTrue([keyA isEqual:[keyA copy]], @"Shortcut is equal to its copy.");
+ XCTAssertFalse([keyA isEqual:keyB], @"Shortcuts not equal when key codes differ.");
+ XCTAssertFalse([keyA isEqual:keyC], @"Shortcuts not equal when modifier flags differ.");
+ XCTAssertTrue([keyA isEqual:keyD], @"Shortcuts are equal when key codes and modifiers are.");
+ XCTAssertFalse([keyA isEqual:@"foo"], @"Shortcut not equal to an object of a different class.");
+}
+
+- (void) testShortcutRecorderCompatibility
+{
+ MASShortcut *key = [MASShortcut shortcutWithKeyCode:87 modifierFlags:1048576];
+ XCTAssertEqualObjects([key description], @"⌘5", @"Basic compatibility with the keycode & modifier combination used by Shortcut Recorder.");
+}
+
+@end
diff --git a/Framework/MASShortcutValidator.h b/Framework/MASShortcutValidator.h
new file mode 100644
index 0000000..cc5f816
--- /dev/null
+++ b/Framework/MASShortcutValidator.h
@@ -0,0 +1,15 @@
+#import "MASShortcut.h"
+
+@interface MASShortcutValidator : NSObject
+
+// The following API enable hotkeys with the Option key as the only modifier
+// For example, Option-G will not generate © and Option-R will not paste ®
+@property(assign) BOOL allowAnyShortcutWithOptionModifier;
+
++ (instancetype) sharedValidator;
+
+- (BOOL) isShortcutValid: (MASShortcut*) shortcut;
+- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation;
+- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation;
+
+@end
diff --git a/Framework/MASShortcutValidator.m b/Framework/MASShortcutValidator.m
new file mode 100644
index 0000000..47dd700
--- /dev/null
+++ b/Framework/MASShortcutValidator.m
@@ -0,0 +1,111 @@
+#import "MASShortcutValidator.h"
+
+@implementation MASShortcutValidator
+
++ (instancetype) sharedValidator
+{
+ static dispatch_once_t once;
+ static MASShortcutValidator *sharedInstance;
+ dispatch_once(&once, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (BOOL) isShortcutValid: (MASShortcut*) shortcut
+{
+ NSUInteger keyCode = [shortcut keyCode];
+ NSUInteger modifiers = [shortcut modifierFlags];
+
+ // Allow any function key with any combination of modifiers
+ BOOL includesFunctionKey = ((keyCode == kVK_F1) || (keyCode == kVK_F2) || (keyCode == kVK_F3) || (keyCode == kVK_F4) ||
+ (keyCode == kVK_F5) || (keyCode == kVK_F6) || (keyCode == kVK_F7) || (keyCode == kVK_F8) ||
+ (keyCode == kVK_F9) || (keyCode == kVK_F10) || (keyCode == kVK_F11) || (keyCode == kVK_F12) ||
+ (keyCode == kVK_F13) || (keyCode == kVK_F14) || (keyCode == kVK_F15) || (keyCode == kVK_F16) ||
+ (keyCode == kVK_F17) || (keyCode == kVK_F18) || (keyCode == kVK_F19) || (keyCode == kVK_F20));
+ if (includesFunctionKey) return YES;
+
+ // Do not allow any other key without modifiers
+ BOOL hasModifierFlags = (modifiers > 0);
+ if (!hasModifierFlags) return NO;
+
+ // Allow any hotkey containing Control or Command modifier
+ BOOL includesCommand = ((modifiers & NSCommandKeyMask) > 0);
+ BOOL includesControl = ((modifiers & NSControlKeyMask) > 0);
+ if (includesCommand || includesControl) return YES;
+
+ // Allow Option key only in selected cases
+ BOOL includesOption = ((modifiers & NSAlternateKeyMask) > 0);
+ if (includesOption) {
+
+ // Always allow Option-Space and Option-Escape because they do not have any bind system commands
+ if ((keyCode == kVK_Space) || (keyCode == kVK_Escape)) return YES;
+
+ // Allow Option modifier with any key even if it will break the system binding
+ if (_allowAnyShortcutWithOptionModifier) return YES;
+ }
+
+ // The hotkey does not have any modifiers or violates system bindings
+ return NO;
+}
+
+- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation
+{
+ NSString *keyEquivalent = [shortcut keyCodeStringForKeyEquivalent];
+ NSUInteger flags = [shortcut modifierFlags];
+
+ for (NSMenuItem *menuItem in menu.itemArray) {
+ if (menuItem.hasSubmenu && [self isShortcut:shortcut alreadyTakenInMenu:[menuItem submenu] explanation:explanation]) return YES;
+
+ BOOL equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask) == flags);
+ BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent];
+
+ // Check if the cases are different, we know ours is lower and that shift is included in our modifiers
+ // If theirs is capitol, we need to add shift to their modifiers
+ if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) {
+ equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags);
+ }
+
+ if (equalFlags && equalHotkeyLowercase) {
+ if (explanation) {
+ *explanation = NSLocalizedString(@"This shortcut cannot be used because it is already used by the menu item ‘%@’.",
+ @"Message for alert when shortcut is already used");
+ *explanation = [NSString stringWithFormat:*explanation, menuItem.title];
+ }
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation
+{
+ CFArrayRef globalHotKeys;
+ if (CopySymbolicHotKeys(&globalHotKeys) == noErr) {
+
+ // Enumerate all global hotkeys and check if any of them matches current shortcut
+ for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) {
+ CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i);
+ CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode);
+ CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers);
+ CFNumberRef enabled = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyEnabled);
+
+ if (([(__bridge NSNumber *)code unsignedIntegerValue] == [shortcut keyCode]) &&
+ ([(__bridge NSNumber *)flags unsignedIntegerValue] == [shortcut carbonFlags]) &&
+ ([(__bridge NSNumber *)enabled boolValue])) {
+
+ if (explanation) {
+ *explanation = NSLocalizedString(@"This combination cannot be used because it is already used by a system-wide "
+ @"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts "
+ @"can be changed in the Keyboard & Mouse panel in System Preferences.",
+ @"Message for alert when shortcut is already used by the system");
+ }
+ return YES;
+ }
+ }
+ CFRelease(globalHotKeys);
+ }
+ return [self isShortcut:shortcut alreadyTakenInMenu:[NSApp mainMenu] explanation:explanation];
+}
+
+@end
diff --git a/Framework/MASShortcutView+Bindings.h b/Framework/MASShortcutView+Bindings.h
new file mode 100644
index 0000000..b0148e7
--- /dev/null
+++ b/Framework/MASShortcutView+Bindings.h
@@ -0,0 +1,25 @@
+#import "MASShortcutView.h"
+
+/**
+ @brief A simplified interface to bind the recorder value to user defaults.
+
+ You can bind the @p shortcutValue to user defaults using the standard
+ @p bind:toObject:withKeyPath:options: call, but since that’s a lot to type
+ and read, here’s a simpler option.
+
+ Setting the @p associatedUserDefaultsKey binds the view’s shortcut value
+ to the given user defaults key. You can supply a value transformer to convert
+ values between user defaults and @p MASShortcut. If you don’t supply
+ a transformer, the @p NSUnarchiveFromDataTransformerName will be used
+ automatically.
+
+ Set @p associatedUserDefaultsKey to @p nil to disconnect the binding.
+*/
+@interface MASShortcutView (Bindings)
+
+@property(copy) NSString *associatedUserDefaultsKey;
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformer: (NSValueTransformer*) transformer;
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformerName: (NSString*) transformerName;
+
+@end
diff --git a/Framework/MASShortcutView+Bindings.m b/Framework/MASShortcutView+Bindings.m
new file mode 100644
index 0000000..54c5111
--- /dev/null
+++ b/Framework/MASShortcutView+Bindings.m
@@ -0,0 +1,50 @@
+#import "MASShortcutView+Bindings.h"
+
+@implementation MASShortcutView (Bindings)
+
+- (NSString*) associatedUserDefaultsKey
+{
+ NSDictionary* bindingInfo = [self infoForBinding:MASShortcutBinding];
+ if (bindingInfo != nil) {
+ NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
+ NSString *key = [keyPath stringByReplacingOccurrencesOfString:@"values." withString:@""];
+ return key;
+ } else {
+ return nil;
+ }
+}
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformer: (NSValueTransformer*) transformer
+{
+ // Break previous binding if any
+ NSString *currentKey = [self associatedUserDefaultsKey];
+ if (currentKey != nil) {
+ [self unbind:currentKey];
+ }
+
+ // Stop if the new binding is nil
+ if (newKey == nil) {
+ return;
+ }
+
+ NSDictionary *options = transformer ?
+ @{NSValueTransformerBindingOption:transformer} :
+ nil;
+
+ [self bind:MASShortcutBinding
+ toObject:[NSUserDefaultsController sharedUserDefaultsController]
+ withKeyPath:[@"values." stringByAppendingString:newKey]
+ options:options];
+}
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformerName: (NSString*) transformerName
+{
+ [self setAssociatedUserDefaultsKey:newKey withTransformer:[NSValueTransformer valueTransformerForName:transformerName]];
+}
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey
+{
+ [self setAssociatedUserDefaultsKey:newKey withTransformerName:NSKeyedUnarchiveFromDataTransformerName];
+}
+
+@end
diff --git a/Framework/MASShortcutView.h b/Framework/MASShortcutView.h
new file mode 100644
index 0000000..166be44
--- /dev/null
+++ b/Framework/MASShortcutView.h
@@ -0,0 +1,24 @@
+@class MASShortcut, MASShortcutValidator;
+
+extern NSString *const MASShortcutBinding;
+
+typedef enum {
+ MASShortcutViewStyleDefault = 0, // Height = 19 px
+ MASShortcutViewStyleTexturedRect, // Height = 25 px
+ MASShortcutViewStyleRounded, // Height = 43 px
+ MASShortcutViewStyleFlat
+} MASShortcutViewStyle;
+
+@interface MASShortcutView : NSView
+
+@property (nonatomic, strong) MASShortcut *shortcutValue;
+@property (nonatomic, strong) MASShortcutValidator *shortcutValidator;
+@property (nonatomic, getter = isRecording) BOOL recording;
+@property (nonatomic, getter = isEnabled) BOOL enabled;
+@property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender);
+@property (nonatomic, assign) MASShortcutViewStyle style;
+
+/// Returns custom class for drawing control.
++ (Class)shortcutCellClass;
+
+@end
diff --git a/Framework/MASShortcutView.m b/Framework/MASShortcutView.m
new file mode 100644
index 0000000..aace67e
--- /dev/null
+++ b/Framework/MASShortcutView.m
@@ -0,0 +1,511 @@
+#import "MASShortcutView.h"
+#import "MASShortcutValidator.h"
+
+NSString *const MASShortcutBinding = @"shortcutValue";
+
+#define HINT_BUTTON_WIDTH 23.0
+#define BUTTON_FONT_SIZE 11.0
+#define SEGMENT_CHROME_WIDTH 6.0
+
+#pragma mark -
+
+@interface MASShortcutView () // Private accessors
+
+@property (nonatomic, getter = isHinting) BOOL hinting;
+@property (nonatomic, copy) NSString *shortcutPlaceholder;
+
+@end
+
+#pragma mark -
+
+@implementation MASShortcutView {
+ NSButtonCell *_shortcutCell;
+ NSInteger _shortcutToolTipTag;
+ NSInteger _hintToolTipTag;
+ NSTrackingArea *_hintArea;
+}
+
+#pragma mark -
+
++ (Class)shortcutCellClass
+{
+ return [NSButtonCell class];
+}
+
+- (id)initWithFrame:(CGRect)frameRect
+{
+ self = [super initWithFrame:frameRect];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (id)initWithCoder:(NSCoder *)coder
+{
+ self = [super initWithCoder:coder];
+ if (self) {
+ [self commonInit];
+ }
+ return self;
+}
+
+- (void)commonInit
+{
+ _shortcutCell = [[[self.class shortcutCellClass] alloc] init];
+ _shortcutCell.buttonType = NSPushOnPushOffButton;
+ _shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
+ _shortcutValidator = [MASShortcutValidator sharedValidator];
+ _enabled = YES;
+ [self resetShortcutCellStyle];
+}
+
+- (void)dealloc
+{
+ [self activateEventMonitoring:NO];
+ [self activateResignObserver:NO];
+}
+
+#pragma mark - Public accessors
+
+- (void)setEnabled:(BOOL)flag
+{
+ if (_enabled != flag) {
+ _enabled = flag;
+ [self updateTrackingAreas];
+ self.recording = NO;
+ [self setNeedsDisplay:YES];
+ }
+}
+
+- (void)setStyle:(MASShortcutViewStyle)newStyle
+{
+ if (_style != newStyle) {
+ _style = newStyle;
+ [self resetShortcutCellStyle];
+ [self setNeedsDisplay:YES];
+ }
+}
+
+- (void)resetShortcutCellStyle
+{
+ switch (_style) {
+ case MASShortcutViewStyleDefault: {
+ _shortcutCell.bezelStyle = NSRoundRectBezelStyle;
+ break;
+ }
+ case MASShortcutViewStyleTexturedRect: {
+ _shortcutCell.bezelStyle = NSTexturedRoundedBezelStyle;
+ break;
+ }
+ case MASShortcutViewStyleRounded: {
+ _shortcutCell.bezelStyle = NSRoundedBezelStyle;
+ break;
+ }
+ case MASShortcutViewStyleFlat: {
+ self.wantsLayer = YES;
+ _shortcutCell.backgroundColor = [NSColor clearColor];
+ _shortcutCell.bordered = NO;
+ break;
+ }
+ }
+}
+
+- (void)setRecording:(BOOL)flag
+{
+ // Only one recorder can be active at the moment
+ static MASShortcutView *currentRecorder = nil;
+ if (flag && (currentRecorder != self)) {
+ currentRecorder.recording = NO;
+ currentRecorder = flag ? self : nil;
+ }
+
+ // Only enabled view supports recording
+ if (flag && !self.enabled) return;
+
+ if (_recording != flag) {
+ _recording = flag;
+ self.shortcutPlaceholder = nil;
+ [self resetToolTips];
+ [self activateEventMonitoring:_recording];
+ [self activateResignObserver:_recording];
+ [self setNeedsDisplay:YES];
+ }
+}
+
+- (void)setShortcutValue:(MASShortcut *)shortcutValue
+{
+ _shortcutValue = shortcutValue;
+ [self resetToolTips];
+ [self setNeedsDisplay:YES];
+ [self propagateValue:shortcutValue forBinding:@"shortcutValue"];
+
+ if (self.shortcutValueChange) {
+ self.shortcutValueChange(self);
+ }
+}
+
+- (void)setShortcutPlaceholder:(NSString *)shortcutPlaceholder
+{
+ _shortcutPlaceholder = shortcutPlaceholder.copy;
+ [self setNeedsDisplay:YES];
+}
+
+#pragma mark - Drawing
+
+- (BOOL)isFlipped
+{
+ return YES;
+}
+
+- (void)drawInRect:(CGRect)frame withTitle:(NSString *)title alignment:(NSTextAlignment)alignment state:(NSInteger)state
+{
+ _shortcutCell.title = title;
+ _shortcutCell.alignment = alignment;
+ _shortcutCell.state = state;
+ _shortcutCell.enabled = self.enabled;
+
+ switch (_style) {
+ case MASShortcutViewStyleDefault: {
+ [_shortcutCell drawWithFrame:frame inView:self];
+ break;
+ }
+ case MASShortcutViewStyleTexturedRect: {
+ [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
+ break;
+ }
+ case MASShortcutViewStyleRounded: {
+ [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
+ break;
+ }
+ case MASShortcutViewStyleFlat: {
+ [_shortcutCell drawWithFrame:frame inView:self];
+ break;
+ }
+ }
+}
+
+- (void)drawRect:(CGRect)dirtyRect
+{
+ if (self.shortcutValue) {
+ [self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(self.recording ? kMASShortcutGlyphEscape : kMASShortcutGlyphDeleteLeft)
+ alignment:NSRightTextAlignment state:NSOffState];
+
+ CGRect shortcutRect;
+ [self getShortcutRect:&shortcutRect hintRect:NULL];
+ NSString *title = (self.recording
+ ? (_hinting
+ ? NSLocalizedString(@"Use Old Shortcut", @"Cancel action button for non-empty shortcut in recording state")
+ : (self.shortcutPlaceholder.length > 0
+ ? self.shortcutPlaceholder
+ : NSLocalizedString(@"Type New Shortcut", @"Non-empty shortcut button in recording state")))
+ : _shortcutValue ? _shortcutValue.description : @"");
+ [self drawInRect:shortcutRect withTitle:title alignment:NSCenterTextAlignment state:self.isRecording ? NSOnState : NSOffState];
+ }
+ else {
+ if (self.recording)
+ {
+ [self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(kMASShortcutGlyphEscape) alignment:NSRightTextAlignment state:NSOffState];
+
+ CGRect shortcutRect;
+ [self getShortcutRect:&shortcutRect hintRect:NULL];
+ NSString *title = (_hinting
+ ? NSLocalizedString(@"Cancel", @"Cancel action button in recording state")
+ : (self.shortcutPlaceholder.length > 0
+ ? self.shortcutPlaceholder
+ : NSLocalizedString(@"Type Shortcut", @"Empty shortcut button in recording state")));
+ [self drawInRect:shortcutRect withTitle:title alignment:NSCenterTextAlignment state:NSOnState];
+ }
+ else
+ {
+ [self drawInRect:self.bounds withTitle:NSLocalizedString(@"Record Shortcut", @"Empty shortcut button in normal state")
+ alignment:NSCenterTextAlignment state:NSOffState];
+ }
+ }
+}
+
+#pragma mark - Mouse handling
+
+- (void)getShortcutRect:(CGRect *)shortcutRectRef hintRect:(CGRect *)hintRectRef
+{
+ CGRect shortcutRect, hintRect;
+ CGFloat hintButtonWidth = HINT_BUTTON_WIDTH;
+ switch (self.style) {
+ case MASShortcutViewStyleTexturedRect: hintButtonWidth += 2.0; break;
+ case MASShortcutViewStyleRounded: hintButtonWidth += 3.0; break;
+ case MASShortcutViewStyleFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - BUTTON_FONT_SIZE); break;
+ default: break;
+ }
+ CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, CGRectMaxXEdge);
+ if (shortcutRectRef) *shortcutRectRef = shortcutRect;
+ if (hintRectRef) *hintRectRef = hintRect;
+}
+
+- (BOOL)locationInShortcutRect:(CGPoint)location
+{
+ CGRect shortcutRect;
+ [self getShortcutRect:&shortcutRect hintRect:NULL];
+ return CGRectContainsPoint(shortcutRect, [self convertPoint:location fromView:nil]);
+}
+
+- (BOOL)locationInHintRect:(CGPoint)location
+{
+ CGRect hintRect;
+ [self getShortcutRect:NULL hintRect:&hintRect];
+ return CGRectContainsPoint(hintRect, [self convertPoint:location fromView:nil]);
+}
+
+- (void)mouseDown:(NSEvent *)event
+{
+ if (self.enabled) {
+ if (self.shortcutValue) {
+ if (self.recording) {
+ if ([self locationInHintRect:event.locationInWindow]) {
+ self.recording = NO;
+ }
+ }
+ else {
+ if ([self locationInShortcutRect:event.locationInWindow]) {
+ self.recording = YES;
+ }
+ else {
+ self.shortcutValue = nil;
+ }
+ }
+ }
+ else {
+ if (self.recording) {
+ if ([self locationInHintRect:event.locationInWindow]) {
+ self.recording = NO;
+ }
+ }
+ else {
+ self.recording = YES;
+ }
+ }
+ }
+ else {
+ [super mouseDown:event];
+ }
+}
+
+#pragma mark - Handling mouse over
+
+- (void)updateTrackingAreas
+{
+ [super updateTrackingAreas];
+
+ if (_hintArea) {
+ [self removeTrackingArea:_hintArea];
+ _hintArea = nil;
+ }
+
+ // Forbid hinting if view is disabled
+ if (!self.enabled) return;
+
+ CGRect hintRect;
+ [self getShortcutRect:NULL hintRect:&hintRect];
+ NSTrackingAreaOptions options = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingAssumeInside);
+ _hintArea = [[NSTrackingArea alloc] initWithRect:hintRect options:options owner:self userInfo:nil];
+ [self addTrackingArea:_hintArea];
+}
+
+- (void)setHinting:(BOOL)flag
+{
+ if (_hinting != flag) {
+ _hinting = flag;
+ [self setNeedsDisplay:YES];
+ }
+}
+
+- (void)mouseEntered:(NSEvent *)event
+{
+ self.hinting = YES;
+}
+
+- (void)mouseExited:(NSEvent *)event
+{
+ self.hinting = NO;
+}
+
+void *kUserDataShortcut = &kUserDataShortcut;
+void *kUserDataHint = &kUserDataHint;
+
+- (void)resetToolTips
+{
+ if (_shortcutToolTipTag) {
+ [self removeToolTip:_shortcutToolTipTag], _shortcutToolTipTag = 0;
+ }
+ if (_hintToolTipTag) {
+ [self removeToolTip:_hintToolTipTag], _hintToolTipTag = 0;
+ }
+
+ if ((self.shortcutValue == nil) || self.recording || !self.enabled) return;
+
+ CGRect shortcutRect, hintRect;
+ [self getShortcutRect:&shortcutRect hintRect:&hintRect];
+ _shortcutToolTipTag = [self addToolTipRect:shortcutRect owner:self userData:kUserDataShortcut];
+ _hintToolTipTag = [self addToolTipRect:hintRect owner:self userData:kUserDataHint];
+}
+
+- (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(CGPoint)point userData:(void *)data
+{
+ if (data == kUserDataShortcut) {
+ return NSLocalizedString(@"Click to record new shortcut", @"Tooltip for non-empty shortcut button");
+ }
+ else if (data == kUserDataHint) {
+ return NSLocalizedString(@"Delete shortcut", @"Tooltip for hint button near the non-empty shortcut");
+ }
+ return nil;
+}
+
+#pragma mark - Event monitoring
+
+- (void)activateEventMonitoring:(BOOL)shouldActivate
+{
+ static BOOL isActive = NO;
+ if (isActive == shouldActivate) return;
+ isActive = shouldActivate;
+
+ static id eventMonitor = nil;
+ if (shouldActivate) {
+ __weak MASShortcutView *weakSelf = self;
+ NSEventMask eventMask = (NSKeyDownMask | NSFlagsChangedMask);
+ eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
+
+ // Create a shortcut from the event
+ MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event];
+
+ // If the shortcut is a plain Delete or Backspace, clear the current shortcut and cancel recording
+ if (!shortcut.modifierFlags && ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete))) {
+ weakSelf.shortcutValue = nil;
+ weakSelf.recording = NO;
+ event = nil;
+ }
+
+ // If the shortcut is a plain Esc, cancel recording
+ else if (!shortcut.modifierFlags && shortcut.keyCode == kVK_Escape) {
+ weakSelf.recording = NO;
+ event = nil;
+ }
+
+ // If the shortcut is Cmd-W or Cmd-Q, cancel recording and pass the event through
+ else if ((shortcut.modifierFlags == NSCommandKeyMask) && (shortcut.keyCode == kVK_ANSI_W || shortcut.keyCode == kVK_ANSI_Q)) {
+ weakSelf.recording = NO;
+ }
+
+ else {
+ // Verify possible shortcut
+ if (shortcut.keyCodeString.length > 0) {
+ if ([_shortcutValidator isShortcutValid:shortcut]) {
+ // Verify that shortcut is not used
+ NSString *explanation = nil;
+ if ([_shortcutValidator isShortcutAlreadyTakenBySystem:shortcut explanation:&explanation]) {
+ // Prevent cancel of recording when Alert window is key
+ [weakSelf activateResignObserver:NO];
+ [weakSelf activateEventMonitoring:NO];
+ NSString *format = NSLocalizedString(@"The key combination %@ cannot be used",
+ @"Title for alert when shortcut is already used");
+ NSAlert* alert = [[NSAlert alloc]init];
+ alert.alertStyle = NSCriticalAlertStyle;
+ alert.informativeText = explanation;
+ alert.messageText = [NSString stringWithFormat:format, shortcut];
+ [alert addButtonWithTitle:NSLocalizedString(@"OK", @"Alert button when shortcut is already used")];
+
+ [alert runModal];
+ weakSelf.shortcutPlaceholder = nil;
+ [weakSelf activateResignObserver:YES];
+ [weakSelf activateEventMonitoring:YES];
+ }
+ else {
+ weakSelf.shortcutValue = shortcut;
+ weakSelf.recording = NO;
+ }
+ }
+ else {
+ // Key press with or without SHIFT is not valid input
+ NSBeep();
+ }
+ }
+ else {
+ // User is playing with modifier keys
+ weakSelf.shortcutPlaceholder = shortcut.modifierFlagsString;
+ }
+ event = nil;
+ }
+ return event;
+ }];
+ }
+ else {
+ [NSEvent removeMonitor:eventMonitor];
+ }
+}
+
+- (void)activateResignObserver:(BOOL)shouldActivate
+{
+ static BOOL isActive = NO;
+ if (isActive == shouldActivate) return;
+ isActive = shouldActivate;
+
+ static id observer = nil;
+ NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
+ if (shouldActivate) {
+ __weak MASShortcutView *weakSelf = self;
+ observer = [notificationCenter addObserverForName:NSWindowDidResignKeyNotification object:self.window
+ queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
+ weakSelf.recording = NO;
+ }];
+ }
+ else {
+ [notificationCenter removeObserver:observer];
+ }
+}
+
+#pragma mark Bindings
+
+// http://tomdalling.com/blog/cocoa/implementing-your-own-cocoa-bindings/
+-(void) propagateValue:(id)value forBinding:(NSString*)binding;
+{
+ NSParameterAssert(binding != nil);
+
+ //WARNING: bindingInfo contains NSNull, so it must be accounted for
+ NSDictionary* bindingInfo = [self infoForBinding:binding];
+ if(!bindingInfo)
+ return; //there is no binding
+
+ //apply the value transformer, if one has been set
+ NSDictionary* bindingOptions = [bindingInfo objectForKey:NSOptionsKey];
+ if(bindingOptions){
+ NSValueTransformer* transformer = [bindingOptions valueForKey:NSValueTransformerBindingOption];
+ if(!transformer || (id)transformer == [NSNull null]){
+ NSString* transformerName = [bindingOptions valueForKey:NSValueTransformerNameBindingOption];
+ if(transformerName && (id)transformerName != [NSNull null]){
+ transformer = [NSValueTransformer valueTransformerForName:transformerName];
+ }
+ }
+
+ if(transformer && (id)transformer != [NSNull null]){
+ if([[transformer class] allowsReverseTransformation]){
+ value = [transformer reverseTransformedValue:value];
+ } else {
+ NSLog(@"WARNING: binding \"%@\" has value transformer, but it doesn't allow reverse transformations in %s", binding, __PRETTY_FUNCTION__);
+ }
+ }
+ }
+
+ id boundObject = [bindingInfo objectForKey:NSObservedObjectKey];
+ if(!boundObject || boundObject == [NSNull null]){
+ NSLog(@"ERROR: NSObservedObjectKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__);
+ return;
+ }
+
+ NSString* boundKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
+ if(!boundKeyPath || (id)boundKeyPath == [NSNull null]){
+ NSLog(@"ERROR: NSObservedKeyPathKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__);
+ return;
+ }
+
+ [boundObject setValue:value forKeyPath:boundKeyPath];
+}
+
+@end
diff --git a/Framework/Prefix.pch b/Framework/Prefix.pch
new file mode 100644
index 0000000..3e71c31
--- /dev/null
+++ b/Framework/Prefix.pch
@@ -0,0 +1,2 @@
+#import <AppKit/AppKit.h>
+#import <Carbon/Carbon.h> \ No newline at end of file
diff --git a/Framework/Shortcut.h b/Framework/Shortcut.h
new file mode 100644
index 0000000..e131395
--- /dev/null
+++ b/Framework/Shortcut.h
@@ -0,0 +1,7 @@
+#import "MASShortcut.h"
+#import "MASShortcutValidator.h"
+#import "MASShortcutMonitor.h"
+#import "MASShortcutBinder.h"
+#import "MASDictionaryTransformer.h"
+#import "MASShortcutView.h"
+#import "MASShortcutView+Bindings.h" \ No newline at end of file