Replacing the ESC button on a NSTouchBar - macos

I am building a NSTouchBar for my app using storyboard.
I want to replace the ESC button with something else.
As usual, there is no doc telling you how to do that.
I have searched the web and I have found vague informations like
You can change the content of "esc" to something else, though, like
"done" or anything, even an icon, by using
escapeKeyReplacementItemIdentifier with the NSTouchBarItem.
But this is too vague to understand.
Any ideas?
This is what I did so far.
I have added a button to NSTouchBar on storyboard and changed its identifier to newESC. I added this line programmatically:
self.touchBar.escapeKeyReplacementItemIdentifier = #"newESC";
When I run the App the ESC key is now invisible but still occupies its space on the bar. The button that was supposed to replace it, appears next to it. So that bar that was
`ESC`, `NEW_ESC`, `BUTTON1`, `BUTTON2`, ...
is now
`ESC` (invisible), `NEW_ESC`, `BUTTON1`, `BUTTON2`, ...
The old ESC is still occupying its space on the bar.

This is done by creating a touch bar item, let's say a NSCustomTouchBarItem containing a NSButton, and associating this item with its own identifier.
Then with another identifier you do your usual logic but you add the previously created identifier as the ESC replacement.
Quick example in Swift:
func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
switch identifier {
case NSTouchBarItemIdentifier.identifierForESCItem:
let item = NSCustomTouchBarItem(identifier: identifier)
let button = NSButton(title: "Button!", target: self, action: #selector(escTapped))
item.view = button
return item
case NSTouchBarItemIdentifier.yourUsualIdentifier:
let item = NSCustomTouchBarItem(identifier: identifier)
item.view = NSTextField(labelWithString: "Example")
touchBar.escapeKeyReplacementItemIdentifier = .identifierForESCItem
return item
default:
return nil
}
}
func escTapped() {
// do additional logic when user taps ESC (optional)
}
I also suggest making an extension (category) for the identifiers, it avoids making typos with string literals:
#available(OSX 10.12.2, *)
extension NSTouchBarItemIdentifier {
static let identifierForESCItem = NSTouchBarItemIdentifier("com.yourdomain.yourapp.touchBar.identifierForESCItem")
static let yourUsualIdentifier = NSTouchBarItemIdentifier("com.yourdomain.yourapp.touchBar.yourUsualIdentifier")
}

Related

NSApplication responder chain for arrow keys

I have an NSTextField in my window and 4 menu items with key equivalents ←↑→↓.
When the text field is selected and I press an arrow key, I would expect the cursor to move in the text field but instead the corresponding menu item action is performed.
So there has to be an issue in the responder chain. To figure out what's wrong I've watched WWDC 2010 Session 145 – Key Event Handling in Cocoa Applications mentioned in this NSMenuItem KeyEquivalent space " " bug thread.
The event flow for keys (hotkeys) is shown in the session as follows:
So I checked the call stack with a menu item which has keyEquivalent = K (just any normal key) and for a menu item which has keyEquivalent = → (right arrow key)
First: K key event call stack; Second: Right arrow key event call stack
So when pressing an arrow key, the event is sent directly to mainMenu.performKeyEquivalent, but it should actually be sent to the keyWindow right?
Why is that and how can I fix this behavior so that my NSTextField receives the arrow key events before the mainMenu does?
Interesting observation about the call stack difference. Since arrow keys play the most important role in navigation they are probably handled differently from the rest of keys, like you saw in the NSMenuItem KeyEquivalent space " " bug thread. Again, it's one of those cases when AppKit takes care of everything behind the scenes to make your life easier in 99.9% situations.
You can see the actual difference in behaviour by pressing k while textfield has the focus. Unlike with arrows, the menu item's key equivalent doesn't get triggered and input goes directly into the control.
For your situation you can use NSMenuItemValidation protocol to override the default action of enabling or disabling a specific menu item. AFAIK this can go into any responder in a chain, e.g., view controller, window, or application. So, you can enable/disable your menu items in a single place when the window's first responder is a textfield or any other control that uses these events to properly operate.
extension ViewController: NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
// Filter menu item by it's assigned action, just as an exampe.
if menuItem.action != #selector(ViewController.menuActionLeftArrowKey(_:)) { return true }
Swift.print("Validating menu item:", menuItem)
// Disable the menu item if first responder is text view.
let isTextView = self.view.window?.firstResponder is NSTextView
return !isTextView
}
}
This will get invoked prior displaying the menu in order to update item state, prior invoking menu item key equivalent in order to check if action needs sending or not, and probably in other cases when AppKit needs to check the item's state – can't think of any from the top of my head.
P.S. Above the first responder check is done against NSTextView not NSTextField, here's why.
This is the solution I've chosen, which resulted from the comments from #Willeke.
I've created a subclass of NSWindow and overridden the keyDown(with:) method. Every Window in my application (currently 2) subclass this new NavigationWindow, so that you can use the arrow keys in every window.
class NavigationWindow: NSWindow {
override func keyDown(with event: NSEvent) {
if event.keyCode == 123 || event.keyCode == 126 || event.specialKey == NSEvent.SpecialKey.pageUp {
print("navigate back")
} else if event.keyCode == 124 || event.keyCode == 125 || event.specialKey == NSEvent.SpecialKey.pageDown {
print("navigate forward")
} else {
super.keyDown(with: event)
}
}
}
This implementation registers all four arrow keys plus the page up and down keys for navigation.
These are the key codes
123: right arrow
124: left arrow
125: down arrow
126: up arrow

NSUndoManager custom coalescing changes to NSTextField results in crash

I have a basic Cocoa app using NSUndoManager to support undo/redo. My data model's title can be updated by editing an NSTextField. Out of the box NSTextField supports coalescing changes into a single "Undo Typing" undo action so that when the user presses Cmd-Z, the text is reverted in full instead of only the last letter.
I'd like to have a single "Undo Changing Model Title" undo action that undoes multiple changes to my model's title (made in a NSTextField). I tried grouping changes myself, but my app crashes (see below).
I can't use the default undo behavior of NSTextField, because I need to update my model and the text field might be gone by the time the user tries to undo an action (the text field is in a popup window).
NSUndoManager by default groups changes that occur within a single run loop cycle, but also allows to disable this behavior and to create custom "undo groups". So I tried the following:
Set undoManager.groupsByEvent = false
In controlTextDidBeginEditing(), begin a new undo group
In controlTextDidChange(), register an undo action
In controlTextDidEndEditing(), end the undo group
This works as long as I end editing the text field by pressing Enter. If I type "abc" and press Cmd-Z to undo before ending editing, the app crashes, because the undo grouping was not closed:
[General] undo: NSUndoManager 0x60000213b1b0 is in invalid state, undo was called
with too many nested undo groups
From the docs, undo() is supposed to close the undo grouping automatically, if needed. The grouping level is 1 in my case.
undo() [...] This method also invokes endUndoGrouping() if the nesting level is 1
However, the undo group is not closed by undo(), no matter if I set groupsByEvent to true or false. My app always crashes.
What is interesting:
I observed the default behavior of NSTextField. When I type the first letter, it begins AND ends an undo group right away. It does not create any other undo groups for subsequent changes after the first change.
To reproduce:
Create a new Cocoa project and paste in the code for the App Delegate (see below)
Type "a" + "b" + "c" + Cmd-Z
Expected:
Text field value should be set to empty string
Actual:
Crash
Alternatively:
Type "a" + "b" + "c" + Enter + Cmd-Z
Result:
The above works, because this time, the undo group is ended. All grouped changes are undone properly.
The problem is that the user can press Cmd-Z at any time while editing. I can't end the undo group after each change or changes cannot be undone all at once.
undo() not closing the undo group might be a bug, but nonetheless, grouping changes and undoing while typing must be possible, because that's what NSTextField does out of the box (and it works).
Source:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDelegate {
#IBOutlet weak var window: NSWindow!
private var textField:NSTextField!
private let useCustomUndo = true
private var myModelTitle = ""
func applicationDidFinishLaunching(_ aNotification: Notification) {
//useCustomUndo = false
observeUndoManger()
setup()
NSLog("Window undo manager: \(Unmanaged.passUnretained(window.undoManager!).toOpaque())")
NSLog("NSTextField undo manager: \(Unmanaged.passUnretained(textField.undoManager!).toOpaque())")
}
func controlTextDidBeginEditing(_ obj: Notification) {
NSLog("Did begin editing & starting undo group")
// With or without grouping the app crashes on undo if the group was not ended:
window.undoManager?.groupsByEvent = true
window.undoManager?.beginUndoGrouping()
}
func controlTextDidEndEditing(_ obj: Notification) {
NSLog("Did end editing & ending undo group")
window.undoManager?.endUndoGrouping()
}
func controlTextDidChange(_ obj: Notification) {
NSLog("Text did change")
setModelTitleWithUndo(title: textField.stringValue)
}
private func setModelTitleWithUndo(title:String) {
NSLog("Current groupingLevel: \(window.undoManager!.groupingLevel)")
window.undoManager?.registerUndo(withTarget: self, handler: {[oldValue = myModelTitle, weak self] _ in
guard let self = self, let undoManager = self.window.undoManager else { return }
NSLog("\(undoManager.isUndoing ? "Undo" : "Redo") from current model : '\(self.myModelTitle)' to: '\(oldValue)'")
NSLog( " from current textfield: '\(self.textField.stringValue)' to: '\(oldValue)'")
self.setModelTitleWithUndo(title: oldValue)
})
window.undoManager?.setActionName("Change Title")
myModelTitle = title
if window.undoManager?.isUndoing ?? false || window.undoManager?.isRedoing ?? false
{
textField.stringValue = myModelTitle
}
NSLog("Model: '\(myModelTitle)'")
}
private func observeUndoManger() {
for i in [(NSNotification.Name.NSUndoManagerCheckpoint , "<checkpoint>" ),
(NSNotification.Name.NSUndoManagerDidOpenUndoGroup , "<did open undo group>" ),
(NSNotification.Name.NSUndoManagerDidCloseUndoGroup, "<did close undo group>"),
(NSNotification.Name.NSUndoManagerDidUndoChange , "<did undo change>" ),
(NSNotification.Name.NSUndoManagerDidRedoChange , "<did redo change>" )]
{
NotificationCenter.default.addObserver(forName: i.0, object: nil, queue: nil) {n in
let undoManager = n.object as! UndoManager
NSLog("\(Unmanaged.passUnretained(undoManager).toOpaque()) \(i.1) grouping level: \(undoManager.groupingLevel), groups by event: \(undoManager.groupsByEvent)")
}
}
}
private func setup() {
textField = NSTextField(string: myModelTitle)
textField.translatesAutoresizingMaskIntoConstraints = false
window.contentView!.addSubview(textField)
textField.leadingAnchor.constraint(equalTo: window.contentView!.leadingAnchor).isActive = true
textField.topAnchor.constraint(equalTo: window.contentView!.topAnchor, constant: 50).isActive = true
textField.trailingAnchor.constraint(equalTo: window.contentView!.trailingAnchor).isActive = true
if useCustomUndo
{
textField.cell?.allowsUndo = false
textField.delegate = self
}
}
}
UPDATE:
I observed the behavior of NSTextField some more. NSTextField does not group across multiple run loop cycles. NSTextField groups by event and uses groupsByEvent = true. It creates a new undo group when I type the first letter, closes the group, and does not create any additional undo groups for the next letters I type. Very strange...

Add a Clear button functionality in Custom Keyboard using Swift

I use a custom keyboard which contains a delete and a clear button.I can able to delete the letters using this code
let obj = UIInputViewController()
(obj.textDocumentProxy as UIKeyInput).deleteBackward()
Is it any keyword available to clear whole text in a textfield.
NB: I found this link but it is not apt for my requirement
There is no keyword available for deleting the whole text in the textfield through custom keyboard.
To clear all the text that the UITextDocumentProxy object can access in the textfield before the cursor position
if let word:String = self.textDocumentProxy.documentContextBeforeInput
{
for _: Int in 0 ..< word.characters.count {
self.textDocumentProxy.deleteBackward()
}
}
To delete the text after cursor position, you have to move the cursor forward to delete it.

Swift 2.0 and Xcode-beta 7.0 – Using UITextField for numeric input

Completely frustrated noob here. Surely this isn't as hard as it looks?
I want to use a number entered by the user, perform a calculation, and send the result back to the screen.
I have code working that can use a string forced in by code, convert it to double, do the math and send the result to the screen. For example:
#IBAction func buttonPressed() {
NSLog("Button Pressed")
let decimalAsString = "123.45"
let decimalAsDouble = NSNumberFormatter().numberFromString(decimalAsString as String)!.doubleValue
TempLabel.text = "\(decimalAsDouble+2.45)"
}
This simply adds 2.45 to the string "123.45" and sends the result 125.9 back as a string to my label for display, all when the button is pressed. Great. This simpler form also works:
let decimalAsDouble = Double(decimalAsString)
What I have been struggling with is using a number entered into the UITextField.
My UITextField uses a decimal pad for entry, and I've always had a number entered there when the errors were thrown. (Or did I? The numbers show on screen but are they really "entered"? Hmmm...)
No matter what I try, I cannot find code that will both compile and then not blow up at execution, when the button is pressed. The error I get generally complains about unwrapping an optional nil.
I can detail some of the things that DON'T work, if that helps.
OK, well I've finally solved my own problem. My problem was with closing the entry field and dismissing the numeric entry pad. Once I did that, the entered value became available.
The critical code, in the view controller, was:
func textFieldShouldReturn(textField: UITextField!) -> Bool { //delegate method
textField.resignFirstResponder()
return true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?){
view.endEditing(true)
super.touchesBegan(touches, withEvent: event)
}
This dismisses the entry pad when the user clicks anywhere in the view except for the field itself or a button. Programming that looks for the entered value doesn't blow up.

NSTextField controlTextDidEndEditing: called while being edited (inside an NSOutlineView)

In my NSOutlineView, I have a NSTextField inside a NSTableCellView. I am listening for the controlTextDidEndEditing: notification to happen when the user finishes the editing. However, in my case, this notification is being fired even while the user is in the middle of typing, or takes even a second-long pause in typing. This seems bizarre. I tested a NSTextField in the same view, but outside of the NSOutlineView, and it doesn't behave this way; it only calls controlTextDidEndEditing: if the user pressed the Tab or Enter keys (as expected).
Is there something I can do to prevent the NSTextField from sending controlTextDidEndEditing: unless a Enter or Tab key is pressed?
Found a solution for this:
- (void)controlTextDidEndEditing:(NSNotification *) notification {
// to prevent NSOutlineView from calling controlTextDidEndEditing by itself
if ([notification.userInfo[#"NSTextMovement"] unsignedIntegerValue]) {
....
It's an old question, but for reference, I ran into a similar problem where controlTextDidEndEditing: was called at the beginning of the editing session.
My workaround is to check if the text field still has the focus (i.e. cursor):
func controlTextDidEndEditing(_ obj: Notification) {
guard
let textField = obj.object as? NSTextField,
!textField.isFocused
else {
return
}
...
}
public extension NSTextField
{
public var isFocused:Bool {
if
window?.firstResponder is NSTextView,
let fieldEditor = window?.fieldEditor(false, for: nil),
let delegate = fieldEditor.delegate as? NSTextField,
self == delegate
{
return true
}
return false
}
}
Note to self:
I ran into this problem when adding a new item to NSOutlineView and making it editable with NSOutlineView.editColumn(row:,with:,select).
controlTextDidEndEditing() would be called right away at the start of the editing session.
It turns out it was a first responder/animation race condition. I used a NSTableView.AnimationOptions.slideDown animation when inserting the row and made the row editable afterwards.
The problem here is that the row is made editable while it is still animating. When the animation finishes, the first responder changes to the window and back to the text field, which causes controlTextDidEndEditing() to be called.
outlineView.beginUpdates()
outlineView.insertItems(at: IndexSet(integer:atIndex),
inParent: intoParent == rootItem ? nil : intoParent,
withAnimation: .slideDown) // Animating!
outlineView.endUpdates()
// Problem: the animation above won't have finished leading to first responder issues.
self.outlineView.editColumn(0, row: insertedRowIndex, with: nil, select: true)
Solution 1:
Don't use an animation when inserting the row.
Solution 2:
Wrap beginUpdates/endUpdates into an NSAnimationContext group, add a completion handler to only start editing once the animation finished.
Debugging tips:
Observe changes to firstResponder in your window controller
Put a breakpoint in controlTextDidEndEditing() and take a very close look at the stack trace to see what is causing it to be called. What gave it away in my case were references to animation calls.
To reproduce, wrap beginUpdates/endUpdates in an NSAnimationContext and increase the animation duration to a few seconds.

Resources