Ошибка NSMenuItem KeyEquivalent (пробел)

Я хочу установить эквивалент ключа (пробел) без каких-либо модификаторов для NSMenuItem (в главном меню приложения).

Как следует из документации:

Например, в приложении, воспроизводящем мультимедиа, команда «Воспроизвести» может быть сопоставлена ​​только с « » (пробелом) без командной клавиши. Вы можете сделать это с помощью следующего кода:

[элемент меню setKeyEquivalent:@];

[menuItem setKeyEquivalentModifierMask:0];

Эквивалент ключа успешно установлен, но не работает. Когда я нажимаю пробел без модификаторов, ничего не происходит, но это работает, когда я нажимаю пробел с модификатором Fn.

Мне нужно использовать пробел без модификаторов. Любая помощь, пожалуйста!


person Kira    schedule 22.06.2012    source источник


Ответы (5)


У меня такая же проблема. Я не очень тщательно исследовал, но, насколько я могу судить, пробел не «выглядит» как сочетание клавиш для 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];
    }
}
person justin k.    schedule 03.08.2012

Это сложный вопрос. Как предполагают многие ответы, перехват события на уровне приложения или окна — надежный способ заставить пункт меню работать. В то же время это, вероятно, сломает другие вещи, например, если у вас есть сфокусированные NSTextField или NSButton, вы хотите, чтобы они потребляли событие, а не пункт меню. Это также может привести к сбою, если пользователь переопределит эквивалент ключа для этого пункта меню в системных настройках, т. е. изменит Space на P.

Тот факт, что вы используете клавишу пробела, эквивалентную пункту меню, еще больше усложняет ситуацию. Пробел — это один из специальных символов событий пользовательского интерфейса, наряду с клавишами со стрелками и некоторыми другими, которые AppKit обрабатывает по-разному и в некоторых случаях будет потреблять их до того, как они перейдут в главное меню.

Итак, есть две вещи, о которых нужно помнить. Во-первых, это стандартная цепочка респондентов:

  1. NSApplication.sendEvent отправляет событие в ключевое окно.
  2. Ключевое окно получает событие в NSWindow.sendEvent, определяет, является ли оно ключевым событием, и вызывает performKeyEquivalent для себя.
  3. performKeyEquivalent отправляет его в firstResponder текущего окна.
  4. Если ответчик не использует его, событие рекурсивно отправляется вверх в nextResponder.
  5. performKeyEquivalent возвращает true, если один из респондентов использует событие, false в противном случае.

Теперь вторая и сложная часть: если событие не используется (то есть когда performKeyEquivalent возвращает false), окно попытается обработать его как специальное событие пользовательского интерфейса клавиатуры – это кратко упоминается в Руководство по обработке событий Cocoa:

Архитектура диспетчера событий Cocoa обрабатывает определенные ключевые события как команды для перемещения фокуса управления на другой объект пользовательского интерфейса в окне, для имитации щелчка мышью по объекту, для закрытия модальных окон и для выбора объектов, которые позволяют выбор . Эта возможность называется управлением интерфейсом клавиатуры. Большинство объектов пользовательского интерфейса, участвующих в управлении интерфейсом с клавиатуры, являются объектами NSControl, но объекты, не являющиеся элементами управления, также могут участвовать.

Принцип работы этой части довольно прост:

  1. Окно преобразует ключевое событие в соответствующее действие (селектор).
  2. Он проверяет у первого респондента, respondsToSelector ли он, и вызывает его.
  3. Если действие было вызвано, событие рассматривается как потребленное, и распространение события прекращается.

Итак, имея все это в виду, вы должны убедиться в двух вещах:

  1. Цепочка респондентов настроена правильно.
  2. Ответчики потребляют только то, что им нужно, и распространяют события иначе.

Первый пункт редко доставляет неприятности. О втором, и это то, что происходит в вашем примере, нужно позаботиться — 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
comment
Это должен быть официальный ответ. - person bithavoc; 23.04.2020
comment
У меня есть дополнительный вопрос: у меня есть 4 пункта меню со стрелками ←/↑/→/↓ в качестве keyEquivalent и NSTextField в моем окне. Когда я нахожусь в текстовом поле, я могу печатать, как и ожидалось, но когда я нажимаю одну из клавиш со стрелками, вместо перемещения курсора выполняется действие пункта меню. Вы знаете, почему это так? Согласно вашему ответу, текстовое поле должно сначала получать эти события? - person Codey; 13.05.2020
comment
Ответ был составлен для немного другого сценария — поведение эквивалентного ключа может резко измениться в зависимости от первого респондента. Есть великолепный сессия 145 WWDC 2010 (вот полный список), который подробно описывает это с отличными примерами, вас особенно интересуют конфликты ключевых эквивалентов в ~ 15:00. Первые 15 минут охватывают основы, так что они тоже очень полезны. - person Ian Bytchek; 13.05.2020
comment
Тем не менее, если что-то не работает должным образом, вы всегда можете попробовать переопределить поведение на более высоком уровне супервизора/контроллера/окна/приложения. В этом нет ничего плохого, если у вас есть хорошее представление о том, что вы делаете! :) - person Ian Bytchek; 13.05.2020
comment
Круто, спасибо за совет, я обязательно посмотрю сеанс. Большое спасибо! - person Codey; 14.05.2020
comment
@IanBytchek Я смотрел сеанс, но я все еще в замешательстве, потому что цепочка респондентов в моем приложении отличается от того, что я ожидал от просмотра сеанса. Я создал новый вопрос для этого stackoverflow.com/q/61839795/3272409. Я был бы более чем счастлив, если бы вы могли взглянуть на это :) - person Codey; 16.05.2020

Я только что столкнулся с такой же проблемой с поворотом ...

Эквивалент клавиши пробела отлично работает в моем приложении, в то время как связанный IBAction NSMenuItem находится в делегате приложения.

Если я перемещу IBAction в выделенный контроллер, он выйдет из строя. Все другие эквиваленты клавиш пункта меню продолжают работать, но пробел не реагирует (это нормально с клавишей-модификатором, но немодифицированный @" " не будет работать).

Я пробовал различные обходные пути, такие как прямое подключение к контроллеру или подключение через цепочку ответчиков, но безрезультатно. Я попробовал код:

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

и способ Interface Builder, поведение такое же

Я пытался создать подкласс NSWindow, согласно ответу Джастина, но до сих пор не смог заставить это работать.

Так что на данный момент я сдался и переместил этот IBAction в App Delegate, где он работает. Я не рассматриваю это как решение, просто делаю... возможно, это ошибка или (что более вероятно) я просто недостаточно хорошо понимаю обмен сообщениями о событиях и цепочку респондентов.

person foundry    schedule 07.11.2012

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

Итак, я создаю подкласс NSApplication и использую селектор sendEvent: с простое решение:

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

Надеюсь, это поможет

person frachop    schedule 27.08.2015

Быстрый метод 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)
}
person scum    schedule 31.10.2019
comment
Как ни странно, у меня это работает один раз, потом приходится снова щелкать по окну, чтобы оно снова заработало, и так далее... жаль, удобно было. Вместо этого я использовал дополнительный скрытый NSMenuItem с .keyEquivalent = " " и .allowsKeyEquivalentWhenHidden = true... - person cdf1982; 14.03.2021