diff options
| -rw-r--r-- | Framework/MASShortcut.h | 8 | ||||
| -rw-r--r-- | Framework/MASShortcut.m | 116 | ||||
| -rw-r--r-- | Framework/MASShortcutValidator.h | 15 | ||||
| -rw-r--r-- | Framework/MASShortcutValidator.m | 109 | ||||
| -rw-r--r-- | Framework/MASShortcutView.h | 3 | ||||
| -rw-r--r-- | Framework/MASShortcutView.m | 20 | ||||
| -rw-r--r-- | MASShortcut.xcodeproj/project.pbxproj | 8 |
7 files changed, 147 insertions, 132 deletions
diff --git a/Framework/MASShortcut.h b/Framework/MASShortcut.h index b7ed55c..123e25e 100644 --- a/Framework/MASShortcut.h +++ b/Framework/MASShortcut.h @@ -11,7 +11,6 @@ @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; @@ -19,11 +18,4 @@ + (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 index 3e1e498..0e834eb 100644 --- a/Framework/MASShortcut.m +++ b/Framework/MASShortcut.m @@ -235,120 +235,4 @@ NSString *const MASShortcutModifierFlags = @"ModifierFlags"; 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 = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask) == flags); - BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent]; - - // Check if the cases are different, we know ours is lower and that shift is included in our modifiers - // If theirs is capitol, we need to add shift to their modifiers - if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) { - equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags); - } - - if (equalFlags && equalHotkeyLowercase) { - if (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/MASShortcutValidator.h b/Framework/MASShortcutValidator.h new file mode 100644 index 0000000..cc5f816 --- /dev/null +++ b/Framework/MASShortcutValidator.h @@ -0,0 +1,15 @@ +#import "MASShortcut.h" + +@interface MASShortcutValidator : NSObject + +// The following API enable hotkeys with the Option key as the only modifier +// For example, Option-G will not generate © and Option-R will not paste ® +@property(assign) BOOL allowAnyShortcutWithOptionModifier; + ++ (instancetype) sharedValidator; + +- (BOOL) isShortcutValid: (MASShortcut*) shortcut; +- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation; +- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation; + +@end diff --git a/Framework/MASShortcutValidator.m b/Framework/MASShortcutValidator.m new file mode 100644 index 0000000..4fcba9b --- /dev/null +++ b/Framework/MASShortcutValidator.m @@ -0,0 +1,109 @@ +#import "MASShortcutValidator.h" + +@implementation MASShortcutValidator + ++ (instancetype) sharedValidator +{ + static dispatch_once_t once; + static MASShortcutValidator *sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + }); + return sharedInstance; +} + +- (BOOL) isShortcutValid: (MASShortcut*) shortcut +{ + NSUInteger keyCode = [shortcut keyCode]; + NSUInteger modifiers = [shortcut modifierFlags]; + + // Allow any function key with any combination of modifiers + BOOL includesFunctionKey = ((keyCode == kVK_F1) || (keyCode == kVK_F2) || (keyCode == kVK_F3) || (keyCode == kVK_F4) || + (keyCode == kVK_F5) || (keyCode == kVK_F6) || (keyCode == kVK_F7) || (keyCode == kVK_F8) || + (keyCode == kVK_F9) || (keyCode == kVK_F10) || (keyCode == kVK_F11) || (keyCode == kVK_F12) || + (keyCode == kVK_F13) || (keyCode == kVK_F14) || (keyCode == kVK_F15) || (keyCode == kVK_F16) || + (keyCode == kVK_F17) || (keyCode == kVK_F18) || (keyCode == kVK_F19) || (keyCode == kVK_F20)); + if (includesFunctionKey) return YES; + + // Do not allow any other key without modifiers + BOOL hasModifierFlags = (modifiers > 0); + if (!hasModifierFlags) return NO; + + // Allow any hotkey containing Control or Command modifier + BOOL includesCommand = ((modifiers & NSCommandKeyMask) > 0); + BOOL includesControl = ((modifiers & NSControlKeyMask) > 0); + if (includesCommand || includesControl) return YES; + + // Allow Option key only in selected cases + BOOL includesOption = ((modifiers & NSAlternateKeyMask) > 0); + if (includesOption) { + + // Always allow Option-Space and Option-Escape because they do not have any bind system commands + if ((keyCode == kVK_Space) || (keyCode == kVK_Escape)) return YES; + + // Allow Option modifier with any key even if it will break the system binding + if (_allowAnyShortcutWithOptionModifier) return YES; + } + + // The hotkey does not have any modifiers or violates system bindings + return NO; +} + +- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation +{ + NSString *keyEquivalent = [shortcut keyCodeStringForKeyEquivalent]; + NSUInteger flags = [shortcut modifierFlags]; + + for (NSMenuItem *menuItem in menu.itemArray) { + if (menuItem.hasSubmenu && [self isShortcut:shortcut alreadyTakenInMenu:[menuItem submenu] explanation:explanation]) return YES; + + BOOL equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask) == flags); + BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent]; + + // Check if the cases are different, we know ours is lower and that shift is included in our modifiers + // If theirs is capitol, we need to add shift to their modifiers + if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) { + equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags); + } + + if (equalFlags && equalHotkeyLowercase) { + if (explanation) { + *explanation = NSLocalizedString(@"This shortcut cannot be used because it is already used by the menu item ‘%@’.", + @"Message for alert when shortcut is already used"); + *explanation = [NSString stringWithFormat:*explanation, menuItem.title]; + } + return YES; + } + } + return NO; +} + +- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation +{ + CFArrayRef globalHotKeys; + if (CopySymbolicHotKeys(&globalHotKeys) == noErr) { + + // Enumerate all global hotkeys and check if any of them matches current shortcut + for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) { + CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i); + CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode); + CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers); + + if (([(__bridge NSNumber *)code unsignedIntegerValue] == [shortcut keyCode]) && + ([(__bridge NSNumber *)flags unsignedIntegerValue] == [shortcut carbonFlags])) { + + if (explanation) { + *explanation = NSLocalizedString(@"This combination cannot be used because it is already used by a system-wide " + @"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts " + @"can be changed in the Keyboard & Mouse panel in System Preferences.", + @"Message for alert when shortcut is already used by the system"); + } + return YES; + } + } + CFRelease(globalHotKeys); + } + return [self isShortcut:shortcut alreadyTakenInMenu:[NSApp mainMenu] explanation:explanation]; +} + +@end diff --git a/Framework/MASShortcutView.h b/Framework/MASShortcutView.h index 9f94bf5..5d788b9 100644 --- a/Framework/MASShortcutView.h +++ b/Framework/MASShortcutView.h @@ -1,4 +1,4 @@ -@class MASShortcut; +@class MASShortcut, MASShortcutValidator; typedef enum { MASShortcutViewAppearanceDefault = 0, // Height = 19 px @@ -10,6 +10,7 @@ typedef enum { @interface MASShortcutView : NSView @property (nonatomic, strong) MASShortcut *shortcutValue; +@property (nonatomic, strong) MASShortcutValidator *shortcutValidator; @property (nonatomic, getter = isRecording) BOOL recording; @property (nonatomic, getter = isEnabled) BOOL enabled; @property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender); diff --git a/Framework/MASShortcutView.m b/Framework/MASShortcutView.m index 0212271..d45efd3 100644 --- a/Framework/MASShortcutView.m +++ b/Framework/MASShortcutView.m @@ -1,5 +1,5 @@ #import "MASShortcutView.h" -#import "MASShortcut.h" +#import "MASShortcutValidator.h" #define HINT_BUTTON_WIDTH 23.0 #define BUTTON_FONT_SIZE 11.0 @@ -52,6 +52,12 @@ self = [super initWithCoder:coder]; if (self) { [self commonInit]; + _shortcutCell = [[[self.class shortcutCellClass] alloc] init]; + _shortcutCell.buttonType = NSPushOnPushOffButton; + _shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE]; + _shortcutValidator = [MASShortcutValidator sharedValidator]; + _enabled = YES; + [self resetShortcutCellStyle]; } return self; } @@ -396,21 +402,21 @@ void *kUserDataHint = &kUserDataHint; else { // Verify possible shortcut if (shortcut.keyCodeString.length > 0) { - if (shortcut.valid) { + if ([_shortcutValidator isShortcutValid:shortcut]) { // Verify that shortcut is not used - NSError *error = nil; - if ([shortcut isTakenError:&error]) { + NSString *explanation = nil; + if ([_shortcutValidator isShortcutAlreadyTakenBySystem:shortcut explanation:&explanation]) { // Prevent cancel of recording when Alert window is key [weakSelf activateResignObserver:NO]; [weakSelf activateEventMonitoring:NO]; NSString *format = NSLocalizedString(@"The key combination %@ cannot be used", @"Title for alert when shortcut is already used"); - NSAlert *alert = [[NSAlert alloc] init]; + NSAlert* alert = [[NSAlert alloc]init]; alert.alertStyle = NSCriticalAlertStyle; alert.informativeText = [NSString stringWithFormat:format, shortcut]; - alert.messageText = error.localizedDescription; + alert.messageText = explanation; [alert addButtonWithTitle:NSLocalizedString(@"OK", @"Alert button when shortcut is already used")]; - + [alert runModal]; weakSelf.shortcutPlaceholder = nil; [weakSelf activateResignObserver:YES]; diff --git a/MASShortcut.xcodeproj/project.pbxproj b/MASShortcut.xcodeproj/project.pbxproj index ece9dbf..1f460f5 100644 --- a/MASShortcut.xcodeproj/project.pbxproj +++ b/MASShortcut.xcodeproj/project.pbxproj @@ -30,6 +30,8 @@ 0D827D9519910C1E0010B8EF /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; }; 0D827D9719910FF70010B8EF /* MASKeyCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9619910FF70010B8EF /* MASKeyCodes.h */; }; 0D827D99199110F60010B8EF /* Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D98199110F60010B8EF /* Prefix.pch */; }; + 0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */; }; + 0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -88,6 +90,8 @@ 0D827D9319910B740010B8EF /* MASShortcutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutTests.m; path = Framework/MASShortcutTests.m; sourceTree = "<group>"; }; 0D827D9619910FF70010B8EF /* MASKeyCodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASKeyCodes.h; path = Framework/MASKeyCodes.h; sourceTree = "<group>"; }; 0D827D98199110F60010B8EF /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Prefix.pch; path = Framework/Prefix.pch; sourceTree = "<group>"; }; + 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutValidator.h; path = Framework/MASShortcutValidator.h; sourceTree = "<group>"; }; + 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutValidator.m; path = Framework/MASShortcutValidator.m; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -160,6 +164,8 @@ 0D827D1B1990D55E0010B8EF /* MASShortcut.h */, 0D827D1C1990D55E0010B8EF /* MASShortcut.m */, 0D827D9319910B740010B8EF /* MASShortcutTests.m */, + 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */, + 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */, 0D827D1D1990D55E0010B8EF /* MASShortcut+Monitoring.h */, 0D827D1E1990D55E0010B8EF /* MASShortcut+Monitoring.m */, 0D827D1F1990D55E0010B8EF /* MASShortcut+UserDefaults.h */, @@ -213,6 +219,7 @@ 0D827D271990D55E0010B8EF /* MASShortcut+Monitoring.h in Headers */, 0D827D771990F81E0010B8EF /* Shortcut.h in Headers */, 0D827D291990D55E0010B8EF /* MASShortcut+UserDefaults.h in Headers */, + 0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -337,6 +344,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */, 0D827D2E1990D55E0010B8EF /* MASShortcutView+UserDefaults.m in Sources */, 0D827D2C1990D55E0010B8EF /* MASShortcutView.m in Sources */, 0D827D2A1990D55E0010B8EF /* MASShortcut+UserDefaults.m in Sources */, |
