Is there anyway to intercept and change/ignore, globally, a given shortcut in Objective-C for a Mac application?
An example is BetterTouchTool, it can override any shortcut you provide.
What I want to do is prevent the 'quit' shortcut (i.e. CMD+q) when a specific application is open (because in this instance the shortcut is inadvertantly pressed and closes the application undesirably for some people).
In short, can I listen for any global key events and then change the event before it gets delivered to its intended application?
Here's how to setup the event listener
CFMachPortRef eventTap = CGEventTapCreate(kCGHIDEventTap,
kCGHeadInsertEventTap,
kCGEventTapOptionDefault,
CGEventMaskBit(kCGEventKeyDown),
&KeyDownCallback,
NULL);
CFRunLoopSourceRef runLoopSource = CFMachPortCreateRunLoopSource(NULL, eventTap, 0);
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
CFRelease(runLoopSource);
CGEventTapEnable(eventTap, true);
And then here is the "callback":
static CGEventRef KeyDownCallback(CGEventTapProxy proxy,
CGEventType type,
CGEventRef event,
void *refcon)
{
/* Do something with the event */
NSEvent *e = [NSEvent eventWithCGEvent:event];
return event;
}
on the parsed NSEvent, there are the modifierFlags and keyCode properties. keyCode is the code of the key that was pressed, and modifierFlags is the different modifiers that were pressed (Shift, Alt/Option, Command, etc).
Simply return NULL; in the KeyDownCallback method to stop the event from propogating.
Note: there seems to be an issue with the event tap timing out, to solve this problem you can 'reset' the event tap.
In the KeyDownCallback method, check if the CGEventType type is kCGEventTapDisabledByTimeout like so:
if (type == kCGEventTapDisabledByTimeout)
{
/* Reset eventTap */
return NULL;
}
and where Reset eventTap is, execute the setup of the event listener above again.
Related
I found that upwork.app can count pressed keys without accessibility access but I can not understand how it is done.
I read a lot of themes like this one:
OSX: Detect system-wide keyDown events?
In all themes are saying that process should be trusted "Enable access for assistive devices" and I can not find how upwork.app can track keys without this.
This is official documentation for tracking events:
https://developer.apple.com/reference/appkit/nsevent/1535472-addglobalmonitorforeventsmatchin
Key-related events may only be monitored if accessibility is enabled or if your application is trusted for accessibility access (see AXIsProcessTrusted).
In apple mail list is saying same:
http://lists.apple.com/archives/carbon-dev/2010/Feb/msg00043.html
I think that upwork.app use some hacks.
How I can count pressed keys without accessibility access?
Still haven't received your answer in the comments, but since that might also help other people in the future I decided to answer anyway.
With the IOKit you can detect the keyboard has a device, and get the key press events like device events. I've used that to detect joystick events, but it should work with keyboards has well. I assume that the modifications that I made are enough and should work, however my Xcode is updating now, so I wasn't able to test it yet.
KeyboardWatcher.h File:
#import <Foundation/Foundation.h>
#import <IOKit/hid/IOHIDManager.h>
#import <IOKit/hid/IOHIDKeys.h>
#interface KeyboardWatcher : NSObject{
IOHIDManagerRef HIDManager;
}
#property (nonatomic) int keysPressedCount;
+(instancetype)sharedWatcher;
-(void)startWatching;
-(void)stopWatching;
#end
KeyboardWatcher.m File:
#import "KeyboardWatcher.h"
#implementation KeyboardWatcher
static KeyboardWatcher *_sharedWatcher;
+(instancetype)sharedWatcher {
#synchronized([self class]) {
if (!_sharedWatcher){
_sharedWatcher = [[KeyboardWatcher alloc] init];
}
return _sharedWatcher;
}
return nil;
}
-(instancetype)init {
self = [super init];
if (self){
self.keysPressedCount = 0;
}
return self;
}
-(void)startWatching {
[self watchDevicesOfType:kHIDUsage_GD_Keyboard];
}
-(void)watchDevicesOfType:(UInt32)deviceType {
// Create an HID Manager
HIDManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
// Create a Matching Dictionary
CFMutableDictionaryRef matchDict = CFDictionaryCreateMutable(kCFAllocatorDefault, 2, &kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
// That will make the app just return the computer keyboards
CFDictionarySetValue(matchDict, CFSTR(kIOHIDPrimaryUsageKey), CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &deviceType));
// Register the Matching Dictionary to the HID Manager
IOHIDManagerSetDeviceMatching(HIDManager, matchDict);
// Register the HID Manager on our app’s run loop
IOHIDManagerScheduleWithRunLoop(HIDManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
// Open the HID Manager
IOReturn IOReturn = IOHIDManagerOpen(HIDManager, kIOHIDOptionsTypeNone);
// Register input calls to Handle_DeviceEventCallback function
IOHIDManagerRegisterInputValueCallback(HIDManager, Handle_DeviceEventCallback, nil);
if (IOReturn) NSLog(#"IOHIDManagerOpen failed.");
}
-(void)stopWatching {
HIDManager = NULL;
}
static void Handle_DeviceEventCallback (void *inContext, IOReturn inResult, void *inSender, IOHIDValueRef value){
IOHIDElementRef element = IOHIDValueGetElement(value); // Keyboard pressed key
uint32_t uniqueIdentifier = IOHIDElementGetCookie(element); // Unique ID of key
int elementValue = (int)IOHIDValueGetIntegerValue(value); // Actual state of key (1=pressed)
NSLog(#"Unique ID = %u; Value = %d", uniqueIdentifier, elementValue);
if (elementValue == 1) KeyboardWatcher.sharedWatcher.keysPressedCount++;
}
#end
In case you want to identify which unique ID is which key, you can use these enums (instead of importing Carbon you can just create a CGKeyboardMapping.h file and paste them there):
https://stackoverflow.com/a/16125341/4370893
At last, in order to use it, you just need to do that to start watching for keyboard events:
[[KeyboardWatcher sharedWatcher] startWatching];
Get the key pressed count with that:
[[KeyboardWatcher sharedWatcher] keysPressedCount];
And that to stop:
[[KeyboardWatcher sharedWatcher] stopWatching];
These were my references to write my original joystick code:
http://ontrak.net/xcode.htm
http://ontrak.net/xcode2.htm
https://developer.apple.com/library/mac/documentation/DeviceDrivers/Conceptual/HID/new_api_10_5/tn2187.html
http://www.opensource.apple.com/source/IOHIDFamily/IOHIDFamily-315.7.16/IOHIDFamily/IOHIDUsageTables.h
As soon as the update finishes I will test the code and inform if it's working or not for sure.
EDIT: Just tested and it's working. Don't forget to add the IOKit framework to the project.
I'm very new to Xcode and Objective C. So most probably this is going to be an easy question (:
I'm trying to create an app that would do something if Caps Lock key is pressed.
And I need to see if Caps lock is pressed even if app is not in focus.
I've managed to check Caps Lock state even if my app is not in focus.
But I have an issue with getting out from while loop.
I cannot understand why my app cannot see what is happening to Caps lock key once it gets inside while loop.
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_spam1 = NO;
[NSEvent addGlobalMonitorForEventsMatchingMask:NSFlagsChangedMask handler:^(NSEvent* event){
[NSThread sleepForTimeInterval:1];
if(event.keyCode == 0x39){ //CAPS LOCK key was pressed
if([event modifierFlags] & NSAlphaShiftKeyMask){
_spam1 = YES;
NSLog(#"caps lock is on");
} else {
_spam1 = NO;
NSLog(#"caps lock is off”);
}
//if I comment this part
//I can see if caps lock is on or off just fine
while(_spam1){
NSLog(#"Spam %#" , event);
NSLog(#"Spam %lu" , [event modifierFlags] & NSAlphaShiftKeyMask);
[NSThread sleepForTimeInterval:1];
if([event modifierFlags] & NSAlphaShiftKeyMask){
//_spam1 = YES;
} else {
_spam1 = NO;
NSLog(#“stop while loop”);
break;
}
}
}
}];
}
Work with framework, not agains:
Create a new subclass of NSView and override one of following methods. Set the new subclassed view in Interface builder. ( read more in Event handling guide )
So your code should look like this:
//multiKey handling
unichar SPACE_CHARACTER = 0x0020;
- (BOOL)performKeyEquivalent:(NSEvent *)theEvent
{
id responder = [[self window] firstResponder];
if (responder != self)
{
return [super performKeyEquivalent:theEvent];
}
NSUInteger numberOfPressedCharacters = [[theEvent charactersIgnoringModifiers] length];
NSEventType eventType = [theEvent type];
if (eventType == NSKeyDown && numberOfPressedCharacters == 1)
{
unichar key = [[theEvent charactersIgnoringModifiers] characterAtIndex:0];
if (key == SPACE_CHARACTER)
{
[self spaceBarPressed];
return YES;
}
}
return NO;
}
or
//single key handling (no modifiers like shift, ctrl...)
- (void)keyDown:(NSEvent *)theEvent
{
// code belongs here
}
Maybe I'm missing something, but why do you have the while loop at all, as your code comment says it works without it?
In outline what is happening is as follows:
Your call to addGlobalMonitorForEventsMatchingMask requests the system to call the block you pass every time an event occurs (that matches). This is done asynchronously, and for this to happen you must be going around your event loop.
The standard application model is event driven, the system sets up the main event loop and calls your code to handle the various events - including any required calls to your event monitoring block. After your code handles the event it returns, the event loop goes around and obtains the next event, calls your code to handle it, etc., etc.
When your block is called it is passed a reference to the event it needs to process. In a single call that event will not be changed asynchronously by the system - the system obtains the event as part of the event loop processing and calls your block, until that call returns the event loop cannot continue.
So after all that preamble what happens with your while loop is the call to sleepForTimeInterval causes your application to pause within the call to the block - the block does not return and so its caller, the event loop, does not continue either. On waking up your loop continues to process exactly the same event it was passed when called, your block then continues around its loop getting nowhere...
HTH
I'd like to construct (fake) an NSEvent of type NSScrollWheel. There are NSEvent constructor methods for a few other types of NSEvents (mouseEvent, keyEvent, enterExitEvent, otherEvent), but none allows me to set, for example, deltaX.
NSEvent doesn't provide a way to do this, but you can create a CGEvent and create an NSEvent wrapper around it. The event will automatically use the current location of the cursor. See CGEventCreateScrollWheelEvent. However, posting this event using NSApplication's postEvent:atStart: doesn't work (probably because it doesn't have a window defined). You can either post the CGEvent directly to the event stream, or send the NSEvent directly to a view's scrollWheel: method.
CGWheelCount wheelCount = 2; // 1 for Y-only, 2 for Y-X, 3 for Y-X-Z
int32_t xScroll = −1; // Negative for right
int32_t yScroll = −2; // Negative for down
CGEventRef cgEvent = CGEventCreateScrollWheelEvent(NULL, kCGScrollEventUnitLine, wheelCount, yScroll, xScroll);
// You can post the CGEvent to the event stream to have it automatically sent to the window under the cursor
CGEventPost(kCGHIDEventTap, cgEvent);
NSEvent *theEvent = [NSEvent eventWithCGEvent:cgEvent];
CFRelease(cgEvent);
// Or you can send the NSEvent directly to a view
[theView scrollWheel:theEvent];
You can create a CGEventRef using CGEventCreateScrollWheelEvent and convert it to an NSEvent with eventWithCGEvent:. To add information to the CGEvent, use CGEventSetIntegerValueField -- among the possible fields are kCGScrollWheelEventDeltaAxis1, ...2, and ...3.
Try following code:
int scrollingDeltaX =... //custom value
//if you have some NSEvent *theEvent
CGEventRef cgEvent = CGEventCreateCopy(theEvent.CGEvent);
CGEventSetIntegerValueField(cgEvent, kCGScrollWheelEventDeltaAxis2, scrollingDeltaX);
NSEvent *newEvent = [NSEvent eventWithCGEvent:cgEvent];
CFRelease(cgEvent);
This has come up for me in 2020. I thought I'd post the Swift 5 syntax for anyone finding this answer. This override in an NSScrollView subclass will swap up/down right/left scroll behavior if the ⌘ key is down:
public override func scrollWheel(with event: NSEvent) {
guard event.modifierFlags.contains(.command) else {
super.scrollWheel(with: event)
return
}
if let cgEvent: CGEvent = event.cgEvent?.copy() {
cgEvent.setDoubleValueField(.scrollWheelEventDeltaAxis1, value: Double(event.scrollingDeltaX))
cgEvent.setDoubleValueField(.scrollWheelEventDeltaAxis2, value: Double(event.scrollingDeltaY))
if let nsEvent = NSEvent(cgEvent: cgEvent) {
super.scrollWheel(with: nsEvent)
}
}
}
I am trying to automate some tasks (there's no applescript support) so I have to use CGEvents and post these events. Mouse clicking works fine, but NSKeyDown (enter) only works if I click on the app in the dock(which makes it front most app)...
Here's my code so far:
for (NSDictionary *dict in windowList) {
NSLog(#"%#", dict);
if ([[dict objectForKey:#"kCGWindowName"] isEqualToString:#"Some Window..."]) {
WIDK = [[dict objectForKey:#"kCGWindowNumber"] intValue];
break;
};
}
CGEventRef CGEvent;
NSEvent *customEvent;
customEvent = [NSEvent keyEventWithType:NSKeyDown
location:NSZeroPoint
modifierFlags:0
timestamp:1
windowNumber:WIDK
context:nil
characters:nil
charactersIgnoringModifiers:nil
isARepeat:NO
keyCode:36];
CGEvent = [customEvent CGEvent];
for (int i=0; i <5; i++) {
sleep(3);
CGEventPostToPSN(&psn, CGEvent);
NSLog(#"posted the event");
}
CFRelease(eOne);
The reason why I have posteventtopsn in a loop is for testing purposes (I just need it to send it once). While the program is in the loop, if I activate my app to front most, then the event works fine.
What am I doing wrong? Is there a way it can work if it in background? Thanks.
Here is a better way to post keyboard events to the front-most application:
CGEventRef a = CGEventCreateKeyboardEvent(NULL, 124, true);
CGEventRef b = CGEventCreateKeyboardEvent(NULL, 124, false);
CGEventPost(kCGHIDEventTap, a);
CGEventPost(kCGHIDEventTap, b);
CGEventPost lets you determine where the event is posted. I used this code recently to allow someone to remotely control a PPT presentation on my laptop. (Character 124 is the right arrow key.)
Note that you should be freeing the event after posting it.
You can send to a specific app (eg not the front app) by using CGEventPostToPSN.
i think it's because you create a NSEvent using "WIDK" for parameter windowNumber , and WIDK is related to your app only.
Using touchesBeganWithEvent, touchesEndedWithEvent, etc you can get the touch data from the multitouch trackpad, but is there a way to block that touch data from moving the mouse/activating the system-wide gestures (similar to what is done in the chinese text input)?
As noted by valexa, using NSEventMask for CGEventTap is a hack. Tarmes also notes that Rob Keniger's answer no longer works (OS X >= 10.8). Luckily, Apple has provided a way to do this quite easily by using kCGEventMaskForAllEvents and converting the CGEventRef to an NSEvent within the callback:
NSEventMask eventMask = NSEventMaskGesture|NSEventMaskMagnify|NSEventMaskSwipe|NSEventMaskRotate|NSEventMaskBeginGesture|NSEventMaskEndGesture;
CGEventRef eventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eventRef, void *refcon) {
// convert the CGEventRef to an NSEvent
NSEvent *event = [NSEvent eventWithCGEvent:eventRef];
// filter out events which do not match the mask
if (!(eventMask & NSEventMaskFromType([event type]))) { return [event CGEvent]; }
// do stuff
NSLog(#"eventTapCallback: [event type] = %d", [event type]);
// return the CGEventRef
return [event CGEvent];
}
void initCGEventTap() {
CFMachPortRef eventTap = CGEventTapCreate(kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, kCGEventMaskForAllEvents, eventTapCallback, nil);
CFRunLoopAddSource(CFRunLoopGetCurrent(), CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0), kCFRunLoopCommonModes);
CGEventTapEnable(eventTap, true);
CFRunLoopRun();
}
Note that the call to CFRunLoopRun() is included since this snippet was taken from a project which could not use NSApplication but instead had a bare-bones CFRunLoop. Omit it if you use NSApplication.
UPDATE: my answer below no longer works. See the answer here.
Usually to do this you'd need to use a Quartz Event Tap, although the touch events don't seem to be "officially" supported by the CGEvent API. The non-multitouch event types in NSEvent.h seem to map to the CGEvent types in CGEventTypes.h, so the multitouch ones will probably work, even if they're not documented.
In order to block the events from propagating, you need to return NULL from the event tap callback.
You'd need some code like this:
#import <ApplicationServices/ApplicationServices.h>
//assume CGEventTap eventTap is an ivar or other global
void createEventTap(void)
{
CFRunLoopSourceRef runLoopSource;
//listen for touch events
//this is officially unsupported/undocumented
//but the NSEvent masks seem to map to the CGEvent types
//for all other events, so it should work.
CGEventMask eventMask = (
NSEventMaskGesture |
NSEventMaskMagnify |
NSEventMaskSwipe |
NSEventMaskRotate |
NSEventMaskBeginGesture |
NSEventMaskEndGesture
);
// Keyboard event taps need Universal Access enabled,
// I'm not sure about multi-touch. If necessary, this code needs to
// be here to check whether we're allowed to attach an event tap
if (!AXAPIEnabled()&&!AXIsProcessTrusted()) {
// error dialog here
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
[alert addButtonWithTitle:#"OK"];
[alert setMessageText:#"Could not start event monitoring."];
[alert setInformativeText:#"Please enable \"access for assistive devices\" in the Universal Access pane of System Preferences."];
[alert runModal];
return;
}
//create the event tap
eventTap = CGEventTapCreate(kCGHIDEventTap, //this intercepts events at the lowest level, where they enter the window server
kCGHeadInsertEventTap,
kCGEventTapOptionDefault,
eventMask,
myCGEventCallback, //this is the callback that we receive when the event fires
nil);
// Create a run loop source.
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0);
// Add to the current run loop.
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes);
// Enable the event tap.
CGEventTapEnable(eventTap, true);
}
//the CGEvent callback that does the heavy lifting
CGEventRef myCGEventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef theEvent, void *refcon)
{
//handle the event here
//if you want to capture the event and prevent it propagating as normal, return NULL.
//if you want to let the event process as normal, return theEvent.
return theEvent;
}