aboutsummaryrefslogtreecommitdiffstats
path: root/Framework/MASShortcut.m
diff options
context:
space:
mode:
Diffstat (limited to 'Framework/MASShortcut.m')
-rw-r--r--Framework/MASShortcut.m354
1 files changed, 354 insertions, 0 deletions
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