这是一个棘手的问题。就像许多答案所暗示的那样,在应用程序或窗口级别拦截事件是强制菜单项工作的可靠方法。同时它可能会破坏其他东西,例如,如果你有一个专注的 NSTextField 或 NSButton 你会希望他们消费事件,而不是菜单项。如果用户在系统偏好设置中重新定义该菜单项的等效键,例如,将 Space 更改为 P,这也可能会失败。
您使用与菜单项等效的空格键这一事实使事情变得更加棘手。空格是特殊的 UI 事件字符之一,与箭头键和其他一些字符一样,AppKit 会以不同的方式处理它们,并且在某些情况下会在它传播到主菜单之前消耗掉。
所以,有两件事要记住。首先是标准的响应者链:
-
NSApplication.sendEvent 向关键窗口发送事件。
- 按键窗口接收
NSWindow.sendEvent中的事件,判断是否为按键事件并在自身上调用performKeyEquivalent。
-
performKeyEquivalent 将其发送到当前窗口的firstResponder。
- 如果响应者不使用它,事件会递归地向上发送到
nextResponder。
-
performKeyEquivalent 返回 true 如果响应者之一消费事件,false 否则。
现在,第二个也是棘手的部分,如果事件没有被消耗(即performKeyEquivalent 返回false)window will try to process it as a special keyboard UI event - 这在Cocoa Event Handling Guide 中有简要提及:
Cocoa 事件调度架构将某些关键事件视为命令,以将控制焦点移动到窗口中的不同用户界面对象、模拟鼠标单击对象、关闭模式窗口以及在对象中进行选择允许选择。这种能力称为键盘界面控制。键盘界面控制中涉及的大部分用户界面对象都是 NSControl 对象,但不是控件的对象也可以参与。
这部分的工作方式非常简单:
- 窗口converts the key event in a corresponding action(选择器)。
- 它检查第一响应者是否
respondsToSelector 并调用它。
- 如果调用了该操作,则该事件将被视为已使用并且事件传播停止。
因此,考虑到所有这些,您必须确保两件事:
- 响应者链已正确设置。
- 响应者只消费他们需要的东西,否则传播事件。
第一点很少引起麻烦。第二个,这就是您的示例中发生的情况,需要注意 - AVPlayer 通常是第一响应者并消耗空格键事件以及其他一些事件。要完成这项工作,您需要覆盖 keyUp 和 keyDown 方法以将事件沿响应者链传播,就像在默认的 NSView 实现中发生的那样。
// All player keyboard gestures are disabled.
override func keyDown(with event: NSEvent) {
self.nextResponder?.keyDown(with: event)
}
// All player keyboard gestures are disabled.
override func keyUp(with event: NSEvent) {
self.nextResponder?.keyUp(with: event)
}
以上将事件向上转发到响应者链,最终将被主菜单接收。有一个问题,如果第一响应者是一个控件,比如NSButton 或任何自定义的NSControl-inheriting 对象,它将 使用该事件。通常您确实希望发生这种情况,但如果不希望发生这种情况,例如在实现自定义控件时,您可以覆盖 respondsToSelector:
override func responds(to selector: Selector!) -> Bool {
if selector == #selector(performClick(_:)) { return false }
return super.responds(to: selector)
}
这将阻止窗口消耗键盘 UI 事件,因此主菜单可以接收它。但是,如果您想拦截 ALL 键盘 UI 事件,包括当第一响应者能够使用它时,您确实希望覆盖窗口或应用程序的 performKeyEquivalent,但不要将其复制为其他答案建议:
override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Attempt to perform the key equivalent on the main menu first.
if NSApplication.shared.mainMenu?.performKeyEquivalent(with: event) == true { return true }
// Continue with the standard implementation if it doesn't succeed.
return super.performKeyEquivalent(with: event)
}
如果您在主菜单上调用 performKeyEquivalent 而不检查结果,您最终可能会调用它两次 - 第一次是手动调用,第二次是从 super 实现中自动调用,如果事件没有被响应链。当AVPlayer 是第一响应者并且keyDown 和keyUp 方法未被覆盖时,就会出现这种情况。
附:片段是 Swift 4,但想法是一样的! ✌️
附言有一个出色的WWDC 2010 Session 145 – Key Event Handling in Cocoa Applications 用优秀的例子深入介绍了这个主题。 WWDC 2010-11 不再在 Apple Developer Portal 上列出,但可以在 here 找到完整的会议列表。