NSTextField - notifications when individual keys are pressed - macos

I am making an app that will add sound to keypresses as the user types in an NSTextField. I need to capture keystrokes and know what each individual keypress is (like "d" or "space" or "6"). The app depends on this. There is no other way around it.
Each window is an NSDocument File Owner, and it has a single NSTextField in it, which is where the document data is parsed, and the user will type.
After hours of parsing the Internet for answers and hacking away at code, the four most commonly repeated answers are:
"that is not how things work, here is (irrelevant answer)"
"you are new to Cocoa, that is a bad idea, use control:textView:doCommandSelector:" that doesn't give me individual keys, and some keys need their own unique sound trigger.
"use controlTextDidChange: or textView:shouldChangeTextInRange:replaceString:" controlTextDidChange doesn't give me individual keys, and the second one only works for textViews or UIKit.
People get confused and answer with recommendations for UIKit instead of AppKit, which is iOS-only.
The weird thing is that if I subclass NSTextField, it receives -keyUp. I don't know where -keyDown is going.
So my ultimate question is: can you tell me some kind of step-by-step way to actually capture the keyDown that is sent to NSTextField? Even if it's a hack. Even if it's a terrible idea.
I would love to solve this problem! I am very grateful for your reading.

controlTextDidChange is quite a good solution, but don't forget this 2 important things:
Set the delegate binding of the textField to the object where you define the controlTextDidChange method. Commonly, in document based apps it is the window controller, otherwise your app delegate.
Set the textField's control to "continous" in the attribute inspector section
If you miss those points, you will have no result.

This is a pretty old question, but as I was trying to implement a NSTextField that could react to keyDown so that I could create a hotkey preferences control I found I wanted the answer to this question.
Unfortunately this is a pretty non-standard use and I didn't find any places that had a direct answer, but I've come up with something that works after digging through the documentation (albeit in Swift 4) and I wanted to post it here in case it helps someone else with a non-standard use case.
This is largely based off of the information gleaned from the Cocoa Text Architecture Guide
There are three components to my solution:
Creating your NSWindowController and setting a NSWindowDelegate on your NSWindow:
guard let windowController = storyboard.instanciateController(withIdentifier:NSStoryboard.SceneIdentifier("SomeSceneIdentifier")) as? NSWindowController else {
fatalError("Error creating window controller");
}
if let viewController = windowController.contentViewController as? MyViewController {
windowController.window?.delegate=viewController;
}
Your NSWindowDelegate
class MyViewController: NSViewController, NSWindowDelegate {
// The TextField you want to capture keyDown on
var hotKeyTextField:NSTextField!;
// Your custom TextView which will handle keyDown
var hotKeySelectionFieldEditor:HotKeySelectionTextView = HotKeySelectionTextView();
func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any? {
// If the client (NSTextField) requesting the field editor is the one you want to capture key events on, return the custom field editor. Otherwise, return nil and get the default field editor.
if let textField = client as? NSTextField, textField.identifier == hotKeyTextField.identifier {
return hotKeySelectionFieldEditor;
}
return nil;
}
}
Your custom TextView where you handle keyDown
class HotKeySelectionTextView: NSTextView {
public override func keyDown(with event: NSEvent) {
// Here you can capture the key presses and perhaps save state or communicate back to the ViewController with a delegate pattern if you prefer.
}
}
I fully admit that this feels like a workaround somewhat, but as I am experimenting with Swift at the moment and not quite up to speed with all of the best practices yet I can't make an authoritative claim as to the "Swift-i-ness" of this solution, only that it does allow a NSTextField to capture keyDown events indirectly while maintaining the rest of the NSTextField functionality.

Try like this if you print nslog you will get individual character record for example you pressd "A" you will get the same in console:-
-(void)controlTextDidChange:(NSNotification*)obj
{
NSLog(#"%#",[yourTextfield stringValue]);
}
Also, not sure this is only your requirement.

Text editing for an NSTextField is handled by an NSTextView provided by the window, called the field editor. See the NSWindow method fieldEditor:forObject: and the NSWindowDelegate method windowWillReturnFieldEditor:toObject:. I suppose you could use one of these to provide your own subclassed NSTextView as the field editor. Or, could you simply use NSTextView instead of NSTextField?

Related

How to intercept keystrokes from within the field editor of an NSTextField?

Intro
When my custom NSTextField is in "text editing mode" and the field editor has been placed in front of it as firstResponder I no longer get the keystrokes through NSTextField.keyDown(...). I understand that the keystrokes are now being routed through the field editor. Most online recommendations are to override the following method within the delegate of the custom NSTextField:
control(_:textView:doCommandBySelector:)
I have indeed overridden this method but it doesn't seem to get called? See code below:
class FocusDelegate: NSObject, NSTextFieldDelegate{
func control(control: NSControl, textView: NSTextView, doCommandBySelector commandSelector: Selector) -> Bool {
println("FocusDelegate")
if commandSelector == Selector("cancelOperation:"){
println("cancelOperation")
control.abortEditing()
return true
}
return false
}
}
The only other thing I do is set the delegate of my custom NSTextField to an instance of the class above. I have also tried placing the method above directly into the custom NSTextField class - it doesn't cause any errors but it also doesn't get called.
Questions
What am I missing above in getting the delegate call to work?
Other than creating my own custom field editor is this the only way? Can the textDidChange(notification: NSNotification) not be interrogated to yield the keys pressed?
In using the control(_:textView:doCommandBySelector:) delegate method, how do I trap the pressing of keys that do NOT have standard key bindings. In particular, I want to intercept the key combination "shift+enter" which does not map to any standard selector. (Implied Question: can you only map to standard key-action methods in NSResponder?)
I believe I have achieved greater clarity with regard to the workings of NSTextField its delegates and its fieldEditor and its delegates. Yes, each one has its own delegate and the NSTextField is automatically set up as the delegate of the fieldEditor.
I was adding a delegate to the host NSTextField. This will NOT get called when the fieldEditor is firstResponder, which was what I was concerned with. This is also not the important delegate for the scenario above.
What appears to be useful is making the NSTextField conform to the NSTextViewDelegate protocol and specifically overriding the method:
func textView(textView: NSTextView, shouldChangeTextInRange affectedCharRange: NSRange, replacementString: String) -> Bool
to trap various keypresses.
The notification in textDidChange(notification: NSNotification) is not of use in this scenario because it is reported AFTER the keypress.
Intercepting pressed keys in textView:shouldChangeTextInRange:replacementString: is not ideal because that is called rather late, and is also called when the user pastes from the clipboard, for instance.
A probably cleaner way is to intercept textView:doCommandBySelector:, which gets called for all kinds of special keys such as tab, cursor, return and enter, for which you'll then get specific selectors that let you determine which special key was invoked.
For some keys, however, such as Return (CR) and Enter (ETX), you can't tell them apart, because both use the same "insertNewline:" selector.
In this case, you can check whether the currently handled event is a keydown event, and if so, check which key it was.
Here's a quick test code that lets you monitor the selector names and the involved keycode:
-(BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
{
NSEvent *ev = self.view.window.currentEvent;
NSLog (#"selector: %s, key: %d", commandSelector, (int)ev.keyCode);
return NO;
}

Override key equivalent for NSButton in NSTextField

I have a window with two NSButtons and an NSTextField along with several views and several other controls. I assign the right and left arrows keys to each of the two NSButtons. The two buttons respond to the right and left arrow keys. However, when in the NSTextField, I want the arrow keys to perform as the normally would in a text field and not trigger the NSButtons. I have tried reading through the Cocoa Key Handling documentation and other questions regarding key events, but I could not find an example of trying change the key equivalent behavior in one control. I tried subclassing the NSTextField but couldn't trap the arrow keys. How can this be implemented?
You can override becomeFirstResponder: and in your implementation call setKeyEquivalent:. If you want to remove the key equivalent when the button loses first responder status, override resignFirstResponder:.
Do this in the control whose first-responder status you want to affect the button's equivalent. For example, if you have a view as a container and it can become first responder, you'd override -becomeFirstResponder: (calling super) then manage the button's equivalent there. If you don't yet understand these topics, you have a lot of prerequisite reading to do because a simple answer isn't possible here.
You could subclass NSButton and override performKeyEquivalent: like so:
override func performKeyEquivalent(event: NSEvent) -> Bool {
guard
let window = window where window.firstResponder.isKindOfClass(NSText.self) == false
else {
return false
}
return super.performKeyEquivalent(event)
}
This essentially disables the key equivalent if the first responder is a text field/view.

NSMenuItem KeyEquivalent " "(space) bug

I want to set key equivalent " "(space) without any modifiers for NSMenuItem (in App Main Menu).
As follows from documentation:
For example, in an application that plays media, the Play command may be mapped to just “ ” (space), without the command key. You can do this with the following code:
[menuItem setKeyEquivalent:#" "];
[menuItem setKeyEquivalentModifierMask:0];
Key Equivalent sets successfully, but it don't work. When I press "Space" key without modifiers nothing happens, but it's works when i press "Space" with "Fn" modifier key.
I need to use "Space" without modifiers. Any help please!
This is a tricky question. Like many answers suggest, intercepting the event at the application or window level is a solid way to force the menu item to work. At the same time it is likely to break other things, for example, if you have a focused NSTextField or NSButton you'd want them to consume the event, not the menu item. This might also fail if the user redefines the key equivalent for that menu item in system preferences, i.e., changes Space to P.
The fact that you're using the space key equivalent with the menu item makes things even trickier. Space is one of the special UI event characters, along with the arrow keys and a few others, that the AppKit treats differently and in certain cases will consume before it propagates up to the main menu.
So, there are two things to keep in mind. First, is the standard responder chain:
NSApplication.sendEvent sends event to the key window.
Key window receives the event in NSWindow.sendEvent, determines if it is a key event and invokes performKeyEquivalent on self.
performKeyEquivalent sends it to the current window's firstResponder.
If the responder doesn't consume it, the event gets recursively sent upwards to the nextResponder.
performKeyEquivalent returns true if one of the responders consumes the event, false otherwise.
Now, the second and tricky part, if the event doesn't get consumed (that is when performKeyEquivalent returns false) the window will try to process it as a special keyboard UI event – this is briefly mentioned in Cocoa Event Handling Guide:
The Cocoa event-dispatch architecture treats certain key events as commands to move control focus to a different user-interface object in a window, to simulate a mouse click on an object, to dismiss modal windows, and to make selections in objects that allow selections. This capability is called keyboard interface control. Most of the user-interface objects involved in keyboard interface control are NSControl objects, but objects that aren’t controls can participate as well.
The way this part works is pretty straightforward:
The window converts the key event in a corresponding action (selector).
It checks with the first responder if it respondsToSelector and invokes it.
If the action was invoked the event gets treated as consumed and the event propagation stops.
So, with all that in mind, you must ensure two things:
The responder chain is correctly set up.
Responders consumes only what they need and propagate events otherwise.
The first point rarely gives troubles. The second one, and this is what happens in your example, needs taking care of – the AVPlayer would typically be the first responder and consume the space key event, as well as a few others. To make this work you need to override keyUp and keyDown methods to propagate the event up the responder chain as would happen in the default NSView implementation.
// All player keyboard gestures are disabled.
override func keyDown(with event: NSEvent) {
self.nextResponder?.keyDown(with: event)
}
// All player keyboard gestures are disabled.
override func keyUp(with event: NSEvent) {
self.nextResponder?.keyUp(with: event)
}
The above forwards the event up the responder chain and it will eventually be received by main menu. There's one gotcha, if first responder is a control, like NSButton or any custom NSControl-inheriting object, it WILL consume the event. Typically you do want this to happen, but if not, for example when implementing custom controls, you can override respondsToSelector:
override func responds(to selector: Selector!) -> Bool {
if selector == #selector(performClick(_:)) { return false }
return super.responds(to: selector)
}
This will prevent the window from consuming the keyboard UI event, so the main menu can receive it instead. However, if you want to intercept ALL keyboard UI events, including when the first responder is able to consume it, you do want to override your window's or application's performKeyEquivalent, but without duplicating it as other answers suggest:
override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Attempt to perform the key equivalent on the main menu first.
if NSApplication.shared.mainMenu?.performKeyEquivalent(with: event) == true { return true }
// Continue with the standard implementation if it doesn't succeed.
return super.performKeyEquivalent(with: event)
}
If you invoke performKeyEquivalent on the main menu without checking for result you might end up invoking it twice – first, manually, and second, automatically from the super implementation, if the event doesn't get consumed by the responder chain. This would be the case when AVPlayer is the first responder and keyDown and keyUp methods not overwritten.
P.S. Snippets are Swift 4, but the idea is the same! ✌️
P.P.S. There's a brilliant WWDC 2010 Session 145 – Key Event Handling in Cocoa Applications that covers this subject in depth with excellent examples. WWDC 2010-11 is no longer listed on Apple Developer Portal but the full session list can be found here.
I had the same problem. I haven't investigated very hard, but as far as I can tell, the spacebar doesn't "look" like a keyboard shortcut to Cocoa so it gets routed to -insertText:. My solution was to subclass the NSWindow, catch it as it goes up the responder chain (presumably you could subclass NSApp instead), and send it off to the menu system explicitly:
- (void)insertText:(id)insertString
{
if ([insertString isEqual:#" "]) {
NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
location:[self mouseLocationOutsideOfEventStream]
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:self.windowNumber
context:[NSGraphicsContext currentContext]
characters:#" "
charactersIgnoringModifiers:#" "
isARepeat:NO
keyCode:49];
[[NSApp mainMenu] performKeyEquivalent:fakeEvent];
} else {
[super insertText:insertString];
}
}
I have just been experiencing the same problem with a twist...
The spacebar key equivalent works fine in my app while the NSMenuItem's linked IBAction is located in the App Delegate.
If I move the IBAction into a dedicated controller it fails. All other menu item key equivalents continue to work but the spacebar does not respond (it is ok with a modifier key, but unmodified #" " will not work).
I have tried various workarounds, like linking directly to the controller vs. linking via the responder chain, to no avail. I tried the code way:
[menuItem setKeyEquivalent:#" "];
[menuItem setKeyEquivalentModifierMask:0];
and the Interface Builder way, the behaviour is the same
I have tried subclassing NSWindow, as per Justin's answer, but so far have failed to get that to work.
So for now I have surrendered and relocated this one IBAction to the App Delegate where it works. I don't regard this as a solution, just making do... perhaps it's a bug, or (more likely) I just don't understand event messaging and the responder chain well enough.
Up this post because i need to use space too but no of those solutions work for me.
So, I subclass NSApplication and use the sendEvent: selector with the justin k solution :
- (void)sendEvent:(NSEvent *)anEvent
{
[super sendEvent:anEvent];
switch ([anEvent type]) {
case NSKeyDown:
if (([anEvent keyCode] == 49) && (![anEvent isARepeat])) {
NSPoint pt; pt.x = pt.y = 0;
NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
location:pt
modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber: 0 // self.windowNumber
context:[NSGraphicsContext currentContext]
characters:#" "
charactersIgnoringModifiers:#" "
isARepeat:NO
keyCode:49];
[[NSApp mainMenu] performKeyEquivalent:fakeEvent];
}
break;
default:
break;
}
}
Hope it will help
Quick Swift 4-5 method:
In view controller:
// Capture space and call main menu
override func keyDown(with event: NSEvent) {
if event.keyCode == 49 && !event.isARepeat{
NSApp.mainMenu?.performKeyEquivalent(with: event)
}
super.keyDown(with: event)
}

Cocoa NSTextField Drag & Drop Requires Subclass... Really?

Until today, I've never had occasion to use anything other than an NSWindow itself as an NSDraggingDestination. When using a window as a one-size-fits-all drag destination, the NSWindow will pass those messages on to its delegate, allowing you to handle drops without subclassing NSWindow.
The docs say:
Although NSDraggingDestination is
declared as an informal protocol, the
NSWindow and NSView subclasses you
create to adopt the protocol need only
implement those methods that are
pertinent. (The NSWindow and NSView
classes provide private
implementations for all of the
methods.) Either a window object or
its delegate may implement these
methods; however, the delegate’s
implementation takes precedence if
there are implementations in both
places.
Today, I had a window with two NSTextFields on it, and I wanted them to have different drop behaviors, and I did not want to allow drops anywhere else in the window. The way I interpret the docs, it seems that I either have to subclass NSTextField, or make some giant spaghetti-conditional drop handlers on the window's delegate that hit-checks the draggingLocation against each view in order to select the different drop-area behaviors for each field.
The centralized NSWindow-delegate-based drop handler approach seems like it would be onerous in any case where you had more than a small handful of drop destination views. Likewise, the subclassing approach seems onerous regardless of the case, because now the drop handling code lives in a view class, so once you accept the drop you've got to come up with some way to marshal the dropped data back to the model. The bindings docs warn you off of trying to drive bindings by setting the UI value programmatically. So now you're stuck working your way back around that too.
So my question is: "Really!? Are those the only readily available options? Or am I missing something straightforward here?"
Thanks.
After a bit more research it appears that "Yes, really, your two options are to either subclass NSTextField or use your NSWindowDelegate to handle drops." I'll go further and make the claim that the better way of the two, for garden variety cases of, "I want multiple drop zones in a single window" is to use the NSWindowDelegate method with hit checks, since you avoid the issue of having your drop-handling code on the view side. I ended up with this draggingUpdated: method on my window delegate class:
- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
NSDragOperation sourceDragMask = [sender draggingSourceOperationMask];
if ([pboard.types containsObject: NSFilenamesPboardType] && (sourceDragMask & NSDragOperationCopy))
{
NSView* hitView = [sender.draggingDestinationWindow.contentView hitTest: sender.draggingLocation];
if (hitView && (hitView == mSourceTextField || hitView == mDestTextField))
{
return NSDragOperationCopy;
}
}
return NSDragOperationNone;
}
Obviously there's more to the whole picture, but this hitTest:-based approach has worked for me so far. I suspect that this would be slightly more complex if one were working with a multi-NSCell based control like an NSTableView or NSOutlineView, but unsurprisingly, those have their own drag handling methods.
Hope this helps someone else.

How to disable drag-n-drop for NSTextField?

I want to disallow dropping anything into my NSTextField. In my app, users can drag and drop iCal events into a different part of the GUI. Now I've had a test user who accidentally dropped the iCal event into the text field – but he didn't realize this because the text is inserted in the lines above the one that I see in my one-line text field.
(You can reveal the inserted text by clicking into the text field and using the keyboard to go one line up – but a normal user wouldn't do this because he/she wouldn't even have realized that something got inserted in the first place!)
I tried registerForDraggedTypes:[NSArray array]] (doesn't seem to have any effect) as well as implementing the draggingEntered: delegate method returning NSDragOperationNone (the delegate method isn't even invoked).
Any ideas?
EDIT: Of course dropping something onto an NSTextField only works when it has focus, as described by ssp in his blog and in the comments to a blog entry by Daniel Jalkut.
I am glad you discovered the comments in my blog post. I think they are the tip of the iceberg to discovering how to achieve what you're looking for.
You need to keep in mind that the reason dragging to an NSTextField works when it has focus, is that the NSTextField has itself been temporarily obscured by a richer, more powerful view (an NSTextView), which is called the "Field Editor."
Check out this section of Apple's documentation on the field editor:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/TextEditing/Tasks/FieldEditor.html
To achieve what you're striving for, I think you might need to intercept the standard provision of the field editor for your NSTextFields, by implementing the window delegate method:
windowWillReturnFieldEditor:toObject:
This gives you the opportunity to either tweak the configuration on the NSTextView, or provide a completely new field editor object.
In the worst case scenario, you could provide your own NSTextView subclass as the field editor, which was designed to reject all drags.
This might work: If you subclass NSTextView and implement -acceptableDragTypes to return nil, then the text view will be disabled as a drag destination. I also had to implement the NSDraggingDestination methods -draggingEntered: and -draggingUpdated: to return NSDragOperationNone.
#implementation NoDragTextView
- (NSArray *)acceptableDragTypes
{
return nil;
}
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
return NSDragOperationNone;
}
- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)sender
{
return NSDragOperationNone;
}
#end
I was able to solve this problem by creating a custom NSTextView and implementing the enter and exit NSDraggingDestination protocol methods to set the NSTextView to hidden. Once the text field is hidden the superview will be able to catch the drag/drop events, or if the superview doesn't implement or want the drag/drop they are discarded
For example:
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
//hide so that the drop event falls through into superview's drag/drop view
[self setHidden:YES];
return NSDragOperationNone;
}
- (void)draggingExited:(id<NSDraggingInfo>)sender {
//show our field editor again since dragging is all over with
[self setHidden:NO];
}
Have you tried - (void)unregisterDraggedTypes from NSView?

Resources