NSTableView cells with a double-click is the default behavior. I wrote about how to make the column header cells editable already. But in order to figure out how to end an active editing session when the user hits ⌘S to save the document, I grew more and more disappointed by the day.
On iOS, everything is
UIView-based. On the Mac, though, you have the legacy of "cells". They are a bit more primitive ancestors to views. They are little boxes that draw stuff and may be editable. They have nothing to do with table cells. Most components we use on the Mac are
NSView-based, though, a lot of them wrapping ancient
NSCells for the action.
I put a few common components in a diagram and highlighted the stuff you need to build a
NSTableView to make sense of all this. You see that the table-related things are scattered all around the place and that
NSTableHeaderCell is a real oddball.
The actual table cells in
NSTableView are of
NSTableCellView kind. They contain a
NSTextField to display text. And as long as they are editable, this very text field turns into a text box on double click. "Turning into a text box" is not very accurate. In fact, the containing
NSWindow owns a so-called field editor. It's a
NSText object that is hidden – unless you edit a text field, in which case it takes the text field's content and draws itself over that very text field. It looks as if you move the cursor into the text field, but in reality you make the field editor draw itself above the text field for editing. Crazy. But I guess it was easy on the system's resources back in the day. We utilized this knowledge to make column header cells appear editable already.
The widgets you place in Interface Builder are mostly either dumb
NSView-based containers to display stuff or
NSControl-based objects to respond to user interaction.
NSButton are such controls. The table header draws each column's header cells. The header cells aren't
NSView-backed though. And
NSTableColumns are no views at all – they're more like view models. In short, it's not unified and can cause quite some trouble.
The thing is that
NSTextField and the field editor's
NSText (or, more specifically,
NSTextView) have nothing in common. One uses the other. Also, the
NSTextFieldCell that backs a column header has nothing in common with
Prize question: How do you end the user's editing session? When she hits ⌘S, the text that is currently being edited is not yet part of the underlying widget. It's only visible in the field editor that is drawn above it. So you have to end editing first to commit the changes.
Editing sessions inevitably utilize a field editor, so the only chance you have is to subscribe to notifications. If you have a
NSDocument-based application, you'll have to filter out the notifications from unrelated documents, though, or else all your documents are going to be synchronized.
In "How do I detect start and end edit sessions of a cell in
NSTableView?" (QA1551), Apple kindly instructs us to use the appropriate delegate methods. These only work for editing table cells, though.
control:textShouldBeginEditing:is called for subclasses of
NSControlthat are being edited. But header cells are no subclasses of
NSControl, so you don't get the same behavior.
NSControlTextDidBeginEditingNotificationis posted by
NSControlsubclasses. The field editor is not a subclass of
NSControl, so you will still only get this notification for table cells.
In terms of delegates, you're left with
NSTextViewDelegate.textShouldBeginEditing(_:) to notice that the header cell is being edited. The best place to put this is in a subclass of
NSTableHeaderCell. Just as we did to make it editable in the first place.
In terms of notifications,
NSTextDidBeginEditingNotification is fired for both the manual summoning of a field editor in the header cells and for editing table cell text fields.
So there is a universal way to get notified when the user edits text on the level of
NSText. But it's so different from the delegate methods I have in place that I wonder how I could make use of this without duplicating logic.
Browse the blog archive