【问题标题】:Custom Carbon key event handler fails after mouse events鼠标事件后自定义 Carbon 键事件处理程序失败
【发布时间】:2018-11-13 03:06:12
【问题描述】:

我正在尝试编写一个自定义的 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 游乐场并运行。看到菜单弹出后,按一些键(最好是箭头键,因为它们不会关闭菜单)并在控制台中观察Response 0。之后,在菜单内移动光标并按更多箭头键。您现在应该在控制台中看到Response -9874

【问题讨论】:

  • 您认为必须为此使用 Carbon 事件处理程序的原因是什么?你试过NSEvent.addLocalMonitorForEvents(matching:handler:)吗?你还考虑和丢弃了什么,你为什么丢弃它?另外,您为什么要监视来自NSMenu 的键输入(键盘快捷键除外)?为什么不使用其他对象,例如控制器或自定义应用程序类?
  • 我尝试过使用NSEvent.addLocalMonitorForEvents(matching:handler:),但看起来菜单子系统根本没有使用它。我也不能使用NSEvent.addGLobalMonitorForEvents(matching:handler:),因为它不允许在必要时停止事件传播。可以从任何地方监视键输入(在这种情况下为NSView),但这并不重要,因为 Carbon 处理程序是全局安装的,这似乎是监视事件和使用 NSMenu 与编写自定义的唯一方法NSMenu 实现,可以使用普通的本地 NSEvent 监视器或视图方法。
  • 您确定需要传播该事件吗?+
  • 即使我没有明确地传播事件并让 Carbon 为我做这件事,它仍然会失败。
  • 我检查了 -9874 错误的含义。根据旧的 Carbon 事件管理器代码 -9874 = eventNotHandledErr,当“当您的处理程序收到当前不想要的事件(或者不是能够)处理。如果你处理一个事件,你应该从你的事件处理程序返回 noErr。我曾经处理并且仍然处理旧式 Carbon 事件,并且在某些情况下忽略这样的错误是完全可以的。那么,“它仍然失败”到底是什么意思?

标签: swift cocoa appkit macos-carbon


【解决方案1】:

不清楚您是否有一个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 类,然后动态构建了一个简单的测试菜单,其中包含一个具有自定义视图的菜单项控制器的视图并将其添加到我的应用程序委托中的菜单栏:

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

在我输入“你好”后看起来像这样:

您可以从控制台为每个键入的字符调用我的处理程序:

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 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)) " )
    }
}

在自定义视图控制器中将根视图的类设置为这种类型,然后像以前一样完成所有操作 - 视图控制器加载到 applicationDidFinishLaunching 和菜单构建和视图控制器的视图(现在是 @987654332 @) 设置为 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)

【讨论】:

  • 抱歉,我仍然无法完成这项工作。我不确定此链中是否需要 ViewController,但我尝试将 CustomView 直接分配给菜单项视图,但它无法接收任何事件。是否需要通过 xib 链接视图控制器?您是否可以调整original Gist code I used in Playground 使其工作?
  • playground 是一个完全不同的执行环境。我不确定事情是否会像在实际应用中那样工作。
  • 做了一个快速测试,你可以让它在操场上工作,但响应链有点不同,所以你必须做一些手动工作才能在响应链中得到一些东西,然后发送事件到你的视野。单击菜单将执行此操作。我在示例应用程序中没有这个问题,它是一个普通菜单,当您单击菜单栏中的菜单时会下拉。
  • 我想知道您的问题是否仅在您使用某种 keydown 挂钩显示菜单时才会发生?如果是这样,那么我怀疑这是因为您没有手动将视图设置为第一响应者,您可能需要这样做。
  • 不知何故,我还没有弄清楚如何重新设计应用程序以遵循您的方法,我找到了一个不同的解决方案来解决我作为答案发布的问题。尽管如此,还是非常感谢您为帮助我所做的努力 - 我已经接受了答案,并对如此缓慢的回复感到抱歉。
【解决方案2】:

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

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

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 处理程序。同样,向上箭头键也可以。

如果是返回键,我最终得到以下代码:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-05
    • 1970-01-01
    • 2012-07-21
    • 2010-11-03
    相关资源
    最近更新 更多