【问题标题】:NSMenuItem KeyEquivalent " "(space) bugNSMenuItem KeyEquivalent " "(空格) 错误
【发布时间】:2012-06-24 16:42:07
【问题描述】:

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

从文档如下:

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

[menuItem setKeyEquivalent:@" "];

[menuItem setKeyEquivalentModifierMask:0];

Key Equivalent 设置成功,但它不起作用。当我在没有修饰符的情况下按“空格”键时没有任何反应,但是当我用“Fn”修饰键按“空格”时它会起作用。

我需要使用不带修饰符的“空格”。请帮忙!

【问题讨论】:

    标签: cocoa keyboard-shortcuts nsmenuitem


    【解决方案1】:

    我遇到了同样的问题。我没有认真调查,但据我所知,空格键“看起来”不像 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];
        }
    }
    

    【讨论】:

      【解决方案2】:

      这是一个棘手的问题。就像许多答案所暗示的那样,在应用程序或窗口级别拦截事件是强制菜单项工作的可靠方法。同时它可能会破坏其他东西,例如,如果你有一个专注的 NSTextFieldNSButton 你会希望他们消费事件,而不是菜单项。如果用户在系统偏好设置中重新定义该菜单项的等效键,例如,将 Space 更改为 P,这也可能会失败。

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

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

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

      现在,第二个也是棘手的部分,如果事件没有被消耗(即performKeyEquivalent 返回falsewindow will try to process it as a special keyboard UI event - 这在Cocoa Event Handling Guide 中有简要提及:

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

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

      1. 窗口converts the key event in a corresponding action(选择器)。
      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-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 是第一响应者并且keyDownkeyUp 方法未被覆盖时,就会出现这种情况。

      附:片段是 Swift 4,但想法是一样的! ✌️

      附言有一个出色的WWDC 2010 Session 145 – Key Event Handling in Cocoa Applications 用优秀的例子深入介绍了这个主题。 WWDC 2010-11 不再在 Apple Developer Portal 上列出,但可以在 here 找到完整的会议列表。

      【讨论】:

      • 这应该是官方的回答。
      • 我有一个后续问题:我有 4 个菜单项,箭头键 ←/↑/→/↓ 作为 keyEquivalent,在我的窗口中有一个 NSTextField。当我在文本字段中时,我可以按预期键入,但是当我按下箭头键之一时,将执行菜单项操作而不是光标移动。你知道这是为什么吗?根据您的回答,文本字段应该首先接收这些事件?
      • 答案是针对一个稍微不同的场景而组合在一起的——关键的等效行为可能会因第一响应者而发生巨大变化。有一个精彩的 WWDC 2010 Session 145 (here's the full list) 用优秀的例子深入介绍了这一点,您在 ~15:00 时对 Key Equivalents Conflicts 特别感兴趣。前 15 分钟涵盖了基础知识,因此也非常有用。
      • 也就是说,如果某些事情没有按预期工作,您可以随时尝试在更高的超级视图/控制器/窗口/应用程序级别覆盖该行为。只要您对自己在做什么有一个很好的了解,这没什么错! :)
      • 酷,感谢您的提示,我一定会看一下会议。非常感谢!
      【解决方案3】:

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

      当 NSMenuItem 的链接 IBAction 位于 App Delegate 中时,空格键等效项在我的应用中工作正常。

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

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

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

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

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

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

      【讨论】:

        【解决方案4】:

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

        所以,I subclass NSApplication 并使用 sendEvent: 选择器和 justin k solution

        - (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;
            }
        }
        

        希望对你有帮助

        【讨论】:

          【解决方案5】:

          快速 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)
          }
          

          【讨论】:

          • 奇怪的是,这对我有用一次,然后我必须再次单击窗口才能让它再次工作,等等......太糟糕了,这很方便。我用.keyEquivalent = " ".allowsKeyEquivalentWhenHidden = true 代替了一个额外的隐藏NSMenuItem...
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-06-23
          • 1970-01-01
          • 2012-12-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多