Global Hotkeys using MASShortcut framework

Hi All,

I hoping someone out there has had some experience using the MASShortcut “framework” in an AppleScriptObjC project and can provide some insight. I haven’t made much progress thus far… The framework project can be found here: https://github.com/shpakovski/MASShortcut

According the the author of the framework, getting everything working should be fairly straight-forward. Below is the ObjC code (commented out) I am trying to translate to ASObjC. I am getting an error with the indicated line. Any insight would be greatly appreciated! Thanks!


    property customView : missing value --custom NSView, class set to MASShortcutView, linked in IB
    property shortcutObject : missing value -- NSObject, class set to MASShortcut, linked in IB
    property shortcutDefaultsObject : missing value -- NSObject, class set to MASShortcutUserDefaultsHotKey, linked in IB
    property exampleKey : missing value
    
    on applicationWillFinishLaunching_(aNotification)
        
        --NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut";
        set exampleKey to "exampleKey"
        
        --self.shortcutView.associatedUserDefaultsKey = kPreferenceGlobalShortcut;
        customView's setAssociatedUserDefaultsKey_(exampleKey)
        
        -- This line below fails. I also tried  "shortcutDefaultsObject's ..." But that also failed :(
        -- both result in unrecognized selector sent to instance error

        --[MASShortcut registerGlobalShortcutWithUserDefaultsKey:kPreferenceGlobalShortcut handler:^{}];
        shortcutObject's registerGlobalShortcutWithUserDefaultsKey_handler_(exampleKey, {"showAlert:"})
        
    end applicationWillFinishLaunching_
    
    on showAlert_(sender)
        
        tell me to activate
        display alert "hello"
        
    end showAlert_


Of note, what is working so far is that the custom view shows the proper interface (MASShortcutView), accepts/records keyboard events, and saves these events/keystrokes to the app’s preferences file.

Hi,

the expression

handler:^{}

is a block which is not supported in AppleScriptObjC

Hmmm… Thanks for the reply Stefan!

Could there be a way around this? It’s late so maybe I’m not thinking straight, but if I modified the Objective-C code to not expect a block, but rather call the AppleScript handler, is there hope?

Here’s the .m file:


#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;
+ (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 = shortcut.data;
    if (shortcutData)
        [[NSUserDefaults standardUserDefaults] setObject:shortcutData forKey:userDefaultsKey];
    else
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:userDefaultsKey];
}

@end

#pragma mark -

@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
{
    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 = [MASShortcut shortcutWithData:data];
    if (shortcut == nil) return;
    self.monitor = [MASShortcut addGlobalHotkeyMonitorWithShortcut:shortcut handler:self.handler];
}

@end


and the .h


#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


No, the block is used as a callback function and is not interchangeable with an AppleScript handler

Right. I guess I was thinking I could try to change the function to something like:

.h file


+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey callHandler:(NSString *)aHandler;

.m file


+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey callHandler: (NSString *)aHandler; {
    
    if ([aHandler isEqualToString:@"runHandler1"]) {
        NSLog(@"Running handler 1'"); // here is where I could call an applescript handler? 
    }

}

But I’m having little luck getting this working.

don’t waste your time, it cannot work at all.

The alternative to a block “ which can be used also in AppleScriptObjC - is a protocol and a delegate method.
But this requires a lot of changes in the MASShortcut class.

There are other solutions/implementations for global hotkeys. Maybe you find one using a delegate.

Or you need an extra ObjC interface as a bridge

Ok. Thanks for your advice Stefan. I have global hotkeys working in my project using


current application's NSEvent's registerHotKeys_notificationName_()
current application's NSNotificationCenter's defaultCenter()'s addObserver_selector_name_object_()

and some ObjC code I found here in the forums at http://macscripter.net/viewtopic.php?id=32217

I just really wanted to implement the ability for the user to define their own shortcuts, to avoid conflicts with users’ existing workflows/hotkeys. This is proving to not be trivial task, though. Thanks again!

Hi all,

I wanted to add to this post that I was able to finally get this working by creating an Objective-C category and adding a new method to the MASShortcut class. Here’s an example of how I got it working:

AppDelegate.applescript


script AppDelegate

	property parent : class "NSObject"

	property shortcutView : missing value -- linked in IB to custom view of class MASShorcutView
	property shortcutObject : missing value -- linked in IB to NSObject of class MASShortcut

	on applicationWillFinishLaunching_(aNotification)

		shortcutView's setAssociatedUserDefaultsKey_("exampleKey") -- associate key name with view
		shortcutObject's executeBlock_("exampleKey") -- call instance method to execute block, pass keyName to block

	end applicationWillFinishLaunching_

	on doSomething_(sender)

		log "It worked!"

	end doSomething_

end script

category .h file


#import "MASShortcut.h"

@protocol ASClass <NSObject>
	- (void)doSomething:self; // call applescript handler, sender is self
@end

@interface MASShortcut (BlockClass)
	- (void)executeBlock:(NSString *)keyName;
@end

category .m file


#import "MASShortcut+BlockClass.h"
#import "MASShortcut+UserDefaults.h"
#import "MASShortcut+Monitoring.h"

@implementation MASShortcut (BlockClass)

	- (void)executeBlock:(NSString *)keyName {
                
		// execute block for keyName, registers handler as action for key
		[MASShortcut registerGlobalShortcutWithUserDefaultsKey:keyName handler:^{
			[[NSApp delegate] doSomething:self];
		}];
	}
        
@end

This what I am looking for. Have you done a step by step guide on how to use this?

Would be great to get it working.

Thanks

I haven’t put together a guide, but it’s pretty straight-forward if I remember correctly:

  1. Add the MASShortcut .m & .h files to your Cocoa-AppleScript Xcode project:
    [i]https://github.com/shpakovski/MASShortcut[/i]

  2. Add a new file to your project (File>New>File…), and choose Objective-C category

  3. Name the category whatever you want (I used “BlockClass”) and set “Category on” to MASShortcut, click Continue

  4. Create/save the new category when prompted

  5. Add some custom views to a window in Interface Builder and set their class to MASShortcutView.
    Height needs to be set to 19, I think

  6. Set up some properties in your AppDelegate.applescript file and connect to Referencing Outlet of the custom views in IB

  7. Use the examples in my previous post (2014-05-27 05:09:20 pm) to get it working

I think you may also need to add the Carbon framework to your project to get it working. You can check out another related post where Shane helped me successfully call an AppleScript handler from an Objective-C .m file here: http://macscripter.net/viewtopic.php?id=42599

If I have some time later perhaps I can post a sample project to Github if it would be useful.

Thanks for getting back, I realise that the post is a little old.

I’ve managed to follow the other stuff, but i’m not sure what I should be doing with the text above? Where should that go?

I think I ought to just clarify, (so that i’m not backing up the wrong tree), If I set a menu item to “Display” I can select a shortcut key to F10.

Then I can press F10 from within any app? If I want to specify another app i.e. photoshop is that any easier?

Again many Thanks

Matt

Hi,

The category .m and .h files are created when you add a category to the MASShortcut class.

After step 4 (4. Create/save the new category when prompted) in the “guide” above, two new files will be added to your project. In my example I named the category “BlockClass” so in my project, my files were named “MASShortcut+BlockClass.h” and “MASShortcut+BlockClass.m”

I don’t understand the other part of your question. Could you rephrase?

Rephrasing, what does this script do?
I interpret it as I can make a shortcut key that works in any application, whether in Finder or in the app (that I have made)

Hi,

My “script” doesn’t really do much of anything. In fact, there’s really not much of a script at all. The MASShortcut framework (which I am not the author of) is where the functionality is. To learn all there is to know about the MASShortcut framework, I would encourage taking a look at the project’s Github page (https://github.com/shpakovski/MASShortcut)

The MASShortcut framework enables you to record global keyboard shortcuts in your app which can be “linked” to specific methods/handlers. This means, for example, if you have a handler in your application which brings up the main window of your application, something like…


on showMainWindow_(sender)
     mainWindow's makeKeyAndOrderFront_(me)
end showMainWindow_

… and if the user of your app wanted to make this handler run when the keys Command-Option-Control-N were pressed, they could go in and record this shortcut for this handler. You, as the developer, would have to set up the “linking” of the handler and the MASShortcutView object in your code first, of course.

This is great and all, and really useful functionality to add to a project, but unfortunately, the MASShortcut framework requires the execution an Objective-C block as part of registering/linking a keyboard shortcut. AppleScript Objective-C cannot execute blocks. What I have done here, with the help of others, is basically figure out a work-around for AppleScript’s inability to execute this block.

By adding a new category to the MASShortcut class, I extended the class’s functionality with a new method. From AppleScript, you are able to call this new method, and pass to it the name of the AppleScript handler you want to be “linked” to a particular MASShortcutView/keyboard shortcut. The new method will take the handler name you pass it, and execute the Objective-C block as needed to “link/register” the new keyboard shortcut.

Hope this helps.