MacOS Quartz Event Tap listening to wrong events - swift

I am trying to intercept mouse move events using the CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) method as shown below:
let cfMachPort = CGEvent.tapCreate(tap: CGEventTapLocation.cghidEventTap,
place: CGEventTapPlacement.headInsertEventTap,
options: CGEventTapOptions.defaultTap,
eventsOfInterest:CGEventMask(CGEventType.mouseMoved.rawValue),
callback: {(eventTapProxy, eventType, event, mutablePointer) -> Unmanaged<CGEvent>? in event
print(event.type.rawValue) //Breakpoint
return nil
}, userInfo: nil)
let runloopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, cfMachPort!, 0)
let runLoop = RunLoop.current
let cfRunLoop = runLoop.getCFRunLoop()
CFRunLoopAddSource(cfRunLoop, runloopSource, CFRunLoopMode.defaultMode)
I pass as event type eventsOfInterest mouseMoved events with a raw value of 5 as seen in the documentation. But for some reason my print() is not executed unless I click with the mouse. Inspecting the send mouse event in the debugger gives me a raw value of 2, which according to the documentation is a leftMouseUp event.
In the documentation for CGEvent.tapCreate(tap:place:options:eventsOfInterest:callback:userInfo:) it says:
Event taps receive key up and key down events [...]
So it seems like the method ignores mouseMoved events in general?! But how am I supposed to listen to mouseMoved events? I am trying to prevent my cursor (custom cursor) from being replaced (for example when I hover over the application dock at the bottom of the screen).

You need to bitshift the CGEventType value used to create the CGEventMask parameter. In Objective-C, there is a macro to do this: CGEventMaskBit.
From the CGEventMask documentation:
to form the bit mask, use the CGEventMaskBit macro to convert each constant into an event mask and then OR the individual masks together
I don't know the equivalent mechanism in swift; but the macro itself looks like this:
*/ #define CGEventMaskBit(eventType) ((CGEventMask)1 << (eventType))
In your example, it's sufficient to just manually shift the argument; e.g.
eventsOfInterest:CGEventMask(1 << CGEventType.mouseMoved.rawValue),
I would point out that the code example given in the question is a little dangerous; as it creates a default event tap and then drops the events rather than allowing them to be processed. This messes up mouse click handling and it was tricky to actually terminate the application using the mouse. Anyone running the example could set the event tap type to CGEventTapOptions.listenOnly to prevent that.

Here is a way to listen for mouseMove global events (tested with Xcode 11.2+, macOS 10.15)
// ... say in AppDelegate
var globalObserver: Any!
var localObserver: Any!
func applicationDidFinishLaunching(_ aNotification: Notification) {
globalObserver = NSEvent.addGlobalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("in background: {\(location.x), \(location.y)}")
}
localObserver = NSEvent.addLocalMonitorForEvents(matching: .mouseMoved) { event in
let location = event.locationInWindow
print("active: {\(location.x), \(location.y)}")
return event
}
...

There's another thing incorrect in your code, although you might be lucky and it isn't normally causing a problem.
As documented for the mode parameter to CFRunLoopAddSource: "Use the constant kCFRunLoopCommonModes to add source to the set of objects monitored by all the common modes."
That third parameter should instead be CFRunLoopMode.commonModes.
What you have, CFRunLoopMode.defaultMode aka kCFRunLoopDefaultMode, is instead for use when calling CFRunLoopRun.

Related

Passing keyboard events from NSTextView to another

I have NSTextView which pops up a NSPopOver that contains another NSTextView. In some cases, I'd like to pass the keyboard events (arrow keys, mostly) from the "top" text view to the bottom one.
I have overridden keyDown on the topmost text view and tried various ways of forwarding the keyboard event to the view below.
Just calling bottomTextView.keyDown(with: event) doesn't do anything. The closest I've gotten is using window.sendEvent(event).
override func keyDown(with event: NSEvent) {
if (NSLocationInRange(Int(event.keyCode), NSMakeRange(123, 4)) &&
self.string.count == 0 && event.modifierFlags.contains(.shift)) {
// Pass the event through somehow
return
}
super.keyDown(with: event)
}
Through trial and error, I found one way which actually did pass on the events: calling the methods twice.
// Make the bottom text field first responder
self.window?.makeFirstResponder(self.delegate.bottomTextView)
self.window?.makeFirstResponder(self.delegate.bottomTextView)
// Send the keydown event to window
self.window?.sendEvent(event)
self.window?.sendEvent(event)
However, this causes strange issues, such as the topmost text view getting a one-byte string as its contents.
Is there a correct way to actually forward any key events from a text view to another, or is it even possible?

Changing the text of a UITextField does not trigger the rx.text binder

I have a UITextField called commentField and I create an Observable<Bool> like this:
let isCommentFieldValid = self.commentField.rx.text.orEmpty.map({ !$0.isEmpty })
This observable determines whether a button is enabled or not.
The problem is that when I change the text property of commentField liked this:
self.commentField.text = ""
The isCommentFieldValid doesn't trigger again and, thus, the button's state doesn't change. Any edition using the UI works: if I remove all text from the field through the keyboard, the isCommentFieldValid updates, but via code it doesn't.
Is there a reason this doesn't work?
If you look at the underlying implementation for rx.text you'll see that it relies on the following UIControlEvents: .allEditingEvents and .valueChanged. Explicitly setting the text property on UITextField does not send actions for these events, so your observable is not updated. You could try sending an action explicitly:
self.commentField.text = ""
self.commentField.sendActions(for: .valueChanged)

Executing a function when key combination is pressed

I'm adding some functionality to a menubar app. I want to execute a few lines of code that copies some text to the clipboard when a combination of keys are pressed (e.g. cmd + alt + L). This should work globally, i.e any time these keys are pressed.
Not sure how to go about doing this, I tried overriding the keyDown method but it gives an error in AppDelegate.swift saying that there's no method to override.
First step you need to add a global monitor.
NSEvent.addGlobalMonitorForEvents(matching: .keyDown, handler: {
self.keyDown(with: $0)
})
But it can be also your func.
Second step is to handle these three keys
Read flags from NSApp.currentEvent?.modifierFlags and check if they contain .option and .command flags
Example
guard let flags = NSApp.currentEvent?.modifierFlags else {
return
}
let optionKeyIsPressed = flags.contains(.option)
At last key you can read from NSEvent property keyCode.
The keyCode of later "L" you can read from kVK_ANSI_L
Hope it's all that you need to solve your problem, good luck.

How to setup printing in cocoa, swift?

I have made printing functionality for custom NSView of NSPopover by the assigning the following action to button for this NSView in mainController:
#IBOutlet var plasmidMapIBOutlet: PlasmidMapView!
#IBAction func actionPrintfMap(sender: AnyObject)
{
plasmidMapIBOutlet.print(sender)
}
It is working, but the print window has no option for Paper Size and Orientation, see screenshot below.
What should I do to get these options in the print window?
And, how to make the NSView fitting to the printable area? Now it is not fitting.
I have figured out some moments, but not completely. So, I can setup the printing by the following code
#IBAction func actionPrintMap(sender: AnyObject)
{
let printInfo = NSPrintInfo.sharedPrintInfo()
let operation: NSPrintOperation = NSPrintOperation(view: plasmidMapIBOutlet, printInfo: printInfo)
operation.printPanel.options = NSPrintPanelOptions.ShowsPaperSize
operation.printPanel.options = NSPrintPanelOptions.ShowsOrientation
operation.runOperation()
//plasmidMapIBOutlet.print(sender)
}
But, I still have problem. From the code above I can get only orientation (the last, ShowsOrientation), but not both PaperSize and Orientation. How can I manage both ShowsPaperSize and ShowsOrientation?
Finally I have found the answer which is simple to write but it is not really obvious from apple documentation.
operation.printPanel.options.insert(NSPrintPanelOptions.showsPaperSize)
operation.printPanel.options.insert(NSPrintPanelOptions.showsOrientation)
The problem in the code originally posted is that options is being assigned twice, so the first value assigned, ShowsPaperSize is overwritten by the value ShowsOrientation. That's why you only see the ShowsOrientation option in the dialog.
By using multiple insert operations, you are adding options rather than overwriting each time. You can also do it this way which I think reads better:
operation.printPanel.options.insert([.showsPaperSize, .showsOrientation])
And finally, it also works to "set" the options, and by supplying the existing options as the first array value, you achieve the affect of appending:
operation.printPanel.options = [
operation.printPanel.options,
.showsPaperSize,
.showsOrientation
]
(The first array element operation.printPanel.options means that the old options are supplied in the list of new options.)

Crossrider : Using mouse position in context menu

We would like to use appAPI.openURL but in place of sending the data.selectedText I woould like to send the text of the element under the mouse. But I can't find the way of getting the mouse position. My idea was to add in the appAPI.ready the following
$().mousemove(function(event) {
myPositionX = event.pageX ;
myPositionY = event.pageY ;
}
And to have two global variable myPositionX and myPositionY which I could access in my background code to transmit as parameters of my URL.
But this doesn't seem to work.
Is what I'm doing crazy?
You'll be pleased to note that your are not crazy but simply missed the selector required to attach the handler to the page. Hence, to make you code work, bind the mousemove handler to the document object per the following tried and tested code:
$(document).mousemove(function(event) {
myPositionX = event.pageX ;
myPositionY = event.pageY ;
});