【问题标题】:NSButton with delayed NSMenu - Objective-C/Cocoa带有延迟 NSMenu 的 NSButton - Objective-C/Cocoa
【发布时间】:2012-03-01 01:16:04
【问题描述】:

我想创建一个NSButton,当它被点击时会发送一个动作,但是当它被按下 1 或 2 秒时它会显示一个 NSMenu。和这个问题here一模一样,但是因为那个答案并没有解决我的问题,所以我决定再问一次。

例如,转到 Finder,打开一个新窗口,浏览一些文件夹,然后单击返回按钮:您转到上一个文件夹。现在单击并按住返回按钮:显示一个菜单。我不知道如何使用NSPopUpButton 来做到这一点。

【问题讨论】:

    标签: cocoa delay show nsbutton nsmenu


    【解决方案1】:

    使用NSSegmentedControl

    通过将setMenu:forSegment: 发送到控件来添加菜单(将任何东西连接到IB 中的menu 插座不会成功)。将操作连接到控件(这很重要)。

    应该完全按照您的描述工作。

    【讨论】:

    • 太糟糕了,您不能为 NSSegmentedControl 设置自定义高度 - 我需要将该菜单附加到一个大按钮上。
    【解决方案2】:

    创建NSPopUpButton 的子类并覆盖mouseDown/mouseUp 事件。

    在调用super 的实现之前让mouseDown 事件延迟片刻,并且仅当鼠标仍被按住时。

    在触发按钮的target/action 之前,让mouseUp 事件将selectedMenuItem 设置为nil(因此selectedMenuItemIndex 将是-1)。

    唯一的另一个问题是处理快速单击,其中单击计时器可能会在鼠标按下以进行将来的单击时触发。我没有使用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
    

    【讨论】:

    • 漂亮!这正是我一直在寻找的。太糟糕了,App Kit 中没有针对此类事情的标准控件(这很奇怪,因为 Apple 在自己的应用中很多使用了这种 UI 约定)。
    • 对于delayInSeconds,请考虑使用NSEvent.doubleClickInterval 而不是常量0.2。这将根据用户的鼠标处理偏好调整延迟。对于双击时间较短的用户,速度更快,延迟更少;对于双击时间较长的用户,速度更慢,延迟更多。
    【解决方案3】:

    如果有人仍然需要这个,这是我基于普通 NSButton 的解决方案,而不是分段控件。

    继承 NSButton 并实现一个自定义 mouseDown,它在当前运行循环中启动一个计时器。在mouseUp 中,检查计时器是否未触发。在这种情况下,取消它并执行默认操作。

    这是一种非常简单的方法,它适用于您可以在 IB 中使用的任何 NSButton。

    代码如下:

    - (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 上发布了working sample

    【讨论】:

      【解决方案4】:

      聚会迟到了,但这里有一点不同的方法,也继承了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)
        }
      }
      

      【讨论】:

        猜你喜欢
        • 2011-03-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-08-18
        • 1970-01-01
        相关资源
        最近更新 更多