NSButton Delayed NSMenu - Objective-C / Cocoa

I want to create an NSButton that dispatches an action when pressed, but when it is pressed for 1 or two seconds, it shows NSMenu. Just like this question here , but since this answer does not solve my problem, I decided to ask again.

As an example, go to Finder, open a new window, go to some folders and click the "Back" button: you go to the previous folder. Now press and hold the back button: a menu will appear. I do not know how to do this using NSPopUpButton .

+7
source share
3 answers

Use NSSegmentedControl .

Add the menu by sending setMenu:forSegment: to the control (connecting anything to the menu output in IB will not do the trick). You have an action associated with the control (this is important).

It should work as you described.

+8
source

Subclass NSPopUpButton and override mouseDown / mouseUp .

Delay the mouseDown event for a moment before calling super and only if the mouse is still held.

Set the mouseUp selectedMenuItem event to nil (and therefore selectedMenuItemIndex will be -1 ) before launching the target / action button.

The only problem is to handle quick clicks, when the one-click timer can fire at a time when the mouse is unavailable for a subsequent click. Instead of using NSTimer and its invalidity, I chose a simple counter for mouseDown events and drank if the counter changed.

Here is the code I use 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 
+5
source

If anyone else needs it, here my solution is based on a simple NSButton, not a segmented control.

Subclass NSButton and implement a custom mouseDown that starts a timer in the current execution loop. In mouseUp check if the timer is off. In this case, cancel it and perform the default action.

This is a very simple approach, it works with any NSButton that you can use in IB.

Code below:

 - (void)mouseDown:(NSEvent *)theEvent { [self setHighlighted:YES]; [self setNeedsDisplay:YES]; _menuShown = NO; _timer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(showContextMenu:) userInfo:nil repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode]; } - (void)mouseUp:(NSEvent *)theEvent { [self setHighlighted:NO]; [self setNeedsDisplay:YES]; [_timer invalidate]; _timer = nil; if(!_menuShown) { [NSApp sendAction:[self action] to:[self target] from:self]; } _menuShown = NO; } - (void)showContextMenu:(NSTimer*)timer { if(!_timer) { return; } _timer = nil; _menuShown = YES; NSMenu *theMenu = [[NSMenu alloc] initWithTitle:@"Contextual Menu"]; [[theMenu addItemWithTitle:@"Beep" action:@selector(beep:) keyEquivalent:@""] setTarget:self]; [[theMenu addItemWithTitle:@"Honk" action:@selector(honk:) keyEquivalent:@""] setTarget:self]; [theMenu popUpMenuPositioningItem:nil atLocation:NSMakePoint(self.bounds.size.width-8, self.bounds.size.height-1) inView:self]; NSWindow* window = [self window]; NSEvent* fakeMouseUp = [NSEvent mouseEventWithType:NSLeftMouseUp location:self.bounds.origin modifierFlags:0 timestamp:[NSDate timeIntervalSinceReferenceDate] windowNumber:[window windowNumber] context:[NSGraphicsContext currentContext] eventNumber:0 clickCount:1 pressure:0.0]; [window postEvent:fakeMouseUp atStart:YES]; [self setState:NSOnState]; } 

I published the desktop on my github.

+1
source

All Articles