aboutsummaryrefslogtreecommitdiffstats
path: root/Framework/MASShortcutValidator.m
blob: b14815c969daad950972d006d9e20251d4104faa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#import "MASShortcutValidator.h"
#import "MASLocalization.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 = MASLocalizedString(@"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);
            CFNumberRef enabled = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyEnabled);

            if (([(__bridge NSNumber *)code unsignedIntegerValue] == [shortcut keyCode]) &&
                ([(__bridge NSNumber *)flags unsignedIntegerValue] == [shortcut carbonFlags]) &&
                ([(__bridge NSNumber *)enabled boolValue])) {

                if (explanation) {
                    *explanation = MASLocalizedString(@"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