8

My app has an NSOutlineView and an NSTableView, and I'm having the same problem with both. With a row in either selected, pressing the tab key puts the first column into edit mode instead of making the next key view first responder. To get to the next key view, you need to tab through all of the columns.

Also, shift-tabbing into either view results in the last column going into edit mode, necessitating more shift-tabs to get into its previous key view.

In case it matters, I'm using the autocalculated key view loop, not my own, with my NSWindow set to autorecalculatesKeyViewLoop = YES. I would like tabbing between the columns once the user elects to edit a column, but I don't think it's standard behavior for the tab key to trigger edit mode.

Update

Thanks to the helpful responses below, I worked it out. Basically, I override -keyDown in my custom table view class, which handles tabbing and shift-tabbing out of the table view. It was tougher to solve shift-tabbing into the table view, however. I set a boolean property to YES in the custom table view's -acceptsFirstResponder if it's accepting control from another view.

The delegate's -tableView:shouldEditTableColumn:row checks for that when the current event is a shift-tab keyDown event. -tableView:shouldEditTableColumn:row is called and it's not a shift-tab event, it sets the table view's property back to NO so it can still be edited as usual.

I've pasted the full solution below.

/* CustomTableView.h */

@interface CustomTableView : NSTableView {}

@property (assign) BOOL justFocused;

@end

/* CustomTableView.m */

@implementation CustomTableView

@synthesize justFocused;

- (BOOL)acceptsFirstResponder {
    if ([[self window] firstResponder] != self) {
        justFocused = YES;
    }

    return YES;
}

- (void)keyDown:(NSEvent *)theEvent
{
    // Handle the Tab key
    if ([[theEvent characters] characterAtIndex:0] == NSTabCharacter) {
        if (([theEvent modifierFlags] & NSShiftKeyMask) != NSShiftKeyMask) {
            [[self window] selectKeyViewFollowingView:self];
        } else {
            [[self window] selectKeyViewPrecedingView:self];
        }
    }
    else {
        [super keyDown:theEvent];
    }
}

@end

/* TableViewDelegate.m */

. . .

- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn
              row:(NSInteger)row
{
    NSEvent *event = [NSApp currentEvent];
    BOOL shiftTabbedIn = ([event type] == NSKeyDown
                          && [[event characters] characterAtIndex:0] == NSBackTabCharacter);

    if (shiftTabbedIn && ((CustomTableView *)tableView).justFocused == YES) {
        return NO;
    } else {
        ((CustomTableView *)tableView).justFocused = NO;
    }

    return YES;
}

. . .
Dov
  • 15,530
  • 13
  • 76
  • 177

5 Answers5

9

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.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • I tried working with `tableView:shouldEditTableColumn:row:` before I posted this question, but couldn't come up with any valid criteria to use as a basis for returning `NO` – Dov Apr 09 '11 at 00:49
  • Yes, if you really only want to change the behavior of Tab and no other key, then, like I said, you'll probably have to subclass. It should be fairly painless, though -- you just need to override `keyDown:` and check for Tab. Any other key you just pass through to `[super keyDown:event]`. – jscs Apr 09 '11 at 00:52
  • That makes sense. Also, is `[event keyCode] == 48` the best way to trap the tab key, or is that why shift-tab is still a problem? – Dov Apr 09 '11 at 00:56
  • You can check the `modifierFlags` of the event to see if Shift is pressed. I don't think there's a better way, no. – jscs Apr 09 '11 at 00:58
  • Oh, I missed your comment below. – jscs Apr 09 '11 at 00:59
  • Sorry, meant to get back earlier. The table view in my test project _is_ getting Shift-Tab, the same as it does plain Tab. The Shift shouldn't change the `keyCode` -- it shows as `48` either way for me. Not sure what the difference might be for yours. – jscs Apr 09 '11 at 05:33
  • Allow me to clarify: when the table view has focus, tab and shift-tab are getting handled properly with its `keyDown` override. The problem is that the table view's `keyDown` doesn't get called when shift-tabbing in from another view. I don't want to presume which control is going to precede my table view's and override its `keyDown`, since that will make maintaining the code harder. – Dov Apr 09 '11 at 11:29
  • @Dov: Well, sure. Whatever is `firstResponder` when you press a key gets the key event; if that object handles the event by passing focus to its `nextResponder` (e.g., the table view), then the event has been handled and that's the end of it. I don't know any good way around that. – jscs Apr 09 '11 at 18:02
4

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/

samwize
  • 25,675
  • 15
  • 141
  • 186
2

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.

willbur1984
  • 1,418
  • 1
  • 12
  • 21
  • I had tried this before I posted, but it didn't solve the shift-tab scenario, since the `NSTableView` doesn't receive that `keyDown:` event. How have you dealt with that? Also, is `[theEvent keyCode] == 48` the best way to trap the tab key, or is that my problem? – Dov Apr 09 '11 at 00:51
  • 4
    @Dov You should be able to get the event character and compare it against `NSTabCharacter` or `NSBackTabCharacter`. –  Apr 09 '11 at 03:27
0

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;
}
tgrbin
  • 56
  • 1
  • 2
0

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...

Rowan
  • 630
  • 4
  • 10
  • In your solution, though, when do you change `tabEditingDisabled`? – Dov Apr 10 '11 at 19:01
  • @Dov Because my NSTableView subclass is used in a few places, it's just a flag to toggle the behaviour depending on where it's used. In the couple of places where I want tab editing disabled, I change the view controller to `setTabEditingDisabled:NO` in the `awakeFromNib`. `tabEditingDisabled` isn't a tracking flag like the `justFocused` in your code, more a behavioural control toggle. – Rowan Apr 10 '11 at 21:23