How to implement shortcut key input in Mac Cocoa App? - objective-c

I've needed to make a global hot key input box in my Cocoa App.
I know about Shortcut Recorder, but it is a very old solution. It has parts implemented using Carbon, which has been deprecated, and I can't publish my app to the Mac App Store if I use it.
Is there any ready-to-use modern solution? Can anybody give me the way to make this by myself (I don't know where to start from)?

There is a modern framework named MASShortcut for implementing Global Shortcuts in OS X 10.7+.

Not all of Carbon is deprecated. You can't make a pure-Carbon application anymore, but some APIs live on and some of them are still the easiest way to do certain things.
One of these is the Carbon Events hotkey API. You certainly can sift through all the events using NSEvent's event-monitor methods, but it's unnecessary work. The Carbon Events hotkey API is still supported and much simpler—you just tell it what key you want to match and what function to call when the key is pressed. And there are Cocoa wrappers such as DDHotKey that make it even simpler.
RegisterEventHotKey, the relevant Carbon Events function (see also UnregisterEventHotKey in the same doc)
DDHotKey
MASShortcut, yet another wrapper (suggested by TongG):
MASShortcut (with ARC)
MASShortcut (without ARC)
KeyboardShortcuts, written in Swift and includes a SwiftUI hotkey recorder view [added to this answer in edit by the project's author].

In Mac OS X 10.6 and higher, you can use the methods +addGlobalMonitorForEventsMatchingMask:handler: and +addLocalMonitorForEventsMatchingMask:handler: defined from the NSEvent class. Monitoring Events reports the following information:
Local and global event monitors are mutually exclusive. For example, the global monitor does not observe the event stream of the application in which it is installed. The local event monitor only observes the event stream of its application. To monitor events from all applications, including the "current" application, you must install both event monitors.
The code shown in that page is for a local event monitor, but the code for a global event monitor is similar; what changes is the invoked NSEvent's method.
_eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:
(NSLeftMouseDownMask | NSRightMouseDownMask | NSOtherMouseDownMask | NSKeyDownMask)
handler:^(NSEvent *incomingEvent) {
NSEvent *result = incomingEvent;
NSWindow *targetWindowForEvent = [incomingEvent window];
if (targetWindowForEvent != _window) {
[self _closeAndSendAction:NO];
} else if ([incomingEvent type] == NSKeyDown) {
if ([incomingEvent keyCode] == 53) {
// Escape
[self _closeAndSendAction:NO];
result = nil; // Don't process the event
} else if ([incomingEvent keyCode] == 36) {
// Enter
[self _closeAndSendAction:YES];
result = nil;
}
}
return result;
}];
Once the monitor is not anymore necessary, you remove it using the following code:
[NSEvent removeMonitor:_eventMonitor];

Related

OSX: How to detect if Mission Control is running?

When Mission Control runs, it prevents applications from receiving keyboard and mouse events. It also leaves the last application running thinking that it still has focus. This is a problem for me because I don't receive keyUp or mouseUp events if I start Mission Control with a mouse button or a key held down and my application will behave as if that mouse button or key is held down.
I would like a way to either read both keyboard and mouse events even when Mission Control is active, or a way of detecting that Mission Control is active. Ideally, I would like to be able to do the latter since I effectively can't use my application when Mission Control is running.
I've tried a couple of things with no luck:
Use addGlobalMonitorForEventsMatchingMask to register a global monitor for keyboard and mouse events. This captures mouse events (but not keyboard events, although the documentation says keyDown events should be sent to the global monitor) when I switch to another application, but Mission Control doesn't seem to let events propagate to global monitors.
Check [[NSRunningApplication currentApplication] {isActive, ownsMenuBar}].
Apparently, my application is active even though it's not receiving events!
Check [NSApp keyWindow] != nil.
Apparently, one of my windows should be receiving key events. None of them are.
Check if Mission Control is one of the running applications returned by [NSWorkspace runningApplications]. Mission Control does not show up in this list when it's running.
Edit:
I've finally worked around this problem (albeit not in a very satisfactory way). For the mouse, it turns out that you can query the state of the pressed buttons with [NSEvent pressedMouseButtons]. I simply keep track of what I think the mouse state should be from NSLeftMouseDown and NSLeftMouseUp events and compare that to [NSEvent pressedMouseButtons] every so often to make sure that they're consistent. If they're not, then I know that something has hijacked my NSLeftMouseUp event and act accordingly.
For the keyboard, I could not find a way to query the keyboard state, so I couldn't do a similar workaround. I ended up disabling application switching using presentation options when keys are pressed.
At least in OS X 10.10, you can use this code to check if Mission Control is active or not:
func missionControlIsActive() -> Bool
{
var result: Bool = false
let windowInfosRef = CGWindowListCopyWindowInfo(CGWindowListOption(kCGWindowListOptionOnScreenOnly), CGWindowID(0)) // CGWindowID(0) is equal to kCGNullWindowID
let windowList: NSArray = windowInfosRef.takeRetainedValue() // We own the returned CFArrayRef
for entry in windowList
{
if (entry.objectForKey("kCGWindowOwnerName") as! String) == "Dock"
{
var bounds: NSDictionary = entry.objectForKey("kCGWindowBounds") as! NSDictionary
if (bounds.objectForKey("Y") as! NSNumber) == -1
{
result = true
}
}
}
return result
}
In a nutshell, the code checks if a specific window owned by the OS X Dock process is visible on the screen and if it is in a specific position. If both conditions are met, Mission Control will be active right now. Code will work in a sandboxed app and no privileges for assistive devices are required.
Did you try on bash level using NSTask? Something like ps -faxU <username> should list all running processes and then you could parse the output, or indeed you could use ps -faxU <username> | grep -i "mission control" (At the top of my head I am not sure how the process may be called, but sth like "mission control" seems legit). Not the most elegant solution maybe, but if nothing else works it may be worth it.
May be i'm missing something, but have you tried to use event taps instead of global monitoring?
It does appear that DTrace has some ability to see Mission Control being activated. Try running:
sudo fs_usage -filesys | grep Mission
from the command line and then launching the Mission Control app from the /Application folder.
You should see a lot of output related to Mission Control starting up. Unfortunately, this same output did not appear by using the keyboard short cut or swiping. Of course, using DTrace in production code is not something I would actually recommend.
C++ and Qt implementation works in latest OS X.
bool Window::missionControlIsActive() {
bool result = false;
CFArrayRef windows = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly+kCGWindowListExcludeDesktopElements, kCGNullWindowID);
for (int i = 0; i < CFArrayGetCount(windows) ; i++) {
auto cfMutableDictionaryRef_dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex( windows, i );
auto cfStringRef_name = (CFStringRef)CFDictionaryGetValue(cfMutableDictionaryRef_dict, kCGWindowName);
if (QString::fromCFString(cfStringRef_name) != u"") continue;
auto cfStringRef_ownerName = (CFStringRef)CFDictionaryGetValue(cfMutableDictionaryRef_dict, kCGWindowOwnerName);
if (QString::fromCFString(cfStringRef_ownerName) != u"Dock") continue;
auto cfDictRef_bounds = (CFDictionaryRef)CFDictionaryGetValue(cfMutableDictionaryRef_dict, kCGWindowBounds);
auto cfNumRef_bounds_Y = (CFNumberRef)CFDictionaryGetValue(cfDictRef_bounds, QString("Y").toCFString());
double num;
CFNumberGetValue(cfNumRef_bounds_Y, kCFNumberFloat64Type, &num);
if (num > 1.0 and num < 1000000) continue;
result = true;
break;
}
CFRelease(windows);
return result;
}

Register a global hotkey without support for assistive devices enabled

Using this code, I may register a global event handler:
[NSEvent addGlobalMonitorForEventsMatchingMask: NSKeyDownMask
handler: ^(NSEvent *incomingEvent) {
NSString *chars = [[incomingEvent characters] lowercaseString];
unichar character = [chars characterAtIndex:0];
// do something useful
NSLog(#"keydown globally! Which key? This key: %c", character);
}];
Unfortunately, events get passed along to this monitor, if support for assistive devices is enabled. Without assistive devices being enabled, no events get passed along.
Form the documentation:
Key-related events may only be monitored if accessibility is enabled or if your
application is trusted for accessibility access (see AXIsProcessTrusted).
I wonder, if another method exists, which passes along events without forcing the user to enable specific features of OS X.
While I didn't find a solution in Apple's docs, a solution must exist. E.g. the MAS-downloaded version of Alfred allows to define a hotkey.
Interestingly, Alfred's preferences only shows special keys and points out, that certain special key combinations may not work.
Since I basically want to show / hide a 'global' non-activating panel, I probably should simply prepare a system service. Should I?
You could try creating a Quartz Event Tap, with kCGSessionEventTap as the location.
Quartz Event Services Reference
Sample code from Mac OS X Internals

How to get keyboard state in Objective-C without referring to NSEvent

Is it possible to get the keyboard state in Objective-C without referring to NSEvent?
In general I can't use NSResponder methods like -[NSResponder flagsChanged:] but I need to know if the Command key is currently pressed.
I'm still wondering why you can't use NSEvent, but I'm going to answer the question anyways. Perhaps you're building a "command-line tool" and are only linked against Foundation? You're going to have to include at least one more framework. If you want to link against AppKit, you can (as I mentioned in the comments) use +[NSEvent modifierFlags]; this is a class method on NSEvent, so you can use it anywhere, without needing to have access to an individual event, to get the current state of the modifier keys as a bitmask. The docs explain the meaning of the bitmask.
if( NSCommandKeyMask & [NSEvent modifierFlags] ){
NSLog(#"Oh, yeah!");
}
You can also get this info using Quartz Event Services. In this case you have to include the ApplicationServices framework*. The CGEventSource functions will give you the same bitmask you get from NSEvent:
CGEventFlags theFlags;
theFlags = CGEventSourceFlagsState(kCGEventSourceStateHIDSystemState);
if( kCGEventFlagMaskCommand & theFlags ){
NSLog(#"Uh huh!");
}
*This is already included if you are, in fact, writing a Cocoa app -- it's part of Quartz.
Use this code when targeting macOS 10.12 or newer:
if (NSEventModifierFlagCommand & NSEvent.modifierFlags) {
// Do whatever is needed
}

What's the modern equivalent of GetNextEvent in Cocoa?

I'm porting an archaic C++/Carbon program to Obj-C and Cocoa. The current version uses asynchronous usb reads and GetNextEvent to read the data.
When I try to compile this in Objective C, GetNextEvent isn't found because it's in the Carbon framework.
Searching Apple support yields nothing of use.
EDIT TO ADD:
Ok, so what I'm trying to do is run a document scanner through USB. I have set up the USBDeviceInterface and the USBInterfaceInterface (who came up with THAT name???) and I call (*usbInterfaceInterface)->WritePipeTO() to ask the scanner to scan. I believe this works. AT least the flatbed light moves across the page...
Then I try to use *(usbInterfaceInterface))->ReadPipeAsyncTO() to read data. I give this function a callback function, USBDoneProc().
The general structure is:
StartScan()
WaitForScan()
StartScan() calls the WritePipeTO and the ReadPipeAsyncTO
WaitForScan() has this:
while (deviceActive) {
EventRecord event;
GetNextEvent(0,&event);
if (gDataPtr != saveDataPtr) { // more data up the timeout
timeoutTicks = TickCount() + 60 * 60;
saveDataPtr = gDataPtr;
}
if (TickCount() > timeoutTicks) {
deviceActive = false;
}
}
Meanwhile, USBDoneProc incrementing gDataPtr to be the end of the data that we've read so far. It gets called several times during the asynchronous read, called automatically by the callback, as far as I can tell.
If I cake out the GetNextEvent() call in the WORKING code the USBDoneProc doesn't get called until the asynchronous readpipe timesout.
So it looks to me that I need something to give control back to the event handler so that the USBRead interrupts can actually interrupt and make the USBDoneProcget called...
Does that make any sense?
thanks.
I suppose the nearest thing to a Cocoa equivalent would be -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]. But bear in mind that GetNextEvent is archaic even for Carbon. The preferred way of handing events is the "don't call us, we'll call you" scheme, where the app calls NSApplicationMain or RunApplicationEventLoop and events are dispatched to you.
EDIT to add: Does you app have a normal event loop? If so, maybe WaitForScan could start a Carbon timer and return to the event loop. Each time the timer fires, do what you did in the WaitForScan loop.
there is a USB hidapi that works for mac on windows.
http://www.signal11.us/oss/hidapi/
may this could be of help to you?
It works fine (I can list the connected USB devices and connect/write/read to a device);
however, if i USB device is connected/disconnected during the runtime of the application, I don't see the new connected/disconnected devices.
See: https://github.com/signal11/hidapi/issues/14
If I add the following code to hidapi, then hidapi detects the new USB devices.
#include <Carbon/Carbon.h>
void check_apple_events() {
printf("check_apple_events\n");
RgnHandle cursorRgn = NULL;
Boolean gotEvent=TRUE;
EventRecord event;
while (gotEvent) {
gotEvent = WaitNextEvent(everyEvent, &event, 0L, cursorRgn);
}
}
I need to compile this on OSX10.5 because it uses Carbon instead of Cocoa.
I am currently looking how to transform this to Cocoa.
you are also trying to move your code to cocoa, right?
let me know if you find out; I'll post it here if I get it.
regards,
David
Have you considered throwing the whole thing out and using Image Kit's new-in-10.6 scanner support instead? Even if it's custom, writing a TWAIN driver for it might be easier (and is certainly better) than trying to twist Cocoa into a GetNextEvent shape.

How to monitor global modifier key state (in any application)?

I'm using some Carbon code in my Cocoa project for handling global key events (shortcuts) from other applications. Currently I have setup a kEventHotKeyReleased event handler and I can successfully obtain hot keys when my application is not active. That triggers some operation in my application.
The problem I have with the behavior of kEventHotKeyReleased is:
Say for example I press the Cmd-Shift-P key combination. As soon as I release the "P" key the hot key event is triggered. I need to be able to trigger the event (or manually trigger it) when all of the keys are unpressed (i.e: the Cmd and Shift keys are released too).
It is easy to monitor for hot keys but I have seen nothing for monitoring individual keystrokes. If I could monitor the modifier key states I would be in business.
Any hints on how to do this?
Thanks in advance!
UPDATE:
I've tried using kEventRawKeyUp and kEventRawKeyModifiersChanged but while kEventHotKeyReleased works those two don't even though I set them up in the exact same way as kEventHotKeyReleased.
EventTypeSpec eventTypes[] = {{kEventClassKeyboard, kEventHotKeyReleased}, {kEventClassKeyboard, kEventRawKeyUp}};
// Changing the order in the list does not help, nor does removing kEventHotKeyReleased
OSStatus err = InstallApplicationEventHandler(&globalHotkeyHandler, GetEventTypeCount(eventTypes), eventTypes, NULL, NULL);
// err == noErr after this line
The globalHotKeyHandler method is called for kEventHotKeyReleased, but not for kEventRawKeyUp for some reason I can't seem to grasp. Here's what my globalHotKeyHandler method looks like:
OSStatus globalHotkeyHandler(EventHandlerCallRef nextHandler, EventRef anEvent, void *userData) {
NSLog(#"Something happened!");
}
Is there an additional call that needs to be made or something else I forgot?
N.B: At first glance, it seems like it could be that Access for Assistive Devices is disabled but it is not. So I'm pretty clueless.
UPDATE 2:
I investigated a bit on the CGEventTap Leibowitzn suggested and I came up with this setup:
CFMachPortRef keyUpEventTap = CGEventTapCreate(kCGHIDEventTap,kCGHeadInsertEventTap,kCGEventTapOptionListenOnly,kCGEventKeyUp,&keyUpCallback,NULL);
CFRunLoopSourceRef keyUpRunLoopSourceRef = CFMachPortCreateRunLoopSource(NULL, keyUpEventTap, 0);
CFRelease(keyUpEventTap);
CFRunLoopAddSource(CFRunLoopGetCurrent(), keyUpRunLoopSourceRef, kCFRunLoopDefaultMode);
CFRelease(keyUpRunLoopSourceRef);
... and the callback:
CGEventRef keyUpCallback (CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
NSLog(#"KeyUp event tapped!");
return event;
}
As you can see I'm using kCGEventKeyUp as the mask for the event tap but somehow I'm receiving mouse down events ??!??
UPDATE 3:
Ok forget that, I overlooked the line in the doc that said to use CGEventMaskBit(kCGEventKeyUp) for this parameter, so the correct call is:
CGEventTapCreate(kCGHIDEventTap,kCGHeadInsertEventTap,kCGEventTapOptionListenOnly,CGEventMaskBit(kCGEventKeyUp),&keyUpCallback,NULL);
I'm still having a problem though: modifier keys do not trigger the kCGEventKeyUp...
UPDATE 4:
Ok forget that again... I'm bound to answer to my own questions 5 minutes after asking them today huh!
To intercept modifier keys, use kCGEventFlagsChanged:
CGEventTapCreate(kCGHIDEventTap,kCGHeadInsertEventTap,kCGEventTapOptionListenOnly,CGEventMaskBit(kCGEventFlagsChanged),&callbackFunction,NULL);
So in essence I got the key and modifier key state detection working, but I'm still interested in knowing why kEventRawKeyUp doesn't work...
N.B: Also note that I'm developing on Tiger with the goal of having support for new and older versions of the OS as much as possible. CGEventTap is 10.4+ only so I'll be using this for now but a backwards-compatible solution would be welcome.
One option is to use EventTaps. This lets you monitor all keyboard events. See:
http://developer.apple.com/mac/library/documentation/Carbon/Reference/QuartzEventServicesRef/Reference/reference.html#//apple_ref/c/func/CGEventTapCreate
Unfortunately event taps will stop working if an application is requesting secure input. For example Quicken.
OSStatus err = InstallApplicationEventHandler(&globalHotkeyHandler, GetEventTypeCount(eventTypes), eventTypes, NULL, NULL);
This is not global. This installs the handler only when your own application is active, and (I believe) after the Carbon Event Manager's own event filters.
You need to use InstallEventHandler, which takes an event target as its first parameter (InstallApplicationEventHandler is a macro that passes the application event target).
For events that occur while your application is not active, the target you want is GetEventMonitorTarget(). For events that occur while your application is active, the target you want is GetEventDispatcherTarget(). To catch events no matter what application is active, install your handler on both targets.
Nowadays, though, I'd just use CGEventTaps, as Leibowitzn suggested.