Это сложный вопрос. Как предполагают многие ответы, перехват события на уровне приложения или окна — надежный способ заставить пункт меню работать. В то же время это, вероятно, сломает другие вещи, например, если у вас есть сфокусированные NSTextField
или NSButton
, вы хотите, чтобы они потребляли событие, а не пункт меню. Это также может привести к сбою, если пользователь переопределит эквивалент ключа для этого пункта меню в системных настройках, т. е. изменит Space
на P
.
Тот факт, что вы используете клавишу пробела, эквивалентную пункту меню, еще больше усложняет ситуацию. Пробел — это один из специальных символов событий пользовательского интерфейса, наряду с клавишами со стрелками и некоторыми другими, которые AppKit обрабатывает по-разному и в некоторых случаях будет потреблять их до того, как они перейдут в главное меню.
Итак, есть две вещи, о которых нужно помнить. Во-первых, это стандартная цепочка респондентов:
NSApplication.sendEvent
отправляет событие в ключевое окно.
- Ключевое окно получает событие в
NSWindow.sendEvent
, определяет, является ли оно ключевым событием, и вызывает performKeyEquivalent
для себя.
performKeyEquivalent
отправляет его в firstResponder
текущего окна.
- Если ответчик не использует его, событие рекурсивно отправляется вверх в
nextResponder
.
performKeyEquivalent
возвращает true
, если один из респондентов использует событие, false
в противном случае.
Теперь вторая и сложная часть: если событие не используется (то есть когда performKeyEquivalent
возвращает false
), окно попытается обработать его как специальное событие пользовательского интерфейса клавиатуры – это кратко упоминается в Руководство по обработке событий Cocoa:
Архитектура диспетчера событий Cocoa обрабатывает определенные ключевые события как команды для перемещения фокуса управления на другой объект пользовательского интерфейса в окне, для имитации щелчка мышью по объекту, для закрытия модальных окон и для выбора объектов, которые позволяют выбор . Эта возможность называется управлением интерфейсом клавиатуры. Большинство объектов пользовательского интерфейса, участвующих в управлении интерфейсом с клавиатуры, являются объектами NSControl, но объекты, не являющиеся элементами управления, также могут участвовать.
Принцип работы этой части довольно прост:
- Окно преобразует ключевое событие в соответствующее действие (селектор).
- Он проверяет у первого респондента,
respondsToSelector
ли он, и вызывает его.
- Если действие было вызвано, событие рассматривается как потребленное, и распространение события прекращается.
Итак, имея все это в виду, вы должны убедиться в двух вещах:
- Цепочка респондентов настроена правильно.
- Ответчики потребляют только то, что им нужно, и распространяют события иначе.
Первый пункт редко доставляет неприятности. О втором, и это то, что происходит в вашем примере, нужно позаботиться — AVPlayer
обычно будет первым ответчиком и использует событие клавиши пробела, а также несколько других. Чтобы это работало, вам нужно переопределить методы keyUp
и keyDown
для распространения события вверх по цепочке респондентов, как это произошло бы в реализации 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)
}
Это предотвратит использование окном события пользовательского интерфейса клавиатуры, поэтому вместо этого его сможет получить главное меню. Однако, если вы хотите перехватывать ВСЕ события пользовательского интерфейса клавиатуры, в том числе когда первый респондент может его использовать, вы хотите переопределить 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
является первым ответчиком, а методы keyDown
и keyUp
не перезаписываются.
P.S. Сниппеты — это Swift 4, но идея та же! ✌️
П.П.С. Есть великолепный WWDC 2010 Session 145 — Key Event Handling in Cocoa Applications который подробно освещает эту тему с отличными примерами. WWDC 2010-11 больше не указан на портале разработчиков Apple, но полный список сеансов можно найти здесь.
person
Ian Bytchek
schedule
02.01.2019