diff options
| author | Tomáš Znamenáček | 2014-08-05 16:24:15 +0200 | 
|---|---|---|
| committer | Tomáš Znamenáček | 2015-01-07 15:27:50 +0100 | 
| commit | 6383b054190d8bc4b7e059718cded0119d25d98c (patch) | |
| tree | a54f4139029f5fff74abd6ae60b022e2325177c7 /Framework | |
| parent | 1c801726d35e56d3bbf4a33279213bc60935c244 (diff) | |
| download | MASShortcut-6383b054190d8bc4b7e059718cded0119d25d98c.tar.bz2 | |
Introduced a standalone MASShortcutValidator class to validate shortcuts.
It’s a natural simplification of the MASShortcut class. All MASShortcutView
objects use a shared validator by default, but can be reconfigured to use a
different validator if needed through the shortcutValidator property.
Diffstat (limited to 'Framework')
| -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 | 
6 files changed, 139 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]; | 
