Capturing all multitouch trackpad input in Cocoa - objective-c

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;
}

Related

Xcode Objective C while loop cannot access global variables

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

Intercept/Disable keyboard shortcuts (ex. CMD+q)

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.

Cocoa Object Idle Timer

I am trying to understand the best way to invalidate an object based on a specified idle time. I have a joystick object that when instantiated from the joystickAdded method below, automatically starts up an NSTimer for that instance:
Joystick
idleTimer = [NSTimer scheduledTimerWithTimeInterval:300 target:self selector:#selector(invalidate) userInfo:nil repeats:YES];
This works fine, but my joysticks array doesnt get cleaned up because the method that should be called when idle is joystickRemoved, but I have no idea how to call this, or if NSTimer is the best way for this.
JoystickController
void joystickAdded(void *inContext, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
JoystickController *self = (__bridge JoystickController*)inContext;
IOHIDDeviceOpen(device, kIOHIDOptionsTypeNone);
// Filter events for joystickAction
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:[NSNumber numberWithInt:kIOHIDElementTypeInput_Button] forKey:(NSString*)CFSTR(kIOHIDElementTypeKey)];
IOHIDDeviceSetInputValueMatching(device, (__bridge CFDictionaryRef)(dict));
// Register callback for action event to find the joystick easier
IOHIDDeviceRegisterInputValueCallback(device, joystickAction, (__bridge void*)self);
Joystick *js = [[Joystick alloc] initWithDevice:device];
[[self joysticks] addObject:js];
}
void joystickRemoved(void *inContext, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
// Find joystick
JoystickController *self = (__bridge JoystickController*)inContext;
Joystick *js = [self findJoystickByRef:device];
if(!js) {
NSLog(#"Warning: No joysticks to remove");
return;
}
[[self joysticks] removeObject:js];
[js invalidate];
}
void joystickAction(void *inContext, IOReturn inResult, void *inSender, IOHIDValueRef value) {
long buttonState;
// Find joystick
JoystickController *self = (__bridge JoystickController*)inContext;
IOHIDDeviceRef device = IOHIDQueueGetDevice((IOHIDQueueRef) inSender);
Joystick *js = [self findJoystickByRef:device];
// Get button state
buttonState = IOHIDValueGetIntegerValue(value);
switch (buttonState) {
// Button pressed
case 1: {
// Reset joystick idle timer
[[js idleTimer] setFireDate:[NSDate dateWithTimeIntervalSinceNow:300]];
break;
}
// Button released
case 0:
break;
}
}
You could use a delegate pattern.
1) Make a protocol JoystickDelegate, declare the method - (void)joystickInvalidated:(Joystick *)joystick.
2) JoystickController should then implement JoystickDelegate. Implement the method to remove joystick from your array.
3) Finally make a weak property called delegate in the Joystick interface and on every init of Joystick in joystickAdded assign self on js.delegate.
4) Now whenever invalidate is called, just call the delegate with self inside Joystick:
[self.delegate joystickInvalidated:self];

How to create an NSEvent of type NSScrollWheel?

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)
}
}
}

CGEvent NSKeyDown only working if app is front most?

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.