Passing keyboard events from NSTextView to another - swift

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?

Related

Swift UI TextField onCommit called when lost focus (macOS)

I am having the following text field in content view:
func doCommit(){
print("\(self.status.textValue)")
}
var body : some View {
TextField("Your input:", text: self.$status.textValue, onCommit:{
self.doCommit()
}).lineLimit(1).focusable(true, onFocusChange: { focused in
print ("\(focused) changed")
})
...
}
When the text field got focus, and type return , the doCommit got called, which is what I want,
but when I click on something else, and then click the text field and click somewhere else, the doCommit got called again, and the focusable onFocusChange is not call.
I do not want the func been called when lost focus , but only when return/entry
It appears onFocusChange only gets called when you use the tab key to iterate through different UI elements, not when you click into and away from the TextField

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 Implement accessibilityCustomActions for VoiceOver on Mac?

I have a button that responds to various mouse clicks (regular click, right click, control+click, option+click, command+click...) to show different popup menus. Since it would be annoying for VoiceOver users to use actual physical mouse, I would like to map those to different VoiceOver actions.
However, I'm not getting the results I expected. Could someone help me to understand better what I'm missing? Here is what I discovered so far.
If I subclass NSButton and override the following functions, they work fine. Except there's one odd thing. If I press vo+command+space to bring up the list of available actions, VoiceOver says Action 1 instead of Show Menu.
override func accessibilityPerformPress() -> Bool {
print("Pressed!")
return true
}
override func accessibilityPerformShowAlternateUI() -> Bool {
print("Show Alternate UI")
return true
}
override func accessibilityPerformShowMenu() -> Bool {
print("Show Menu")
return true
}
In the same NSButton subclass, if I also override accessibilityCustomActions function, "Do Something" never comes up in the list of available actions when I press vo+command+space.
override func accessibilityCustomActions() -> [NSAccessibilityCustomAction]? {
let custom = NSAccessibilityCustomAction(name: "Do Something", target: self, selector: #selector(doSomething))
return [custom]
}
#objc func doSomething() -> Bool {
print("Done something.")
return true
}
If I subclass NSView instead of NSButton, and override the same functions from #1, everything works fine. Unlike first case, even VoiceOver correctly says "Show Menu" for the action from accessibilityPerformShowMenu instead of "Action 1".
in the same NSView subclass, if I override accessibilityCustomActions along with accessibilityPerformPress, accessibilityPerformShowMenu, or accessibilityPerformShowAlternateUI, "Do Something" doesn't come up in the action list.
However, "Do Something" does come up in the action list if I just override accessibilityCustomActions by itself without accessibilityPerformPress, accessibilityPerformShowMenu, and accessibilityPerformShowAlternateUI.
I tried creating another action with the name "Press" that does the same thing when pressing vo+space, and including in the return value of accessibilityCustomActions. However, Vo+space did not trigger the action. Instead, I had to press vo+command+space, and then select "Press". I guess the action just has the name "Press", but it's not actually connected to vo+space. I'm not sure how I can actually make that particular custom action to respond to vo+space.
I would appreciate if someone could help me to implement accessibilityCustomActions as well as accessibilityPerformPress, accessibilityPerformShowMenu, and accessibilityPerformShowAlternateUI together into NSButton.
Thanks so much!
The problem is that you are overriding these AX methods on the NSButton, not the NSButtonCell. For nearly everything to do with accessibility in NSControls, you will want to deal with the NSCell in question. If you use the custom action code you've written above and stick it in a subclass of NSButtonCell used by your button, then it will work.

Show and hide line chart highlight on touch

I want to only highlight a data point when the finger is on the chart, as soon as it lifts off the screen I want to call, or simple deselect the highlight.
func chartValueNothingSelected(chartView: ChartViewBase) {
print("Nothing Selected")
markerView.hidden = true
}
I've tried to override the touch ended but haven't gotten it to work.
You can turn off highlighting any bars/data all together using the highlightEnabled property.
Example of this is:
barChartView.data?.highlightEnabled = false
If you still want to be able to highlight values, but want them to automatically deselect after the touch has ended, I also found another function highlightValues(highs: [ChartHighlight]?) which says in the documentation..
Provide null or an empty array to undo all highlighting.
Call this when you want to deselect all the values and I believe this will work. Example of this could be:
let emptyVals = [ChartHighlight]()
barChartView.highlightValues(emptyVals)
Ref:
Charts Docs: highlightValues documentation
If you don't have to do anything with the tapped data you can use:
barChartView.data?.highlightEnabled = false
If you want to use the tapped data point without displaying the highlight lines, you can use the selection delegate (don't forget to add ChartViewDelegate to your class):
yourChartView.delegate = self // setup the delegate
Add delegate function:
func chartValueSelected(_ chartView: ChartViewBase, entry: ChartDataEntry, highlight: Highlight) {
// do something with the selected data entry here
yourChartView.highlightValue(nil) // deselect selected data point
}

makeFirstResponder: does not always take cursor

When I display my app with a keyboard shortcut or by tapping an icon in the status bar I set first responder. makeFirstResponder: always succeeds (returns true) with a not-nil window and a not-nil NSTextField. However it doesn't always 'take the cursor' (i.e. move the flashing | to the NSTextField).
For example,
display app - takes cursor ✓
tap outside app
display app again - does not take cursor (even though makeFirstResponder returns true).
Here's how I'm trying to do this:
//Find the key window. I don't think this is the problem because self.window produces the same results.
var keyWindow:NSWindow = popover.contentViewController!.view.window!
for window in NSApplication.sharedApplication().windows{
if (window.keyWindow){
Swift.print("window \(window) is the key window")
keyWindow = window
}
}
//iFR is a variable that I use to keep track of which NSTextField I want to focus on. It's always a valid textField and most of the time (but not always) it's the only NSTextField in the view.
if (initialFirstResponder != nil)
{
if keyWindow.makeFirstResponder(initialFirstResponder)
{
Swift.print("first responder success")
}else
{
//Never happens:
Swift.print("first responder FAIL")
}
}else
{
Swift.print("no initial firstResponder")
}
I feel like I'm bludgeoning the responder chain into submission but,
NSApp.activateIgnoringOtherApps(true)
causes the cursor to always be captured (and asked nicely) to go where I want it to.