Distinguishing a single click from a double click in Cocoa on the Mac

Viewed 9853

I have a custom NSView (it's one of many and they all live inside an NSCollectionView — I don't think that's relevant, but who knows). When I click the view, I want it to change its selection state (and redraw itself accordingly); when I double-click the view, I want it to pop up a larger preview window for the object that was just double-clicked.

My first looked like this:

- (void)mouseUp: (NSEvent *)theEvent {
    if ([theEvent clickCount] == 1) [model setIsSelected: ![model isSelected]];
    else if ([theEvent clickCount] == 2) if ([model hasBeenDownloaded]) [mainWindowController showPreviewWindowForPicture:model];
}

which mostly worked fine. Except, when I double-click the view, the selection state changes and the window pops up. This is not exactly what I want.

It seems like I have two options. I can either revert the selection state when responding to a double-click (undoing the errant single-click) or I can finagle some sort of NSTimer solution to build in a delay before responding to the single click. In other words, I can make sure that a second click is not forthcoming before changing the selection state.

This seemed more elegant, so it was the approach I took at first. The only real guidance I found from Google was on an unnamed site with a hyphen in its name. This approach mostly works with one big caveat.

The outstanding question is "How long should my NSTimer wait?". The unnamed site suggests using the Carbon function GetDblTime(). Aside from being unusable in 64-bit apps, the only documentation I can find for it says that it's returning clock-ticks. And I don't know how to convert those into seconds for NSTimer.

So what's the "correct" answer here? Fumble around with GetDblTime()? "Undo" the selection on a double-click? I can't figure out the Cocoa-idiomatic approach.

5 Answers

@Dave DeLong's solution in Swift 4.2 (Xcode 10, macOS 10.13), amended for use with event.location(in: view)

var singleClickPoint: CGPoint?

override func mouseDown(with event: NSEvent) {
singleClickPoint = event.location(in: self)
perform(#selector(GameScene.singleClickAction), with: nil, afterDelay: NSEvent.doubleClickInterval)
 if event.clickCount == 2 {
    RunLoop.cancelPreviousPerformRequests(withTarget: self)
    singleClickPoint = nil
//do whatever you want on double-click
}
}

@objc func singleClickAction(){
guard let singleClickPoint = singleClickPoint else {return}
//do whatever you want on single-click
}

The reason I'm not using singleClickAction(at point: CGPoint) and calling it with: event.location(in: self) is that any point I pass in - including CGPoint.zero - ends up arriving in the singleClick Action as (0.0, 9.223372036854776e+18). I will be filing a radar for that, but for now, bypassing perform is the way to go. (Other objects seem to work just fine, but CGPoints do not.)

Related