NSTableView: checking for mouse-driven selection changes on macOS 27

I have an NSTableView used as a source list and, alongside it, two editors. When the user selects anything in the table view, its content is opened in the editor that has the focus. When the user Opt-clicks an item in the table, though, the content is opened in the other editor, making it easy for the user to load something in the other editor without having to change the focus first.

This has worked for many years using NSTableView.selectiondDidChange / the NSTableViewDelegate as follows:

func tableViewSelectionDidChange(_ notification: Notification) {
  if let event = tableView.window?.currentEvent,
     event.type == .leftMouseUp || event.type == .leftMouseDown,
    // (Real app does some other checks here too.)
     event.modifierFlags.contains(.option) {
       openInOtherEditor()
       return
  }
  openInCurrentEditor()
}

However, on macOS 27, it seems that things need to be done differently because of the transition to gesture recognisers for event handling. According to the WWDC video "Modernise Your AppKit App", and to Tech Note TN3212, currentEvent can no longer be relied upon to provide the event that actually triggered an action in NSControl subclasses:

The transition to gesture recognizers on NSControl objects changes the timing of when AppKit delivers control action messages with respect to event processing. As a result, currentEvent no longer returns the event that triggered an action.

It's unclear whether this new limitation refers only to NSControl.action or to all mouse-driven actions, but from the context and what the rest of the Tech Note has to say, I assume it's the latter. (Especially since you are no longer supposed to override mouseDown(with:), and the Console warns about gestures being disabled if you do override mouseDown(with:) in an NSTableView subclass on macOS 27.)

currentEvent still seems to work fine in this situation in the first macOS 27 beta, but it sounds as though we cannot rely on this continuing to be the case.

If we should no longer be using currentEvent, then, what should we use instead to determine whether a selection change was triggered by a mouse click? The Tech Note and WWDC video have nothing to say about this. They simply say that instead of overriding mouseDown(with:), you should use the selection-did-change delegate methods, which is of no help here. (By contrast, checking the modifier flags is still straightforward; the Tech Note says to use NSEvent.modifierFlags instead of currentEvent.modifierFlags.)

Two solutions sprung to mind, but neither worked:

  1. Check tableView.clickedRow != -1 in the selectionDidChange delegate method/notification response. This doesn't work, however, because clickedRow has been reset to -1 by the time NSTableView.selectionDidChange is sent.
  2. Add an action to the table view and check clickedRow there. This doesn't work either, though, because although clickedRow is available in the action method, I would now have to load content in response to both an action and a selection change, and since the selection changes before the action is called, there is no way of telling my selection-did-change method not to load in the main editor if Option is held down in the action.

The only solution I have found is to override selectRowIndexes(_:byExtendingSelection:), check for clickedRow != -1 there, set a didChangeSelectionWithMouse flag to true if so, and check that in the selection-did-change delegate method. That works, but it's not the most elegant of solutions.

So:

  1. Am I misunderstanding the Tech Note? Can currentEvent still in fact be used safely in tableViewSelectionDidChange(_:) in macOS 27 and beyond?
  2. If not, what is the recommended way of checking that the table selection has been changed by a mouse click?

Many thanks!

Answered by Frameworks Engineer in 893966022

With gesture recognizers, NSApp.currentEvent is not reliable. Since you're just trying to get at the state of the modifier flags it should be sufficient to just change to the class function NSEvent.modifierFlags inside your table view delegate call.

With gesture recognizers, NSApp.currentEvent is not reliable. Since you're just trying to get at the state of the modifier flags it should be sufficient to just change to the class function NSEvent.modifierFlags inside your table view delegate call.

Since you're just trying to get at the state of the modifier flags

I appreciate the answer and also that in retrospect I should have made my actual question clear sooner in my post, but my question was about how to check if the selection was changed by a mouse click, not about how to get the state of modifier flags. (I mentioned in my post that getting the modifier flags is not a problem and covered by the Tech Note.)

So how do I check if a table or outline view selection change was triggered by a mouse click (as opposed to e.g. keyboard navigation) without relying on currentEvent?

In my example, opening content in the other editor should happen only if the selection has changed because the user is pressing Option while mouse-clicking on a row. (We wouldn't want this behaviour while using the arrow keys to select while holding Option, for instance, because Option already has a meaning in this case: selecting the first or last item.) Option-click has a long tradition on macOS of providing alternative actions like this, and it's a handy trick that has been available in my app for years.

Note that using NSEvent.pressedMouseButtons doesn't work here, either, because the value of that will be 0 by the time tableViewSelectionDidChange is called.

So withoutcurrentEvent, how do I check to see if tableViewSelectionDidChange was triggered by a mouse click?

Thank you.

Actually, referring to the ancient DragNDropOutlineView code linked to from the documentation for NSTableView.clickedRow, it seems that although clickedRow has been reset to -1 by the time tableViewDidChangeSelection(_:) is called, it is available and correct in tableView(_:shouldSelectRow:) and its more modern equivalent, tableView(_:selectionIndexesForProposedSelection:).

So a simple solution is to check for clickedRow in one of these delegate methods to see if the selection is changing owing to a click, and then save the information in a property that can be used in the didChange delegate method, like this:

private var didClick = false
    
func tableView(_ tableView: NSTableView, selectionIndexesForProposedSelection proposedSelectionIndexes: IndexSet) -> IndexSet {
  didClick = tableView.clickedRow != -1 && proposedSelectionIndexes.contains(tableView.clickedRow)
  return proposedSelectionIndexes
}
    
func tableViewSelectionDidChange(_ notification: Notification) {

  // We must reset this because `tableViewSelectionDidChange` could be called without the `proposedSelection` delegate method being called first.
  defer {
    didClick = false
  }

  if didClick && NSEvent.modifierFlags.contains(.option) {
    openInOtherEditor()
    return
  }
  openInCurrentEditor()
}
NSTableView: checking for mouse-driven selection changes on macOS 27
 
 
Q