Detecting key press event in Swift - macos

I'm trying to find a way to detect if a key (on a keyboard) has been pressed on Swift. Any ideas and suggestions will be greatly appreciated.

Since you updated your question and you wanted to know how to do this for a window, here's an answer. Subclass NSWindow and use this subclass instead.
Your custom class should look like this:
import Cocoa
class EditorWindow: NSWindow {
override func keyDown(event: NSEvent) {
super.keyDown(event)
Swift.print("Caught a key down: \(event.keyCode)!")
}
}
If you've made your window in Interface Builder/XCode, click the window object and go to the Attribute Inspector (⌥+⌘+3). The Attribute Inspector will be in the sidebar on the right. Making sure your window is selected in Interface Builder, at the top of the Attribute Inspector in the Custom Class area put your new class in the class input.
In order to communicate the event from the this window class to my app I add a function to the window that accepts a callback function that I then store in an array of callback functions. I get access to this window through the AppDelegate which can get a weak reference to the current main window. Then in the above function I iterate overall the callbacks and call it with the NSEvent as the argument. I also first check to see if any command keys like the option keys are being pressed first through modifierFlags property on the event. It ends up looking like this:
import Cocoa
typealias Callback = (NSEvent) -> ()
class KeyCaptureWindow: NSWindow {
var keyEventListeners = Array<Callback>()
override func keyDown(event: NSEvent) {
if event.modifierFlags.contains(NSEventModifierFlags.CommandKeyMask) {
super.keyDown(event)
return
}
for callback in keyEventListeners {
callback(event)
}
}
func addKeyEventCallback(callback: Callback) {
keyEventListeners.append(callback)
}
}
And then elsewhere in my code I have a line like so:
let appDelegate = NSApplication.sharedApplication().delegate as! AppDelegate
let mainWindow = appDelegate.getWindow()
mainWindow.addKeyEventCallback(handleKeyEvent)
I added the getWindow method to my app delegate class. This method returns the NSWindow cast to KeyCaptureWindow. There may be a better way to do all this but this works for me. Another way to possibly do this is to use first responders and NSView, but that's not how I've been doing it.

You have to override the keyDown-method.
var direction:String = ""
override func keyDown(theEvent: NSEvent!) // A key is pressed
{
if theEvent.keyCode == 123
{
direction = "left" //get the pressed key
}
else if theEvent.keyCode == 124
{
direction = "right" //get the pressed key
}
println("Key with number: \(theEvent.keyCode) was pressed")
}

Related

SwiftUI Update the mainMenu [SOLVED] kludgey

The Real Question
How do you update the mainMenu in SwiftUI so that it actually works?
I have built a MacOS Document Based application in SwiftUI which includes all of the in-built File menu commands (i.e. Close, Save, Duplicate. Rename... etc.)
Before saving the document, I validate the structure and would like to present a modal dialog to the user if there are any validation errors.
The modal dialog is just a simple OK/Cancel dialog - 'OK' meaning that the user is happy to save the file with validation errors, 'Cancel' would need to stop the save operation.
So the question is: "How do I intercept the in-built 'Save' menu command to present this dialog?
I have tried to overwrite the .saveItem CommandGroup - but this replaces all of the menu items and I only want to override a couple of the commands ('Save' and 'Save As') and don't want to re-implement them all (and I am not sure that I have the skills to do so)
.commands {
CommandGroup(replacing: .saveItem) {
// code goes here - but removes all of the in-built menus
}
}
I have tried this solution (In a SwiftUI Document App, how to save a document from within a function)
and have put it into my AppDelegate
public func applicationDidBecomeActive(_ notification: Notification) {
let menu = NSApplication.shared.mainMenu!.items.first(where: { $0.title == "File" })!
let submenu = menu.submenu!.items.first(where: { $0.title == "Save" })!
submenu.action = #selector(showDialog)
}
#objc func showDialog() {
var retVal: Int = 0
let thisWindow: NSWindow? = NSApplication.shared.mainWindow
let nsAlert: NSAlert = NSAlert()
let cancelButton: NSButton = nsAlert.addButton(withTitle: "Cancel")
cancelButton.tag = 1
let okButton: NSButton = nsAlert.addButton(withTitle: "OK")
okButton.tag = 0
// The below code is replaced
nsAlert.beginSheetModal(for: thisWindow!) { modalResponse in
print(modalResponse)
retVal = modalResponse.rawValue
if retVal == 0 {
print("save")
} else {
print("cancel")
}
}
}
However it doesn't actually call the showDialog function.
Edit/Update
I am still having difficulties updating the menus, but in the above example the call to beginModalSheet is incorrect as the process will run in the background. Updated the call to runModal() which will stop any background process writing the file.
#objc func showDialog() {
let nsAlert: NSAlert = NSAlert()
let cancelButton: NSButton = nsAlert.addButton(withTitle: "Cancel")
cancelButton.tag = 1
let okButton: NSButton = nsAlert.addButton(withTitle: "OK")
okButton.tag = 0
let response: Int = nsAlert.runModal().rawValue
if response == 0 {
print("save")
NSApp.sendAction(#selector(NSDocument.save(_:)), to: nil, from: nil)
} else {
print("cancel")
}
}
I have read somewhere that you need to set the menu before the window appears, and I have also read that you need to set the menus before the AppDelegate is set.
Yet another edit
See this post Hiding Edit Menu of a SwiftUI / MacOS app
and this comment
Thoughts: SwiftUI either has a bug or they really don't want you to remove the top level menus in NSApp.mainMenu. SwiftUI seems to reset the whole menu with no way to override or customize most details currently (Xcode 13.4.1). The CommandGroup(replacing: .textEditing) { }-esque commands don't let you remove or clear a whole menu. Assigning a new NSApp.mainMenu just gets clobbered when SwiftUI wants even if you specify no commands.
XCode 14.1
Swift 5
After a lot of super frustrating searching an attempts and lots of code - I reduced the problem to being just trying to change the name of the save menu item - If I could do this - then I can change the action for it as well.
Here is how I did it
My Tetsing App is called YikesRedux
Steps:
Register the AppDelegate
Override the applicationWillUpdate method
Put the menu updating in a DispatchQueue.main.async closure
Cry tears of joy that you have solved this problem after days of searching
YikesAppRedux.swift
import SwiftUI
#main
struct YikesReduxApp: App {
#NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate // <- Don't forget the AppDelegate
var body: some Scene {
DocumentGroup(newDocument: YikesReduxDocument()) { file in
ContentView(document: file.$document)
}
}
}
AppDelegate.swift
import Foundation
import AppKit
public class AppDelegate: NSObject, NSApplicationDelegate {
public func applicationWillUpdate(_ notification: Notification) {
DispatchQueue.main.async {
let currentMainMenu = NSApplication.shared.mainMenu
let fileMenu: NSMenuItem? = currentMainMenu?.item(withTitle: "File")
if nil != fileMenu {
let saveMenu = fileMenu?.submenu!.item(withTitle: "Save")
if nil != saveMenu {
print("updated menu")
saveMenu?.title = "Save Updated"
}
}
}
}
}
I put this down as a bit kludgey - as it runs on every application update (which is not a lot, but you can see the print out in the console "updated menu" when it does occur)
I did try to keep a state variable as to whether the menu was updated, to try and not do it again - but in a multi document window environment you would need to keep track of every window... (Also swift just clobbers the menu whenever it wants - so it didn't work as well as expected.)
I put the menu updating code in almost everywhere I could think of
Every single AppDelegate function override
init methods for the App, the ContentView
on the document read function/write function
You name it - I put it in there (I even had a hosting controller, a NSViewRepresentable)
I then removed them one by one until I found the solution.
I would be happy if there was a less kludgey way to do this.

How to clear NSTextView selection without it becoming first responder?

I have a basic Cocoa app with a number of NSTextViews. When a text view loses focus (i.e. resigns its first responder status), I'd like to clear its selection.
My strategy was to extend NSTextView and override resignFirstResponder():
override func resignFirstResponder() -> Bool {
// Both result in the text view becoming first responder again:
clearSelection(nil)
setSelectedRange(NSRange(location: 0, length: 0))
return super.resignFirstResponder()
}
The problem is that calling clearSelection() and setSelectedRange() both cause the text view to become first responder again.
Is there a way to clear the selection without it becoming the first responder?
I tried to also override acceptsFirstResponder and temporarily return false, but that didn't work either.
Met the same issue today and found the solution
You can do setSelectedRange in NSTextView's delegate method textDidEndEditing and it wouldn't cause NSTextView become first responder.
class TextView: NSTextView {
init() {
self.delegate = self
....
}
....
}
extension TextView: NSTextViewDelegate {
public func textDidEndEditing(_ notification: Notification) {
setSelectedRange(NSMakeRange(string.count, 0))
}
}

NSOutlineView how to show blue outline during right click

Everyone discussed about how to get rid of the blue outline during right click... but me.
Instead, I'm trying to display the blue outline.
I didn't get any outline when I right clicked my outline view row. The menu appeared but the outline wasn't. You can see that the blue outline is not visible in this picture below:
Below is what I'm trying to achieve.
Update
This is how I implemented the NSMenu. I subclassed the NSOutlineView and made a new protocol to override NSOutlineViewDelegate.
This idea was to make it simple by letting the NSOutlineView ask the NSMenu for each item, so we can implement different menu for each item. It works but the blue outline view doesn't show up during right click.
KRMenuOutlineView.swift
import Cocoa
#objc protocol KRMenuOutlineViewDelegate: NSOutlineViewDelegate {
// This method will ask NSMenu for each item in outline view
func outlineView(_ outlineView: KRMenuOutlineView, menuFor item: Any, event: NSEvent) -> NSMenu?
}
class KRMenuOutlineView: NSOutlineView {
override var delegate: NSOutlineViewDelegate? {
didSet {
if let newValue = delegate {
/*
* Swift doesn't support overriding inherited properties with different type
* like Objective C Does, therefore we need internal delegate.
*/
internalDelegate = unsafeBitCast(newValue, to: KRMenuOutlineViewDelegate.self)
} else {
internalDelegate = nil
}
}
}
private var internalDelegate: KRMenuOutlineViewDelegate?
override func menu(for event: NSEvent) -> NSMenu? {
let point = self.convert(event.locationInWindow, from: nil)
if let item = self.item(atRow: self.row(at: point)) {
return self.internalDelegate?.outlineView(self, menuFor: item, event: event)
}
return super.menu(for: event)
}
}
Then, I use it in my view controller like this:
KRTreeViewController.swift
extension KRTreeViewController: KRMenuOutlineViewDelegate {
func outlineView(_ outlineView: KRMenuOutlineView, menuFor item: Any, event: NSEvent) -> NSMenu? {
let menu = NSMenu(title: "Contextual Menu")
menu.delegate = self
let key = String(utf16CodeUnits: [unichar(NSBackspaceCharacter)], count: 1) as String
let deleteMenuItem = menu.addItem(withTitle: "Delete",
action: #selector(didClickMenuItem(_:)),
keyEquivalent: key)
deleteMenuItem.representedObject = myItem
deleteMenuItem.target = self
return menu
}
#objc fileprivate func didClickMenuItem(_ menuItem: NSMenuItem) {
// ...
}
}
How to properly show a context menu:
If you have created your menu using a storyboard:
First, go to the storyboard and add the menu to the viewController that contains the outlineView.
Then make it an #IBOutlet so you can reference it later.
In a method like viewDidLoad(), add the menu to the outlineView by calling
outlineView.menu = myMenu
where myMenu can either be the one you created in Interface Builder or in code.
You can run the app now and should see the blue outline around the cell.
The problem now is that you don't know which cell the user has clicked.
To fix this, set yourself as the delegate of myMenu and adopt the NSMenuDelegate protocol.
func menuNeedsUpdate(_ menu: NSMenu) {
let row = self.outlineView.clickedRow
guard row != -1 else { return }
for item in menu.items {
item.representedObject = row
}
}
Here you can do whatever you need. This implementation sets the rowIndex as the representedObject of each menu item. Keep in mind that this only works on static outlineViews (ones that don't change in the background) and menus which only go one level deep.
You could also store the index or object represented by the cell (if the outlineView is not static) in a local variable.

Move a NSWindow by dragging a NSView

I have a NSWindow, on which i apply this:
window.styleMask = window.styleMask | NSFullSizeContentViewWindowMask
window.titleVisibility = NSWindowTitleVisibility.Hidden;
window.titlebarAppearsTransparent = true;
I then add a NSView behind the titlebar to simulate a bigger one.
Now it looks like this:
I want to be able to move the window, by dragging the light-blue view. I have already tried to subclass NSView and always returning true for mouseDownCanMoveWindow using this code:
class LSViewD: NSView {
override var mouseDownCanMoveWindow:Bool {
get {
return true
}
}
}
This didn't work.
After some googling i found this INAppStoreWindow on GitHub. However it doesn't support OS X versions over 10.9, so it's completely useless for me.
Edit1
This is how it looks in the Interface Builder.
How can i move the window, by dragging on this NSView?
None of the answers here worked for me. They all either don't work at all, or make the whole window draggable (note that OP is not asking for this).
Here's how to actually achieve this:
To make a NSView control the window with it's drag events, simply subclass it and override the mouseDown as such:
class WindowDragView: NSView {
override public func mouseDown(with event: NSEvent) {
window?.performDrag(with: event)
}
}
That's it. The mouseDown function will transfer further event tracking to it's parent window.
No need for window masks, isMovableByWindowBackground or mouseDownCanMoveWindow.
Try setting the window's movableByWindowBackground property to true.
There are two ways to do this. The first one would be to set the NSTexturedBackgroundWindowMask as well as the windows background color to the one of your view. This should work.
Otherwise you can take a look at this Sample Code
I somehow managed to solve my problem, i don't really know how, but here are some screenshots.
In the AppDelegate file where i edit the properties of my window, i added an IBOutlet of my contentView. This IBOutlet is a subclass of NSView, in which i've overriden the variable mouseDownCanMoveWindow so it always returns false.
I tried this before in only one file, but it didn't work. This however solved the problem.
Thanks to Ken Thomases and Max for leading me into the right direction.
Swift3.0 Version
override func viewDidAppear() {
//for hide the TitleBar
self.view.window?.styleMask = .borderless
self.view.window?.titlebarAppearsTransparent = true
self.view.window?.titleVisibility = .hidden
//for Window movable with NSView
self.view.window?.isMovableByWindowBackground = true
}
Swift 3:
I needed this but dynamically. It's a little long but well worth it (IMHO).
So I decided to enable this only while the command key is down. This is achieved by registering a local key handler in the delegate:
// MARK:- Local key monitor
var localKeyDownMonitor : Any? = nil
var commandKeyDown : Bool = false {
didSet {
let notif = Notification(name: Notification.Name(rawValue: "commandKeyDown"),
object: NSNumber(booleanLiteral: commandKeyDown))
NotificationCenter.default.post(notif)
}
}
func keyDownMonitor(event: NSEvent) -> Bool {
switch event.modifierFlags.intersection(.deviceIndependentFlagsMask) {
case [.command]:
self.commandKeyDown = true
return true
default:
self.commandKeyDown = false
return false
}
}
which is enabled within the delegate startup:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Watch local keys for window movenment, etc.
localKeyDownMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEventMask.flagsChanged) { (event) -> NSEvent? in
return self.keyDownMonitor(event: event) ? nil : event
}
}
and its removal
func applicationWillTerminate(_ aNotification: Notification) {
// Forget key down monitoring
NSEvent.removeMonitor(localKeyDownMonitor!)
}
Note that when the commandKeyDown value is changed by the key down handler. This value change is caught by the didset{} to post a notification. This notification is registered by any view you wish to have its window so moved - i.e., in the view delegate
override func viewDidLoad() {
super.viewDidLoad()
// Watch command key changes
NotificationCenter.default.addObserver(
self,
selector: #selector(ViewController.commandKeyDown(_:)),
name: NSNotification.Name(rawValue: "commandKeyDown"),
object: nil)
}
and discarded when the viewWillDisappear() (delegate) or the window controller windowShouldClose(); add this
<your-view>.removeObserver(self, forKeyPath: "commandKeyDown")
So sequence goes like this:
key pressed/release
handler called
notification posted
The view's window isMovableByWindowBackground property is changed by notification - placed within view controller / delegate or where you registered the observer.
internal func commandKeyDown(_ notification : Notification) {
let commandKeyDown : NSNumber = notification.object as! NSNumber
if let window = self.view.window {
window.isMovableByWindowBackground = commandKeyDown.boolValue
Swift.print(String(format: "command %#", commandKeyDown.boolValue ? "v" : "^"))
}
}
Remove the tracer output when happy. See it in action in SimpleViewer on github.

Xcode_OSX/Swift_NSPopUpButton.

I am incredibly new to this, so please keep that in mind!
I've been at this all night, watched countless videos/haunted
countless forums...I can't find one single answer!
I am trying to make a basic popup menu in Swift/OSX What I need to figure out is:
How can I add more than the 'three items' to this menu
Whatever is selected in the popup, for that info to send an integer
value to another number.
I very much would appreciate your help, Thanks.
A NSPopupButton is a container for a bunch of NSMenuItem objects so to add an item you can use
func addItemWithTitle(_ title: String!)
The NSMenuItem gets constructed for you by the call.
and as you may wish to start from scratch you can use
func removeAllItems()
To clean existing items from the button.
There are also other methods around moving and removing menu items from the button.
A NSPopupButton is-a NSControl so you can use var action: Selector to set the action sent when an item is selected and var target: AnyObject! to control which object receives the message. Or just wire it up in Interface Builder.
protocol FooViewDelegate{
func itemWithIndexWasSelected(value:Int)
}
class FooViewController: NSViewController {
#IBOutlet weak var myPopupButton: NSPopUpButton!
var delegate: FooViewDelegate?
let allTheThings = ["Mother", "Custard", "Axe", "Cactus"]
override func viewDidLoad() {
super.viewDidLoad()
buildMyButton()
}
func buildMyButton() {
myPopupButton.removeAllItems()
myPopupButton.addItemsWithTitles(allTheThings)
myPopupButton.target = self
myPopupButton.action = "myPopUpButtonWasSelected:"
}
#IBAction func myPopUpButtonWasSelected(sender:AnyObject) {
if let menuItem = sender as? NSMenuItem, mindex = find(allTheThings, menuItem.title) {
self.delegate?.itemWithIndexWasSelected(mindex)
}
}
}
All the button construction can be done in Interface Builder rather than code too. Remember that you can duplicate items with CMD-D or you can drag new NSMenuItem objects into the button.

Resources