How to create an NSEvent of type NSScrollWheel? - objective-c

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

Related

Unsupported action method signature. Must return MPRemoteCommandHandlerStatus

I'm getting this error whenever I'm running simulator on iOS 13+. Everything works for iOS 12 and below so I'm not sure what to do here. Is there anything I can change/edit on my end to make react-native-music-control work for iOS 13?
Exception 'Unsupported action method signature. Must return MPRemoteCommandHandlerStatus or take a completion handler as the second argument.' was thrown while invoking enableControl on target MusicControlManager with params ( pause, 1, { } )
This react-native-music-control probably hasn't updated its iOS MediaPlayer methods.
One common method is MediaPlayer's addTarget. Starting iOS 13, this method must return a MPRemoteCommandHandlerStatus. It used to return nothing in previous iOS versions.
For example, if you have a play method that gets called when the Play button is tapped:
- (void) play {
/* start playing */
}
You are probably registering this play to be called when the Media Player's play command is trigged:
[[MPRemoteCommandCenter sharedCommandCenter].playCommand addTarget:self action:#selector(play)];
Then, all you need is to simply change your play method to return a MPRemoteCommandHandlerStatus like this:
- (MPRemoteCommandHandlerStatus) play
{
// if successfully played
return MPRemoteCommandHandlerStatusSuccess;
// else if there's an issue
// return MPRemoteCommandHandlerStatusCommandFailed;
}
This is in Objective-C. Changing the return value in Swift will be simple too.
Reference: https://developer.apple.com/documentation/mediaplayer/mpremotecommand/1622895-addtarget?language=objc
So this plugin is not being maintained anymore I don't know why, so for those using this plugin the solution is really Simple, based on what #CSawy. I was able to fix it.
1 - Change the method signature to MPRemoteCommandHandlerStatus
All the methods that interact with the control are defined as void so change them to MPRemoteCommandHandlerStatus. They are defined in the file MusicControls and I guess you would find it in your plugins folder. For me it was in myProject/plugins/MusiControls.h
These are the methods that require the change
playEvent
pauseEvent
nextTrackEvent
prevTrackEvent
skipForwardEvent
skipBackwardEvent
After changing all the methods signature u should have
(MPRemoteCommandHandlerStatus) playEvent:(MPRemoteCommandEvent *) event;
(MPRemoteCommandHandlerStatus) pauseEvent:(MPRemoteCommandEvent *) event;
(MPRemoteCommandHandlerStatus) nextTrackEvent:(MPRemoteCommandEvent *) event;
(MPRemoteCommandHandlerStatus) prevTrackEvent:(MPRemoteCommandEvent *) event;
(MPRemoteCommandHandlerStatus) skipForwardEvent: (MPSkipIntervalCommandEvent *) event;
(MPRemoteCommandHandlerStatus) skipBackwardEvent: (MPSkipIntervalCommandEvent *) event;
2-Add return to the implementations
Now you should change the implementation of these methods otherwise your app would still be crushing.
Simply open the file MusicControls.m. If you are using Xcode it will highlight the methods for you ... If it's no highlighted then just search for them
For all these functions you will need to add a return ;
For your play event do:
-(MPRemoteCommandHandlerStatus) playEvent:(MPRemoteCommandEvent *)event {
NSString * action = #"music-controls-play";
NSString * jsonAction = [NSString stringWithFormat:#"{\"message\":\"%#\"}", action];
CDVPluginResult * pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:jsonAction];
[self.commandDelegate sendPluginResult:pluginResult callbackId:[self latestEventCallbackId]];
return MPRemoteCommandHandlerStatusSuccess;
}
Do this for all the methods and I hope it works.

Simulating mouse-down event on another window from CGWindowListCreate

You can list the currently open windows using CGWindowListCreate, e.g:
CFArrayRef windowListArray = CGWindowListCreate(kCGWindowListOptionOnScreenOnly|kCGWindowListExcludeDesktopElements, kCGNullWindowID);
NSArray *windows = CFBridgingRelease(CGWindowListCreateDescriptionFromArray(windowListArray));
// stuff here with windows array
CFRelease(windowListArray);
With this you can get a specific window, e.g. a window from Chrome that's somewhere in the background of the current workspace, but not minimized. I also found that you can simulate mouse-clicks anywhere on-screen using CGEventCreateMouseEvent:
CGEventRef click_down = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, point, kCGMouseButtonLeft);
CGEventPost(kCGHIDEventTap, click_down);
Instead of sending this event to the front-most window under this point on the screen, can I send this to a window in background? Or is the only way to temporarily switch to that window (bring it to front), click, and switch back to the previous frontmost window?
This post suggests the latter is possible:
Cocoa switch focus to application and then switch it back
Although I'm very interested in seeing whether this can be avoided, whether we can send mouse-clicks directly to a specific window in background without bringing it into view. I can consider any Objective-C, C, or C++ options for this.
Finally got this working after finding an alternative to the deprecated GetPSNForPID function:
NSEvent *customEvent = [NSEvent mouseEventWithType: NSEventTypeLeftMouseDown
location: point
modifierFlags: NSEventModifierFlagCommand
timestamp:[NSDate timeIntervalSinceReferenceDate]
windowNumber:[self.windowID intValue]
context: nil
eventNumber: 0
clickCount: 1
pressure: 0];
CGEvent = [customEvent CGEvent];
CGEventPostToPid(PID, CGEvent);
Didn't realise there was a CGEventPostToPid method instead of CGEventPostToPSN. No need for the ProcessSerialNumber struct.

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.

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.

Capturing all multitouch trackpad input in Cocoa

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