2012-02-08 19 views
7

Chcę utworzyć NSButton, który wysyła akcję po kliknięciu, ale po naciśnięciu przez 1 lub 2 sekundy pokazuje NSMenu. Dokładnie to samo co to pytanie here, ale ponieważ ta odpowiedź nie rozwiązuje mojego problemu, postanowiłem ponownie zapytać.NSButton z opóźnionym NSMenu - Objective-C/kakao

Jako przykład, przejdź do Findera, otwórz nowe okno, przejdź do niektórych folderów, a następnie kliknij przycisk Wstecz: przechodzisz do poprzedniego folderu. Teraz kliknij i przytrzymaj przycisk Wstecz: wyświetlane jest menu. Nie wiem jak to zrobić z NSPopUpButton.

Odpowiedz

8

Użyj NSSegmentedControl.

Dodaj menu, wysyłając setMenu:forSegment: do kontrolki (podłączenie czegokolwiek do gniazda menu w IB nie wystarczy). Podejmij działanie związane z kontrolą (jest to ważne).

Powinien działać dokładnie tak, jak opisano.

+1

Szkoda, że ​​nie można ustawić niestandardowej wysokości dla NSSegmentedControl - Potrzebuję tego menu dołączonego do dużego przycisku. – zrxq

+0

Działa doskonale! Dzięki! – Alex

+0

Przyjemna sztuczka, przy odrobinie zabawy w IB, możesz uzyskać naprawdę elegancką kontrolę. –

5

Utwórz podklasę NSPopUpButton i nadpisaj zdarzenia mouseDown/mouseUp.

Mieć opóźnienie zdarzenia mouseDown przez chwilę przed wywołaniem implementacji super i tylko wtedy, gdy mysz jest nadal przytrzymana.

Czy zdarzenie mouseUp ustawić selectedMenuItem do nil (a więc selectedMenuItemIndex będzie -1) przed wypalaniem przycisk na target/action.

Jedynym innym problemem jest obsługa szybkich kliknięć, w których licznik czasu na jedno kliknięcie może wystrzelić w momencie, gdy mysz nie działa, aby uzyskać kolejne kliknięcie. Zamiast używać NSTimer i unieważniam go, wybrałem prosty licznik zdarzeń mouseDown i wycofam, jeśli licznik się zmienił.

Oto kod używam w moim podklasy:

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

Piękny! Właśnie tego szukałem. Szkoda, że ​​nie ma standardowej kontroli w App Kit dla tego typu rzeczy (co jest dziwne, ponieważ Apple używa tej konwencji UI w * wielu * własnych aplikacjach). – aapierce

+1

Dla 'delayInSeconds' rozważ użycie' NSEvent.doubleClickInterval' zamiast stałej '0.2'. Spowoduje to dostosowanie opóźnienia zgodnie z preferencjami obsługi myszy przez użytkownika. Szybsze i mniejsze opóźnienie dla użytkowników o krótkich czasach podwójnego kliknięcia i wolniejszym, większym opóźnieniu dla użytkowników z dłuższym dwukrotnym kliknięciem. –

+1

Dzięki @GrahamMiln, zaktualizowałem swoją odpowiedź, aby to zrobić. –

1

Jeśli ktoś jeszcze tego potrzebuje, oto moje rozwiązanie oparte na prostym NSButton, a nie segmentowej kontroli.

Podklasa NSButton i zaimplementuj niestandardowy mouseDown, który uruchamia zegar w bieżącej pętli uruchamiania. W mouseUp sprawdź, czy timer nie został uruchomiony. W takim przypadku anuluj i wykonaj domyślną akcję.

Jest to bardzo proste podejście, działa z dowolnym NSButtonem, którego można użyć w IB.

kod poniżej:

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

mam napisali working sample na moim GitHub.