summaryrefslogtreecommitdiffstats
path: root/src/plugins/platforms/cocoa/qnsview_keys.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/plugins/platforms/cocoa/qnsview_keys.mm')
-rw-r--r--src/plugins/platforms/cocoa/qnsview_keys.mm408
1 files changed, 245 insertions, 163 deletions
diff --git a/src/plugins/platforms/cocoa/qnsview_keys.mm b/src/plugins/platforms/cocoa/qnsview_keys.mm
index 57f7fe1fdd..abee622e65 100644
--- a/src/plugins/platforms/cocoa/qnsview_keys.mm
+++ b/src/plugins/platforms/cocoa/qnsview_keys.mm
@@ -1,145 +1,143 @@
-/****************************************************************************
-**
-** Copyright (C) 2018 The Qt Company Ltd.
-** Contact: https://www.qt.io/licensing/
-**
-** This file is part of the plugins of the Qt Toolkit.
-**
-** $QT_BEGIN_LICENSE:LGPL$
-** Commercial License Usage
-** Licensees holding valid commercial Qt licenses may use this file in
-** accordance with the commercial license agreement provided with the
-** Software or, alternatively, in accordance with the terms contained in
-** a written agreement between you and The Qt Company. For licensing terms
-** and conditions see https://www.qt.io/terms-conditions. For further
-** information use the contact form at https://www.qt.io/contact-us.
-**
-** GNU Lesser General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU Lesser
-** General Public License version 3 as published by the Free Software
-** Foundation and appearing in the file LICENSE.LGPL3 included in the
-** packaging of this file. Please review the following information to
-** ensure the GNU Lesser General Public License version 3 requirements
-** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
-**
-** GNU General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU
-** General Public License version 2.0 or (at your option) the GNU General
-** Public license version 3 or any later version approved by the KDE Free
-** Qt Foundation. The licenses are as published by the Free Software
-** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
-** included in the packaging of this file. Please review the following
-** information to ensure the GNU General Public License requirements will
-** be met: https://www.gnu.org/licenses/gpl-2.0.html and
-** https://www.gnu.org/licenses/gpl-3.0.html.
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
+// Copyright (C) 2018 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// This file is included from qnsview.mm, and only used to organize the code
-@implementation QNSView (Keys)
+/*
+ Determines if the text represents one of the "special keys" on macOS
+
+ As a legacy from OpenStep, macOS reserves the range 0xF700-0xF8FF of the
+ Unicode private use area for representing function keys on the keyboard:
+
+ http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT
+
+ https://developer.apple.com/documentation/appkit/nsevent/specialkey
+
+ These code points are not supposed to have any glyphs associated with them,
+ but since we can't guarantee that the system doesn't have a font that does
+ provide glyphs for this range (Arial Unicode MS e.g.) we need to filter
+ the text of our key events up front.
+*/
+static bool isSpecialKey(const QString &text)
+{
+ if (text.length() != 1)
+ return false;
+
+ const char16_t unicode = text.at(0).unicode();
+ if (unicode >= 0xF700 && unicode <= 0xF8FF)
+ return true;
-- (bool)handleKeyEvent:(NSEvent *)nsevent eventType:(int)eventType
+ return false;
+}
+
+static bool sendAsShortcut(const KeyEvent &keyEvent, QWindow *window)
{
- ulong timestamp = [nsevent timestamp] * 1000;
- ulong nativeModifiers = [nsevent modifierFlags];
- Qt::KeyboardModifiers modifiers = QAppleKeyMapper::fromCocoaModifiers(nativeModifiers);
- NSString *charactersIgnoringModifiers = [nsevent charactersIgnoringModifiers];
- NSString *characters = [nsevent characters];
- if (m_inputSource != characters) {
- [m_inputSource release];
- m_inputSource = [characters retain];
+ KeyEvent shortcutEvent = keyEvent;
+ shortcutEvent.type = QEvent::Shortcut;
+ qCDebug(lcQpaKeys) << "Trying potential shortcuts in" << window
+ << "for" << shortcutEvent;
+
+ if (shortcutEvent.sendWindowSystemEvent(window)) {
+ qCDebug(lcQpaKeys) << "Found matching shortcut; will not send as key event";
+ return true;
}
+ qCDebug(lcQpaKeys) << "No matching shortcuts; continuing with key event delivery";
+ return false;
+}
- // Scan codes are hardware dependent codes for each key. There is no way to get these
- // from Carbon or Cocoa, so leave it 0, as documented in QKeyEvent::nativeScanCode().
- const quint32 nativeScanCode = 0;
-
- // Virtual keys on the other hand are mapped to be the same keys on any system
- const quint32 nativeVirtualKey = nsevent.keyCode;
-
- QChar ch = QChar::ReplacementCharacter;
- int keyCode = Qt::Key_unknown;
-
- // If a dead key occurs as a result of pressing a key combination then
- // characters will have 0 length, but charactersIgnoringModifiers will
- // have a valid character in it. This enables key combinations such as
- // ALT+E to be used as a shortcut with an English keyboard even though
- // pressing ALT+E will give a dead key while doing normal text input.
- if ([characters length] != 0 || [charactersIgnoringModifiers length] != 0) {
- if (nativeModifiers & (NSEventModifierFlagControl | NSEventModifierFlagOption)
- && [charactersIgnoringModifiers length] != 0)
- ch = QChar([charactersIgnoringModifiers characterAtIndex:0]);
- else if ([characters length] != 0)
- ch = QChar([characters characterAtIndex:0]);
- keyCode = QAppleKeyMapper::fromCocoaKey(ch);
+@implementation QNSView (Keys)
+
+- (bool)performKeyEquivalent:(NSEvent *)nsevent
+{
+ // Implemented to handle shortcuts for modified Tab keys, which are
+ // handled by Cocoa and not delivered to your keyDown implementation.
+ if (nsevent.type == NSEventTypeKeyDown && m_composingText.isEmpty()) {
+ const bool ctrlDown = [nsevent modifierFlags] & NSEventModifierFlagControl;
+ const bool isTabKey = nsevent.keyCode == kVK_Tab;
+ if (ctrlDown && isTabKey && sendAsShortcut(KeyEvent(nsevent), [self topLevelWindow]))
+ return YES;
}
+ return NO;
+}
- // we will send a key event unless the input method sets m_sendKeyEvent to false
- m_sendKeyEvent = true;
- QString text;
- // ignore text for the U+F700-U+F8FF range. This is used by Cocoa when
- // delivering function keys (e.g. arrow keys, backspace, F1-F35, etc.)
- if (!(modifiers & (Qt::ControlModifier | Qt::MetaModifier)) && (ch.unicode() < 0xf700 || ch.unicode() > 0xf8ff))
- text = QString::fromNSString(characters);
+- (bool)handleKeyEvent:(NSEvent *)nsevent
+{
+ qCDebug(lcQpaKeys) << "Handling" << nsevent;
+ KeyEvent keyEvent(nsevent);
+ // FIXME: Why is this the top level window and not m_platformWindow?
QWindow *window = [self topLevelWindow];
- // Popups implicitly grab key events; forward to the active popup if there is one.
- // This allows popups to e.g. intercept shortcuts and close the popup in response.
- if (QCocoaWindow *popup = QCocoaIntegration::instance()->activePopupWindow()) {
- if (!popup->window()->flags().testFlag(Qt::ToolTip))
- window = popup->window();
- }
+ // We will send a key event unless the input method handles it
+ QBoolBlocker sendKeyEventGuard(m_sendKeyEvent, true);
- if (eventType == QEvent::KeyPress) {
+ // Assume we should send key events with text, unless told
+ // otherwise by doCommandBySelector.
+ m_sendKeyEventWithoutText = false;
- if (m_composingText.isEmpty()) {
- m_sendKeyEvent = !QWindowSystemInterface::handleShortcutEvent(window, timestamp, keyCode,
- modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers, text, [nsevent isARepeat], 1);
+ bool didInterpretKeyEvent = false;
- // Handling a shortcut may result in closing the window
- if (!m_platformWindow)
+ if (keyEvent.type == QEvent::KeyPress) {
+
+ if (m_composingText.isEmpty()) {
+ if (sendAsShortcut(keyEvent, window))
return true;
}
- QObject *fo = m_platformWindow->window()->focusObject();
- if (m_sendKeyEvent && fo) {
- QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImHints);
- if (QCoreApplication::sendEvent(fo, &queryEvent)) {
- bool imEnabled = queryEvent.value(Qt::ImEnabled).toBool();
- Qt::InputMethodHints hints = static_cast<Qt::InputMethodHints>(queryEvent.value(Qt::ImHints).toUInt());
- // make sure we send dead keys and the next key to the input method for composition
- const bool ignoreHidden = (hints & Qt::ImhHiddenText) && !text.isEmpty() && !m_lastKeyDead;
- if (imEnabled && !(hints & Qt::ImhDigitsOnly || hints & Qt::ImhFormattedNumbersOnly || ignoreHidden)) {
- // pass the key event to the input method. note that m_sendKeyEvent may be set to false during this call
+ QObject *focusObject = m_platformWindow ? m_platformWindow->window()->focusObject() : nullptr;
+ if (m_sendKeyEvent && focusObject) {
+ if (auto queryResult = queryInputMethod(focusObject, Qt::ImHints)) {
+ auto hints = static_cast<Qt::InputMethodHints>(queryResult.value(Qt::ImHints).toUInt());
+
+ // Make sure we send dead keys and the next key to the input method for composition
+ const bool isDeadKey = !nsevent.characters.length;
+ const bool ignoreHidden = (hints & Qt::ImhHiddenText) && !isDeadKey && !m_lastKeyDead;
+
+ if (!(hints & Qt::ImhDigitsOnly || hints & Qt::ImhFormattedNumbersOnly || ignoreHidden)) {
+ // Pass the key event to the input method, and assume it handles the event,
+ // unless we explicit set m_sendKeyEvent to deliver as a normal key event.
+ m_sendKeyEvent = false;
+
+ // Match NSTextView's keyDown behavior of hiding the cursor before
+ // interpreting key events. Shortcuts should not trigger this though.
+ // Unfortunately many of our controls handle shortcuts by accepting
+ // the ShortcutOverride event and then handling the shortcut in the
+ // following key event, and QWSI::handleShortcutEvent doesn't reveal
+ // whether this will be the case. For NSTextView this is not an issue
+ // as shortcuts are handled via performKeyEquivalent, which happens
+ // prior to keyDown. To work around this until we can get the info
+ // we need from handleShortcutEvent we match AppKit and assume that
+ // any key press with a command or control modifier is a shortcut.
+ if (!(nsevent.modifierFlags & (NSEventModifierFlagCommand | NSEventModifierFlagControl)))
+ [NSCursor setHiddenUntilMouseMoves:YES];
+
+ qCDebug(lcQpaKeys) << "Interpreting key event for focus object" << focusObject;
m_currentlyInterpretedKeyEvent = nsevent;
- [self interpretKeyEvents:@[nsevent]];
- // If the receiver opens an editor in response to a key press, then the focus will change, the input
- // method will be reset, and the first key press will be gone. If the focus object changes, then we
- // need to pass the key event to the input method once more.
- if (qApp->focusObject() != fo)
- [self interpretKeyEvents:@[nsevent]];
+ if (![self.inputContext handleEvent:nsevent]) {
+ qCDebug(lcQpaKeys) << "Input context did not consume event";
+ m_sendKeyEvent = true;
+ }
m_currentlyInterpretedKeyEvent = 0;
- // if the last key we sent was dead, then pass the next key to the IM as well to complete composition
- m_lastKeyDead = text.isEmpty();
+ didInterpretKeyEvent = true;
+
+ // If the last key we sent was dead, then pass the next
+ // key to the IM as well to complete composition.
+ m_lastKeyDead = isDeadKey;
}
+
}
}
- if (m_resendKeyEvent)
- m_sendKeyEvent = true;
}
bool accepted = true;
if (m_sendKeyEvent && m_composingText.isEmpty()) {
- QWindowSystemInterface::handleExtendedKeyEvent(window, timestamp, QEvent::Type(eventType), keyCode, modifiers,
- nativeScanCode, nativeVirtualKey, nativeModifiers, text, [nsevent isARepeat], 1, false);
- accepted = QWindowSystemInterface::flushWindowSystemEvents();
+ // Trust text input system on whether to send the event with text or not,
+ // or otherwise apply heuristics to filter out private use symbols.
+ if (didInterpretKeyEvent ? m_sendKeyEventWithoutText : isSpecialKey(keyEvent.text))
+ keyEvent.text = {};
+ qCDebug(lcQpaKeys) << "Sending as" << keyEvent;
+ accepted = keyEvent.sendWindowSystemEvent(window);
}
- m_sendKeyEvent = false;
- m_resendKeyEvent = false;
return accepted;
}
@@ -148,7 +146,7 @@
if ([self isTransparentForUserInput])
return [super keyDown:nsevent];
- const bool accepted = [self handleKeyEvent:nsevent eventType:int(QEvent::KeyPress)];
+ const bool accepted = [self handleKeyEvent:nsevent];
// When Qt is used to implement a plugin for a native application we
// want to propagate unhandled events to other native views. However,
@@ -159,7 +157,7 @@
// Track keyDown acceptance/forward state for later acceptance of the keyUp.
if (!shouldPropagate)
- m_acceptedKeyDowns.insert([nsevent keyCode]);
+ m_acceptedKeyDowns.insert(nsevent.keyCode);
if (shouldPropagate)
[super keyDown:nsevent];
@@ -170,12 +168,12 @@
if ([self isTransparentForUserInput])
return [super keyUp:nsevent];
- const bool keyUpAccepted = [self handleKeyEvent:nsevent eventType:int(QEvent::KeyRelease)];
+ const bool keyUpAccepted = [self handleKeyEvent:nsevent];
// Propagate the keyUp if neither Qt accepted it nor the corresponding KeyDown was
- // accepted. Qt text controls wil often not use and ignore keyUp events, but we
+ // accepted. Qt text controls will often not use and ignore keyUp events, but we
// want to avoid propagating unmatched keyUps.
- const bool keyDownAccepted = m_acceptedKeyDowns.remove([nsevent keyCode]);
+ const bool keyDownAccepted = m_acceptedKeyDowns.remove(nsevent.keyCode);
if (!keyUpAccepted && !keyDownAccepted)
[super keyUp:nsevent];
}
@@ -184,73 +182,157 @@
{
Q_UNUSED(sender);
- NSEvent *currentEvent = [NSApp currentEvent];
+ NSEvent *currentEvent = NSApp.currentEvent;
if (!currentEvent || currentEvent.type != NSEventTypeKeyDown)
return;
// Handling the key event may recurse back here through interpretKeyEvents
// (when IM is enabled), so we need to guard against that.
- if (currentEvent == m_currentlyInterpretedKeyEvent)
+ if (currentEvent == m_currentlyInterpretedKeyEvent) {
+ m_sendKeyEvent = true;
return;
+ }
// Send Command+Key_Period and Escape as normal keypresses so that
// the key sequence is delivered through Qt. That way clients can
// intercept the shortcut and override its effect.
- [self handleKeyEvent:currentEvent eventType:int(QEvent::KeyPress)];
+ [self handleKeyEvent:currentEvent];
}
- (void)flagsChanged:(NSEvent *)nsevent
{
- ulong timestamp = [nsevent timestamp] * 1000;
- ulong nativeModifiers = [nsevent modifierFlags];
- Qt::KeyboardModifiers modifiers = QAppleKeyMapper::fromCocoaModifiers(nativeModifiers);
-
- // Scan codes are hardware dependent codes for each key. There is no way to get these
- // from Carbon or Cocoa, so leave it 0, as documented in QKeyEvent::nativeScanCode().
- const quint32 nativeScanCode = 0;
-
- // Virtual keys on the other hand are mapped to be the same keys on any system
- const quint32 nativeVirtualKey = nsevent.keyCode;
-
- // calculate the delta and remember the current modifiers for next time
- static ulong m_lastKnownModifiers;
- ulong lastKnownModifiers = m_lastKnownModifiers;
- ulong delta = lastKnownModifiers ^ nativeModifiers;
- m_lastKnownModifiers = nativeModifiers;
-
- struct qt_mac_enum_mapper
- {
- ulong mac_mask;
- Qt::Key qt_code;
- };
- static qt_mac_enum_mapper modifier_key_symbols[] = {
+ // FIXME: Why are we not checking isTransparentForUserInput here?
+
+ KeyEvent keyEvent(nsevent);
+ qCDebug(lcQpaKeys) << "Flags changed resulting in" << keyEvent.modifiers;
+
+ // Calculate the delta and remember the current modifiers for next time
+ static NSEventModifierFlags m_lastKnownModifiers;
+ NSEventModifierFlags lastKnownModifiers = m_lastKnownModifiers;
+ NSEventModifierFlags newModifiers = lastKnownModifiers ^ keyEvent.nativeModifiers;
+ m_lastKnownModifiers = keyEvent.nativeModifiers;
+
+ static constexpr std::tuple<NSEventModifierFlags, Qt::Key> modifierMap[] = {
{ NSEventModifierFlagShift, Qt::Key_Shift },
{ NSEventModifierFlagControl, Qt::Key_Meta },
{ NSEventModifierFlagCommand, Qt::Key_Control },
{ NSEventModifierFlagOption, Qt::Key_Alt },
- { NSEventModifierFlagCapsLock, Qt::Key_CapsLock },
- { 0ul, Qt::Key_unknown } };
- for (int i = 0; modifier_key_symbols[i].mac_mask != 0u; ++i) {
- uint mac_mask = modifier_key_symbols[i].mac_mask;
- if ((delta & mac_mask) == 0u)
+ { NSEventModifierFlagCapsLock, Qt::Key_CapsLock }
+ };
+
+ for (auto [macModifier, qtKey] : modifierMap) {
+ if (!(newModifiers & macModifier))
continue;
- Qt::Key qtCode = modifier_key_symbols[i].qt_code;
+ // FIXME: Use QAppleKeyMapper helper
if (qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta)) {
- if (qtCode == Qt::Key_Meta)
- qtCode = Qt::Key_Control;
- else if (qtCode == Qt::Key_Control)
- qtCode = Qt::Key_Meta;
+ if (qtKey == Qt::Key_Meta)
+ qtKey = Qt::Key_Control;
+ else if (qtKey == Qt::Key_Control)
+ qtKey = Qt::Key_Meta;
}
- QWindowSystemInterface::handleExtendedKeyEvent(m_platformWindow->window(),
- timestamp,
- (lastKnownModifiers & mac_mask) ? QEvent::KeyRelease
- : QEvent::KeyPress,
- qtCode,
- modifiers ^ QAppleKeyMapper::fromCocoaModifiers(mac_mask),
- nativeScanCode, nativeVirtualKey,
- nativeModifiers ^ mac_mask);
+
+ KeyEvent modifierEvent = keyEvent;
+ modifierEvent.type = lastKnownModifiers & macModifier
+ ? QEvent::KeyRelease : QEvent::KeyPress;
+
+ modifierEvent.key = qtKey;
+
+ // FIXME: Shouldn't this be based on lastKnownModifiers?
+ modifierEvent.modifiers ^= QAppleKeyMapper::fromCocoaModifiers(macModifier);
+ modifierEvent.nativeModifiers ^= macModifier;
+
+ // FIXME: Why are we sending to m_platformWindow here, but not for key events?
+ QWindow *window = m_platformWindow->window();
+
+ qCDebug(lcQpaKeys) << "Sending" << modifierEvent;
+ modifierEvent.sendWindowSystemEvent(window);
}
}
@end
+
+// -------------------------------------------------------------------------
+
+KeyEvent::KeyEvent(NSEvent *nsevent)
+{
+ timestamp = nsevent.timestamp * 1000;
+ nativeModifiers = nsevent.modifierFlags;
+ modifiers = QAppleKeyMapper::fromCocoaModifiers(nativeModifiers);
+
+ switch (nsevent.type) {
+ case NSEventTypeKeyDown: type = QEvent::KeyPress; break;
+ case NSEventTypeKeyUp: type = QEvent::KeyRelease; break;
+ default: break; // Must be manually set
+ }
+
+ switch (nsevent.type) {
+ case NSEventTypeKeyDown:
+ case NSEventTypeKeyUp:
+ case NSEventTypeFlagsChanged:
+ nativeVirtualKey = nsevent.keyCode;
+ default:
+ break;
+ }
+
+ if (nsevent.type == NSEventTypeKeyDown || nsevent.type == NSEventTypeKeyUp) {
+ NSString *charactersIgnoringModifiers = nsevent.charactersIgnoringModifiers;
+ NSString *characters = nsevent.characters;
+
+ QChar character = QChar::ReplacementCharacter;
+
+ // If a dead key occurs as a result of pressing a key combination then
+ // characters will have 0 length, but charactersIgnoringModifiers will
+ // have a valid character in it. This enables key combinations such as
+ // ALT+E to be used as a shortcut with an English keyboard even though
+ // pressing ALT+E will give a dead key while doing normal text input.
+ if (characters.length || charactersIgnoringModifiers.length) {
+ if (nativeModifiers & (NSEventModifierFlagControl | NSEventModifierFlagOption)
+ && charactersIgnoringModifiers.length)
+ character = QChar([charactersIgnoringModifiers characterAtIndex:0]);
+ else if (characters.length)
+ character = QChar([characters characterAtIndex:0]);
+ key = QAppleKeyMapper::fromCocoaKey(character);
+ }
+
+ text = QString::fromNSString(characters);
+
+ isRepeat = nsevent.ARepeat;
+ }
+}
+
+bool KeyEvent::sendWindowSystemEvent(QWindow *window) const
+{
+ switch (type) {
+ case QEvent::Shortcut: {
+ return QWindowSystemInterface::handleShortcutEvent(window, timestamp,
+ key, modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers,
+ text, isRepeat);
+ }
+ case QEvent::KeyPress:
+ case QEvent::KeyRelease: {
+ static const int count = 1;
+ QWindowSystemInterface::handleExtendedKeyEvent(window, timestamp,
+ type, key, modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers,
+ text, isRepeat, count);
+ // FIXME: Make handleExtendedKeyEvent synchronous
+ return QWindowSystemInterface::flushWindowSystemEvents();
+ }
+ default:
+ qCritical() << "KeyEvent can not send event type" << type;
+ return false;
+ }
+}
+
+QDebug operator<<(QDebug debug, const KeyEvent &e)
+{
+ QDebugStateSaver saver(debug);
+ debug.nospace().verbosity(0) << "KeyEvent("
+ << e.type << ", timestamp=" << e.timestamp
+ << ", key=" << e.key << ", modifiers=" << e.modifiers
+ << ", text="<< e.text << ", isRepeat=" << e.isRepeat
+ << ", nativeVirtualKey=" << e.nativeVirtualKey
+ << ", nativeModifiers=" << e.nativeModifiers
+ << ")";
+ return debug;
+}