2012-02-08 14 views
7

Quiero crear un NSButton que envía una acción cuando se hace clic, pero cuando se presiona durante 1 o dos segundos muestra un NSMenu. Exactamente lo mismo que esta pregunta here, pero como esa respuesta no resuelve mi problema, decidí preguntar nuevamente.NSButton con retraso NSMenu - Objective-C/Cocoa

Como ejemplo, vaya a Finder, abra una nueva ventana, navegue por algunas carpetas y luego haga clic en el botón Atrás: vaya a la carpeta anterior. Ahora haz clic y mantén presionado el botón Atrás: se muestra un menú. No sé cómo hacer esto con un NSPopUpButton.

Respuesta

8

Use NSSegmentedControl.

Agregue un menú enviando setMenu:forSegment: al control (conectar cualquier cosa a la toma menu en IB no hará el truco). Tener una acción conectada al control (esto es importante).

Debería funcionar exactamente como lo describió.

+1

Lástima que no puede establecer la altura de personalización para NSSegmentedControl - Necesito ese menú adjunto a un botón grande. – zrxq

+0

¡Funcionó a la perfección! ¡Gracias! – Alex

+0

Buen truco, con un poco de juego en IB puedes obtener un control realmente elegante como este. –

5

Crea una subclase de NSPopUpButton y anula los eventos mouseDown/mouseUp.

Tienen la demora de evento mouseDown por un momento antes de llamar a la implementación de super y solo si el mouse aún se mantiene presionado.

tienen el evento mouseUp establece la selectedMenuItem a nil (y por lo tanto habrá selectedMenuItemIndex-1) antes de disparar el botón de target/action.

El único otro problema es manejar clics rápidos, donde el temporizador de un clic puede disparar en el momento en que el mouse está apagado para un clic futuro. En lugar de usar un NSTimer e invalidarlo, elegí tener un contador simple para los eventos mouseDown y rescatarme si el contador ha cambiado.

Aquí está el código que estoy utilizando en mi subclase:

// 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 
+1

¡Hermoso! Esto es exactamente lo que estaba buscando. Es una lástima que no haya un control estándar en el Kit de aplicaciones para este tipo de cosas (lo cual es extraño porque Apple usa esta convención de interfaz de usuario en * muchas * de sus propias aplicaciones). – aapierce

+1

Para 'delayInSeconds' considere usar' NSEvent.doubleClickInterval' en lugar de la constante '0.2'. Esto ajustará la demora según las preferencias de manejo del mouse del usuario. Retraso más rápido, menos, para usuarios con tiempos cortos de doble clic y más lento, más retraso, para usuarios con tiempos de doble clic más largos. –

+1

Gracias @GrahamMiln, he actualizado mi respuesta para hacer eso. –

1

Si alguien todavía necesita esto, aquí está mi solución basada en un NSButton normal, no un control segmentado.

Subclase NSEjecute e implemente un mouseDown personalizado que inicie un temporizador dentro del bucle de ejecución actual. En mouseUp, compruebe si el temporizador no se ha disparado. En ese caso, cancélelo y realice la acción predeterminada.

Este es un enfoque muy simple, funciona con cualquier NSButton que pueda usar en IB.

código de abajo

:

- (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]; 
} 

he publicado un working sample en mi GitHub.