11

我想为 NSMenuItem (在应用程序主菜单中)设置等效键“”(空格),而不需要任何修饰符。

从文档如下:

例如,在播放媒体的应用程序中,播放命令可能只映射到“”(空格),而没有命令键。您可以使用以下代码执行此操作:

[menuItem setKeyEquivalent:@" "];

[menuItem setKeyEquivalentModifierMask:0];

等效键设置成功,但不起作用。当我在没有修饰符的情况下按“空格”键时,什么也没有发生,但是当我用“Fn”修饰键按“空格”时它会起作用。

我需要使用没有修饰符的“空格”。请提供任何帮助!

4

5 回答 5

4

我有同样的问题。我没有认真调查,但据我所知,空格键“看起来”不像 Cocoa 的键盘快捷键,所以它被路由到-insertText:. 我的解决方案是对 NSWindow 进行子类化,在它沿着响应者链上升时将其捕获(假设您可以将 NSApp 子类化),然后将其显式发送到菜单系统:

- (void)insertText:(id)insertString
{
    if ([insertString isEqual:@" "]) {
        NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
                                              location:[self mouseLocationOutsideOfEventStream]
                                         modifierFlags:0
                                             timestamp:[[NSProcessInfo processInfo] systemUptime]
                                          windowNumber:self.windowNumber
                                               context:[NSGraphicsContext currentContext]
                                            characters:@" "
                           charactersIgnoringModifiers:@" "
                                             isARepeat:NO
                                               keyCode:49];
        [[NSApp mainMenu] performKeyEquivalent:fakeEvent];
    } else {
        [super insertText:insertString];
    }
}
于 2012-08-03T17:57:58.817 回答
3

这是一个棘手的问题。就像许多答案所暗示的那样,在应用程序或窗口级别拦截事件是强制菜单项工作的可靠方法。同时它可能会破坏其他东西,例如,如果您有一个焦点NSTextField或者NSButton您希望他们消费事件,而不是菜单项。如果用户在系统首选项中重新定义该菜单项的等效键(即更改SpaceP.

您使用与菜单项等效的空格键这一事实使事情变得更加棘手。空格是特殊的 UI 事件字符之一,与箭头键和其他一些字符一样,AppKit 会以不同的方式处理它们,并且在某些情况下会在它传播到主菜单之前消耗掉。

所以,有两件事要记住。首先,是标准的响应者链:

  1. NSApplication.sendEvent将事件发送到键窗口。
  2. 按键窗口接收到事件NSWindow.sendEvent,判断是否为按键事件并performKeyEquivalent在自身上调用。
  3. performKeyEquivalent将其发送到当前窗口的firstResponder.
  4. 如果响应者不使用它,则事件会递归地向上发送到nextResponder.
  5. performKeyEquivalenttrue如果其中一个响应者消费了该事件,则返回,false否则。

现在,第二个也是棘手的部分,如果事件没有被消耗(即performKeyEquivalent返回时false),窗口将尝试将其作为特殊的键盘 UI 事件进行处理——这在Cocoa 事件处理指南中有简要提及:

Cocoa 事件调度架构将某些关键事件视为命令,以将控制焦点移动到窗口中的不同用户界面对象、模拟鼠标单击对象、关闭模式窗口以及在允许选择的对象中进行选择. 这种能力称为键盘界面控制。键盘界面控制中涉及的大多数用户界面对象都是 NSControl 对象,但不是控件的对象也可以参与。

这部分的工作方式非常简单:

  1. 窗口将按键事件转换为相应的操作(选择器)。
  2. 它与第一响应者检查是否它respondsToSelector并调用它。
  3. 如果调用了该操作,则该事件被视为已消耗,并且事件传播停止。

因此,考虑到所有这些,您必须确保两件事:

  1. 响应者链已正确设置。
  2. 响应者只消费他们需要的东西,否则传播事件。

第一点很少给人带来麻烦。第二个,这就是您的示例中发生的情况,需要注意 -AVPlayer通常是第一响应者并消耗空格键事件,以及其他一些事件。要完成这项工作,您需要重写keyUpkeyDown方法以将事件沿响应者链传播,就像在默认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继承对象,它将消耗该事件。通常您确实希望发生这种情况,但如果不是,例如在实现自定义控件时,您可以覆盖respondsToSelector

override func responds(to selector: Selector!) -> Bool {
    if selector == #selector(performClick(_:)) { return false }
    return super.responds(to: selector)
}

这将阻止窗口消耗键盘 UI 事件,因此主菜单可以接收它。但是,如果您想拦截所有键盘 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第一响应者和keyDownkeyUp方法未被覆盖时,就会出现这种情况。

PS Snippets 是 Swift 4,但想法是一样的!✌️

PPS 有一个精彩的WWDC 2010 Session 145 – Cocoa 应用程序中的关键事件处理,它通过出色的示例深入介绍了这个主题。WWDC 2010-11 不再在 Apple Developer Portal 上列出,但可以在此处找到完整的会议列表。

于 2019-01-02T12:21:09.960 回答
1

我刚刚遇到了同样的问题。

空格键等效项在我的应用程序中工作正常,而 NSMenuItem 的链接 IBAction 位于应用程序委托中。

如果我将 IBAction 移动到专用控制器中,它将失败。所有其他菜单项键等效项继续工作,但空格键不响应(使用修饰键可以,但未修改的 @" " 将不起作用)。

我尝试了各种解决方法,例如直接链接到控制器与通过响应者链链接,但无济于事。我尝试了代码方式:

[menuItem setKeyEquivalent:@" "];  
[menuItem setKeyEquivalentModifierMask:0];  

和 Interface Builder 方式,行为是一样的

根据贾斯汀的回答,我已经尝试对 NSWindow 进行子类化,但到目前为止还没有让它发挥作用。

所以现在我已经放弃并将这个 IBAction 重新定位到它工作的 App Delegate。我不认为这是一个解决方案,只是凑合……也许这是一个错误,或者(更有可能)我只是不太了解事件消息传递和响应者链。

于 2012-11-07T22:03:08.923 回答
1

发布这篇文章是因为我也需要使用空间,但这些解决方案都不适合我。

因此,我将 NSApplication 子类化并将sendEvent:选择器与justin k 解决方案一起使用:

- (void)sendEvent:(NSEvent *)anEvent
{
    [super sendEvent:anEvent];
    switch ([anEvent type]) {
    case NSKeyDown:
        if (([anEvent keyCode] == 49) && (![anEvent isARepeat])) {

            NSPoint pt; pt.x = pt.y = 0;
            NSEvent *fakeEvent = [NSEvent keyEventWithType:NSKeyDown
                                                  location:pt
                                             modifierFlags:0
                                                 timestamp:[[NSProcessInfo processInfo] systemUptime]
                                              windowNumber: 0 // self.windowNumber
                                                   context:[NSGraphicsContext currentContext]
                                                characters:@" "
                               charactersIgnoringModifiers:@" "
                                                 isARepeat:NO
                                                   keyCode:49];
            [[NSApp mainMenu] performKeyEquivalent:fakeEvent];
        }
        break;

    default:
        break;
    }
}

希望它会有所帮助

于 2015-08-27T10:13:01.447 回答
0

快速 Swift 4-5 方法:

在视图控制器中:

// Capture space and call main menu
override func keyDown(with event: NSEvent) {
    if event.keyCode == 49 && !event.isARepeat{
        NSApp.mainMenu?.performKeyEquivalent(with: event)
    }
    super.keyDown(with: event)
}
于 2019-10-31T13:22:32.110 回答