Executing a function when key combination is pressed - swift

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.

Related

How to detect fn key press in Swift?

I want to detect when the user presses the fn key and do some tasks. I tried the below but it doesn't work:
if event.keyCode == kVK_Function {
print("fn key pressed")
}
I have similar code for other keys like left bracket, right bracket, slash, alphabets, and numbers. For these, similar code, as shown above, works fine but it doesn't work for fn key. I think this is handled differently.
You need to check the NSEvent's modifierFlags:
if event.modifierFlags.contains(.function) {
print("fn key pressed")
}

MacOS Quartz Event Tap listening to wrong events

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.

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

How to have a subject in RxSwift push values to itself without creating an infinite loop

I have a UITableView, which I want to put into an editing state if certain conditions are met. The primary way to toggling edit is through an edit button.
So the view elements I have are
let tableView = UITableView()
let editButton = UIButton()
And whether the tableView should be in editing mode is fed from:
let editing = BehaviorSubject(value: false)
Which will be hooked up to the tableView using something like:
editing.subscribeNext { isEditing in
tableView.setEditing(isEditing, animated: true)
}
When the edit button is tapped, I want that to push a new value to editing, that is the negation of the most recent value sent to editing. The most recently value may have been set by a tap on editButton, or it may have come from somewhere else.
I don't understand how to combine the stream for the button press with the stream for editing in such a way that allows this without an infinite loop e.g.
Obervable.combineLatest(editButton.rx_tap.asObservable(), editing) { _, isEditing in
editing.onNext(!isEditing)
}
I'm aware that the tableView has an editing property, but I don't want to rely on that as I am looking for a more general solution that I can re-use elsewhere. I'm also not looking to track the value of isEditing in an instance var, or even as a Variable(), as I am looking for a stateless, stream based solution (if this is at all possible).
Thank you!
With some help from the RxSwift GitHub issues forum I've now worked it out :). The key was withLatestFrom. I've included an example of this below in case it will help anyone else. editButton is the primary way to trigger editing mode on or off, and I've included an event sent via tableView.rx_itemSelected as an additional input example (in this case, I want editing to end any time an item is selected).
let isEditing = BehaviorSubject(value: false)
let tableView = UITableView()
let editButton = UIButton()
tableView.rx_itemSelected
.map { _ in false }
.bindTo(isEditing)
editButton.rx_tap.withLatestFrom(isEditing)
.map { !$0 }
.bindTo(isEditing)
isEditing.subscribeNext { editing in
tableView.setEditing(editing, animated: true)
}
Note: This solution sends .Next(false) to isEditing every time an item is selected, even if the table isn't currently in editing mode. If you feel this is a bad thing, and want to filter rx_itemSelected to only send .Next(false) if the table is actually in editing mode, you could probably do this using a combination of withLatestFrom and filter.
What if you define editing as a Variable instead of a BehaviourSubject. A Variable cannot error out which makes sense in this case. The declaration would look like this:
let editing = Variable(value: false)
You could subscribe to a button tap and change the value of editing to the negated current one:
editButton.rx_tap.asObservable().subscribeNext { editing.value = !editing.value }
With changing the value property of editing this method is called
editing.subscribeNext { isEditing in
tableView.setEditing(isEditing, animated: true)
}
All of this is not tested, but might lead you in the right direction for the right solution.

Clicking keyboard 'Next' key using UIAutomation

I have a search field in my app and I have set the return key type of the keyboard for this field to UIReturnKeyNext. I am attempting to write a UIAutomation test that clicks the Next button on the keyboard using the following line:
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().keys().firstWithName("next");
This call is failing because the key with name 'next' is not being found. I have done a dump of all of the elements in my app using:
UIATarget.localTarget().frontMostApp().logElementTree();
This reveals that there is indeed a key in the keyboard with name 'next', but somehow my attempt to retrieve it as show above still fails. I can however retrieve other keys (like the key for the letter 'u') using this method. Is there a known issue here or am I doing something wrong?
I've tried other variations with no luck:
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().elements()["next"];
Here is a screen capture of the elements in my UIAKeyboard:
If you just want to click it, and you know the keyboard has "next" as "Return key" (defined in your nib), then you can use this:
app.keyboard().typeString("\n");
Jelle's approach worked for me. But I also found an alternative way if anybody needed it.
XCUIApplication().keyboards.buttons["return"].tap()
Where you can create XCUIApplication() as a singleton on each UI Test session. The thing about this approach is you can now distinguish between return and done and other variants and even check for their existence.
You can go extra and do something like following:
extension UIReturnKeyType {
public var title: String {
switch self {
case .next:
return "Next"
case .default:
return "return"
case .continue:
return "Continue"
case .done:
return "Done"
case .emergencyCall:
return "Emergency call"
case .go:
return "Go"
case .join:
return "Join"
case .route:
return "Route"
case .yahoo, .google, .search:
return "Search"
case .send:
return "Send"
}
}
}
extension XCUIElement {
func tap(button: UIReturnKeyType) {
XCUIApplication().keyboards.buttons[button.title].tap()
}
}
And you can use it like:
let usernameTextField = XCUIApplication().textFields["username"]
usernameTextField.typeText("username")
usernameTextField.tap(button: .next)
I dont't have an example to test, but as the "Next" button is an UIAButton, and not an UIAKey you could try :
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().buttons()["next"];
If it doesn't work, you can also try
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().buttons()[4];
The following works for me:
UIATarget.localTarget().frontMostApp().mainWindow().keyboard().buttons().firstWi‌​thPredicate("name contains[c] 'next'");
For me, keyboard does not fall under mainWindow() in the View Hierarchy. It is at the same level as mainWindow() when you logElementTree() from top level. So, what you want to do is:
UIATarget.localTarget().frontMostApp().keyboard().buttons()["next"];
This worked for me when I was trying to press the "Search" button on keyboard.