Matt Rajca

blog projects github twitter email

Handling Text Editing in View-based NSTableViews

February 17, 2016

Historically, adding editing support to cell-based NSTableViews has been as easy as implementing NSTableViewDataSource's -tableView:setObjectValue:forTableColumn:row: method. Double-clicking a text cell would make it editable and the aforementioned data source method would be called with the updated value.

View-based table view cells, however, use standard NSTextFields which, out-of-the-box, don't have support for the double-click-to-edit behavior that users expect of tables on OS X.

To implement that behavior, we have to subclass NSTextField and override -mouseDown: to handle double-clicks and make the text field editable. Below is a sample implementation that takes care of the basics; it's actually very similar to Pixen's implementation of the text field used to display (and edit) layer names.

    
    @interface EditableTextField () <NSTextFieldDelegate>
    @end

    @implementation EditableTextField

    ...

    - (void)_commonInit
    {
        self.drawsBackground = YES;
        self.delegate = self;
    }

    - (void)mouseDown:(NSEvent *)theEvent
    {
        if (theEvent.clickCount == 2 && !self.isEditable)
            [self beginEditing];
        else
            [super mouseDown:theEvent];
    }

    - (void)beginEditing
    {
        self.editable = YES;
        self.backgroundColor = [NSColor whiteColor];
        self.selectable = YES;

        [self selectText:nil];
        self.needsDisplay = YES;
    }

    - (void)endEditing
    {
        self.editable = NO;
        self.backgroundColor = [NSColor clearColor];
        self.selectable = NO;

        self.needsDisplay = YES;
    }

    - (void)controlTextDidEndEditing:(NSNotification *)notification
    {
        [self endEditing];
    }

    @end
    

If you change your text field's class to EditableTextField and try this out, you'll notice -mouseDown: is actually never called when you double-click the text field. It appears as if NSTableView is eating up the mouse down events to handle row selection (among other things). If you search around the web, the most common solution is to override -mouseDown: on the table view itself and forward events to the text field manually.

There's a better way. Buried deep inside NSResponder.h is a hidden gem:

    
    /*
      This is a responder chain method to allow controls to determine when they should
      become first responder or not. Some controls, such as NSTextField, should only
      become first responder when the enclosing NSTableView/NSBrowser indicates that
      the view can begin editing. It is up to the particular control that wants to be
      validated to call this method in its -mouseDown: (or other time) to determine
      if it should attempt to become the first responder or not. The default
      implementation returns YES when there is no -nextResponder, otherwise, it is
      forwarded up the responder chain. NSTableView/NSBrowser implements this to only
      allow first responder status if the responder is a view in a selected row. It
      also delays the first responder assignment if a doubleAction needs to (possibly)
      be sent. 'event' may be nil if there is no applicable event.
     */
    - (BOOL)validateProposedFirstResponder:(NSResponder *)responder forEvent:(nullable NSEvent *)event NS_AVAILABLE_MAC(10_7);
    

Instead of overriding -mouseDown: on NSTableView, we merely have to implement -validateProposedFirstResponder:forEvent: such that it returns YES for the text field we wish to edit. This will let -mouseDown: events propagate to the text field rather than stop at the table view.

Here's a simple implementation that does just what we need:

    
    - (BOOL)validateProposedFirstResponder:(NSResponder *)responder forEvent:(NSEvent *)event
    {
        if ([responder isKindOfClass:[EditableTextField class]])
            return YES;

        return [super validateProposedFirstResponder:responder forEvent:event];
    }
    

Sure enough, double-clicking the text field now makes it editable, and hitting the Return key turns it back into a label.