Enabling "Use keyboard navigation" messes with NSTableView - macos

I have an NSTableView where I use the
tableView(tableView:shouldTypeSelectFor:withCurrentSearch:) -> Bool
delegate method to select certain lines with the SPACE key like this
func tableView(_ tableView: NSTableView,
shouldTypeSelectFor event: NSEvent,
withCurrentSearch searchString: String?) -> Bool {
if event.charactersIgnoringModifiers == " " {
// Perform action on currently selected row(s)
}
return false
}
That works fine, but when the setting in
Preferences > Keyboard > Shortcuts > "Use keyboard navigation to move focus between controls"
Is turned on, the table view loses focus entirely after hitting the SPACE key.
If I check my responder chain afterwards, I can se that the focus moved back to the window.
Another sideeffect of turning this setting on is that if I have a check box (NSButton) in the selected table view cell, its action will be automatically called when hitting SPACE.
The setting explicitly states it only moves focus on TAB and SHIFT+TAB, not change how controls work.
Why does this happen?
Can I disable this behavior at least partially for the table view of the app?

After almost losing my last hair here is what resolves it
Make sure all your cell subviews have refusesFirstResponder set to false
Now the table views first responder status is not lost.

Related

NSTabView: Toolbar does not display

macOS 10.12.6; Xcode 9.3, storyboards
I have an NSTabView (tabless) that in itself contains two NSTabViews. One is tabless, the other one uses the 'toolbar' style.
When I start my app with the toolbar visible, everything is fine: it displays my tabs in the toolbar, I can change them, etc etc. Once I change to the other branch of my storyboard, the toolbar disappears... and when I come back, instead of a toolbar proper, with buttons and all that, I get a slightly widened bar that has no content in it.
I've set up a sample project to show my problem, where - for ease of switching - I have left the other two tabViewControllers to show their tabs (bottom/top, but this makes no difference).
1) First run (starting with 'toolbar' branch):
2) (not shown): switch to 'top' branch
3) After switching back to 'toolbar':
As a diagnostic aid, I've created a 'displayToolbarStatus' IBAction in the AppController:
#IBAction func displayToolbarStatus(_ sender: NSMenuItem){
if let window = NSApplication.shared.windows.first {
print(window.toolbar?.isVisible)
}
}
The results are as follows:
1) optional(true)
2) nil
3) optional(true)
which is very much in line with how things should work: the toolbar exists and is displayed, there is no toolbar, the toolbar exists and is displayed. Only, of course, it is not usable as a toolbar. (turning visibility off and on, or trying to force a size change with window.toolbar?.sizeMode = .regular has no effect whatsoever, nor does assigning icons to the toolbar items; the toolbar remains squashed and without functioning buttons.
I haven't worked in any depth with NSToolbar: is this a known problem with a workaround; is this new to Xcode 9.2 (which, after all, thinks that no window is valid, so obviously has some problems in that field)?
I really want to use the NSTabView 'toolbar' functionality: how do I proceed?
I've now had more time to play with toolbars. The 'weird' appearance of the non responsive toolbar is simply an empty toolbar, which gave me a clue as to what was going on.
0) The NSTabView overrides the window's toolbar; it does not hand back control when it vanishes; this means that if you have another toolbar in your window that will never show up the moment you're using an NSTabView with 'toolbar' style.
1) I have added a print statement to every relevant method in the ToolbarTabViewController and a 'Switching Tabs' in the containing TabViewController's DidSelect TabViewItem, as well as logging when Toolbar items are added to the window.
(The ToolbarTabViewController is the second controller in the containing TabViewController; it is selected. Otherwise the stack looks slightly different):
ViewDidLoad
Switching tabs
viewWillAppear
viewDidAppear
Switching tabs
Toolbar will add item
Toolbar will add item
viewWillAppear
viewDidAppear
Switching away to the other tab:
viewWillDisappear
Switching tabs
Toolbar did remove item
Toolbar did remove item
viewDidDisappear
So far, so good.
Switching back to the ToolbarTabController, we get
viewWillAppear
Switching tabs
viewDidAppear
Whatever method is called that adds the tabs-related items to the toolbar on first appearance does never get called again. (Note also that the the order of switching tabs and viewDidAppear is not consistent between the first and subsequent appearances.)
2) So, the logical thing to do seems to be to capture the items that are being created and to add them back for future iterations. In the ToolbarTabViewController:
var defaultToolbarItems: [NSToolbarItem] = []
#IBAction func addTabsBack(_ sender: Any){
if let window = NSApplication.shared.windows.first {
if let toolbar = window.toolbar{
for (index, item) in defaultToolbarItems.enumerated() {
toolbar.insertItem(withItemIdentifier: item.itemIdentifier, at: index)
}
}
}
}
override func toolbarWillAddItem(_ notification: Notification) {
// print("Toolbar will add item")
if let toolbarItem = notification.userInfo?["item"] as? NSToolbarItem {
if defaultToolbarItems.count < tabView.numberOfTabViewItems{
defaultToolbarItems.append(toolbarItem)
}
}
}
3) The last question was when (and where) to call addTabsBack() - I found that if I try to call it in viewWillAppear I start out with four toolbarItems, though the number of tabViewItems is 2. (and they do, in fact, seem to be duplications: same name, same functionality). Therefore, I am calling addTabsBack()in the surrounding TabViewController's 'didSelect TabViewItem' method - willSelect is too early; but didSelect gives me exactly the functionality I need.
4) There probably is a more elegant way of capturing the active toolbarItems, but for now, I have a working solution.

macOS NSTextfield looses focus when NSPopover open

I am trying to make a search autocomplete in my macOS application:
I have a window with a NSTextField. When value changes, I display an NSPopover which contains NSOutlineView and the list is updated as the user is typing text.
The user can select a result in the NSOutlineView then.
But, if I make NSOutlineView enabled to be able to click on a result, the NSTextField looses focus when NSPopover opens.
If I set isEnabled to false for the NSOutlineView, NSTextField keeps focus but I can't select a result in the list.
Do you have any idea to keep focus on NSTextField without disable NSOutlineView ?
Thank you.
I have found a solution:
Before NSPopover.show(), I store the current textField selected range:
let range = textField.currentEditor()?.selectedRange
And after:
textField.selectText(self)
textField.currentEditor()?.selectedRange = range ?? NSMakeRange(0, 0)
If I call selectText() without setting the selected range, it will select all text.
Moreover, save the selected range keeps cursor position.

Editable NSTextField in NSTableView

I have a view based NSTableView with some labels in my customized and subclassed view. One of the label should be editable, so therefore I set this NSTextField to editable.
But now I have two problems, I can't solve:
1) If I move the mouse over the editable NSTextField, the cursor don't change to the IBeamCursor (the edit cursor).
2) I need to double click at the label, to be able to edit. I want to have a single click. I found some solutions for this problem here at stackoverflow, the best one is to override the acceptsFirstResponder to return always true, but then, clicking at the NSTextField selects the whole text instead of placing the cursor at the clicked position.
Sorry... this is a duplicate. I found this:
NSTableView - select row and respond to mouse events immediately
You have to subclass NSTableView. My swift code:
class TableViewEditing: NSTableView {
...
override func validateProposedFirstResponder(responder: NSResponder, forEvent event: NSEvent?) -> Bool {
return true
}
}
EDIT:
Just one disadvantage: Sometimes entering the edit mode, it seems that the text is just shortly selected and deselected. But you can see, that this is a cocoa problem, it's the same for example in Apple reminders app.

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.

Interface Builder: determine which element has the initial focus

I have a Cocoa app with a table view and a few other controls. When the app launches and the window is shown, a blue focus ring is drawn around the table view.
How can I get rid of that focus ring? I'd like nothing to have the focus when the window first shows.
The window has initialFirstResponder binding that shows which control will be active when the window becomes active. Change the initialFirstResponder or adjust tableview settings in interface builder to hide the focus ring
The best way I've found of stopping any of the controls from being the first responder when a window is first displayed is in the window controller:
Swift 3:
class YourWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Wait a frame before setting the first responder to be the window itself.
// We can't just set it right now, because if the first responder is set
// to the window now the system just interprets that as meaning that we
// want the default behavior where it automatically selects a view to be
// the first responder.
DispatchQueue.main.async {
window!.makeFirstResponder(nil)
}
}
}
It's messy, and sometimes when the window loads you see the focus ring starting to appear on one of the controls for one frame, but I haven't found a better way yet.

Resources