diff options
Diffstat (limited to 'Framework')
| -rw-r--r-- | Framework/Info.plist | 28 | ||||
| -rw-r--r-- | Framework/MASShortcut+Monitoring.h | 8 | ||||
| -rw-r--r-- | Framework/MASShortcut+Monitoring.m | 165 | ||||
| -rw-r--r-- | Framework/MASShortcut+UserDefaults.h | 9 | ||||
| -rw-r--r-- | Framework/MASShortcut+UserDefaults.m | 98 | ||||
| -rw-r--r-- | Framework/MASShortcut.h | 60 | ||||
| -rw-r--r-- | Framework/MASShortcut.m | 354 | ||||
| -rw-r--r-- | Framework/MASShortcutView+UserDefaults.h | 7 | ||||
| -rw-r--r-- | Framework/MASShortcutView+UserDefaults.m | 125 | ||||
| -rw-r--r-- | Framework/MASShortcutView.h | 23 | ||||
| -rw-r--r-- | Framework/MASShortcutView.m | 463 | ||||
| -rw-r--r-- | Framework/Prefix.pch | 3 |
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 |
