Indicate that keyUp event has been handled in Swift? - swift

In an NSTextView I am trying to make tab and shift-tab play a role in editing text, without tabs being inserted in the text. Currently I am detecting keypresses via NSTextViewDelegate's keyUp method:
override func keyUp(with event: NSEvent) {
if let currChar = event.characters {
if event.keyCode == 48 { // detect tab key
if event.modifierFlags.rawValue == 256 { // detect no modifier key
// do something
} else if event.modifierFlags.rawValue == 131330 { // detect shift modifier
// do another thing
}
}
}
I can't see anything in the documentation how to indicate to the NSTextView that I want it to ignore the tab key (I have tried the answer shown here, but tabs do not appear to trigger this event), or to prevent the event from moving up the responder chain.
I have also tried calling interpretKeyEvents([event]) at the beginning of my keyUp method, and overriding insertTab and insertBacktab. These are successfully called with the right keypresses, but a tab is still inserted into the text. The documentation seems to suggest it should prevent the tab being inserted:
It [keyDown] can pass the event to Cocoa’s text input management system by invoking the NSResponder method interpretKeyEvents:. The input management system checks the pressed key against entries in all relevant key-binding dictionaries and, if there is a match, sends a doCommandBySelector: message back to the view. Otherwise, it sends an insertText: message back to the view, and the view implements this method to extract and display the text. (emphasis mine)
The documentation talks about an event continuing up the responder chain if it has not been handled - how is this indicated? Is it important that I am using keyUp, not keyDown?

Yes, it matters that you’re overriding keyUp: instead of keyDown:. The key-down event happens before the key-up event, and NSTextView acts on the key-down event. By the time the system has called your keyUp: override, it’s too late to prevent the default handling of the key-down event.

Use custom subclass. If these methods are not being called it means the first responder is someone else and has eaten your event. As long as your textview is first responder your keyDown method will be called.
class MyTextView: NSTextView {
override func insertTab(_ sender: Any?) {
self.insertText("HELLO", replacementRange: NSMakeRange(0, 0))
//OR ANY CUSTOM TEXT EDITING, ACTION TO CHANGE FIRST RESPONDER...
}
override func insertBacktab(_ sender: Any?) {
self.insertText("AAAAA", replacementRange: NSMakeRange(0, 0))
//OR ANY CUSTOM TEXT EDITING, ACTION TO CHANGE FIRST RESPONDER...
}
}
Educational: "Key Event Handling in Cocoa Applications from WWDC 2010"

Related

Why keyDown method does not work in Cocoa macOS?

I want use keyDown method to see which key pressed in keyboard but it does not work also my computer makes sounds to tell the key even does not work.
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func keyDown(with event: NSEvent) {
print(event)
}
}
Only the responders in the current responder chain get a shot at handling events like key events, and then only if some other responder in the chain doesn't handle it first. So, you'll need to make sure that your view controller is managing a view that's the keyboard focus when the event actually takes place, and that the focused view doesn't handle the event itself.

In Swift keyDown(with event: NSEvent) not working when collectionView is selectable

I have an NSCollectionView which lives inside an NSViewController. NSViewController overrides the following:
class myViewController : NSViewController{
weak var collectionView : NSCollectionView!
public override keyDown(with event: NSEvent){
print("Key pressed.")
}
}
However, when collectionView.selectable = true, the view controller no longer receives key down events. I have tried this several ways, and in each case I either capture key too many key events (for example when the view controller is not even in focus) or too few (I don't get they key events at all). Please advise.
KeyDown, keyUp, insertTab, insertText etc. This methods for keyboard click event.
mouseUp, mouseDown, mouseDragged, mouseMoved etc. This methods for mouse click event.
Your mouse event is fired when you click on any point in your application.
And when you write text(A to Z, 0 to 9) in any textfield/textview while keyboard event is fired and ctrl, shift, capsLock, tab etc. click fired without write.

How to prevent right-click textfield renaming in NSOutlineView

I am having a NSOutlineView in which the textfield is editable by either pressing return or using the right click. I am also overriding the menu(for event: NSEvent) -> NSMenu? to menu based on which row is clicked
When I right click on the textfield, it opens the correct menu as expected but it also makes the textfield to go into edit mode. Is there a way to handle this behaviour?
However, when I click outside the textfield then it works without setting the textfield in edit mode:
I have a similar NSTableView where right-clicking is supported to display a menu, but the fields are also directly editable. I took a look to see why/how the editing doesn't seem to be a problem in my case, and I narrowed the behavior down to the presence of this line of code in my NSTableView subclass:
// This trick convinces the accessibility system to bother checking whether
// we have a menu to export to e.g. VoiceOver.
[self setMenu:[[NSMenu alloc] init]];
As you can see from the comment, the rationale for this "setting an empty menu" was for a different reason than avoiding the editing behavior you're seeing, but it appears to have the side-effect of also fixing that.
So, please try adding a line like the above to your NSOutlineView subclass and see if it fixes the problem!
I ran into this same problem trying to get my Swift-based app on Monterey working. I was surprised there was not a more simple/obvious solution, but here is what I came up with:
class MyOutlineView: NSOutlineView {
// prevent text field from becoming editable when context menu is used
override func validateProposedFirstResponder(_ responder: NSResponder,
for event: NSEvent?) -> Bool
{
guard let validEvent = event,
contextMenuTriggered(event: validEvent),
selectedRow != -1,
selectedRow == row(at: convert(validEvent.locationInWindow, from: nil))
else {
return super.validateProposedFirstResponder(responder, for: event)
}
return false
}
private func contextMenuTriggered(event: NSEvent) -> Bool {
return event.modifierFlags.contains(.control)
&& (event.type == .leftMouseDown || event.type == .leftMouseUp)
}
}
This detects if a control-click happened on the selected row in the outline view, then blocks it from receiving focus so the text field does not become editable.
Credit for this approach goes to the answer provided by #strangetimes here: Disable editing of a NSTextFieldCell on right clicking on a row

First responder on mouse down behavior NSControl and NSView

I have a custom control. If it inherits from NSView, it automatically becomes the first responder when I click on it. If it inherits from NSControl, it does not. This difference in behavior persists, even if I override mouseDown(with:) and don't call super.
Code:
class MyControl: NSView {
override var canBecomeKeyView: Bool { return true }
override var acceptsFirstResponder: Bool { return true }
override func drawFocusRingMask() { bounds.fill() }
override var focusRingMaskBounds: NSRect { return bounds }
override func draw(_ dirtyRect: NSRect) {
NSColor.white.set()
bounds.fill()
}
}
As you can see, I override acceptsFirstResponder among other methods and properties that are key view and responder related. I have also checked the refusesFirstResponder property. It is set to false.
What is the reason for this difference in behavior?
Is there a method or property that I can override to influence it?
Say I want the behavior where the view becomes the first responder when clicked and the view inherits from NSControl, is calling window!.makeFirstResponder(self) at the beginning of my mouse-down event handler a good solution or is there a better one?
The property to override is needsPanelToBecomeKey.
A Boolean value indicating whether the view needs its panel to become the key window before it can handle keyboard input and navigation.
The default value of this property is false. Subclasses can override this property and use their implementation to determine if the view requires its panel to become the key window so that it can handle keyboard input and navigation. Such a subclass should also override acceptsFirstResponder to return true.
This property is also used in keyboard navigation. It determines if a mouse click should give focus to a view—that is, make it the first responder). Some views (for example, text fields) want to receive the keyboard focus when you click in them. Other views (for example, buttons) receive focus only when you tab to them. You wouldn't want focus to shift from a textfield that has editing in progress simply because you clicked on a check box.
NSView returns true, NSControl returns false.

Custom Carbon key event handler fails after mouse events

I'm trying to write a custom NSMenu which will be able to list for key input and intercept the necessary events. This is to provide a simple search-as-you-type functionality for my open source clipboard manager.
It seems like the only way to do this is to install a custom Carbon event handler which will listen for key events and handler them accordingly, but it seems like there is an issue with such a custom handler.
Normally, I can propagate events downwards to other handlers (e.g. system ones) and they should be gracefully handled. This can be done by a simple callback:
let eventHandlerCallback: EventHandlerUPP = { eventHandlerCallRef, eventRef, userData in
let response = CallNextEventHandler(eventHandlerCallRef, eventRef!)
print("Response \(response)")
return response
}
This callback works perfectly and prints Response 0 all the time. This response means that the event is handled correctly.
However, things get weird once we send mouse events before keyboard events. In such case, the callback fails and prints Response -9874. This response means that the event was not handled correctly.
It seems like the event fails to be handled somewhere below my custom view and I don't know where exactly or how to overcome this issue.
To reproduce, I've uploaded the code to Gist which can be added to XCode playground and run. Once you see menu popup, press some keys (preferably arrow keys as they won't close the menu) and observe Response 0 in the console. After that, move cursor inside the menu and press more arrow keys. You should see Response -9874 in the console now.
Unclear if you have an NSTextField as your menu view, but if you use one then it's easy to setup a delegate for that text field that can get the current contents of the field as the user types (this takes care of them moving backwards with the arrow keys and then deleting characters, using the delete key, etc). Your delegate implements the appropriate delegate method and gets called each time the text changes:
extension CustomMenuItemViewController: NSTextFieldDelegate {
func controlTextDidChange( _ obj: Notification) {
if let postingObject = obj.object as? NSTextField {
let text = postingObject.stringValue
print("the text is now: \(text)")
}
}
}
Just to confirm this works as expected, I created the ViewController class for the custom menu item views (label + edit field) in a xib file and then dynamically built a simple test menu with a single menu item that has the custom view controller's view and added it to the menubar inside my app delegate:
func installCustomMenuItem() {
let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
let menu = NSMenu(title: "TestMenu" )
let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")
subMenuBarItem.view = menuItemVC.view
menu.addItem(subMenuBarItem)
menuBarItem.submenu = menu
NSApp.mainMenu?.addItem(menuBarItem)
}
Looks like this after I typed "hello":
And you can from the console that my handler got called for every character typed:
the text is now: H
the text is now: He
the text is now: Hel
the text is now: Hell
the text is now: Hello
Your situation is probably a little different, but it seems like this approach is very clean and might work for you. If it won't for some reason, add a clarifying comment and we'll see if we can't make it work for you.
Addition:
It occurred to me that you might wish to not use NSTextField and so I was curious if it was as easy to do this with a custom view and it's relatively easy.
Make a subclass of NSView:
class CustomMenuView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
override func keyDown(with event: NSEvent) {
print("key down with character: \(String(describing: event.characters)) " )
}
}
Set the class of your root view in the custom view controller to be of this type of class and then all done as before - view controller loaded in applicationDidFinishLaunching and menus built and view controller's view (which is now a CustomMenuView) is set as the menuBarItem.view.
That's it. You now get your keyDown method called for every key down when the menu is dropped down.
key down with character: Optional("H")
key down with character: Optional("e")
key down with character: Optional("l")
key down with character: Optional("l")
key down with character: Optional("o")
key down with character: Optional(" ")
key down with character: Optional("T")
key down with character: Optional("h")
key down with character: Optional("i")
key down with character: Optional("s")
key down with character: Optional(" ")
key down with character: Optional("i")
key down with character: Optional("s")
key down with character: Optional(" ")
key down with character: Optional("c")
key down with character: Optional("o")
key down with character: Optional("o")
key down with character: Optional("l")
:)
Now your custom view (and subviews if you like) can do their own drawing and so on.
Addition with requested sample without the ViewController:
// Simple swift playground test
// The pop-up menu will show up onscreen in the playground at a fixed location.
// Click in the popup and then all key commands will be logged.
// The ViewController in my example above may be taking care of putting the custom view in the responder chain, or the fact that it's in a menubar and being invoked via a MenuItem might be.
// I'd suggest trying it in the actual environment rather than in a playground. In my test app you click the menu name in the menubar to drop down the menu and it is added to the responder chain and works as expected without having to click in the menu first to get the events flowing.
// There is no reason you need to be hooking events either with carbon events or the newer format. If you're in the responder chain of and implement the necessary, method then you'll get the key events you're looking for.
import AppKit
class CustomMenuView: NSView {
override var acceptsFirstResponder: Bool {
return true
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
}
override func keyDown(with event: NSEvent) {
print("key down with character: \(String(describing: event.characters)) " )
}
}
func installCustomMenuItem() -> NSMenu {
// let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
let resultMenu = NSMenu(title: "TestMenu" )
let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")
subMenuBarItem.view = CustomMenuView(frame: NSRect(x: 0, y: 0, width: 40, height: 44))
resultMenu.addItem(subMenuBarItem)
// menuBarItem.submenu = menu
return resultMenu
}
var menu = installCustomMenuItem()
menu.popUp(positioning: nil, at: NSPoint(x:600,y:400), in: nil)
I didn't manage to figure out why this issue was happening or how to fix it, but I understood that it's possible to work around this issue by intercepting all the keys and simulating their behavior manually.
For example, this is how I now handle down arrow key which is supposed to select next item in menu list:
class Menu: NSMenu {
func selectNext() {
var indexToHighlight = 1
if let item = highlightedItem {
indexToHighlight = index(of: item) + 1
}
if let itemToHighlight = self.item(at: indexToHighlight) {
let highlightItemSelector = NSSelectorFromString("highlightItem:")
perform(highlightItemSelector, with: itemToHighlight)
if itemToHighlight.isSeparatorItem || !itemToHighlight.isEnabled || itemToHighlight.isHidden {
selectNext()
}
}
}
}
This way, when I receive a key down event with down arrow key - I can just call the function and return true to prevent the event from reaching default NSMenu handler. Similarly, up arrow key can be done.
In case of a return key, I ended up with the following code:
class Menu: NSMenu {
func select() {
if let item = highlightedItem {
performActionForItem(at: index(of: item))
cancelTracking()
}
}
}
The full commit implementing this is https://github.com/p0deje/Maccy/commit/158610d1d.