带有延迟 NSMenu 的 NSButton - Objective-C/Cocoa
Posted
技术标签:
【中文标题】带有延迟 NSMenu 的 NSButton - Objective-C/Cocoa【英文标题】:NSButton with delayed NSMenu - Objective-C/Cocoa 【发布时间】:2012-03-01 01:16:04 【问题描述】:我想创建一个NSButton
,当它被点击时会发送一个动作,但是当它被按下 1 或 2 秒时它会显示一个 NSMenu。和这个问题here一模一样,但是因为那个答案并没有解决我的问题,所以我决定再问一次。
例如,转到 Finder,打开一个新窗口,浏览一些文件夹,然后单击返回按钮:您转到上一个文件夹。现在单击并按住返回按钮:显示一个菜单。我不知道如何使用NSPopUpButton
来做到这一点。
【问题讨论】:
【参考方案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)
【讨论】:
以上是关于带有延迟 NSMenu 的 NSButton - Objective-C/Cocoa的主要内容,如果未能解决你的问题,请参考以下文章
Mac之button的使用Show+NSMenu+next+to+NSButton+in+Swift+OSX
使用 NSViewController 加载 nib 后...如何访问 nib 中的 NSButton?
以编程方式创建带有 NSMenuItems 的 NSMenu?
如何在 Firebreath NPAPI 插件中将 NSMenu 连接到 NSStatusItem,以便在单击状态栏项时出现菜单?