aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomáš Znamenáček2014-08-05 16:24:15 +0200
committerTomáš Znamenáček2015-01-07 15:27:50 +0100
commit6383b054190d8bc4b7e059718cded0119d25d98c (patch)
treea54f4139029f5fff74abd6ae60b022e2325177c7
parent1c801726d35e56d3bbf4a33279213bc60935c244 (diff)
downloadMASShortcut-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.
-rw-r--r--Framework/MASShortcut.h8
-rw-r--r--Framework/MASShortcut.m116
-rw-r--r--Framework/MASShortcutValidator.h15
-rw-r--r--Framework/MASShortcutValidator.m109
-rw-r--r--Framework/MASShortcutView.h3
-rw-r--r--Framework/MASShortcutView.m20
-rw-r--r--MASShortcut.xcodeproj/project.pbxproj8
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 */,