NSButton с задержкой NSMenu - Objective-C / Какао - PullRequest
9 голосов
/ 08 февраля 2012

Я хочу создать NSButton, который отправляет действие при щелчке, но когда он нажимается в течение 1 или двух секунд, отображается NSMenu.Точно так же, как этот вопрос здесь , но так как этот ответ не решает мою проблему, я решил спросить еще раз.

В качестве примера перейдите в Finder, откройте новое окно,перейдите в некоторые папки, а затем нажмите кнопку «Назад»: вы перейдете в предыдущую папку.Теперь нажмите и удерживайте кнопку «Назад»: появится меню.Я не знаю, как это сделать с NSPopUpButton.

Ответы [ 4 ]

12 голосов
/ 20 марта 2012

Используйте NSSegmentedControl.

Добавьте меню, отправив setMenu:forSegment: элементу управления (подключение чего-либо к выходу menu в IB не поможет).Подключите действие к элементу управления (это важно).

Должно работать точно так, как вы описали.

6 голосов
/ 30 декабря 2012

Создайте подкласс NSPopUpButton и переопределите события mouseDown / mouseUp.

Имейте задержку события mouseDown на мгновение перед вызовом реализации super и только еслимышь все еще удерживается нажатой.

Для события mouseUp установите selectedMenuItem в nil (и, следовательно, selectedMenuItemIndex будет -1), прежде чем нажать кнопку target / * 1016.*.

Единственная другая проблема заключается в обработке быстрых щелчков, когда таймер для одного щелчка может срабатывать в тот момент, когда мышь не работает в течение какого-то будущего щелчка.Вместо использования NSTimer и аннулирования его, я решил использовать простой счетчик для mouseDown событий и выручить, если счетчик изменился.

Вот код, который я использую в своем подклассе:

// 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
4 голосов
/ 10 октября 2016

Если кому-то все еще это нужно, вот мое решение, основанное на простом NSButton, а не на сегментированном элементе управления.

Подкласс NSButton и реализация пользовательского mouseDown, который запускает таймер в текущем цикле выполнения.В mouseUp проверьте, не сработал ли таймер.В этом случае отмените его и выполните действие по умолчанию.

Это очень простой подход, он работает с любым NSButton, который вы можете использовать в IB.

Код ниже:

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

Я разместил рабочий образец на моем GitHub.

1 голос
/ 03 августа 2018

Поздно к вечеринке, но здесь немного другой подход, также подклассы NSButton:

///
/// @copyright © 2018 Vadim Shpakovski. All rights reserved.
///

import AppKit

/// Button with a delayed menu like Safari Go Back & Forward buttons.
public class DelayedMenuButton: NSButton {

  /// Click & Hold menu, appears after `NSEvent.doubleClickInterval` seconds.
  public var delayedMenu: NSMenu?
}

// MARK: -

extension DelayedMenuButton {

  public override func mouseDown(with event: NSEvent) {

    // Run default implementation if delayed menu is not assigned
    guard delayedMenu != nil, isEnabled else {
      super.mouseDown(with: event)
      return
    }

    /// Run the popup menu if the mouse is down during `doubleClickInterval` seconds
    let delayedItem = DispatchWorkItem { [weak self] in
      self?.showDelayedMenu()
    }
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(NSEvent.doubleClickInterval * 1000)), execute: delayedItem)

    /// Action will be set to nil if the popup menu runs during `super.mouseDown`
    let defaultAction = self.action

    // Run standard tracking
    super.mouseDown(with: event)

    // Restore default action if popup menu assigned it to nil
    self.action = defaultAction

    // Cancel popup menu once tracking is over
    delayedItem.cancel()
  }
}

// MARK: - Private API

private extension DelayedMenuButton {

  /// Cancels current tracking and runs the popup menu
  func showDelayedMenu() {

    // Simulate mouse up to stop native tracking
    guard
      let delayedMenu = delayedMenu, delayedMenu.numberOfItems > 0, let window = window, let location = NSApp.currentEvent?.locationInWindow,
      let mouseUp = NSEvent.mouseEvent(
        with: .leftMouseUp, location: location, modifierFlags: [], timestamp: Date.timeIntervalSinceReferenceDate,
        windowNumber: window.windowNumber, context: NSGraphicsContext.current, eventNumber: 0, clickCount: 1, pressure: 0
      )
    else {
        return
    }

    // Cancel default action
    action = nil

    // Show the default menu
    delayedMenu.popUp(positioning: nil, at: .init(x: -4, y: bounds.height + 2), in: self)

    // Send mouse up when the menu is on screen
    window.postEvent(mouseUp, atStart: false)
  }
}
...