NSTableView & NSOutlineView editing on tab key

陌路散爱 提交于 2019-11-30 14:17:30

This is the default behavior. If there's no row selected, the table view as a whole has focus, and the Tab key switches to the next key view. If there is a row selected, the table view begins editing or moves to the next cell if already editing.

From AppKit Release Notes:

Tables now support inter-cell navigation as follows:

  • Tabbing forward to a table focuses the entire table.
  • Hitting Space will attempt to 'performClick:' on a NSButtonCell in the selected row, if there is only one instance in that row.
  • Tabbing again focuses the first "focusable" (1) cell, if there is one.
  • If the newly focused cell can be edited, editing will begin.
  • Hitting Space calls 'performClick:' on the cell and sets the datasource value afterwards, if changed. (2)
  • If a text cell is editing, hitting Enter will commit editing and focus will be returned to the tableview, and Tab/Shift-tab will commit the editing and then perform the new tab-loop behavior.
  • Tabbing will only tab through a single row
  • Once the last cell in a row is reached, tab will take the focus to the next focusable control.
  • Back tabbing into a table will select the last focusable cell.

If you want to change this behavior, the delegate method tableView:shouldEditTableColumn:row: may be helpful. You may also have to subclass NSTableView if you really want to affect only the behavior of the Tab key.

I've had to deal with this before as well. My solution was to subclass NSTableView or NSOutlineView and override keyDown: to catch the tab key presses there, then act on them.

The solution using keyDown didn't work for me. Perhaps because it is for cell-based table view.

My solution for a view-based table view, in Swift, looks like this:

extension MyTableView: NSTextFieldDelegate {
    func controlTextDidEndEditing(_ obj: Notification) {
        guard
            let view = obj.object as? NSView,
            let textMovementInt = obj.userInfo?["NSTextMovement"] as? Int,
            let textMovement = NSTextMovement(rawValue: textMovementInt) else { return }

        let columnIndex = column(for: view)
        let rowIndex = row(for: view)

        let newRowIndex: Int
        switch textMovement {
        case .tab:
            newRowIndex = rowIndex + 1
            if newRowIndex >= numberOfRows { return }
        case .backtab:
            newRowIndex = rowIndex - 1
            if newRowIndex < 0 { return }
        default: return
        }

        DispatchQueue.main.async {
            self.editColumn(columnIndex, row: newRowIndex, with: nil, select: true)
        }
    }
}

You also need to set the cell.textField.delegate so that the implementation works.

My blog post on this tricky workaround: https://samwize.com/2018/11/13/how-to-tab-to-next-row-in-nstableview-view-based-solution/

How convenient! I was just looking at this myself yesterday, and it's good to see some confirmation of the approach I took - keyDown: handling.

However, I have one small possible refinement to your approach: I worked out that the method triggering editing on shift-tabbing back to the table was the becomeFirstResponder call. So what I did on a NSTableView subclass was:

  1. Add a synthesized property to control whether tab-editing behaviour was disabled
  2. On keydown, check the first character (also check for [[theEvent characters] length] to avoid exceptions for dead keys!) for tab; if tab editing is disabled, move on to the next/previous view, as per your code sample.
  3. Override becomeFirstResponder:
    - (BOOL)becomeFirstResponder {
        if (tabEditingDisabled) {
            [self display];
            return YES;
        }
        return [super becomeFirstResponder];
    }

This keeps all the code in the tableview subclass, keeping the delegate cleaner :)

The only danger is I don't know what else NSTableView does in becomeFirstResponder; I didn't notice anything breaking, but...

This worked for me:

- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
  NSEvent *e = [NSApp currentEvent];
  if (e.type == NSKeyDown && e.keyCode == 48) return NO;
  return YES;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!