Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a3a459b4e4 | |||
| 443e2db73d | |||
| 5c7dd53380 | |||
| 72904d4dde | |||
| aa6c9fbab9 | |||
| 7308a0927d | |||
| d95c03289c | |||
| 14a9a4c4a6 | |||
| 9266e38517 | |||
| ddab66d2c0 | |||
| 8e196a14a0 | |||
| e53eed24bc | |||
| 724376092d | |||
| a5c2e15935 | |||
| 0633545a46 | |||
| b30a0b02c4 | |||
| 549b3ef29e | |||
| f3a8a9a95a | |||
| 6e161f95fa | |||
| 42dfc38ef6 | |||
| dc088a3a77 | |||
| a21ac331ad | |||
| 7d3604820e | |||
| ea5a30fe6d | |||
| da27736330 | |||
| 48a311eb90 | |||
| 9c0b4a327c | |||
| 28c656d654 | |||
| ce5760d61c | |||
| f1228d6594 | |||
| 9edbf670f5 | |||
| b32a19ff7a | |||
| fb4ac110a0 | |||
| abda36331f | |||
| 86443bc53c | |||
| 278c5fa2a5 | |||
| b3e2e54830 | |||
| 7321b16163 | |||
| 9890a61538 | |||
| 75c763f697 | |||
| e5b32d7d47 | |||
| 8ede004687 | |||
| fe33039c18 | |||
| ffafb30498 | |||
| c6131623a7 | |||
| caf0c1e95e | |||
| 7f0769adf4 | |||
| 20d323b59c | |||
| dfdcd5655e | |||
| 42be5135fb | |||
| 0266a4cee2 | |||
| 7300a064ec | |||
| 0f89f3b962 | |||
| 9239e1c98d | |||
| 2c688abec9 | |||
| a89afec679 | |||
| c4bc7c135f | |||
| bc56cdc907 | |||
| 3d4235f879 |
@@ -0,0 +1,22 @@
|
||||
Copyright (c) 2012-2013, Vadim Shpakovski
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -1,9 +1,9 @@
|
||||
#import "MASShortcut+Monitoring.h"
|
||||
|
||||
NSMutableDictionary *MASRegisteredHotKeys();
|
||||
BOOL InstallCommonEventHandler();
|
||||
void UninstallEventHandler();
|
||||
void InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey);
|
||||
NSMutableDictionary *MASRegisteredHotKeys(void);
|
||||
BOOL InstallCommonEventHandler(void);
|
||||
BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey);
|
||||
void UninstallEventHandler(void);
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@@ -24,8 +24,12 @@ void InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID,
|
||||
|
||||
+ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler
|
||||
{
|
||||
NSString *monitor = [NSString stringWithFormat:@"%p: %@", shortcut, shortcut.description];
|
||||
NSString *monitor = [NSString stringWithFormat:@"%@", shortcut.description];
|
||||
if ([MASRegisteredHotKeys() objectForKey:monitor]) return nil;
|
||||
|
||||
MASShortcutHotKey *hotKey = [[MASShortcutHotKey alloc] initWithShortcut:shortcut handler:handler];
|
||||
if (hotKey == nil) return nil;
|
||||
|
||||
[MASRegisteredHotKeys() setObject:hotKey forKey:monitor];
|
||||
return monitor;
|
||||
}
|
||||
@@ -59,7 +63,9 @@ void InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID,
|
||||
if (self) {
|
||||
_shortcut = shortcut;
|
||||
_handler = [handler copy];
|
||||
InstallHotkeyWithShortcut(shortcut, &_carbonHotKeyID, &_carbonHotKey);
|
||||
|
||||
if (!InstallHotkeyWithShortcut(shortcut, &_carbonHotKeyID, &_carbonHotKey))
|
||||
self = nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -95,19 +101,20 @@ NSMutableDictionary *MASRegisteredHotKeys()
|
||||
|
||||
FourCharCode const kMASShortcutSignature = 'MASS';
|
||||
|
||||
void InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey)
|
||||
BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey)
|
||||
{
|
||||
if ((shortcut == nil) || !InstallCommonEventHandler()) return;
|
||||
if ((shortcut == nil) || !InstallCommonEventHandler()) return NO;
|
||||
|
||||
static UInt32 sCarbonHotKeyID = 0;
|
||||
EventHotKeyID hotKeyID = { .signature = kMASShortcutSignature, .id = ++ sCarbonHotKeyID };
|
||||
EventHotKeyRef carbonHotKey = NULL;
|
||||
if (RegisterEventHotKey(shortcut.carbonKeyCode, shortcut.carbonFlags, hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &carbonHotKey) != noErr) {
|
||||
carbonHotKey = NULL;
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (outCarbonHotKeyID) *outCarbonHotKeyID = hotKeyID.id;
|
||||
if (outCarbonHotKey) *outCarbonHotKey = carbonHotKey;
|
||||
return YES;
|
||||
}
|
||||
|
||||
static OSStatus CarbonCallback(EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData)
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
|
||||
+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
|
||||
+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey;
|
||||
+ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey;
|
||||
|
||||
@end
|
||||
|
||||
+25
-14
@@ -25,7 +25,7 @@
|
||||
return shared;
|
||||
}
|
||||
|
||||
+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
|
||||
+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler
|
||||
{
|
||||
MASShortcutUserDefaultsHotKey *hotKey = [[MASShortcutUserDefaultsHotKey alloc] initWithUserDefaultsKey:userDefaultsKey handler:handler];
|
||||
[[self registeredUserDefaultsHotKeys] setObject:hotKey forKey:userDefaultsKey];
|
||||
@@ -37,17 +37,24 @@
|
||||
[registeredHotKeys removeObjectForKey:userDefaultsKey];
|
||||
}
|
||||
|
||||
+ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey
|
||||
{
|
||||
NSData *shortcutData = shortcut.data;
|
||||
if (shortcutData)
|
||||
[[NSUserDefaults standardUserDefaults] setObject:shortcutData forKey:userDefaultsKey];
|
||||
else
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:userDefaultsKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation MASShortcutUserDefaultsHotKey
|
||||
@implementation MASShortcutUserDefaultsHotKey {
|
||||
NSString *_observableKeyPath;
|
||||
}
|
||||
|
||||
@synthesize monitor = _monitor;
|
||||
@synthesize handler = _handler;
|
||||
@synthesize userDefaultsKey = _userDefaultsKey;
|
||||
|
||||
#pragma mark -
|
||||
void *MASShortcutUserDefaultsContext = &MASShortcutUserDefaultsContext;
|
||||
|
||||
- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler
|
||||
{
|
||||
@@ -55,25 +62,29 @@
|
||||
if (self) {
|
||||
_userDefaultsKey = userDefaultsKey.copy;
|
||||
_handler = [handler copy];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:)
|
||||
name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
|
||||
[self installHotKeyFromUserDefaults];
|
||||
_observableKeyPath = [@"values." stringByAppendingString:_userDefaultsKey];
|
||||
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:_observableKeyPath options:NSKeyValueObservingOptionInitial context:MASShortcutUserDefaultsContext];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
|
||||
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:_observableKeyPath context:MASShortcutUserDefaultsContext];
|
||||
[MASShortcut removeGlobalHotkeyMonitor:self.monitor];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)userDefaultsDidChange:(NSNotification *)note
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
[MASShortcut removeGlobalHotkeyMonitor:self.monitor];
|
||||
[self installHotKeyFromUserDefaults];
|
||||
if (context == MASShortcutUserDefaultsContext) {
|
||||
[MASShortcut removeGlobalHotkeyMonitor:self.monitor];
|
||||
[self installHotKeyFromUserDefaults];
|
||||
}
|
||||
else {
|
||||
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)installHotKeyFromUserDefaults
|
||||
|
||||
+7
-1
@@ -1,4 +1,5 @@
|
||||
#import <Carbon/Carbon.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
#define MASShortcutChar(char) [NSString stringWithFormat:@"%C", (unsigned short)(char)]
|
||||
#define MASShortcutClear(flags) (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask))
|
||||
@@ -30,7 +31,7 @@ enum {
|
||||
kMASShortcutGlyphSoutheastArrow = 0x2198,
|
||||
} MASShortcutGlyph;
|
||||
|
||||
@interface MASShortcut : NSObject <NSCoding>
|
||||
@interface MASShortcut : NSObject <NSSecureCoding>
|
||||
|
||||
@property (nonatomic) NSUInteger keyCode;
|
||||
@property (nonatomic) NSUInteger modifierFlags;
|
||||
@@ -51,4 +52,9 @@ enum {
|
||||
|
||||
- (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
|
||||
|
||||
+69
-23
@@ -1,7 +1,7 @@
|
||||
#import "MASShortcut.h"
|
||||
|
||||
NSString *const kMASShortcutKeyCode = @"KeyCode";
|
||||
NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
NSString *const MASShortcutKeyCode = @"KeyCode";
|
||||
NSString *const MASShortcutModifierFlags = @"ModifierFlags";
|
||||
|
||||
@implementation MASShortcut {
|
||||
NSUInteger _keyCode; // NSNotFound if empty
|
||||
@@ -13,19 +13,24 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)supportsSecureCoding
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(NSCoder *)coder
|
||||
{
|
||||
[coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:kMASShortcutKeyCode];
|
||||
[coder encodeInteger:(NSInteger)self.modifierFlags forKey:kMASShortcutModifierFlags];
|
||||
[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:kMASShortcutKeyCode];
|
||||
NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode];
|
||||
self.keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
|
||||
self.modifierFlags = [decoder decodeIntegerForKey:kMASShortcutModifierFlags];
|
||||
self.modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -105,6 +110,9 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
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 @"";
|
||||
}
|
||||
@@ -133,6 +141,9 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
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);
|
||||
@@ -183,7 +194,7 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
UniCharCount length = 0;
|
||||
UniChar chars[256] = { 0 };
|
||||
UInt32 deadKeyState = 0;
|
||||
error = UCKeyTranslate(layoutData, self.keyCode, kUCKeyActionDisplay, 0, // No modifiers
|
||||
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] : @"");
|
||||
@@ -232,19 +243,50 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
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
|
||||
{
|
||||
BOOL hasFlags = (_modifierFlags > 0);
|
||||
BOOL hasCommand = ((_modifierFlags & NSCommandKeyMask) > 0);
|
||||
BOOL hasControl = ((_modifierFlags & NSControlKeyMask) > 0);
|
||||
BOOL hasOption = ((_modifierFlags & NSAlternateKeyMask) > 0);
|
||||
BOOL isFunction = ((_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));
|
||||
BOOL isSpecial = ((_keyCode == kVK_Space) || (_keyCode == kVK_Escape) || (_keyCode == kVK_Return));
|
||||
return ((hasFlags && (hasCommand || hasControl || (hasOption && isSpecial))) || isFunction);
|
||||
// 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
|
||||
@@ -263,7 +305,7 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
|
||||
if (equalFlags && equalHotkeyLowercase) {
|
||||
if (outError) {
|
||||
NSString *format = NSLocalizedString(@"This shortcut cannot be used used because it is already used by the menu item ‘%@’.",
|
||||
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];
|
||||
@@ -278,6 +320,7 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
- (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
|
||||
@@ -285,24 +328,27 @@ NSString *const kMASShortcutModifierFlags = @"ModifierFlags";
|
||||
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 *)flags unsignedIntegerValue] == self.carbonFlags) &&
|
||||
([(__bridge NSNumber *)enabled boolValue])) {
|
||||
|
||||
if (outError) {
|
||||
NSString *description = NSLocalizedString(@"This combination cannot be used used because it is already used by a system-wide "
|
||||
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];
|
||||
}
|
||||
return YES;
|
||||
isTaken = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
CFRelease(globalHotKeys);
|
||||
}
|
||||
return [self isKeyEquivalent:self.keyCodeStringForKeyEquivalent flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError];
|
||||
return (isTaken || [self isKeyEquivalent:self.keyCodeStringForKeyEquivalent flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError]);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.platform = :osx
|
||||
s.osx.deployment_target = "10.7"
|
||||
s.name = 'MASShortcut'
|
||||
s.version = '1.3.1'
|
||||
s.summary = 'Modern framework for managing global keyboard shortcuts compatible with Mac App Store'
|
||||
s.homepage = 'https://github.com/shpakovski/MASShortcut'
|
||||
s.authors = { 'Vadim Shpakovski' => 'vadim@shpakovski.com' }
|
||||
s.license = 'BSD 2-clause'
|
||||
s.source = { :git => 'https://github.com/shpakovski/MASShortcut.git', :tag => '1.3.1' }
|
||||
s.source_files = '*.{h,m}'
|
||||
s.osx.frameworks = 'Carbon', 'AppKit'
|
||||
s.requires_arc = true
|
||||
end
|
||||
@@ -15,22 +15,24 @@
|
||||
|
||||
@implementation MASShortcutView (UserDefaults)
|
||||
|
||||
void *kDefaultsObserver = &kDefaultsObserver;
|
||||
void *MASAssociatedDefaultsObserver = &MASAssociatedDefaultsObserver;
|
||||
|
||||
- (NSString *)associatedUserDefaultsKey
|
||||
{
|
||||
MASShortcutDefaultsObserver *defaultsObserver = objc_getAssociatedObject(self, kDefaultsObserver);
|
||||
MASShortcutDefaultsObserver *defaultsObserver = objc_getAssociatedObject(self, MASAssociatedDefaultsObserver);
|
||||
return defaultsObserver.userDefaultsKey;
|
||||
}
|
||||
|
||||
- (void)setAssociatedUserDefaultsKey:(NSString *)associatedUserDefaultsKey
|
||||
{
|
||||
// First, stop observing previous shortcut view
|
||||
objc_setAssociatedObject(self, kDefaultsObserver, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
objc_setAssociatedObject(self, MASAssociatedDefaultsObserver, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
|
||||
if (associatedUserDefaultsKey.length == 0) return;
|
||||
|
||||
// Next, start observing current shortcut view
|
||||
MASShortcutDefaultsObserver *defaultsObserver = [[MASShortcutDefaultsObserver alloc] initWithShortcutView:self userDefaultsKey:associatedUserDefaultsKey];
|
||||
objc_setAssociatedObject(self, kDefaultsObserver, defaultsObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
objc_setAssociatedObject(self, MASAssociatedDefaultsObserver, defaultsObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -43,11 +45,6 @@ void *kDefaultsObserver = &kDefaultsObserver;
|
||||
BOOL _internalShortcutChange;
|
||||
}
|
||||
|
||||
@synthesize userDefaultsKey = _userDefaultsKey;
|
||||
@synthesize shortcutView = _shortcutView;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
+7
-1
@@ -1,9 +1,12 @@
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
@class MASShortcut;
|
||||
|
||||
typedef enum {
|
||||
MASShortcutViewAppearanceDefault = 0, // Height = 19 px
|
||||
MASShortcutViewAppearanceTexturedRect, // Height = 25 px
|
||||
MASShortcutViewAppearanceRounded // Height = 43 px
|
||||
MASShortcutViewAppearanceRounded, // Height = 43 px
|
||||
MASShortcutViewAppearanceFlat
|
||||
} MASShortcutViewAppearance;
|
||||
|
||||
@interface MASShortcutView : NSView
|
||||
@@ -14,4 +17,7 @@ typedef enum {
|
||||
@property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender);
|
||||
@property (nonatomic) MASShortcutViewAppearance appearance;
|
||||
|
||||
/// Returns custom class for drawing control.
|
||||
+ (Class)shortcutCellClass;
|
||||
|
||||
@end
|
||||
|
||||
+45
-10
@@ -29,22 +29,42 @@
|
||||
@synthesize shortcutPlaceholder = _shortcutPlaceholder;
|
||||
@synthesize shortcutValueChange = _shortcutValueChange;
|
||||
@synthesize recording = _recording;
|
||||
@synthesize appearance = _appearance;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (Class)shortcutCellClass
|
||||
{
|
||||
return [NSButtonCell class];
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frameRect
|
||||
{
|
||||
self = [super initWithFrame:frameRect];
|
||||
if (self) {
|
||||
_shortcutCell = [[NSButtonCell alloc] init];
|
||||
_shortcutCell.buttonType = NSPushOnPushOffButton;
|
||||
_shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
|
||||
_enabled = YES;
|
||||
[self resetShortcutCellStyle];
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
[self commonInit];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)commonInit
|
||||
{
|
||||
_shortcutCell = [[[self.class shortcutCellClass] alloc] init];
|
||||
_shortcutCell.buttonType = NSPushOnPushOffButton;
|
||||
_shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
|
||||
_enabled = YES;
|
||||
[self resetShortcutCellStyle];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self activateEventMonitoring:NO];
|
||||
@@ -87,6 +107,12 @@
|
||||
_shortcutCell.bezelStyle = NSRoundedBezelStyle;
|
||||
break;
|
||||
}
|
||||
case MASShortcutViewAppearanceFlat: {
|
||||
self.wantsLayer = YES;
|
||||
_shortcutCell.backgroundColor = [NSColor clearColor];
|
||||
_shortcutCell.bordered = NO;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +182,10 @@
|
||||
[_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
|
||||
break;
|
||||
}
|
||||
case MASShortcutViewAppearanceFlat: {
|
||||
[_shortcutCell drawWithFrame:frame inView:self];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +199,7 @@
|
||||
[self getShortcutRect:&shortcutRect hintRect:NULL];
|
||||
NSString *title = (self.recording
|
||||
? (_hinting
|
||||
? NSLocalizedString(@"Use Old Shortuct", @"Cancel action button for non-empty shortcut in recording state")
|
||||
? NSLocalizedString(@"Use Old Shortcut", @"Cancel action button for non-empty shortcut in recording state")
|
||||
: (self.shortcutPlaceholder.length > 0
|
||||
? self.shortcutPlaceholder
|
||||
: NSLocalizedString(@"Type New Shortcut", @"Non-empty shortcut button in recording state")))
|
||||
@@ -207,6 +237,7 @@
|
||||
switch (self.appearance) {
|
||||
case MASShortcutViewAppearanceTexturedRect: hintButtonWidth += 2.0; break;
|
||||
case MASShortcutViewAppearanceRounded: hintButtonWidth += 3.0; break;
|
||||
case MASShortcutViewAppearanceFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - BUTTON_FONT_SIZE); break;
|
||||
default: break;
|
||||
}
|
||||
CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, CGRectMaxXEdge);
|
||||
@@ -353,7 +384,7 @@ void *kUserDataHint = &kUserDataHint;
|
||||
weakSelf.recording = NO;
|
||||
event = nil;
|
||||
}
|
||||
else if (shortcut.keyCode == kVK_Escape) {
|
||||
else if (shortcut.keyCode == kVK_Escape && !shortcut.modifierFlags) {
|
||||
// Cancel recording
|
||||
weakSelf.recording = NO;
|
||||
event = nil;
|
||||
@@ -374,9 +405,13 @@ void *kUserDataHint = &kUserDataHint;
|
||||
[weakSelf activateEventMonitoring:NO];
|
||||
NSString *format = NSLocalizedString(@"The key combination %@ cannot be used",
|
||||
@"Title for alert when shortcut is already used");
|
||||
NSRunCriticalAlertPanel([NSString stringWithFormat:format, shortcut], error.localizedDescription,
|
||||
NSLocalizedString(@"OK", @"Alert button when shortcut is already used"),
|
||||
nil, nil);
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
alert.alertStyle = NSCriticalAlertStyle;
|
||||
alert.informativeText = [NSString stringWithFormat:format, shortcut];
|
||||
alert.messageText = error.localizedDescription;
|
||||
[alert addButtonWithTitle:NSLocalizedString(@"OK", @"Alert button when shortcut is already used")];
|
||||
|
||||
[alert runModal];
|
||||
weakSelf.shortcutPlaceholder = nil;
|
||||
[weakSelf activateResignObserver:YES];
|
||||
[weakSelf activateEventMonitoring:YES];
|
||||
|
||||
@@ -8,23 +8,64 @@ The project MASShortcut introduces modern API and user interface for recording,
|
||||
|
||||
I hope, it is really easy:
|
||||
|
||||
// Drop a custom view into XIB and set its class to MASShortcutView
|
||||
@property (nonatomic, weak) IBOutlet MASShortcutView *shortcutView;
|
||||
```objective-c
|
||||
// Drop a custom view into XIB, set its class to MASShortcutView and its height to 19. If you select another appearance style look up the correct values in MASShortcutView.h
|
||||
@property (nonatomic, weak) IBOutlet MASShortcutView *shortcutView;
|
||||
|
||||
// Think up a preference key to store a global shortcut between launches
|
||||
NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut";
|
||||
// Think up a preference key to store a global shortcut between launches
|
||||
NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut";
|
||||
|
||||
// Assign the preference key and the shortcut view will take care of persistence
|
||||
self.shortcutView.associatedUserDefaultsKey = kPreferenceGlobalShortcut;
|
||||
// Assign the preference key and the shortcut view will take care of persistence
|
||||
self.shortcutView.associatedUserDefaultsKey = kPreferenceGlobalShortcut;
|
||||
|
||||
// Execute your block of code automatically when user triggers a shortcut from preferences
|
||||
[MASShortcut registerGlobalShortcutWithUserDefaultsKey:kPreferenceGlobalShortcut handler:^{
|
||||
|
||||
// Let me know if you find a better or more convenient API.
|
||||
}];
|
||||
// Execute your block of code automatically when user triggers a shortcut from preferences
|
||||
[MASShortcut registerGlobalShortcutWithUserDefaultsKey:kPreferenceGlobalShortcut handler:^{
|
||||
// Let me know if you find a better or more convenient API.
|
||||
}];
|
||||
```
|
||||
|
||||
To set an example, I made a demo project: [MASShortcutDemo](https://github.com/shpakovski/MASShortcutDemo). Enjoy!
|
||||
|
||||
#Notifications
|
||||
By registering for KVO notifications from `NSUserDefaultsController`, you can get a callback whenever a user changes the shortcut, allowing you to perform any UI updates, or other code handling tasks.
|
||||
|
||||
This is just as easy to implement:
|
||||
|
||||
```objective-c
|
||||
// Declare an ivar for key path in the user defaults controller
|
||||
NSString *_observableKeyPath;
|
||||
|
||||
// Make a global context reference
|
||||
void *kGlobalShortcutContext = &kGlobalShortcutContext;
|
||||
|
||||
// Implement when loading view
|
||||
_observableKeyPath = [@"values." stringByAppendingString:kPreferenceGlobalShortcut];
|
||||
[[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:_observableKeyPath
|
||||
options:NSKeyValueObservingOptionInitial
|
||||
context:kGlobalShortcutContext];
|
||||
|
||||
// Capture the KVO change and do something
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)obj
|
||||
change:(NSDictionary *)change context:(void *)ctx
|
||||
{
|
||||
if (ctx == kGlobalShortcutContext) {
|
||||
NSLog(@"Shortcut has changed");
|
||||
}
|
||||
else {
|
||||
[super observeValueForKeyPath:keyPath ofObject:obj change:change context:ctx];
|
||||
}
|
||||
}
|
||||
|
||||
// Do not forget to remove the observer
|
||||
[[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self
|
||||
forKeyPath:_observableKeyPath
|
||||
context:kGlobalShortcutContext];
|
||||
```
|
||||
|
||||
# Non-ARC Version
|
||||
|
||||
If you like retain/release, please check out these forks: [heardrwt/MASShortcut](https://github.com/heardrwt/MASShortcut) and [chendo/MASShortcut](https://github.com/chendo/MASShortcut). However, the preferred way is to enable the `-fobjc-arc` in Xcode source options.
|
||||
|
||||
# Copyright
|
||||
|
||||
MASShortcut is licensed under the BSD license.
|
||||
MASShortcut is licensed under the 2-clause BSD license.
|
||||
|
||||
Reference in New Issue
Block a user