Create a subclass of NSPopUpButton
and override the mouseDown
/mouseUp
events.
Have the mouseDown
event delay for a moment before calling super
's implementation and only if the mouse is still being held down.
Have the mouseUp
event set the selectedMenuItem
to nil
(and therefore selectedMenuItemIndex
will be -1
) before firing the button's target
/action
.
The only other issue is to handle rapid clicks, where the timer for one click might fire at the moment when the mouse is down for some future click. Instead of using an NSTimer
and invalidating it, I chose to have a simple counter for mouseDown
events and bail out if the counter has changed.
Here's the code I'm using in my subclass:
// MyClickAndHoldPopUpButton.h
@interface MyClickAndHoldPopUpButton : NSPopUpButton
@end
// MyClickAndHoldPopUpButton.m
@interface MyClickAndHoldPopUpButton ()
@property BOOL mouseIsDown;
@property BOOL menuWasShownForLastMouseDown;
@property int mouseDownUniquenessCounter;
@end
@implementation MyClickAndHoldPopUpButton
// highlight the button immediately but wait a moment before calling the super method (which will show our popup menu) if the mouse comes up
// in that moment, don't tell the super method about the mousedown at all.
- (void)mouseDown:(NSEvent *)theEvent
{
self.mouseIsDown = YES;
self.menuWasShownForLastMouseDown = NO;
self.mouseDownUniquenessCounter++;
int mouseDownUniquenessCounterCopy = self.mouseDownUniquenessCounter;
[self highlight:YES];
float delayInSeconds = [NSEvent doubleClickInterval];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
if (self.mouseIsDown && mouseDownUniquenessCounterCopy == self.mouseDownUniquenessCounter) {
self.menuWasShownForLastMouseDown = YES;
[super mouseDown:theEvent];
}
});
}
// if the mouse was down for a short enough period to avoid showing a popup menu, fire our target/action with no selected menu item, then
// remove the button highlight.
- (void)mouseUp:(NSEvent *)theEvent
{
self.mouseIsDown = NO;
if (!self.menuWasShownForLastMouseDown) {
[self selectItem:nil];
[self sendAction:self.action to:self.target];
}
[self highlight:NO];
}
@end