April 14 2009
Hitting space bar with two NSButtonCells in an NSTableView
On Mac OS X Leopard, AppKit’s release notes mention a great NSTableView feature: “Hitting Space will attempt to ‘performClick:’ on a NSButtonCell in the selected row, if there is only one instance in that row.” I’m not sure if this trick was built-in for free in previous OS version, but it’s a convenient keyboard action to quickly move through a workflow.
One NSButtonCell is nice, but having the space bar work with two NSButtonCells in an NSTableView row would be fantastic. I didn’t think that code should be difficult. I realized I could enable that action through custom code. There were two potential paths: using a custom selector on the delegate or leverage performClick: on the NSButtonCell. I posted a small sample app on Google Code to illustrate both paths.
For the first path, I create a NSTableView subclass and override the keyDown: method to catch the space bar key stroke; then, I send a selector to the subclass’s delegate. The delegate identifies which NSTableView called it and does the appropriate action. This path doesn’t leverage any bindings the NSTableColumn might have, but it works.
And yet, what I want is to call performClick: on the NSButtonCell within the keyDown: method in the subclass. The release notes indicate that AppKit takes this approach, and I can then leverage bindings. Elegant. Unfortunately, I can’t figure out how.
As the sample app shows, I can get as far as sending performClick: to the seemingly appropriate NSButtonCell. But nothing happens. To compare the default NSTableView path and my path, I subclassed NSButtonCell and put a breakpoint in performClick:. Then I removed the second NSButtonCell, so that the trick discussed in the release notes would trigger. I found an interesting distinction. When I call
I use one instance of NSButtonCell. But when I call
I use a different instance. I verified using po in gdb.
The call stack is slightly different. In the second instance, the keyDown: selector leads to _attemptToPerformClickOnFocusedColumn. That internal method finds the right NSButtonCell and sends it the performClick: selector. Somehow, the paths lead to different cells. This second path leads to the correct cell; it fires the column’s bindings and updates the underlying object’s state, which is then reflected in the button’s state. But the cell in my path isn’t valid. I even tried playing with its state without any bindings, but the button’s state never changed.
Frustrating. Maybe I’m just doing it wrong.