鼠标事件后自定义 Carbon 键事件处理程序失败

Custom Carbon key event handler fails after mouse events

我正在尝试编写一个自定义 NSMenu,它将能够列出键输入并拦截必要的事件。这是为了为我的开放 source clipboard manager.

提供简单的“按类型搜索”功能

似乎唯一的方法是安装一个自定义的 Carbon 事件处理程序,它将侦听关键事件并相应地处理它们,但这样的自定义处理程序似乎存在问题。

通常,我可以将事件向下传播到其他处理程序(例如系统处理程序),它们应该得到妥善处理。这可以通过一个简单的回调来完成:

let eventHandlerCallback: EventHandlerUPP = { eventHandlerCallRef, eventRef, userData in
  let response = CallNextEventHandler(eventHandlerCallRef, eventRef!)
  print("Response \(response)")
  return response
}

此回调完美运行,并始终打印 Response 0。此响应表示事件已正确处理。

但是,一旦我们在 键盘事件 之前发送 鼠标事件 ,事情就会变得很奇怪。在这种情况下,回调失败并打印 Response -9874。此响应表示事件未正确处理。

似乎在我的自定义视图下方的某处无法处理该事件,我不知道确切的位置或如何解决这个问题。

为了重现,我已经 uploaded the code to Gist 可以添加到 XCode playground 和 运行。看到弹出菜单后,按一些键(最好是箭头键,因为它们不会关闭菜单)并在控制台中观察 Response 0。之后,将光标移到菜单内并按更多箭头键。您现在应该在控制台中看到 Response -9874

不清楚您是否有 NSTextField 作为您的菜单视图,但如果您使用一个,那么很容易为该文本字段设置一个委托,它可以在用户键入时获取该字段的当前内容 (这会照顾他们使用箭头键向后移动,然后删除字符,使用删除键等)。您的委托实施适当的委托方法并在每次文本更改时被调用:

extension CustomMenuItemViewController: NSTextFieldDelegate {
    func controlTextDidChange( _ obj: Notification) {
        if let postingObject = obj.object as? NSTextField {
            let text = postingObject.stringValue
            print("the text is now: \(text)")
        }
    }
}

为了确认这是否按预期工作,我在 xib 文件中为自定义菜单项视图(标签 + 编辑字段)创建了 ViewController class,然后动态构建了一个简单的测试菜单使用具有自定义视图控制器视图的单个菜单项并将其添加到我的应用程序委托内的菜单栏:

func installCustomMenuItem() {
    let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
    let menu = NSMenu(title: "TestMenu" )
    let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")

    subMenuBarItem.view = menuItemVC.view
    menu.addItem(subMenuBarItem)
    menuBarItem.submenu = menu
    NSApp.mainMenu?.addItem(menuBarItem)
}

我输入 "hello" 后看起来像这样:

你可以从控制台看到我的处理程序为输入的每个字符调用:

the text is now: H 
the text is now: He 
the text is now: Hel 
the text is now: Hell
the text is now: Hello

您的情况可能有点不同,但看起来这种方法非常干净,可能适合您。如果由于某种原因它不会,请添加一个澄清的评论,我们会看看我们是否不能让它为您工作。


加法:

我突然想到您可能不希望使用 NSTextField,所以我很好奇使用自定义视图是否同样容易,而且相对容易。

创建 NSView 的子class:

class CustomMenuView: NSView {

    override var acceptsFirstResponder: Bool {
        return true
    }

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        // Drawing code here.
    }

    override func keyDown(with event: NSEvent) {
        print("key down with character: \(String(describing: event.characters)) " )
    }
}

在自定义视图控制器中将根视图的 class 设置为这种类型的 class 然后像以前一样完成所有操作 - 在 applicationDidFinishLaunching 和菜单中加载视图控制器构建和视图控制器的视图(现在是 CustomMenuView)被设置为 menuBarItem.view.

就是这样。您现在可以在下拉菜单时为按下的每个键调用 keyDown 方法。

key down with character: Optional("H") 
key down with character: Optional("e") 
key down with character: Optional("l") 
key down with character: Optional("l") 
key down with character: Optional("o") 
key down with character: Optional(" ")
key down with character: Optional("T") 
key down with character: Optional("h") 
key down with character: Optional("i") 
key down with character: Optional("s") 
key down with character: Optional(" ") 
key down with character: Optional("i") 
key down with character: Optional("s") 
key down with character: Optional(" ") 
key down with character: Optional("c") 
key down with character: Optional("o") 
key down with character: Optional("o") 
key down with character: Optional("l") 

:)

现在您的自定义视图(和子视图,如果您愿意)可以进行自己的绘图等等。


添加没有 ViewController 的请求样本:

// Simple swift playground test
// The pop-up menu will show up onscreen in the playground at a fixed location.   
// Click in the popup and then all key commands will be logged.
// The ViewController in my example above may be taking care of putting the custom view in the responder chain, or the fact that it's in a menubar and being invoked via a MenuItem might be.
// I'd suggest trying it in the actual environment rather than in a playground. In my test app you click the menu name in the menubar to drop down the menu and it is added to the responder chain and works as expected without having to click in the menu first to get the events flowing.
// There is no reason you need to be hooking events either with carbon events or the newer format.  If you're in the responder chain of and implement the necessary, method then you'll get the key events you're looking for.

import AppKit

class CustomMenuView: NSView {

    override var acceptsFirstResponder: Bool {
        return true
    }

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)

        // Drawing code here.
    }

    override func keyDown(with event: NSEvent) {
        print("key down with character: \(String(describing: event.characters)) " )
    }
}


func installCustomMenuItem() -> NSMenu {
//  let menuBarItem = NSMenuItem(title: "Test", action: nil, keyEquivalent: "")
    let resultMenu = NSMenu(title: "TestMenu" )
    let subMenuBarItem = NSMenuItem(title: "Custom View", action: nil, keyEquivalent: "")

    subMenuBarItem.view = CustomMenuView(frame: NSRect(x: 0, y: 0, width: 40, height: 44))
    resultMenu.addItem(subMenuBarItem)
//  menuBarItem.submenu = menu

    return resultMenu
}

var menu = installCustomMenuItem()

menu.popUp(positioning: nil, at: NSPoint(x:600,y:400), in: nil)

我没弄清楚为什么会发生这个问题或如何解决它,但我知道可以通过拦截所有键并手动模拟它们的行为来解决这个问题。

例如,这就是我现在处理向下箭头键的方式,它应该是 select 菜单列表中的下一个项目:

class Menu: NSMenu {
  func selectNext() {
    var indexToHighlight = 1
    if let item = highlightedItem {
      indexToHighlight = index(of: item) + 1
    }

    if let itemToHighlight = self.item(at: indexToHighlight) {
      let highlightItemSelector = NSSelectorFromString("highlightItem:")
      perform(highlightItemSelector, with: itemToHighlight)

      if itemToHighlight.isSeparatorItem || !itemToHighlight.isEnabled || itemToHighlight.isHidden {
        selectNext()
      }
    }
  }
}

这样,当我收到带有向下箭头键的按键事件时 - 我可以调用该函数和 return true 以防止事件到达默认的 NSMenu 处理程序。同样,向上键也可以完成。

如果是 return 键,我得到以下代码:

class Menu: NSMenu {
  func select() {
    if let item = highlightedItem {
      performActionForItem(at: index(of: item))
      cancelTracking()
    }
  }
}

实现这个的完整提交是https://github.com/p0deje/Maccy/commit/158610d1d