パーティーに遅れましたが、これは少し異なるアプローチであり、サブクラス化も行っています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)
}
}