Пользовательский обработчик события ключа Carbon завершается ошибкой после событий мыши - PullRequest
0 голосов
/ 13 ноября 2018

Я пытаюсь написать собственное NSMenu, которое сможет перечислять для ввода с клавиатуры и перехватывать необходимые события.Это сделано для того, чтобы обеспечить простую функциональность поиска по мере ввода для моего открытого менеджера буфера обмена с исходным кодом .

Кажется, что единственный способ сделать это - установить пользовательский обработчик событий Carbonкоторый будет прослушивать ключевые события и обрабатывать их соответственно, но кажется, что есть проблема с таким пользовательским обработчиком.

Обычно я могу передавать события вниз другим обработчикам (например, системным), и они должны бытьизящно обработан.Это может быть сделано простым обратным вызовом:

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

Этот обратный вызов работает отлично и печатает Response 0 все время.Этот ответ означает, что событие обрабатывается правильно.

Однако все становится странным, когда мы отправляем события мыши до события клавиатуры .В таком случае обратный вызов завершается неудачно и печатает Response -9874.Этот ответ означает, что событие не было обработано правильно.

Кажется, что событие не может быть обработано где-то ниже моего пользовательского представления, и я не знаю, где именно или как преодолеть эту проблему.

Для воспроизведения я загрузил код в Gist , который можно добавить на игровую площадку XCode и запустить.Когда вы увидите всплывающее меню, нажмите некоторые клавиши (желательно клавиши со стрелками, поскольку они не закрывают меню) и наблюдайте Response 0 в консоли.После этого переместите курсор в меню и нажмите дополнительные клавиши со стрелками.Вы должны увидеть Response -9874 в консоли сейчас.

Ответы [ 2 ]

0 голосов
/ 13 июня 2019

Мне не удалось выяснить, почему возникла эта проблема или как ее исправить, но я понял, что можно обойти эту проблему, перехватывая все клавиши и моделируя их поведение вручную.

Например, вот как я теперь работаю с клавишей со стрелкой вниз, которая должна выбирать следующий элемент в списке меню:

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.

0 голосов
/ 28 ноября 2018

Непонятно, есть ли у вас 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)")
        }
    }
}

Просто чтобы подтвердить, что это работает должным образом, я создал класс ViewController для настраиваемых представлений элементов меню (поле label + edit) в файле xib, а затем динамически создал простое тестовое меню с одним элементом меню, имеющим настраиваемое представление. контроллер и добавил его в меню в моем делегате приложения:

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

Выглядит так после того, как я набрал "привет":

enter image description here

И вы можете из консоли, чтобы мой обработчик вызывался для каждого набранного символа:

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, построение меню и просмотр представления контроллера (который теперь * 1031) *) устанавливается как меню BarItem.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)
...