views:

810

answers:

4

Is it possible to navigate an NSTableView's editable cell around the NSTableView using arrow keys and enter/tab? For example, I want to make it feel more like a spreadsheet.

The users of this application are expected to edit quite a lot of cells (but not all of them), and I think it would be easier to do so if they didn't have to double-click on each cell.

+2  A: 

Well it isn't easy but I managed to do it without having to use RRSpreadSheet or even another control. Here's what you have to do:

  1. Create a subclass of NSTextView, this will be the field editor. For this example the name MyFieldEditorClass will be used and myFieldEditor will refer to an instance of this class.

  2. Add a method to MyFieldEditorClass called "- (void) setLastKnownColumn:(unsigned)aCol andRow:(unsigned) aRow" or something similar, and have it save both the input parameter values somewhere.

  3. Add another method called "setTableView:" and have it save the NSTableView object somewhere, or unless there is another way to get the NSTableView object from the field editor, use that.

  4. Add another method called - (void) keyDown:(NSEvent *) event. This is actually overriding the NSResponder's keyDown:. The source code should be (be aware that StackOverflow's MarkDown is changing < and > to &lt; and &gt;):

    - (void) keyDown:(NSEvent *) event
    {
        unsigned newRow = row, newCol = column;
        switch ([event keyCode])
        {
            case 126: // Up
                if (row)
                newRow = row - 1;
                break;
    
    
    
        case 125: // Down
            if (row &lt; [theTable numberOfRows] - 1)
                newRow = row + 1;
            break;
    
    
        case 123: // Left
            if (column &gt; 1)
                newCol = column - 1;
            break;
    
    
        case 124: // Right
            if (column &lt; [theTable numberOfColumns] - 1)
                newCol = column + 1;
            break;
    
    
        default:
            [super keyDown:event];
            return;
    }
    
    
    [theTable selectRow:newRow byExtendingSelection:NO];
    [theTable editColumn:newCol row:newRow withEvent:nil select:YES];
    row = newRow;
    column = newCol;
    
    }
  5. Give the NSTableView in your nib a delegate, and in the delegate add the method:

    - (BOOL) tableView:(NSTableView *)aTableView shouldEditColumn:(NSTableColumn *) aCol row:aRow
    {
        if ([aTableView isEqual:TheTableViewYouWantToChangeBehaviour])
            [myFieldEditor setLastKnownColumn:[[aTableView tableColumns] indexOfObject:aCol] andRow:aRow];
        return YES;
    }
    
  6. Finally, give the Table View's main window a delegate and add the method:

    - (id) windowWillReturnFieldEditor:(NSWindow *) aWindow toObject:(id) anObject
    {
        if ([anObject isEqual:TheTableViewYouWantToChangeBehaviour])
        {
            if (!myFieldEditor)
            {
                myFieldEditor = [[MyFieldEditorClass alloc] init];
                [myFieldEditor setTableView:anObject];
            }
            return myFieldEditor;
        }
        else
        {
            return nil;
        }
    }
    

Run the program and give it a go!

dreamlax
+1  A: 

Rather than forcing NSTableView to do something it wasn't designed for, you may want to look at using something designed for this purpose. I've got an open source spreadsheet control which may do what you need, or you may at least be able to extend it to do what you need: MBTableGrid

Matt Ball
Great work on MBTableGrid (and a generous license too), but that's vastly more code than what I've done to do what I need though. +1 for the effort you've put into MBTableGrid.
dreamlax
+1  A: 

In Sequel Pro we used a different (and in my eyes simpler) method: We implemented control:textView:doCommandBySelector: in the delegate of the TableView. This method is hard to find -- it can be found in the NSControlTextEditingDelegate Protocol Reference. (Remember that NSTableView is a subclass of NSControl)

Long story short, here's what we came up with (we didn't override left/right arrow keys, as those are used to navigate within the cell. We use Tab to go left/right)

Please note that this is just a snippet from the Sequel Pro source code, and does not work as is

- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
{
    NSUInteger row, column;

    row = [tableView editedRow];
    column = [tableView editedColumn];

    // Trap down arrow key
    if (  [textView methodForSelector:command] == [textView methodForSelector:@selector(moveDown:)] )
    {
        NSUInteger newRow = row+1;
        if (newRow>=numRows) return TRUE; //check if we're already at the end of the list
        if (column>= numColumns) return TRUE; //the column count could change

        [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
        [tableContentView editColumn:column row:newRow withEvent:nil select:YES];
        return TRUE;
    }

    // Trap up arrow key
    else if (  [textView methodForSelector:command] == [textView methodForSelector:@selector(moveUp:)] )
    {
        if (row==0) return TRUE; //already at the beginning of the list
        NSUInteger newRow = row-1;

        if (newRow>=numRows) return TRUE;
        if (column>= numColumns) return TRUE;

        [tableContentView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
        [tableContentView editColumn:column row:newRow withEvent:nil select:YES];
        return TRUE;
    }
Jakob Egger
+2  A: 

I wanted to reply to the answers here but the reply button seems to be missing so I'm forced to proved an answer when I really just want to ask a question about the replies.

Anyway, I've seen a few answers for overriding the -keyDown event of the table view that say to subclass the TableView but according to every Objective-C book I've read so far, and several Apple training videos, you should very rarely if ever subclass one of the core classes. In fact every single one of them makes the point that C programmers have a fascination with subclassing and that's not how Objective-C works; that Objective-C is all about helpers and delegates not subclassing.

So, should I just ignore any of the responses that say to subclass as this seems to be in direct contradiction to the precepts of Objective-C?

--- Edit ---

I found something that worked without subclassing the NSTableView. While I do move the inheritance up one notch on the chain from NSObject to NSResponder I'm not totally subclassing the NSTableView. I'm just adding the ability to override the keyDown event.

I made the class I was using as a delegate inherit from NSResponder instead of NSObject and set the nextResponder to that class in awakeFromNib. I was then able to trap key presses using the keydown event. I of course connected the IBOutlet and set the delegate in Interface Builder.

Here's my code with the minimum needed to show the trapping of the key:

Header file

//  AppController.h

#import <Cocoa/Cocoa.h>

@interface AppController : NSResponder {

    IBOutlet NSTableView *toDoListView;
    NSMutableArray *toDoArray;
}

-(int)numberOfRowsInTableView:(NSTableView *)aTableView;

-(id)tableView:(NSTableView *)tableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
           row:(int)rowIndex;

@end

Here's the m file.

//  AppController.m
#import "AppController.h"

@implementation AppController

-(id)init
{
    [super init];
    toDoArray = [[NSMutableArray alloc] init];
    return self;
}

-(void)dealloc
{
    [toDoArray release];
    toDoArray = nil;
    [super dealloc];
}

-(void)awakeFromNib
{
    [toDoListView setNextResponder:self];
}

-(int)numberOfRowsInTableView:(NSTableView *)aTableView
{
    return [toDoArray count];
}

-(id)tableView:(NSTableView *)tableView
    objectValueForTableColumn:(NSTableColumn *)aTableColumn
                          row:(int)rowIndex
{
    NSString *value = [toDoArray objectAtIndex:rowIndex];
    return value;
}

- (void)keyDown:(NSEvent *)theEvent
{
    //NSLog(@"key pressed: %@", theEvent);
    if (theEvent.keyCode == 51 || theEvent.keyCode == 117)
    {
        [toDoArray removeObjectAtIndex:[toDoListView selectedRow]];
        [toDoListView reloadData];
    }
}
@end
Mike Bethany
There are some classes that you should not subclass, but there are plenty of Core (i.e. Foundation and AppKit) classes that you *should* or *must* subclass. `NSObject` is the most obvious one.
dreamlax
I've learned a little more, still very far from being even a novice with Objective-C, but I'm now more sure my solution was the better one. There's no need to do the whole NSTableView class when I just needed to trap the keydown event. From the very little bit of knowledge I have this seems to fit better with Apple's vision of how code should be written in Objective-C and after dealing with more and more of Apple's API's I'm really starting to respect the heck out of their standards; especially after coming from a Windows world where often their own code didn't follow their own standards.
Mike Bethany