How to continue to receive key events in SwiftUI on macOS? - macos

I have an application that needs to respond to key down/up and modifier changed events. This is the code for the view that receives the keyboard events:
struct KeyAwareView: NSViewRepresentable {
let onEvent: (NSEvent) -> Void
func makeNSView(context: Context) -> NSView {
let view = KeyView()
view.onEvent = onEvent
DispatchQueue.main.async {
view.window?.makeFirstResponder(view)
}
return view
}
func updateNSView(_ nsView: NSView, context: Context) {}
}
private class KeyView: NSView {
var onEvent: (NSEvent) -> Void = { _ in }
override var acceptsFirstResponder: Bool { true }
override func keyDown(with event: NSEvent) {
onEvent(event)
}
override func keyUp(with event: NSEvent) {
onEvent(event)
}
override func flagsChanged(with event: NSEvent) {
onEvent(event)
}
}
I then have this view layered in a ZStack:
KeyAwareView(onEvent: { event in
switch event.type {
case .keyDown:
KeyboardEventResponder.shared.handleKeyDown(keyCode: Int(event.keyCode))
case .keyUp:
KeyboardEventResponder.shared.handleKeyUp(keyCode: Int(event.keyCode))
case .flagsChanged:
KeyboardEventResponder.shared.updateModifiers(newModifiers: event.modifierFlags)
default:
break
}
})
The main window is a master-detail view which looks like this (some detail removed):
ZStack() {
NavigationView {
LayoutList(layoutsList: $document.layouts, selectedLayout: $selectedLayout)
if selectedLayout != nil {
let index = document.layouts.firstIndex(of: selectedLayout!)!
if let layout = document.loadLayout(atIndex: index) {
LayoutEditor(layoutData: $document.layouts[index])
.environmentObject(keyboardStatus)
}
}
}
KeyAwareView { event in
switch event.type {
case .keyDown:
KeyboardEventResponder.shared.handleKeyDown(keyCode: Int(event.keyCode))
case .keyUp:
KeyboardEventResponder.shared.handleKeyUp(keyCode: Int(event.keyCode))
case .flagsChanged:
KeyboardEventResponder.shared.updateModifiers(newModifiers: event.modifierFlags)
default:
break
}
}
}
This all works properly when the first layout is selected. But then when you click on another layout, the KeyAwareView doesn't receive any events until I click somewhere in the window apart from selecting another layout in the list.
What can I do to make the view receive events? I tried adding
DispatchQueue.main.async {
nsView.window?.makeFirstResponder(nsView)
}
to the updateNSView method, but that made no difference.

Related

SwiftUI ForEach's onMove() modifier stop working if the content view has tap gesture action

I want to make a list view which contents could be dragged for reordering and tapped for detail.
But when I add the onTapGesture() action in the content view, the onMove() action stop working.
Example code:
import SwiftUI
struct TestTapAndMove: View {
#State private var users = ["Paul", "Taylor", "Adele"]
var body: some View {
List {
ForEach(users, id: \.self) { user in
VStack {
Text("user: \(user)").font(.headline)
Divider()
}.background(Color.gray)
// The TapGesture will result in onMove action not working.
.onTapGesture(perform: {
print("tap!")
})
}
.onMove(perform: move)
}
}
func move(from source: IndexSet, to destination: Int) {
print("onMove!")
users.move(fromOffsets: source, toOffset: destination)
}
}
Is there any solution that could make the tap and move action work together?
As workaround you can make some specific part of row clickable, like text in below example (or some added custom shape or plain button)
VStack {
Text("user: \(user)").font(.headline)
.onTapGesture(perform: {
print("tap!")
})
Divider()
}.background(Color.gray)
Alternate: you can add helper custom view as row overlay which would handle tap/mouseDown action for view (and does not break drug)
class TapHandlerView: NSView {
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event) // keep this to allow drug !!
print("tap")
}
}
struct TapHandler: NSViewRepresentable {
func makeNSView(context: Context) -> TapHandlerView {
TapHandlerView()
}
func updateNSView(_ nsView: TapHandlerView, context: Context) {
}
}
and use it
VStack {
Text("user: \(user)").font(.headline)
Divider()
}.background(Color.gray)
.overlay(TapHandler()) // << here !!
Tested with Xcode 12.0 / macOS 10.15.6

NSView does not receive mouseUp: event when mouse button is released outside of view

I have a custom NSView subclass with (for example) the following methods:
override func mouseDown(with event: NSEvent) { Swift.print("mouseDown") }
override func mouseDragged(with event: NSEvent) { Swift.print("mouseDragged") }
override func mouseUp(with event: NSEvent) { Swift.print("mouseUp") }
As long as the mouse (button) is pressed, dragged and released all inside the view, this works fine. However, when the mouse is depressed inside the view, moved outside the view, and only then released, I never receive the mouseUp event.
P.S.: Calling the super implementations does not help.
The Handling Mouse Dragging Operations section of Apple's mouse events documentation provided a solution: Apparently, we do receive the mouseUp event when tracking events with a mouse-tracking loop.
Here's a variant of the sample code from the documentation, adapted for Swift 3:
override func mouseDown(with event: NSEvent) {
var keepOn = true
mouseDownImpl(with: event)
// We need to use a mouse-tracking loop as otherwise mouseUp events are not delivered when the mouse button is
// released outside the view.
while true {
guard let nextEvent = self.window?.nextEvent(matching: [.leftMouseUp, .leftMouseDragged]) else { continue }
let mouseLocation = self.convert(nextEvent.locationInWindow, from: nil)
let isInside = self.bounds.contains(mouseLocation)
switch nextEvent.type {
case .leftMouseDragged:
if isInside {
mouseDraggedImpl(with: nextEvent)
}
case .leftMouseUp:
mouseUpImpl(with: nextEvent)
return
default: break
}
}
}
func mouseDownImpl(with event: NSEvent) { Swift.print("mouseDown") }
func mouseDraggedImpl(with event: NSEvent) { Swift.print("mouseDragged") }
func mouseUpImpl(with event: NSEvent) { Swift.print("mouseUp") }
Am posting this as an answer to a similar question that I had where I needed to know that the user had stopped using a slider. I needed to capture the mouseUp event from NSSlider or actually NSView. The solution that worked out for me was to simply capture the mouseDown event and add some code when it exited and does the job that I needed. Hope that this is of use to somebody else who needs to do a similar thing. Code written using XCode 11.3.1 Swift 5
import Cocoa
class SMSlider: NSSlider {
var calledOnExit:(()->())?
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
if self.calledOnExit != nil {
self.calledOnExit!()
}
}
}
// In my main swift app
func sliderStopped() {
print("Slider stopped moving")
}
//...
if slider == nil {
slider = SMSlider()
}
slider?.isContinuous = true
slider?.target = self
slider?.calledOnExit = sliderStopped
//...

Cocoa: react to keyDown in QLPreviewPanel

I implemented quick look in my project in the following way in Swift 2 (I'm including this here for reference and because it might help someone else set it up).
My NSViewController contains a NSTableView subclass where I implemented keyDown to listen to the spacebar key being pressed (maybe not the best way but it works):
override func keyDown(theEvent: NSEvent) {
let s = theEvent.charactersIgnoringModifiers!
let s1 = s.unicodeScalars
let s2 = s1[s1.startIndex].value
let s3 = Int(s2)
if s3 == Int(" ".utf16.first!) {
NSNotificationCenter.defaultCenter().postNotification(NSNotification(name: "MyTableViewSpacebar", object: nil))
return
}
super.keyDown(theEvent)
}
In my view controller, I have an observer for this notification and the functions required by the QLPreviewPanel:
//...
class ViewController: NSViewController {
#IBOutlet weak var myTableView: MyTableView!
var files = [FilesListData]() //array of custom class
//...
override func viewDidLoad() {
//...
NSNotificationCenter.defaultCenter().addObserver(self, selector: "spaceBarKeyDown:", name: "MyTableViewSpacebar", object: nil)
}
func spaceBarKeyDown(notification: NSNotification) {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
panel.makeKeyAndOrderFront(self)
}
}
override func acceptsPreviewPanelControl(panel: QLPreviewPanel!) -> Bool {
return true
}
override func beginPreviewPanelControl(panel: QLPreviewPanel!) {
panel.delegate = self
panel.dataSource = self
}
override func endPreviewPanelControl(panel: QLPreviewPanel!) {
}
}
extension ViewController: QLPreviewPanelDataSource {
func numberOfPreviewItemsInPreviewPanel(panel: QLPreviewPanel!) -> Int {
return self.myTableView.selectedRowIndexes.count
}
func previewPanel(panel: QLPreviewPanel!, previewItemAtIndex index: Int) -> QLPreviewItem! {
if self.myTableView.selectedRow != -1 {
var items = [QLPreviewItem]()
let manager = NSFileManager.defaultManager()
for i in self.myTableView.selectedRowIndexes {
let path = self.files[i].path //path to a MP3 file
if manager.fileExistsAtPath(path) {
items.append(NSURL(fileURLWithPath: path))
} else {
items.append(qm_url) //image of a question mark used as placeholder
}
}
return items[index]
} else {
return qm_url //image of a question mark used as placeholder
}
}
}
What I would like to do now is listen to the keys "up arrow" and "down arrow" being pressed while the quick look panel is open, in order to change the selected row in the NSTableView, much like Finder behaves when you preview files with quick look. I have no clue as to how I could implement this. Any ideas?
Thanks.
Finally found what I was looking for and it's actually pretty simple.
Since my main view controller is also my delegate for the QLPreviewPanel, I added this:
extension ViewController: QLPreviewPanelDelegate {
func previewPanel(panel: QLPreviewPanel!, handleEvent event: NSEvent!) -> Bool {
let kc = event.keyCode
if (kc == 126 || kc == 125) { //up and down arrows
if event.type == NSEventType.KeyDown {
self.myTableView.keyDown(event) //send the event to the table
} else if event.type == NSEventType.KeyUp {
self.myTableView.keyUp(event)
}
return true
}
return false
}
}
Then in my table view delegate:
func tableViewSelectionDidChange(notification: NSNotification) {
guard myTableView.numberOfSelectedRows > 0 else {
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.close()
}
}
return
}
if let panel = QLPreviewPanel.sharedPreviewPanel() {
if panel.visible {
panel.reloadData()
}
}
}
That's it! The QLPreviewPanelDataSource handles the rest.

Swift - Capture keydown from NSViewController

I would like to capture keyevents in my little app.
What I have done:
class ViewController : NSViewController {
...
override func keyDown(theEvent: NSEvent) {
if theEvent.keyCode == 124 {
println("abc")
} else {
println("abcd")
}
}
override var acceptsFirstResponder: Bool {
return true
}
override func becomeFirstResponder() -> Bool {
return true
}
override func resignFirstResponder() -> Bool {
return true
}
...
}
What happens:
When a key pressed, the Funk sound effect plays.
I've seen many posts talking about how this is a delegate the belongs to NSView and NSViewController does not have access. But the keydown function override auto completes in a class of type NSViewController leading me to believe that this is wrong.
Xcode 8.2.1 • Swift 3.0.2
import Cocoa
class ViewController: NSViewController {
#IBOutlet var textField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) {
self.flagsChanged(with: $0)
return $0
}
NSEvent.addLocalMonitorForEvents(matching: .keyDown) {
self.keyDown(with: $0)
return $0
}
}
override func keyDown(with event: NSEvent) {
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.command] where event.characters == "l",
[.command, .shift] where event.characters == "l":
print("command-l or command-shift-l")
default:
break
}
textField.stringValue = "key = " + (event.charactersIgnoringModifiers
?? "")
textField.stringValue += "\ncharacter = " + (event.characters ?? "")
}
override func flagsChanged(with event: NSEvent) {
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.shift]:
print("shift key is pressed")
case [.control]:
print("control key is pressed")
case [.option] :
print("option key is pressed")
case [.command]:
print("Command key is pressed")
case [.control, .shift]:
print("control-shift keys are pressed")
case [.option, .shift]:
print("option-shift keys are pressed")
case [.command, .shift]:
print("command-shift keys are pressed")
case [.control, .option]:
print("control-option keys are pressed")
case [.control, .command]:
print("control-command keys are pressed")
case [.option, .command]:
print("option-command keys are pressed")
case [.shift, .control, .option]:
print("shift-control-option keys are pressed")
case [.shift, .control, .command]:
print("shift-control-command keys are pressed")
case [.control, .option, .command]:
print("control-option-command keys are pressed")
case [.shift, .command, .option]:
print("shift-command-option keys are pressed")
case [.shift, .control, .option, .command]:
print("shift-control-option-command keys are pressed")
default:
print("no modifier keys are pressed")
}
}
}
To get rid of the purr sound when pressing the character keys you need to subclass your view, override the method performKeyEquivalent and return true.
import Cocoa
class View: NSView {
override func performKeyEquivalent(with event: NSEvent) -> Bool {
return true
}
}
Sample Project
Swift4
Just found a solution for the very same problem, Swift4. The idea behind that: if the pressed key was handled by a custom logic, the handler shall return nil, otherwise the (unhandled) event...
class MyViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// ...
NSEvent.addLocalMonitorForEvents(matching: .keyDown) {
if self.myKeyDown(with: $0) {
return nil
} else {
return $0
}
}
}
func myKeyDown(with event: NSEvent) -> Bool {
// handle keyDown only if current window has focus, i.e. is keyWindow
guard let locWindow = self.view.window,
NSApplication.shared.keyWindow === locWindow else { return false }
switch Int( event.keyCode) {
case kVK_Escape:
// do what you want to do at "Escape"
return true
default:
return false
}
}
}
And here we are: no Purr / Funk sound when key is pressed...
[Update] Added check of keyWindow. Without this, keyDown() is fired even if another view/window contains the first responder...
I was trying to find an answer for swift 3, here is what worked for me:
Swift 3
import Cocoa
// We subclass an NSView
class MainView: NSView {
// Allow view to receive keypress (remove the purr sound)
override var acceptsFirstResponder : Bool {
return true
}
// Override the NSView keydown func to read keycode of pressed key
override func keyDown(with theEvent: NSEvent) {
Swift.print(theEvent.keyCode)
}
}
I manage to get it work from subclass of NSWindowController
class MyWindowController: NSWindowController {
override func keyDown(theEvent: NSEvent) {
print("keyCode is \(theEvent.keyCode)")
}
}
UPDATE:
import Cocoa
protocol WindowControllerDelegate {
func keyDown(aEvent: NSEvent)
}
class WindowController: NSWindowController {
var delegate: WindowControllerDelegate?
override func windowDidLoad() {
super.windowDidLoad()
delegate = window?.contentViewController as! ViewController
}
override func keyDown(theEvent: NSEvent) {
delegate?.keyDown(theEvent)
}
}
and ViewController:
class ViewController: NSViewController, WindowControllerDelegate {
#IBOutlet weak var textField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
override func keyDown(theEvent: NSEvent) {
textField.stringValue = "key = " + (theEvent.charactersIgnoringModifiers
?? "")
textField.stringValue += "\ncharacter = " + (theEvent.characters ?? "")
textField.stringValue += "\nmodifier = " + theEvent.modifierFlags.rawValue.description
}
}
let kLeftArrowKeyCode: UInt16 = 123
let kRightArrowKeyCode: UInt16 = 124
let kDownArrowKeyCode: UInt16 = 125
let kUpArrowKeyCode: UInt16 = 126
override func keyDown(with event: NSEvent) {
switch event.keyCode {
case kLeftArrowKeyCode:
print("left")
break
case kRightArrowKeyCode:
print("right")
break
case kDownArrowKeyCode:
print("down")
break
case kUpArrowKeyCode:
print("up")
break
default:
print("other")
super.keyDown(with: event)
break
}
print("Key with number: \(event.keyCode) was pressed")
}

Cocoa Button that will light up with mouse over

Is there a flag that can be set that will cause a Cocoa button to be highlighted when it is moused over. I need to this programatically with objective C on OSX.
Setup a tracking area for the view with addTrackingArea (provided you are using Leopard or newer OS X). You'll get events on mouse enter and mouse exit.
something below maybe the answer.
class HoverButton: NSButton{
var backgroundColor: NSColor?
var hoveredBackgroundColor: NSColor?
var pressedBackgroundColor: NSColor?
private var hovered: Bool = false
override var wantsUpdateLayer:Bool{
return true
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInit()
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.commonInit()
}
func commonInit(){
self.wantsLayer = true
self.createTrackingArea()
self.hovered = false
self.hoveredBackgroundColor = NSColor.selectedTextBackgroundColor()
self.pressedBackgroundColor = NSColor.selectedTextBackgroundColor()
self.backgroundColor = NSColor.clearColor()
}
private var trackingArea: NSTrackingArea!
func createTrackingArea(){
if(self.trackingArea != nil){
self.removeTrackingArea(self.trackingArea!)
}
let circleRect = self.bounds
let flag = NSTrackingAreaOptions.MouseEnteredAndExited.rawValue + NSTrackingAreaOptions.ActiveInActiveApp.rawValue
self.trackingArea = NSTrackingArea(rect: circleRect, options: NSTrackingAreaOptions(rawValue: flag), owner: self, userInfo: nil)
self.addTrackingArea(self.trackingArea)
}
override func mouseEntered(theEvent: NSEvent) {
self.hovered = true
NSCursor.pointingHandCursor().set()
self.needsDisplay = true
}
override func mouseExited(theEvent: NSEvent) {
self.hovered = false
NSCursor.arrowCursor().set()
self.needsDisplay = true
}
override func updateLayer() {
if(hovered){
if (self.cell!.highlighted){
self.layer?.backgroundColor = pressedBackgroundColor?.CGColor
}
else{
self.layer?.backgroundColor = hoveredBackgroundColor?.CGColor
}
}
else{
self.layer?.backgroundColor = backgroundColor?.CGColor
}
}
}
link: https://github.com/fancymax/HoverButton
It is also possible to override the button cell to propagate the mouse enter/exit event to the view. I did not test all buttons styles, I use Recessed style.
The drawBezel function is called on mouseEnter if showsBorderOnlyWhileMouseInside is true.
That's why I simply override it, and manage my custom display behaviour in the button.
class myButtonCell: NSButtonCell {
override func mouseEntered(with event: NSEvent) {
controlView?.mouseEntered(with: event)
// Comment this to remove title highlight (for button style)
super.mouseEntered(with: event)
}
override func mouseExited(with event: NSEvent) {
controlView?.mouseExited(with: event)
// Comment this to remove title un-highlight (for recessed button style)
// Here, we un-hilight the title only if the button is not selected
if state != .on { super.mouseExited(with: event) }
}
// removes the default highlight behavior
override func drawBezel(withFrame frame: NSRect, in controlView: NSView) {}
}
// Set the cell class to MyButtonCell in interface builder
class MyButton: NSButton {
var mouseIn: Bool = false { didSet {
// Up to you here - setNeedsDisplay() or layer update
}}
override func mouseEntered(with event: NSEvent) { mouseIn = true }
override func mouseExited(with event: NSEvent) { mouseIn = false }
}

Resources