aboutsummaryrefslogtreecommitdiffstats
path: root/Framework
diff options
context:
space:
mode:
authorTomáš Znamenáček2014-08-05 11:13:13 +0200
committerTomáš Znamenáček2015-01-07 15:05:11 +0100
commit377b44220f2a4a8b7ffc3eda9e93cf073e8a74da (patch)
treeb83239fd741773451bd9a75480ebe5a276c7d885 /Framework
parenta3a459b4e4e47bf18dccd5dc7f315389346e3d6c (diff)
downloadMASShortcut-377b44220f2a4a8b7ffc3eda9e93cf073e8a74da.tar.bz2
Repackaged the code as a framework and included the demo.
Packaging the code as a framework is mostly just a formality. It doesn’t really change much, it just turns the code into a regular component. What it does change is that the code now has its own Xcode settings, which could make compatibility easier in the long run. Including the demo in the main repository makes it easier to hack on the library, since you can try the changes immediately. It also shows how to bundle the framework into an app that uses it.
Diffstat (limited to 'Framework')
-rw-r--r--Framework/Info.plist28
-rw-r--r--Framework/MASShortcut+Monitoring.h8
-rw-r--r--Framework/MASShortcut+Monitoring.m165
-rw-r--r--Framework/MASShortcut+UserDefaults.h9
-rw-r--r--Framework/MASShortcut+UserDefaults.m98
-rw-r--r--Framework/MASShortcut.h60
-rw-r--r--Framework/MASShortcut.m354
-rw-r--r--Framework/MASShortcutView+UserDefaults.h7
-rw-r--r--Framework/MASShortcutView+UserDefaults.m125
-rw-r--r--Framework/MASShortcutView.h23
-rw-r--r--Framework/MASShortcutView.m463
-rw-r--r--Framework/Prefix.pch3
12 files changed, 1343 insertions, 0 deletions
diff --git a/Framework/Info.plist b/Framework/Info.plist
new file mode 100644
index 0000000..36f6087
--- /dev/null
+++ b/Framework/Info.plist
@@ -0,0 +1,28 @@
+<?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>CFBundleIconFile</key>
+ <string></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>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2014 Vadim Shpakovski. All rights reserved.</string>
+ <key>NSPrincipalClass</key>
+ <string></string>
+</dict>
+</plist>
diff --git a/Framework/MASShortcut+Monitoring.h b/Framework/MASShortcut+Monitoring.h
new file mode 100644
index 0000000..aa8f224
--- /dev/null
+++ b/Framework/MASShortcut+Monitoring.h
@@ -0,0 +1,8 @@
+#import "MASShortcut.h"
+
+@interface MASShortcut (Monitoring)
+
++ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler;
++ (void)removeGlobalHotkeyMonitor:(id)monitor;
+
+@end
diff --git a/Framework/MASShortcut+Monitoring.m b/Framework/MASShortcut+Monitoring.m
new file mode 100644
index 0000000..b6750b4
--- /dev/null
+++ b/Framework/MASShortcut+Monitoring.m
@@ -0,0 +1,165 @@
+#import "MASShortcut+Monitoring.h"
+
+NSMutableDictionary *MASRegisteredHotKeys(void);
+BOOL InstallCommonEventHandler(void);
+BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey);
+void UninstallEventHandler(void);
+
+#pragma mark -
+
+@interface MASShortcutHotKey : NSObject
+
+@property (nonatomic, readonly) MASShortcut *shortcut;
+@property (nonatomic, readonly, copy) void (^handler)();
+@property (nonatomic, readonly) EventHotKeyRef carbonHotKey;
+@property (nonatomic, readonly) UInt32 carbonHotKeyID;
+
+- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler;
+
+@end
+
+#pragma mark -
+
+@implementation MASShortcut (Monitoring)
+
++ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler
+{
+ NSString *monitor = [NSString stringWithFormat:@"%@", shortcut.description];
+ if ([MASRegisteredHotKeys() objectForKey:monitor]) return nil;
+
+ MASShortcutHotKey *hotKey = [[MASShortcutHotKey alloc] initWithShortcut:shortcut handler:handler];
+ if (hotKey == nil) return nil;
+
+ [MASRegisteredHotKeys() setObject:hotKey forKey:monitor];
+ return monitor;
+}
+
++ (void)removeGlobalHotkeyMonitor:(id)monitor
+{
+ if (monitor == nil) return;
+ NSMutableDictionary *registeredHotKeys = MASRegisteredHotKeys();
+ [registeredHotKeys removeObjectForKey:monitor];
+ if (registeredHotKeys.count == 0) {
+ UninstallEventHandler();
+ }
+}
+
+@end
+
+#pragma mark -
+
+@implementation MASShortcutHotKey
+
+@synthesize carbonHotKeyID = _carbonHotKeyID;
+@synthesize handler = _handler;
+@synthesize shortcut = _shortcut;
+@synthesize carbonHotKey = _carbonHotKey;
+
+#pragma mark -
+
+- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler
+{
+ self = [super init];
+ if (self) {
+ _shortcut = shortcut;
+ _handler = [handler copy];
+
+ if (!InstallHotkeyWithShortcut(shortcut, &_carbonHotKeyID, &_carbonHotKey))
+ self = nil;
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [self uninstallExisitingHotKey];
+}
+
+- (void)uninstallExisitingHotKey
+{
+ if (_carbonHotKey) {
+ UnregisterEventHotKey(_carbonHotKey);
+ _carbonHotKey = NULL;
+ }
+}
+
+@end
+
+#pragma mark - Carbon magic
+
+NSMutableDictionary *MASRegisteredHotKeys()
+{
+ static NSMutableDictionary *shared = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ shared = [NSMutableDictionary dictionary];
+ });
+ return shared;
+}
+
+#pragma mark -
+
+FourCharCode const kMASShortcutSignature = 'MASS';
+
+BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey)
+{
+ if ((shortcut == nil) || !InstallCommonEventHandler()) return NO;
+
+ static UInt32 sCarbonHotKeyID = 0;
+ EventHotKeyID hotKeyID = { .signature = kMASShortcutSignature, .id = ++ sCarbonHotKeyID };
+ EventHotKeyRef carbonHotKey = NULL;
+ if (RegisterEventHotKey(shortcut.carbonKeyCode, shortcut.carbonFlags, hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &carbonHotKey) != noErr) {
+ return NO;
+ }
+
+ if (outCarbonHotKeyID) *outCarbonHotKeyID = hotKeyID.id;
+ if (outCarbonHotKey) *outCarbonHotKey = carbonHotKey;
+ return YES;
+}
+
+static OSStatus CarbonCallback(EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData)
+{
+ if (GetEventClass(inEvent) != kEventClassKeyboard) return noErr;
+
+ EventHotKeyID hotKeyID;
+ OSStatus status = GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID);
+ if (status != noErr) return status;
+
+ if (hotKeyID.signature != kMASShortcutSignature) return noErr;
+
+ [MASRegisteredHotKeys() enumerateKeysAndObjectsUsingBlock:^(id key, MASShortcutHotKey *hotKey, BOOL *stop) {
+ if (hotKeyID.id == hotKey.carbonHotKeyID) {
+ if (hotKey.handler) {
+ hotKey.handler();
+ }
+ *stop = YES;
+ }
+ }];
+
+ return noErr;
+}
+
+#pragma mark -
+
+static EventHandlerRef sEventHandler = NULL;
+
+BOOL InstallCommonEventHandler()
+{
+ if (sEventHandler == NULL) {
+ EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed };
+ OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), CarbonCallback, 1, &hotKeyPressedSpec, NULL, &sEventHandler);
+ if (status != noErr) {
+ sEventHandler = NULL;
+ return NO;
+ }
+ }
+ return YES;
+}
+
+void UninstallEventHandler()
+{
+ if (sEventHandler) {
+ RemoveEventHandler(sEventHandler);
+ sEventHandler = NULL;
+ }
+}
diff --git a/Framework/MASShortcut+UserDefaults.h b/Framework/MASShortcut+UserDefaults.h
new file mode 100644
index 0000000..9f2ecb9
--- /dev/null
+++ b/Framework/MASShortcut+UserDefaults.h
@@ -0,0 +1,9 @@
+#import "MASShortcut.h"
+
+@interface MASShortcut (UserDefaults)
+
++ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
++ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey;
++ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey;
+
+@end
diff --git a/Framework/MASShortcut+UserDefaults.m b/Framework/MASShortcut+UserDefaults.m
new file mode 100644
index 0000000..94b035d
--- /dev/null
+++ b/Framework/MASShortcut+UserDefaults.m
@@ -0,0 +1,98 @@
+#import "MASShortcut+UserDefaults.h"
+#import "MASShortcut+Monitoring.h"
+
+@interface MASShortcutUserDefaultsHotKey : NSObject
+
+@property (nonatomic, readonly) NSString *userDefaultsKey;
+@property (nonatomic, copy) void (^handler)();
+@property (nonatomic, weak) id monitor;
+
+- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
+
+@end
+
+#pragma mark -
+
+@implementation MASShortcut (UserDefaults)
+
++ (NSMutableDictionary *)registeredUserDefaultsHotKeys
+{
+ static NSMutableDictionary *shared = nil;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ shared = [NSMutableDictionary dictionary];
+ });
+ return shared;
+}
+
++ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler
+{
+ MASShortcutUserDefaultsHotKey *hotKey = [[MASShortcutUserDefaultsHotKey alloc] initWithUserDefaultsKey:userDefaultsKey handler:handler];
+ [[self registeredUserDefaultsHotKeys] setObject:hotKey forKey:userDefaultsKey];
+}
+
++ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey
+{
+ NSMutableDictionary *registeredHotKeys = [self registeredUserDefaultsHotKeys];
+ [registeredHotKeys removeObjectForKey:userDefaultsKey];
+}
+
++ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey
+{
+ NSData *shortcutData = shortcut.data;
+ if (shortcutData)
+ [[NSUserDefaults standardUserDefaults] setObject:shortcutData forKey:userDefaultsKey];
+ else
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:userDefaultsKey];
+}
+
+@end
+
+#pragma mark -
+
+@implementation MASShortcutUserDefaultsHotKey {
+ NSString *_observableKeyPath;
+}
+
+void *MASShortcutUserDefaultsContext = &MASShortcutUserDefaultsContext;
+
+- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler
+{
+ self = [super init];
+ if (self) {
+ _userDefaultsKey = userDefaultsKey.copy;
+ _handler = [handler copy];
+ _observableKeyPath = [@"values." stringByAppendingString:_userDefaultsKey];
+ [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:_observableKeyPath options:NSKeyValueObservingOptionInitial context:MASShortcutUserDefaultsContext];
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:_observableKeyPath context:MASShortcutUserDefaultsContext];
+ [MASShortcut removeGlobalHotkeyMonitor:self.monitor];
+}
+
+#pragma mark -
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
+{
+ if (context == MASShortcutUserDefaultsContext) {
+ [MASShortcut removeGlobalHotkeyMonitor:self.monitor];
+ [self installHotKeyFromUserDefaults];
+ }
+ else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
+}
+
+- (void)installHotKeyFromUserDefaults
+{
+ NSData *data = [[NSUserDefaults standardUserDefaults] dataForKey:_userDefaultsKey];
+ MASShortcut *shortcut = [MASShortcut shortcutWithData:data];
+ if (shortcut == nil) return;
+ self.monitor = [MASShortcut addGlobalHotkeyMonitorWithShortcut:shortcut handler:self.handler];
+}
+
+@end
diff --git a/Framework/MASShortcut.h b/Framework/MASShortcut.h
new file mode 100644
index 0000000..c9081a2
--- /dev/null
+++ b/Framework/MASShortcut.h
@@ -0,0 +1,60 @@
+#import <Carbon/Carbon.h>
+#import <AppKit/AppKit.h>
+
+#define MASShortcutChar(char) [NSString stringWithFormat:@"%C", (unsigned short)(char)]
+#define MASShortcutClear(flags) (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask))
+#define MASShortcutCarbonFlags(cocoaFlags) (\
+ (cocoaFlags & NSCommandKeyMask ? cmdKey : 0) | \
+ (cocoaFlags & NSAlternateKeyMask ? optionKey : 0) | \
+ (cocoaFlags & NSControlKeyMask ? controlKey : 0) | \
+ (cocoaFlags & NSShiftKeyMask ? shiftKey : 0))
+
+// 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;
+
+@interface MASShortcut : NSObject <NSSecureCoding>
+
+@property (nonatomic) NSUInteger keyCode;
+@property (nonatomic) NSUInteger modifierFlags;
+@property (nonatomic, readonly) UInt32 carbonKeyCode;
+@property (nonatomic, readonly) UInt32 carbonFlags;
+@property (nonatomic, readonly) NSString *keyCodeString;
+@property (nonatomic, readonly) NSString *keyCodeStringForKeyEquivalent;
+@property (nonatomic, readonly) NSString *modifierFlagsString;
+@property (nonatomic, readonly) NSData *data;
+@property (nonatomic, readonly) BOOL shouldBypass;
+@property (nonatomic, readonly, getter = isValid) BOOL valid;
+
+- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
+
++ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
++ (MASShortcut *)shortcutWithEvent:(NSEvent *)anEvent;
++ (MASShortcut *)shortcutWithData:(NSData *)aData;
+
+- (BOOL)isTakenError:(NSError **)error;
+
+// 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 ®
++ (void)setAllowsAnyHotkeyWithOptionModifier:(BOOL)allow;
++ (BOOL)allowsAnyHotkeyWithOptionModifier;
+
+@end
diff --git a/Framework/MASShortcut.m b/Framework/MASShortcut.m
new file mode 100644
index 0000000..ccc2035
--- /dev/null
+++ b/Framework/MASShortcut.m
@@ -0,0 +1,354 @@
+#import "MASShortcut.h"
+
+NSString *const MASShortcutKeyCode = @"KeyCode";
+NSString *const MASShortcutModifierFlags = @"ModifierFlags";
+
+@implementation MASShortcut {
+ NSUInteger _keyCode; // NSNotFound if empty
+ NSUInteger _modifierFlags; // 0 if empty
+}
+
+@synthesize modifierFlags = _modifierFlags;
+@synthesize keyCode = _keyCode;
+
+#pragma mark -
+
++ (BOOL)supportsSecureCoding
+{
+ return YES;
+}
+
+- (void)encodeWithCoder:(NSCoder *)coder
+{
+ [coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:MASShortcutKeyCode];
+ [coder encodeInteger:(NSInteger)self.modifierFlags forKey:MASShortcutModifierFlags];
+}
+
+- (id)initWithCoder:(NSCoder *)decoder
+{
+ self = [super init];
+ if (self) {
+ NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode];
+ self.keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
+ self.modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags];
+ }
+ return self;
+}
+
+- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
+{
+ self = [super init];
+ if (self) {
+ _keyCode = code;
+ _modifierFlags = MASShortcutClear(flags);
+ }
+ return self;
+}
+
++ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
+{
+ return [[self alloc] initWithKeyCode:code modifierFlags:flags];
+}
+
++ (MASShortcut *)shortcutWithEvent:(NSEvent *)event
+{
+ return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags];
+}
+
++ (MASShortcut *)shortcutWithData:(NSData *)data
+{
+ id shortcut = (data ? [NSKeyedUnarchiver unarchiveObjectWithData:data] : nil);
+ return shortcut;
+}
+
+#pragma mark - Shortcut accessors
+
+- (NSData *)data
+{
+ return [NSKeyedArchiver archivedDataWithRootObject:self];
+}
+
+- (void)setModifierFlags:(NSUInteger)value
+{
+ _modifierFlags = MASShortcutClear(value);
+}
+
+- (UInt32)carbonKeyCode
+{
+ return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode);
+}
+
+- (UInt32)carbonFlags
+{
+ return MASShortcutCarbonFlags(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 MASShortcutChar(0xF704);
+ case kVK_F2: return MASShortcutChar(0xF705);
+ case kVK_F3: return MASShortcutChar(0xF706);
+ case kVK_F4: return MASShortcutChar(0xF707);
+ case kVK_F5: return MASShortcutChar(0xF708);
+ case kVK_F6: return MASShortcutChar(0xF709);
+ case kVK_F7: return MASShortcutChar(0xF70a);
+ case kVK_F8: return MASShortcutChar(0xF70b);
+ case kVK_F9: return MASShortcutChar(0xF70c);
+ case kVK_F10: return MASShortcutChar(0xF70d);
+ case kVK_F11: return MASShortcutChar(0xF70e);
+ case kVK_F12: return MASShortcutChar(0xF70f);
+ // From this point down I am guessing F13 etc come sequentially, I don't have a keyboard to test.
+ case kVK_F13: return MASShortcutChar(0xF710);
+ case kVK_F14: return MASShortcutChar(0xF711);
+ case kVK_F15: return MASShortcutChar(0xF712);
+ case kVK_F16: return MASShortcutChar(0xF713);
+ case kVK_F17: return MASShortcutChar(0xF714);
+ case kVK_F18: return MASShortcutChar(0xF715);
+ case kVK_F19: return MASShortcutChar(0xF716);
+ case kVK_Space: return MASShortcutChar(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 MASShortcutChar(kMASShortcutGlyphEscape);
+ case kVK_Delete: return MASShortcutChar(kMASShortcutGlyphDeleteLeft);
+ case kVK_ForwardDelete: return MASShortcutChar(kMASShortcutGlyphDeleteRight);
+ case kVK_LeftArrow: return MASShortcutChar(kMASShortcutGlyphLeftArrow);
+ case kVK_RightArrow: return MASShortcutChar(kMASShortcutGlyphRightArrow);
+ case kVK_UpArrow: return MASShortcutChar(kMASShortcutGlyphUpArrow);
+ case kVK_DownArrow: return MASShortcutChar(kMASShortcutGlyphDownArrow);
+ case kVK_Help: return MASShortcutChar(kMASShortcutGlyphHelp);
+ case kVK_PageUp: return MASShortcutChar(kMASShortcutGlyphPageUp);
+ case kVK_PageDown: return MASShortcutChar(kMASShortcutGlyphPageDown);
+ case kVK_Tab: return MASShortcutChar(kMASShortcutGlyphTabRight);
+ case kVK_Return: return MASShortcutChar(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 MASShortcutChar(kMASShortcutGlyphPadClear);
+ case kVK_ANSI_KeypadDivide: return @"/";
+ case kVK_ANSI_KeypadEnter: return MASShortcutChar(kMASShortcutGlyphReturn);
+ case kVK_ANSI_KeypadMinus: return @"–";
+ case kVK_ANSI_KeypadEquals: return @"=";
+
+ // Hardcode
+ case 119: return MASShortcutChar(kMASShortcutGlyphSoutheastArrow);
+ case 115: return MASShortcutChar(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 - Validation logic
+
+- (BOOL)shouldBypass
+{
+ NSString *codeString = self.keyCodeString;
+ return (self.modifierFlags == NSCommandKeyMask) && ([codeString isEqualToString:@"W"] || [codeString isEqualToString:@"Q"]);
+}
+
+BOOL MASShortcutAllowsAnyHotkeyWithOptionModifier = NO;
+
++ (void)setAllowsAnyHotkeyWithOptionModifier:(BOOL)allow
+{
+ MASShortcutAllowsAnyHotkeyWithOptionModifier = allow;
+}
+
++ (BOOL)allowsAnyHotkeyWithOptionModifier
+{
+ return MASShortcutAllowsAnyHotkeyWithOptionModifier;
+}
+
+- (BOOL)isValid
+{
+ // 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 = (_modifierFlags > 0);
+ if (!hasModifierFlags) return NO;
+
+ // Allow any hotkey containing Control or Command modifier
+ BOOL includesCommand = ((_modifierFlags & NSCommandKeyMask) > 0);
+ BOOL includesControl = ((_modifierFlags & NSControlKeyMask) > 0);
+ if (includesCommand || includesControl) return YES;
+
+ // Allow Option key only in selected cases
+ BOOL includesOption = ((_modifierFlags & 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 ([[self class] allowsAnyHotkeyWithOptionModifier]) return YES;
+ }
+
+ // The hotkey does not have any modifiers or violates system bindings
+ return NO;
+}
+
+- (BOOL)isKeyEquivalent:(NSString *)keyEquivalent flags:(NSUInteger)flags takenInMenu:(NSMenu *)menu error:(NSError **)outError
+{
+ for (NSMenuItem *menuItem in menu.itemArray) {
+ if (menuItem.hasSubmenu && [self isKeyEquivalent:keyEquivalent flags:flags takenInMenu:menuItem.submenu error:outError]) return YES;
+
+ BOOL equalFlags = (MASShortcutClear(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 = (MASShortcutClear(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags);
+ }
+
+ if (equalFlags && equalHotkeyLowercase) {
+ if (outError) {
+ NSString *format = NSLocalizedString(@"This shortcut cannot be used because it is already used by the menu item ‘%@’.",
+ @"Message for alert when shortcut is already used");
+ NSDictionary *info = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:format, menuItem.title]
+ forKey:NSLocalizedDescriptionKey];
+ *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
+ }
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (BOOL)isTakenError:(NSError **)outError
+{
+ CFArrayRef globalHotKeys;
+ BOOL isTaken = NO;
+ 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] == self.keyCode) &&
+ ([(__bridge NSNumber *)flags unsignedIntegerValue] == self.carbonFlags) &&
+ ([(__bridge NSNumber *)enabled boolValue])) {
+
+ if (outError) {
+ NSString *description = 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");
+ NSDictionary *info = [NSDictionary dictionaryWithObject:description forKey:NSLocalizedDescriptionKey];
+ *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
+ }
+ isTaken = YES;
+ break;
+ }
+ }
+ CFRelease(globalHotKeys);
+ }
+ return (isTaken || [self isKeyEquivalent:self.keyCodeStringForKeyEquivalent flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError]);
+}
+
+@end
diff --git a/Framework/MASShortcutView+UserDefaults.h b/Framework/MASShortcutView+UserDefaults.h
new file mode 100644
index 0000000..05d3c5b
--- /dev/null
+++ b/Framework/MASShortcutView+UserDefaults.h
@@ -0,0 +1,7 @@
+#import "MASShortcutView.h"
+
+@interface MASShortcutView (UserDefaults)
+
+@property (nonatomic, copy) NSString *associatedUserDefaultsKey;
+
+@end
diff --git a/Framework/MASShortcutView+UserDefaults.m b/Framework/MASShortcutView+UserDefaults.m
new file mode 100644
index 0000000..a84f0c9
--- /dev/null
+++ b/Framework/MASShortcutView+UserDefaults.m
@@ -0,0 +1,125 @@
+#import "MASShortcutView+UserDefaults.h"
+#import "MASShortcut.h"
+#import <objc/runtime.h>
+
+@interface MASShortcutDefaultsObserver : NSObject
+
+@property (nonatomic, readonly) NSString *userDefaultsKey;
+@property (nonatomic, readonly, weak) MASShortcutView *shortcutView;
+
+- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey;
+
+@end
+
+#pragma mark -
+
+@implementation MASShortcutView (UserDefaults)
+
+void *MASAssociatedDefaultsObserver = &MASAssociatedDefaultsObserver;
+
+- (NSString *)associatedUserDefaultsKey
+{
+ MASShortcutDefaultsObserver *defaultsObserver = objc_getAssociatedObject(self, MASAssociatedDefaultsObserver);
+ return defaultsObserver.userDefaultsKey;
+}
+
+- (void)setAssociatedUserDefaultsKey:(NSString *)associatedUserDefaultsKey
+{
+ // First, stop observing previous shortcut view
+ objc_setAssociatedObject(self, MASAssociatedDefaultsObserver, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+
+ if (associatedUserDefaultsKey.length == 0) return;
+
+ // Next, start observing current shortcut view
+ MASShortcutDefaultsObserver *defaultsObserver = [[MASShortcutDefaultsObserver alloc] initWithShortcutView:self userDefaultsKey:associatedUserDefaultsKey];
+ objc_setAssociatedObject(self, MASAssociatedDefaultsObserver, defaultsObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+@end
+
+#pragma mark -
+
+@implementation MASShortcutDefaultsObserver {
+ MASShortcut *_originalShortcut;
+ BOOL _internalPreferenceChange;
+ BOOL _internalShortcutChange;
+}
+
+- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey
+{
+ self = [super init];
+ if (self) {
+ _originalShortcut = shortcutView.shortcutValue;
+ _shortcutView = shortcutView;
+ _userDefaultsKey = userDefaultsKey.copy;
+ [self startObservingShortcutView];
+ }
+ return self;
+}
+
+- (void)dealloc
+{
+ // __weak _shortcutView is not yet deallocated because it refers MASShortcutDefaultsObserver
+ [self stopObservingShortcutView];
+}
+
+#pragma mark -
+
+void *kShortcutValueObserver = &kShortcutValueObserver;
+
+- (void)startObservingShortcutView
+{
+ // Read initial shortcut value from user preferences
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSData *data = [defaults dataForKey:_userDefaultsKey];
+ _shortcutView.shortcutValue = [MASShortcut shortcutWithData:data];
+
+ // Observe user preferences to update shortcut value when it changed
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:defaults];
+
+ // Observe the keyboard shortcut that user inputs by hand
+ [_shortcutView addObserver:self forKeyPath:@"shortcutValue" options:0 context:kShortcutValueObserver];
+}
+
+- (void)userDefaultsDidChange:(NSNotification *)note
+{
+ // Ignore notifications posted from -[self observeValueForKeyPath:]
+ if (_internalPreferenceChange) return;
+
+ _internalShortcutChange = YES;
+ NSData *data = [note.object dataForKey:_userDefaultsKey];
+ _shortcutView.shortcutValue = [MASShortcut shortcutWithData:data];
+ _internalShortcutChange = NO;
+}
+
+- (void)stopObservingShortcutView
+{
+ // Stop observing keyboard hotkeys entered by user in the shortcut view
+ [_shortcutView removeObserver:self forKeyPath:@"shortcutValue" context:kShortcutValueObserver];
+
+ // Stop observing user preferences
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
+
+ // Restore original hotkey in the shortcut view
+ _shortcutView.shortcutValue = _originalShortcut;
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
+{
+ if (context == kShortcutValueObserver) {
+ if (_internalShortcutChange) return;
+ MASShortcut *shortcut = [object valueForKey:keyPath];
+ _internalPreferenceChange = YES;
+
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ [defaults setObject:(shortcut.data ?: [NSKeyedArchiver archivedDataWithRootObject:nil]) forKey:_userDefaultsKey];
+ [defaults synchronize];
+
+ _internalPreferenceChange = NO;
+ }
+ else {
+ [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
+ }
+}
+
+@end
diff --git a/Framework/MASShortcutView.h b/Framework/MASShortcutView.h
new file mode 100644
index 0000000..c8a46bb
--- /dev/null
+++ b/Framework/MASShortcutView.h
@@ -0,0 +1,23 @@
+#import <AppKit/AppKit.h>
+
+@class MASShortcut;
+
+typedef enum {
+ MASShortcutViewAppearanceDefault = 0, // Height = 19 px
+ MASShortcutViewAppearanceTexturedRect, // Height = 25 px
+ MASShortcutViewAppearanceRounded, // Height = 43 px
+ MASShortcutViewAppearanceFlat
+} MASShortcutViewAppearance;
+
+@interface MASShortcutView : NSView
+
+@property (nonatomic, strong) MASShortcut *shortcutValue;
+@property (nonatomic, getter = isRecording) BOOL recording;
+@property (nonatomic, getter = isEnabled) BOOL enabled;
+@property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender);
+@property (nonatomic) MASShortcutViewAppearance appearance;
+
+/// 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..dd75729
--- /dev/null
+++ b/Framework/MASShortcutView.m
@@ -0,0 +1,463 @@
+#import "MASShortcutView.h"
+#import "MASShortcut.h"
+
+#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;
+}
+
+@synthesize enabled = _enabled;
+@synthesize hinting = _hinting;
+@synthesize shortcutValue = _shortcutValue;
+@synthesize shortcutPlaceholder = _shortcutPlaceholder;
+@synthesize shortcutValueChange = _shortcutValueChange;
+@synthesize recording = _recording;
+@synthesize appearance = _appearance;
+
+#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];
+ _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)setAppearance:(MASShortcutViewAppearance)appearance
+{
+ if (_appearance != appearance) {
+ _appearance = appearance;
+ [self resetShortcutCellStyle];
+ [self setNeedsDisplay:YES];
+ }
+}
+
+- (void)resetShortcutCellStyle
+{
+ switch (_appearance) {
+ case MASShortcutViewAppearanceDefault: {
+ _shortcutCell.bezelStyle = NSRoundRectBezelStyle;
+ break;
+ }
+ case MASShortcutViewAppearanceTexturedRect: {
+ _shortcutCell.bezelStyle = NSTexturedRoundedBezelStyle;
+ break;
+ }
+ case MASShortcutViewAppearanceRounded: {
+ _shortcutCell.bezelStyle = NSRoundedBezelStyle;
+ break;
+ }
+ case MASShortcutViewAppearanceFlat: {
+ 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];
+
+ 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 (_appearance) {
+ case MASShortcutViewAppearanceDefault: {
+ [_shortcutCell drawWithFrame:frame inView:self];
+ break;
+ }
+ case MASShortcutViewAppearanceTexturedRect: {
+ [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
+ break;
+ }
+ case MASShortcutViewAppearanceRounded: {
+ [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
+ break;
+ }
+ case MASShortcutViewAppearanceFlat: {
+ [_shortcutCell drawWithFrame:frame inView:self];
+ break;
+ }
+ }
+}
+
+- (void)drawRect:(CGRect)dirtyRect
+{
+ if (self.shortcutValue) {
+ [self drawInRect:self.bounds withTitle:MASShortcutChar(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:MASShortcutChar(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.appearance) {
+ case MASShortcutViewAppearanceTexturedRect: hintButtonWidth += 2.0; break;
+ case MASShortcutViewAppearanceRounded: hintButtonWidth += 3.0; break;
+ case MASShortcutViewAppearanceFlat: 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) {
+
+ MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event];
+ if ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete)) {
+ // Delete shortcut
+ weakSelf.shortcutValue = nil;
+ weakSelf.recording = NO;
+ event = nil;
+ }
+ else if (shortcut.keyCode == kVK_Escape && !shortcut.modifierFlags) {
+ // Cancel recording
+ weakSelf.recording = NO;
+ event = nil;
+ }
+ else if (shortcut.shouldBypass) {
+ // Command + W, Command + Q, ESC should deactivate recorder
+ weakSelf.recording = NO;
+ }
+ else {
+ // Verify possible shortcut
+ if (shortcut.keyCodeString.length > 0) {
+ if (shortcut.valid) {
+ // Verify that shortcut is not used
+ NSError *error = nil;
+ if ([shortcut isTakenError:&error]) {
+ // 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 = [NSString stringWithFormat:format, shortcut];
+ alert.messageText = error.localizedDescription;
+ [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];
+ }
+}
+
+@end
diff --git a/Framework/Prefix.pch b/Framework/Prefix.pch
new file mode 100644
index 0000000..aabef47
--- /dev/null
+++ b/Framework/Prefix.pch
@@ -0,0 +1,3 @@
+#ifdef __OBJC__
+ #import <Cocoa/Cocoa.h>
+#endif