Enabling Row Selection and User Actions

Displaying a list of data in a table view is of little use if the user can’t select the rows. Using the methods of the NSTableView class, you let users select one or more rows, drag to change a selection, and type to select. In addition, you can enable programmatic selection and display a contextual menu that’s associated with the table.

Getting the Current Row Selection

The NSTableView class provides several methods for getting information about a table view’s currently selected rows: selectedRowIndexes, selectedRow, numberOfSelectedRows, and isRowSelected:.

The selectedRowIndexes method provides the full and correct selection whether a single item is selected or multiple items are selected. The method returns an NSIndexSet object containing the indexes of the selected rows.

The selectedRow method returns the index of only the last selected row, or -1 if no row is selected. Using this method is the easiest way to get the current selection when multiple selection is disabled.

The numberOfSelectedRows method, which returns the number of selected rows, can be used as a shortcut to determine whether any objects are selected when empty selection is permitted. Its value can also be used to determine whether user interface items should be enabled or disabled if they depend upon multiple items being selected in the table view. For example, if the numberOfSelectedRows is greater than 1, you can disable the portions of the user interface that are relevant only when a single item is selected.

The isRowSelected: method allows an app to find out whether a row at a specific index is selected.

Apps often need to iterate over the selected rows. You can implement this by iterating over the contents of the selection returned by selectedRowIndexes or by using the NSIndexSet block enumeration method enumerateIndexesUsingBlock:. For more information about using these techniques, see Iterating Through Index Sets in Collections Programming Topics.

Responding to Changes in Row Selection

Users can select multiple rows using the Shift key (for continuous selections) or the Command key (for non-contiguous selections). For more information about user selection actions, see Make User Input Convenient in OS X Human Interface Guidelines.

As the user selects rows by dragging (using a trackpad or mouse), delegate methods can be informed of the changes in row selection. The delegate receives the following messages as changes in the selection occurs by the user. (Delegates for table views that are managed by Cocoa bindings also receive these messages and can act on them as required.) An app can receive these messages on a continuous basis, so it’s important to create efficient implementations.

The tableViewSelectionIsChanging: and tableViewSelectionDidChange: messages are notifications. Each is passed an NSNotification object. Sending the notification instance an object message returns the table view relevant to the selection change.

A table view delegate can also implement the tableView:selectionIndexesForProposedSelection: delegate method to allow or disallow the selection of a specific set of rows in a table view. The method is passed the indexes of the rows that are proposed to be selected and it returns the rows that will actually be selected. Implementing this method allows the app to selectively allow and disallow row selection as appropriate. For example, if the user clicks in an area of the table row that shouldn’t trigger selection, this method can be used to prevent that selection from taking place.

Allowing Multiple and Empty Selection

A table view can be configure selection in three ways:

These attributes can be configured in Interface Builder or programmatically.

Programmatically, the table view methods for enabling and disabling these options are set using the following methods: setAllowsMultipleSelection: and setAllowsEmptySelection:.

Selecting and Deselecting Rows Programmatically

To select rows programmatically, the NSTableView class provides the selectRowIndexes:byExtendingSelection: instance method.

The selectRowIndexes:byExtendingSelection: method expects an NSIndexSet containing the indexes (zero-based) of the rows to be selected, and a parameter that specifies whether the current selection should be extended. If the extending selection parameter is YES, the specified row indexes are selected in addition to any previously selected rows; if it’s NO, the selection is changed to the newly specified rows. When this method is called, the delegate receives only the tableViewSelectionDidChange: notification.

To deselect a row, pass the index of the row to deselect to deselectRow:. When this method is called the delegate receives only the tableViewSelectionDidChange: notification.

There are also two convenience methods that allow the selection and deselection of all the items in the table view. In general, these methods are connected to the user interface so that users can select or deselect all items in a table. The deselectAll: method deselects all the selected rows, but only if allowsEmptySelection returns YES. Similarly, selectAll: selects all the table view rows, but only if allowsMultipleSelection returns YES. Unlike the other programmatic methods, these two methods call selectionShouldChangeInTableView: on the delegate object, if implemented, followed by tableViewSelectionDidChange:. If either deselectAll: or selectAll: is called without the proper allows... setting, it is ignored.

Using Type Selection to Select Rows

To simplify navigation in tables or to allow a user to select items using the keyboard, a table view can support type selection. Using type selection, a user types the first few letters of an entry and the table view content is searched for a matching row. You can use the setAllowsTypeSelect: method to enable or disable type selection (by default, type selection is enabled).

Type selection uses the delegate method tableView:typeSelectStringForTableColumn:row: to figure out which rows to match against. This delegate method returns a string that type selection uses for matching.

When the tableView:typeSelectStringForTableColumn:row: method is not implemented by the delegate, the behavior is to call preparedCellAtColumn:row:, passing the column index and row and then returning the stringValue. This allows type selection to work on the values in any column and row in the table view. To restrict type selection to specific parts of the table, return nil for columns that should be excluded. For example, the following method implementation allows type selection to match values only in the “name” column.

- (NSString *)tableView:(NSTableView *)tableView typeSelectStringForTableColumn:(NSTableColumn *)tableColumn
                                                                            row:(NSInteger)row
{
    if ([[tableColumn identifier] isEqualToString:@"name"])
    {
        NSUInteger tableColumnIndex=[[tableView tableColumns] indexOfObject:tableColumn];
        return [[tableView preparedCellAtColumn:tableColumnIndex
                                            row:row] stringValue];
    }
    return nil;
}

Implementing the delegate method tableView:nextTypeSelectMatchFromRow:toRow:forString: allows the delegate to further customize type selection to match only a specific range of rows. This method is passed the current type selection string, and it compares the appropriate values within the selected rows, returning the row to select, or -1 if no row matches.

Finally, the tableView:shouldTypeSelectForEvent:withCurrentSearchString: method gives the delegate the option to allow or disallow type selection for a particular keyboard event. You can implement this method to prevent certain characters from causing type selection. Note that returning NO from this method prevents the specified event from being used for type selection; it doesn’t prevent standard event processing. Don’t use tableView:shouldTypeSelectForEvent:withCurrentSearchString: to handle your own keyboard shortcuts. If you need to provide custom keyboard shortcut handling, override keyDown: instead.

Displaying a Contextual Menu in a Table

You can use a contextual menu to offer users a convenient way to access a small set of commands that act on one or more items in a table. For example, when users Command-click a song listed in iTunes, a contextual menu appears that makes it easy to (among other things) play, copy, or delete the song.

An easy way to add a contextual menu to a table is to set the menu outlet of the table to an NSMenu object. And if you want to customize the contextual menu, you set an appropriate object as the menu’s delegate and implement the menuWillOpen: method to customize the menu before it appears.

In the action method for a menu item, determine whether the clicked table row is in the set of indexes returned by selectedRowIndexes. If it is, apply the action to all indexes in the set; otherwise, apply the action only to the clicked row. Here’s a convenience method that checks the selected row indexes and returns the set of indexes the action method should process:

- (NSIndexSet *)_indexesToProcessForContextMenu {
    NSIndexSet *selectedIndexes = [_tableViewMain selectedRowIndexes];
    // If the clicked row is in selectedIndexes, then process all selectedIndexes. Otherwise, process only clickedRow.
    if ([_tableViewMain clickedRow] != -1 && ![selectedIndexes containsIndex:[_tableViewMain clickedRow]]) {
        selectedIndexes = [NSIndexSet indexSetWithIndex:[_tableViewMain clickedRow]];
    }
    return selectedIndexes;
}

To see an example that uses the _indexesToProcessForContextMenu method, download the TableViewPlayground: Using View-Based NSTableView and NSOutlineView sample project and look at the mnuRevealInFinderSelected: method in ATComplexTableViewController.m.

Specifying How Subviews Should Respond to Events

Views or controls in a table sometimes need to respond to incoming events. To determine whether a particular subview should receive the current mouse event, a table view calls validateProposedFirstResponder:forEvent: in its implementation of hitTest. If you create a table view subclass, you can override validateProposedFirstResponder:forEvent: to specify which views can become the first responder. In this way, you receive mouse events.

The default NSTableView implementation of validateProposedFirstResponder:forEvent: uses the following logic:

  1. Return YES for all proposed first responder views unless they are instances or subclasses of NSControl.

  2. Determine whether the proposed first responder is an NSControl instance or subclass.

    • If the control is an NSButton object, return YES.

    • If the control is not an NSButton, call the control’s hitTestForEvent:inRect:ofView: to see whether the hit area is trackable (that is, NSCellHitTrackableArea) or is an editable text area (that is, NSCellHitEditableTextArea), and return the appropriate value. Note that if a text area is hit, NSTableView also delays the first responder action.