diff options
| author | Tomáš Znamenáček | 2014-08-06 18:05:43 +0200 | 
|---|---|---|
| committer | Tomáš Znamenáček | 2015-01-07 15:39:39 +0100 | 
| commit | 444d1bccb9770738fa4ea40383c23f44a55089c2 (patch) | |
| tree | a4f06e82263b751685e51ee3e1479fd9eebb1d16 /Framework | |
| parent | d8efb0755ab99ef60411f77fa4b635f466973d0f (diff) | |
| download | MASShortcut-444d1bccb9770738fa4ea40383c23f44a55089c2.tar.bz2 | |
Refactored the shortcut dispatcher and bindings to user defaults.
This is a big change that was hard to split into smaller commits. There’s now
a new class to bind shortcuts to actions, a new class to bind user defaults’
keys to actions, and a new way to associate user defaults with the recorder
control (MASShortcutView). I have also updated the demo app to go with the
changes.
The new class to associate shortcuts with actions is called MASShortcutMonitor.
It wraps the Carbon hotkey magic and offers a simple interface to add a
shortcut along with a block that should be run when the shortcut is pressed.
It’s the lowest-level interface.
Since the usual requirement is to store the shortcuts into user defaults,
there’s also a higher-level interface offered by the MASShortcutBinder class.
That takes a defaults key and associates it with a block. When the shortcut
stored under the defaults key changes, the binder automatically switches to the
new shortcut. The class is a wrapper built atop of the previous one, the
MASShortcutMonitor – it simply adds, updates and removes shortcuts as the
user defaults change.
I have removed the special user defaults integration code from the recorder
control (MASShortcutView) and replaced it with a small Cocoa Bindings shim.
This means that in order to keep the recorder control in sync with the defaults
you just have to call the usual bind:toObject:withKeyPath:options: method,
like this:
[_shortcutView bind:MASShortcutBinding
    toObject:[NSUserDefaultsController sharedUserDefaultsController]
    withKeyPath[@"values.ExampleDefaultsKey"
    options:@{NSValueTransformerNameBindingOption:NSKeyedUnarchiveFromDataTransformerName}];
That’s more verbose than the previous solution, but it’s much cleaner and can
be swept under a convenience call if needed. I might also add a dictionaryValue
property later that would make it possible to bind the value to user defaults
directly, without a transformer, and would enable backward compatibility with
Shortcut Recorder.
Diffstat (limited to 'Framework')
| -rw-r--r-- | Framework/MASHotKey.h | 12 | ||||
| -rw-r--r-- | Framework/MASHotKey.m | 44 | ||||
| -rw-r--r-- | Framework/MASShortcut+Monitoring.h | 8 | ||||
| -rw-r--r-- | Framework/MASShortcut+Monitoring.m | 165 | ||||
| -rw-r--r-- | Framework/MASShortcut+UserDefaults.h | 9 | ||||
| -rw-r--r-- | Framework/MASShortcut+UserDefaults.m | 98 | ||||
| -rw-r--r-- | Framework/MASShortcutBinder.h | 10 | ||||
| -rw-r--r-- | Framework/MASShortcutBinder.m | 83 | ||||
| -rw-r--r-- | Framework/MASShortcutBinderTests.m | 82 | ||||
| -rw-r--r-- | Framework/MASShortcutMonitor.h | 9 | ||||
| -rw-r--r-- | Framework/MASShortcutMonitor.m | 84 | ||||
| -rw-r--r-- | Framework/MASShortcutView+UserDefaults.h | 7 | ||||
| -rw-r--r-- | Framework/MASShortcutView+UserDefaults.m | 125 | ||||
| -rw-r--r-- | Framework/MASShortcutView.h | 2 | ||||
| -rw-r--r-- | Framework/MASShortcutView.m | 50 | ||||
| -rw-r--r-- | Framework/Prefix.pch | 1 | ||||
| -rw-r--r-- | Framework/Shortcut.h | 7 | 
17 files changed, 381 insertions, 415 deletions
| diff --git a/Framework/MASHotKey.h b/Framework/MASHotKey.h new file mode 100644 index 0000000..1d267e4 --- /dev/null +++ b/Framework/MASHotKey.h @@ -0,0 +1,12 @@ +#import "MASShortcut.h" + +extern FourCharCode const MASHotKeySignature; + +@interface MASHotKey : NSObject + +@property(readonly) UInt32 carbonID; +@property(copy) dispatch_block_t action; + ++ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut; + +@end diff --git a/Framework/MASHotKey.m b/Framework/MASHotKey.m new file mode 100644 index 0000000..7886440 --- /dev/null +++ b/Framework/MASHotKey.m @@ -0,0 +1,44 @@ +#import "MASHotKey.h" + +FourCharCode const MASHotKeySignature = 'MASS'; + +@interface MASHotKey () +@property(assign) EventHotKeyRef hotKeyRef; +@property(assign) UInt32 carbonID; +@end + +@implementation MASHotKey + +- (instancetype) initWithShortcut: (MASShortcut*) shortcut +{ +    self = [super init]; + +    static UInt32 CarbonHotKeyID = 0; + +    _carbonID = ++CarbonHotKeyID; +    EventHotKeyID hotKeyID = { .signature = MASHotKeySignature, .id = _carbonID }; + +    OSStatus status = RegisterEventHotKey([shortcut carbonKeyCode], [shortcut carbonFlags], +        hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &_hotKeyRef); + +    if (status != noErr) { +        return nil; +    } + +    return self; +} + ++ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut +{ +    return [[self alloc] initWithShortcut:shortcut]; +} + +- (void) dealloc +{ +    if (_hotKeyRef) { +        UnregisterEventHotKey(_hotKeyRef); +        _hotKeyRef = NULL; +    } +} + +@end diff --git a/Framework/MASShortcut+Monitoring.h b/Framework/MASShortcut+Monitoring.h deleted file mode 100644 index aa8f224..0000000 --- a/Framework/MASShortcut+Monitoring.h +++ /dev/null @@ -1,8 +0,0 @@ -#import "MASShortcut.h" - -@interface MASShortcut (Monitoring) - -+ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler; -+ (void)removeGlobalHotkeyMonitor:(id)monitor; - -@end diff --git a/Framework/MASShortcut+Monitoring.m b/Framework/MASShortcut+Monitoring.m deleted file mode 100644 index b6750b4..0000000 --- a/Framework/MASShortcut+Monitoring.m +++ /dev/null @@ -1,165 +0,0 @@ -#import "MASShortcut+Monitoring.h" - -NSMutableDictionary *MASRegisteredHotKeys(void); -BOOL InstallCommonEventHandler(void); -BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey); -void UninstallEventHandler(void); - -#pragma mark - - -@interface MASShortcutHotKey : NSObject - -@property (nonatomic, readonly) MASShortcut *shortcut; -@property (nonatomic, readonly, copy) void (^handler)(); -@property (nonatomic, readonly) EventHotKeyRef carbonHotKey; -@property (nonatomic, readonly) UInt32 carbonHotKeyID; - -- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler; - -@end - -#pragma mark - - -@implementation MASShortcut (Monitoring) - -+ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler -{ -    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; -} - -+ (void)removeGlobalHotkeyMonitor:(id)monitor -{ -    if (monitor == nil) return; -    NSMutableDictionary *registeredHotKeys = MASRegisteredHotKeys(); -    [registeredHotKeys removeObjectForKey:monitor]; -    if (registeredHotKeys.count == 0) { -        UninstallEventHandler(); -    } -} - -@end - -#pragma mark - - -@implementation MASShortcutHotKey - -@synthesize carbonHotKeyID = _carbonHotKeyID; -@synthesize handler = _handler; -@synthesize shortcut = _shortcut; -@synthesize carbonHotKey = _carbonHotKey; - -#pragma mark - - -- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler -{ -    self = [super init]; -    if (self) { -        _shortcut = shortcut; -        _handler = [handler copy]; - -        if (!InstallHotkeyWithShortcut(shortcut, &_carbonHotKeyID, &_carbonHotKey)) -            self = nil; -    } -    return self; -} - -- (void)dealloc -{ -    [self uninstallExisitingHotKey]; -} - -- (void)uninstallExisitingHotKey -{ -    if (_carbonHotKey) { -        UnregisterEventHotKey(_carbonHotKey); -        _carbonHotKey = NULL; -    } -} - -@end - -#pragma mark - Carbon magic - -NSMutableDictionary *MASRegisteredHotKeys() -{ -    static NSMutableDictionary *shared = nil; -    static dispatch_once_t onceToken; -    dispatch_once(&onceToken, ^{ -        shared = [NSMutableDictionary dictionary]; -    }); -    return shared; -} - -#pragma mark - - -FourCharCode const kMASShortcutSignature = 'MASS'; - -BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey) -{ -    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) { -        return NO; -    } - -    if (outCarbonHotKeyID) *outCarbonHotKeyID = hotKeyID.id; -    if (outCarbonHotKey) *outCarbonHotKey = carbonHotKey; -    return YES; -} - -static OSStatus CarbonCallback(EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData) -{ -	if (GetEventClass(inEvent) != kEventClassKeyboard) return noErr; - -	EventHotKeyID hotKeyID; -	OSStatus status = GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID); -	if (status != noErr) return status; - -	if (hotKeyID.signature != kMASShortcutSignature) return noErr; - -    [MASRegisteredHotKeys() enumerateKeysAndObjectsUsingBlock:^(id key, MASShortcutHotKey *hotKey, BOOL *stop) { -        if (hotKeyID.id == hotKey.carbonHotKeyID) { -            if (hotKey.handler) { -                hotKey.handler(); -            } -            *stop = YES; -        } -    }]; - -	return noErr; -} - -#pragma mark - - -static EventHandlerRef sEventHandler = NULL; - -BOOL InstallCommonEventHandler() -{ -    if (sEventHandler == NULL) { -        EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed }; -        OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), CarbonCallback, 1, &hotKeyPressedSpec, NULL, &sEventHandler); -        if (status != noErr) { -            sEventHandler = NULL; -            return NO; -        } -    } -    return YES; -} - -void UninstallEventHandler() -{ -    if (sEventHandler) { -        RemoveEventHandler(sEventHandler); -        sEventHandler = NULL; -    } -} diff --git a/Framework/MASShortcut+UserDefaults.h b/Framework/MASShortcut+UserDefaults.h deleted file mode 100644 index 9f2ecb9..0000000 --- a/Framework/MASShortcut+UserDefaults.h +++ /dev/null @@ -1,9 +0,0 @@ -#import "MASShortcut.h" - -@interface MASShortcut (UserDefaults) - -+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler; -+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey; -+ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey; - -@end diff --git a/Framework/MASShortcut+UserDefaults.m b/Framework/MASShortcut+UserDefaults.m deleted file mode 100644 index 006c579..0000000 --- a/Framework/MASShortcut+UserDefaults.m +++ /dev/null @@ -1,98 +0,0 @@ -#import "MASShortcut+UserDefaults.h" -#import "MASShortcut+Monitoring.h" - -@interface MASShortcutUserDefaultsHotKey : NSObject - -@property (nonatomic, readonly) NSString *userDefaultsKey; -@property (nonatomic, copy) void (^handler)(); -@property (nonatomic, weak) id monitor; - -- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler; - -@end - -#pragma mark - - -@implementation MASShortcut (UserDefaults) - -+ (NSMutableDictionary *)registeredUserDefaultsHotKeys -{ -    static NSMutableDictionary *shared = nil; -    static dispatch_once_t onceToken; -    dispatch_once(&onceToken, ^{ -        shared = [NSMutableDictionary dictionary]; -    }); -    return shared; -} - -+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler -{ -    MASShortcutUserDefaultsHotKey *hotKey = [[MASShortcutUserDefaultsHotKey alloc] initWithUserDefaultsKey:userDefaultsKey handler:handler]; -    [[self registeredUserDefaultsHotKeys] setObject:hotKey forKey:userDefaultsKey]; -} - -+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey -{ -    NSMutableDictionary *registeredHotKeys = [self registeredUserDefaultsHotKeys]; -    [registeredHotKeys removeObjectForKey:userDefaultsKey]; -} - -+ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey -{ -    NSData *shortcutData = [NSArchiver archivedDataWithRootObject:shortcut]; -    if (shortcutData) -        [[NSUserDefaults standardUserDefaults] setObject:shortcutData forKey:userDefaultsKey]; -    else -        [[NSUserDefaults standardUserDefaults] removeObjectForKey:userDefaultsKey]; -} - -@end - -#pragma mark - - -@implementation MASShortcutUserDefaultsHotKey { -    NSString *_observableKeyPath; -} - -void *MASShortcutUserDefaultsContext = &MASShortcutUserDefaultsContext; - -- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler -{ -    self = [super init]; -    if (self) { -        _userDefaultsKey = userDefaultsKey.copy; -        _handler = [handler copy]; -        _observableKeyPath = [@"values." stringByAppendingString:_userDefaultsKey]; -        [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:_observableKeyPath options:NSKeyValueObservingOptionInitial context:MASShortcutUserDefaultsContext]; -    } -    return self; -} - -- (void)dealloc -{ -    [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:_observableKeyPath context:MASShortcutUserDefaultsContext]; -    [MASShortcut removeGlobalHotkeyMonitor:self.monitor]; -} - -#pragma mark - - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ -    if (context == MASShortcutUserDefaultsContext) { -        [MASShortcut removeGlobalHotkeyMonitor:self.monitor]; -        [self installHotKeyFromUserDefaults]; -    } -    else { -        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; -    } -} - -- (void)installHotKeyFromUserDefaults -{ -    NSData *data = [[NSUserDefaults standardUserDefaults] dataForKey:_userDefaultsKey]; -    MASShortcut *shortcut = [NSKeyedUnarchiver unarchiveObjectWithData:data]; -    if (shortcut == nil) return; -    self.monitor = [MASShortcut addGlobalHotkeyMonitorWithShortcut:shortcut handler:self.handler]; -} - -@end diff --git a/Framework/MASShortcutBinder.h b/Framework/MASShortcutBinder.h new file mode 100644 index 0000000..74842f3 --- /dev/null +++ b/Framework/MASShortcutBinder.h @@ -0,0 +1,10 @@ +#import "MASShortcutMonitor.h" + +@interface MASShortcutBinder : NSObject + +@property(strong) MASShortcutMonitor *shortcutMonitor; + +- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action; +- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName; + +@end diff --git a/Framework/MASShortcutBinder.m b/Framework/MASShortcutBinder.m new file mode 100644 index 0000000..d6249a8 --- /dev/null +++ b/Framework/MASShortcutBinder.m @@ -0,0 +1,83 @@ +#import "MASShortcutBinder.h" +#import "MASShortcut.h" + +@interface MASShortcutBinder () +@property(strong) NSMutableDictionary *actions; +@property(strong) NSMutableDictionary *shortcuts; +@end + +@implementation MASShortcutBinder + +#pragma mark Initialization + +- (id) init +{ +    self = [super init]; +    [self setActions:[NSMutableDictionary dictionary]]; +    [self setShortcuts:[NSMutableDictionary dictionary]]; +    return self; +} + +- (void) dealloc +{ +    for (NSString *bindingName in [_actions allKeys]) { +        [self unbind:bindingName]; +    } +} + +#pragma mark Bindings + +- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action +{ +    [_actions setObject:[action copy] forKey:defaultsKeyName]; +    NSDictionary *bindingOptions = @{NSValueTransformerNameBindingOption: NSKeyedUnarchiveFromDataTransformerName}; +    [self bind:defaultsKeyName toObject:[NSUserDefaultsController sharedUserDefaultsController] +        withKeyPath:[@"values." stringByAppendingString:defaultsKeyName] options:bindingOptions]; +} + +- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName +{ +    [_shortcutMonitor unregisterShortcut:[_shortcuts objectForKey:defaultsKeyName]]; +    [_shortcuts removeObjectForKey:defaultsKeyName]; +    [_actions removeObjectForKey:defaultsKeyName]; +    [self unbind:defaultsKeyName]; +} + +- (BOOL) isRegisteredAction: (NSString*) name +{ +    return !![_actions objectForKey:name]; +} + +- (id) valueForUndefinedKey: (NSString*) key +{ +    return [self isRegisteredAction:key] ? +        [_shortcuts objectForKey:key] : +        [super valueForUndefinedKey:key]; +} + +- (void) setValue: (id) value forUndefinedKey: (NSString*) key +{ +    if (![self isRegisteredAction:key]) { +        [super setValue:value forUndefinedKey:key]; +        return; +    } + +    MASShortcut *newShortcut = value; +    MASShortcut *currentShortcut = [_shortcuts objectForKey:key]; + +    // Unbind previous shortcut if any +    if (currentShortcut != nil) { +        [_shortcutMonitor unregisterShortcut:currentShortcut]; +    } + +    // Just deleting the old shortcut +    if (newShortcut == nil) { +        return; +    } + +    // Bind new shortcut +    [_shortcuts setObject:newShortcut forKey:key]; +    [_shortcutMonitor registerShortcut:newShortcut withAction:[_actions objectForKey:key]]; +} + +@end diff --git a/Framework/MASShortcutBinderTests.m b/Framework/MASShortcutBinderTests.m new file mode 100644 index 0000000..f416304 --- /dev/null +++ b/Framework/MASShortcutBinderTests.m @@ -0,0 +1,82 @@ +#import "MASShortcutBinder.h" + +static NSString *const SampleDefaultsKey = @"sampleShortcut"; + +@interface MASShortcutBinderTests : XCTestCase +@property(strong) MASShortcutBinder *binder; +@property(strong) MASShortcutMonitor *monitor; +@property(strong) NSUserDefaults *defaults; +@end + +@implementation MASShortcutBinderTests + +- (void) setUp +{ +    [super setUp]; + +    [self setBinder:[[MASShortcutBinder alloc] init]]; +    [self setMonitor:[[MASShortcutMonitor alloc] init]]; +    [_binder setShortcutMonitor:_monitor]; + +    [self setDefaults:[[NSUserDefaults alloc] init]]; +    [_defaults removeObjectForKey:SampleDefaultsKey]; +} + +- (void) tearDown +{ +    [self setMonitor:nil]; +    [self setDefaults:nil]; +    [self setBinder:nil]; +    [super tearDown]; +} + +- (void) testInitialValueReading +{ +    MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; +    [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; +    [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; +    XCTAssertTrue([_monitor isShortcutRegistered:shortcut], +        @"Pass the initial shortcut from defaults to shortcut monitor."); +} + +- (void) testValueChangeReading +{ +    MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; +    [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; +    [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; +    XCTAssertTrue([_monitor isShortcutRegistered:shortcut], +        @"Pass the shortcut from defaults to shortcut monitor after defaults change."); +} + +- (void) testValueClearing +{ +    MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; +    [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; +    [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; +    [_defaults removeObjectForKey:SampleDefaultsKey]; +    XCTAssertFalse([_monitor isShortcutRegistered:shortcut], +        @"Unregister shortcut from monitor after value is cleared from defaults."); +} + +- (void) testBindingRemoval +{ +    MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; +    [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; +    [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; +    [_binder breakBindingWithDefaultsKey:SampleDefaultsKey]; +    XCTAssertFalse([_monitor isShortcutRegistered:shortcut], +        @"Unregister shortcut from monitor after binding was removed."); +} + +- (void) testRebinding +{ +    MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; +    [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; +    [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; +    [_binder breakBindingWithDefaultsKey:SampleDefaultsKey]; +    [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; +    XCTAssertTrue([_monitor isShortcutRegistered:shortcut], +        @"Bind after unbinding."); +} + +@end diff --git a/Framework/MASShortcutMonitor.h b/Framework/MASShortcutMonitor.h new file mode 100644 index 0000000..c29747c --- /dev/null +++ b/Framework/MASShortcutMonitor.h @@ -0,0 +1,9 @@ +#import "MASShortcut.h" + +@interface MASShortcutMonitor : NSObject + +- (void) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action; +- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut; +- (void) unregisterShortcut: (MASShortcut*) shortcut; + +@end diff --git a/Framework/MASShortcutMonitor.m b/Framework/MASShortcutMonitor.m new file mode 100644 index 0000000..f1c7e9c --- /dev/null +++ b/Framework/MASShortcutMonitor.m @@ -0,0 +1,84 @@ +#import "MASShortcutMonitor.h" +#import "MASHotKey.h" + +@interface MASShortcutMonitor () +@property(assign) EventHandlerRef eventHandlerRef; +@property(strong) NSMutableDictionary *hotKeys; +@end + +static OSStatus MASCarbonEventCallback(EventHandlerCallRef, EventRef, void*); + +@implementation MASShortcutMonitor + +- (instancetype) init +{ +    self = [super init]; +    [self setHotKeys:[NSMutableDictionary dictionary]]; +    EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed }; +    OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), MASCarbonEventCallback, +        1, &hotKeyPressedSpec, (__bridge void*)self, &_eventHandlerRef); +    if (status != noErr) { +        return nil; +    } +    return self; +} + +- (void) dealloc +{ +    if (_eventHandlerRef) { +        RemoveEventHandler(_eventHandlerRef); +        _eventHandlerRef = NULL; +    } +} + +#pragma mark Registration + +- (void) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action +{ +    MASHotKey *hotKey = [MASHotKey registeredHotKeyWithShortcut:shortcut]; +    [hotKey setAction:action]; +    [_hotKeys setObject:hotKey forKey:shortcut]; +} + +- (void) unregisterShortcut: (MASShortcut*) shortcut +{ +    [_hotKeys removeObjectForKey:shortcut]; +} + +- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut +{ +    return !![_hotKeys objectForKey:shortcut]; +} + +#pragma mark Event Handling + +- (void) handleEvent: (EventRef) event +{ +	if (GetEventClass(event) != kEventClassKeyboard) { +        return; +    } + +	EventHotKeyID hotKeyID; +	OSStatus status = GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID); +	if (status != noErr || hotKeyID.signature != MASHotKeySignature) { +        return; +    } + +    [_hotKeys enumerateKeysAndObjectsUsingBlock:^(MASShortcut *shortcut, MASHotKey *hotKey, BOOL *stop) { +        if (hotKeyID.id == [hotKey carbonID]) { +            if ([hotKey action]) { +                dispatch_async(dispatch_get_main_queue(), [hotKey action]); +            } +            *stop = YES; +        } +    }]; +} + +@end + +static OSStatus MASCarbonEventCallback(EventHandlerCallRef _, EventRef event, void *context) +{ +    MASShortcutMonitor *dispatcher = (__bridge id)context; +    [dispatcher handleEvent:event]; +    return noErr; +} diff --git a/Framework/MASShortcutView+UserDefaults.h b/Framework/MASShortcutView+UserDefaults.h deleted file mode 100644 index 05d3c5b..0000000 --- a/Framework/MASShortcutView+UserDefaults.h +++ /dev/null @@ -1,7 +0,0 @@ -#import "MASShortcutView.h" - -@interface MASShortcutView (UserDefaults) - -@property (nonatomic, copy) NSString *associatedUserDefaultsKey; - -@end diff --git a/Framework/MASShortcutView+UserDefaults.m b/Framework/MASShortcutView+UserDefaults.m deleted file mode 100644 index 55584f4..0000000 --- a/Framework/MASShortcutView+UserDefaults.m +++ /dev/null @@ -1,125 +0,0 @@ -#import "MASShortcutView+UserDefaults.h" -#import "MASShortcut.h" -#import <objc/runtime.h> - -@interface MASShortcutDefaultsObserver : NSObject - -@property (nonatomic, readonly) NSString *userDefaultsKey; -@property (nonatomic, readonly, weak) MASShortcutView *shortcutView; - -- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey; - -@end - -#pragma mark - - -@implementation MASShortcutView (UserDefaults) - -void *MASAssociatedDefaultsObserver = &MASAssociatedDefaultsObserver; - -- (NSString *)associatedUserDefaultsKey -{ -    MASShortcutDefaultsObserver *defaultsObserver = objc_getAssociatedObject(self, MASAssociatedDefaultsObserver); -    return defaultsObserver.userDefaultsKey; -} - -- (void)setAssociatedUserDefaultsKey:(NSString *)associatedUserDefaultsKey -{ -    // First, stop observing previous shortcut view -    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, MASAssociatedDefaultsObserver, defaultsObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -@end - -#pragma mark - - -@implementation MASShortcutDefaultsObserver { -    MASShortcut *_originalShortcut; -    BOOL _internalPreferenceChange; -    BOOL _internalShortcutChange; -} - -- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey -{ -    self = [super init]; -    if (self) { -        _originalShortcut = shortcutView.shortcutValue; -        _shortcutView = shortcutView; -        _userDefaultsKey = userDefaultsKey.copy; -        [self startObservingShortcutView]; -    } -    return self; -} - -- (void)dealloc -{ -    // __weak _shortcutView is not yet deallocated because it refers MASShortcutDefaultsObserver -    [self stopObservingShortcutView]; -} - -#pragma mark - - -void *kShortcutValueObserver = &kShortcutValueObserver; - -- (void)startObservingShortcutView -{ -    // Read initial shortcut value from user preferences -    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; -    NSData *data = [defaults dataForKey:_userDefaultsKey]; -    _shortcutView.shortcutValue = [NSKeyedUnarchiver unarchiveObjectWithData:data]; - -    // Observe user preferences to update shortcut value when it changed -    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:defaults]; - -    // Observe the keyboard shortcut that user inputs by hand -    [_shortcutView addObserver:self forKeyPath:@"shortcutValue" options:0 context:kShortcutValueObserver]; -} - -- (void)userDefaultsDidChange:(NSNotification *)note -{ -    // Ignore notifications posted from -[self observeValueForKeyPath:] -    if (_internalPreferenceChange) return; - -    _internalShortcutChange = YES; -    NSData *data = [note.object dataForKey:_userDefaultsKey]; -    _shortcutView.shortcutValue = [NSKeyedUnarchiver unarchiveObjectWithData:data]; -    _internalShortcutChange = NO; -} - -- (void)stopObservingShortcutView -{ -    // Stop observing keyboard hotkeys entered by user in the shortcut view -    [_shortcutView removeObserver:self forKeyPath:@"shortcutValue" context:kShortcutValueObserver]; - -    // Stop observing user preferences -    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]]; - -    // Restore original hotkey in the shortcut view -    _shortcutView.shortcutValue = _originalShortcut; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ -    if (context == kShortcutValueObserver) { -        if (_internalShortcutChange) return; -        MASShortcut *shortcut = [object valueForKey:keyPath]; -        _internalPreferenceChange = YES; - -        NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; -        [defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:_userDefaultsKey]; -        [defaults synchronize]; - -        _internalPreferenceChange = NO; -    } -    else { -        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; -    } -} - -@end diff --git a/Framework/MASShortcutView.h b/Framework/MASShortcutView.h index 5d788b9..8cff0ca 100644 --- a/Framework/MASShortcutView.h +++ b/Framework/MASShortcutView.h @@ -1,5 +1,7 @@  @class MASShortcut, MASShortcutValidator; +extern NSString *const MASShortcutBinding; +  typedef enum {      MASShortcutViewAppearanceDefault = 0,  // Height = 19 px      MASShortcutViewAppearanceTexturedRect, // Height = 25 px diff --git a/Framework/MASShortcutView.m b/Framework/MASShortcutView.m index 6f562a5..d03efbc 100644 --- a/Framework/MASShortcutView.m +++ b/Framework/MASShortcutView.m @@ -1,6 +1,8 @@  #import "MASShortcutView.h"  #import "MASShortcutValidator.h" +NSString *const MASShortcutBinding = @"shortcutValue"; +  #define HINT_BUTTON_WIDTH 23.0  #define BUTTON_FONT_SIZE 11.0  #define SEGMENT_CHROME_WIDTH 6.0 @@ -149,6 +151,7 @@      _shortcutValue = shortcutValue;      [self resetToolTips];      [self setNeedsDisplay:YES]; +    [self propagateValue:shortcutValue forBinding:@"shortcutValue"];      if (self.shortcutValueChange) {          self.shortcutValueChange(self); @@ -462,4 +465,51 @@ void *kUserDataHint = &kUserDataHint;      }  } +#pragma mark Bindings + +// http://tomdalling.com/blog/cocoa/implementing-your-own-cocoa-bindings/ +-(void) propagateValue:(id)value forBinding:(NSString*)binding; +{ +	NSParameterAssert(binding != nil); + +	//WARNING: bindingInfo contains NSNull, so it must be accounted for +	NSDictionary* bindingInfo = [self infoForBinding:binding]; +	if(!bindingInfo) +		return; //there is no binding + +	//apply the value transformer, if one has been set +	NSDictionary* bindingOptions = [bindingInfo objectForKey:NSOptionsKey]; +	if(bindingOptions){ +		NSValueTransformer* transformer = [bindingOptions valueForKey:NSValueTransformerBindingOption]; +		if(!transformer || (id)transformer == [NSNull null]){ +			NSString* transformerName = [bindingOptions valueForKey:NSValueTransformerNameBindingOption]; +			if(transformerName && (id)transformerName != [NSNull null]){ +				transformer = [NSValueTransformer valueTransformerForName:transformerName]; +			} +		} + +		if(transformer && (id)transformer != [NSNull null]){ +			if([[transformer class] allowsReverseTransformation]){ +				value = [transformer reverseTransformedValue:value]; +			} else { +				NSLog(@"WARNING: binding \"%@\" has value transformer, but it doesn't allow reverse transformations in %s", binding, __PRETTY_FUNCTION__); +			} +		} +	} + +	id boundObject = [bindingInfo objectForKey:NSObservedObjectKey]; +	if(!boundObject || boundObject == [NSNull null]){ +		NSLog(@"ERROR: NSObservedObjectKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__); +		return; +	} + +	NSString* boundKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey]; +	if(!boundKeyPath || (id)boundKeyPath == [NSNull null]){ +		NSLog(@"ERROR: NSObservedKeyPathKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__); +		return; +	} + +	[boundObject setValue:value forKeyPath:boundKeyPath]; +} +  @end diff --git a/Framework/Prefix.pch b/Framework/Prefix.pch index 7f2c544..3e71c31 100644 --- a/Framework/Prefix.pch +++ b/Framework/Prefix.pch @@ -1 +1,2 @@  #import <AppKit/AppKit.h> +#import <Carbon/Carbon.h>
\ No newline at end of file diff --git a/Framework/Shortcut.h b/Framework/Shortcut.h index 3087a51..dce07a5 100644 --- a/Framework/Shortcut.h +++ b/Framework/Shortcut.h @@ -1,4 +1,5 @@  #import "MASShortcut.h" -#import "MASShortcutView+UserDefaults.h" -#import "MASShortcut+UserDefaults.h" -#import "MASShortcut+Monitoring.h"
\ No newline at end of file +#import "MASShortcutValidator.h" +#import "MASShortcutMonitor.h" +#import "MASShortcutBinder.h" +#import "MASShortcutView.h" | 
