Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qnsview_keys.mm
Go to the documentation of this file.
1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant reason:default
4
5// This file is included from qnsview.mm, and only used to organize the code
6
7/*
8 Determines if the text represents one of the "special keys" on macOS
9
10 As a legacy from OpenStep, macOS reserves the range 0xF700-0xF8FF of the
11 Unicode private use area for representing function keys on the keyboard:
12
13 http://www.unicode.org/Public/MAPPINGS/VENDORS/APPLE/CORPCHAR.TXT
14
15 https://developer.apple.com/documentation/appkit/nsevent/specialkey
16
17 These code points are not supposed to have any glyphs associated with them,
18 but since we can't guarantee that the system doesn't have a font that does
19 provide glyphs for this range (Arial Unicode MS e.g.) we need to filter
20 the text of our key events up front.
21*/
22static bool isSpecialKey(const QString &text)
23{
24 if (text.length() != 1)
25 return false;
26
27 const char16_t unicode = text.at(0).unicode();
28 if (unicode >= 0xF700 && unicode <= 0xF8FF)
29 return true;
30
31 return false;
32}
33
34static bool sendAsShortcut(const KeyEvent &keyEvent, QWindow *window)
35{
36 KeyEvent shortcutEvent = keyEvent;
37 shortcutEvent.type = QEvent::Shortcut;
38 qCDebug(lcQpaKeys) << "Trying potential shortcuts in" << window
39 << "for" << shortcutEvent;
40
41 if (shortcutEvent.sendWindowSystemEvent(window)) {
42 qCDebug(lcQpaKeys) << "Found matching shortcut; will not send as key event";
43 return true;
44 }
45 qCDebug(lcQpaKeys) << "No matching shortcuts; continuing with key event delivery";
46 return false;
47}
48
49@implementation QNSView (Keys)
50
51- (bool)performKeyEquivalent:(NSEvent *)nsevent
52{
53 // Implemented to handle shortcuts for modified Tab keys, which are
54 // handled by Cocoa and not delivered to your keyDown implementation.
55 if (nsevent.type == NSEventTypeKeyDown && m_composingText.isEmpty()) {
56 const bool ctrlDown = [nsevent modifierFlags] & NSEventModifierFlagControl;
57 const bool isTabKey = nsevent.keyCode == kVK_Tab;
58 if (ctrlDown && isTabKey && sendAsShortcut(KeyEvent(nsevent), [self topLevelWindow]))
59 return YES;
60 }
61 return NO;
62}
63
64- (bool)handleKeyEvent:(NSEvent *)nsevent
65{
66 qCDebug(lcQpaKeys) << "Handling" << nsevent;
67 KeyEvent keyEvent(nsevent);
68
69 // FIXME: Why is this the top level window and not m_platformWindow?
70 QWindow *window = [self topLevelWindow];
71
72 // We will send a key event unless the input method handles it
73 QScopedValueRollback sendKeyEventGuard(m_sendKeyEvent, true);
74
75 // Assume we should send key events with text, unless told
76 // otherwise by doCommandBySelector.
77 m_sendKeyEventWithoutText = false;
78
79 bool didInterpretKeyEvent = false;
80
81 if (keyEvent.type == QEvent::KeyPress) {
82
83 if (m_composingText.isEmpty()) {
84 if (sendAsShortcut(keyEvent, window))
85 return true;
86 }
87
88 QObject *focusObject = m_platformWindow ? m_platformWindow->window()->focusObject() : nullptr;
89 if (m_sendKeyEvent && focusObject) {
90 if (auto queryResult = queryInputMethod(focusObject, Qt::ImHints)) {
91 auto hints = static_cast<Qt::InputMethodHints>(queryResult.value(Qt::ImHints).toUInt());
92
93 // Make sure we send dead keys and the next key to the input method for composition
94 const bool isDeadKey = !nsevent.characters.length;
95 const bool ignoreHidden = (hints & Qt::ImhHiddenText) && !isDeadKey && !m_lastKeyDead;
96
97 if (!(hints & Qt::ImhDigitsOnly || hints & Qt::ImhFormattedNumbersOnly || ignoreHidden)) {
98 // Pass the key event to the input method, and assume it handles the event,
99 // unless we explicit set m_sendKeyEvent to deliver as a normal key event.
100 m_sendKeyEvent = false;
101
102 // Match NSTextView's keyDown behavior of hiding the cursor before
103 // interpreting key events. Shortcuts should not trigger this though.
104 // Unfortunately many of our controls handle shortcuts by accepting
105 // the ShortcutOverride event and then handling the shortcut in the
106 // following key event, and QWSI::handleShortcutEvent doesn't reveal
107 // whether this will be the case. For NSTextView this is not an issue
108 // as shortcuts are handled via performKeyEquivalent, which happens
109 // prior to keyDown. To work around this until we can get the info
110 // we need from handleShortcutEvent we match AppKit and assume that
111 // any key press with a command or control modifier is a shortcut.
112 if (!(nsevent.modifierFlags & (NSEventModifierFlagCommand | NSEventModifierFlagControl)))
113 [NSCursor setHiddenUntilMouseMoves:YES];
114
115 qCDebug(lcQpaKeys) << "Interpreting key event for focus object" << focusObject;
116 m_currentlyInterpretedKeyEvent = nsevent;
117 if (![self.inputContext handleEvent:nsevent]) {
118 qCDebug(lcQpaKeys) << "Input context did not consume event";
119 m_sendKeyEvent = true;
120 }
121 m_currentlyInterpretedKeyEvent = 0;
122 didInterpretKeyEvent = true;
123
124 // If the last key we sent was dead, then pass the next
125 // key to the IM as well to complete composition.
126 m_lastKeyDead = isDeadKey;
127 }
128
129 }
130 }
131 }
132
133 bool accepted = true;
134 if (m_sendKeyEvent && m_composingText.isEmpty()) {
135 // Trust text input system on whether to send the event with text or not,
136 // or otherwise apply heuristics to filter out private use symbols.
137 if (didInterpretKeyEvent ? m_sendKeyEventWithoutText : isSpecialKey(keyEvent.text))
138 keyEvent.text = {};
139 qCDebug(lcQpaKeys) << "Sending as" << keyEvent;
140 accepted = keyEvent.sendWindowSystemEvent(window);
141 }
142 return accepted;
143}
144
145- (void)keyDown:(NSEvent *)nsevent
146{
147 if ([self isTransparentForUserInput])
148 return [super keyDown:nsevent];
149
150 const bool accepted = [self handleKeyEvent:nsevent];
151
152 // When Qt is used to implement a plugin for a native application we
153 // want to propagate unhandled events to other native views. However,
154 // Qt does not always set the accepted state correctly (in particular
155 // for return key events), so do this for plugin applications only
156 // to prevent incorrect forwarding in the general case.
157 const bool shouldPropagate = QCoreApplication::testAttribute(Qt::AA_PluginApplication) && !accepted;
158
159 // Track keyDown acceptance/forward state for later acceptance of the keyUp.
160 if (!shouldPropagate)
161 m_acceptedKeyDowns.insert(nsevent.keyCode);
162
163 if (shouldPropagate)
164 [super keyDown:nsevent];
165}
166
167- (void)keyUp:(NSEvent *)nsevent
168{
169 if ([self isTransparentForUserInput])
170 return [super keyUp:nsevent];
171
172 const bool keyUpAccepted = [self handleKeyEvent:nsevent];
173
174 // Propagate the keyUp if neither Qt accepted it nor the corresponding KeyDown was
175 // accepted. Qt text controls will often not use and ignore keyUp events, but we
176 // want to avoid propagating unmatched keyUps.
177 const bool keyDownAccepted = m_acceptedKeyDowns.remove(nsevent.keyCode);
178 if (!keyUpAccepted && !keyDownAccepted)
179 [super keyUp:nsevent];
180}
181
182- (void)cancelOperation:(id)sender
183{
184 Q_UNUSED(sender);
185
186 NSEvent *currentEvent = NSApp.currentEvent;
187 if (!currentEvent || currentEvent.type != NSEventTypeKeyDown)
188 return;
189
190 // Handling the key event may recurse back here through interpretKeyEvents
191 // (when IM is enabled), so we need to guard against that.
192 if (currentEvent == m_currentlyInterpretedKeyEvent) {
193 m_sendKeyEvent = true;
194 return;
195 }
196
197 // Send Command+Key_Period and Escape as normal keypresses so that
198 // the key sequence is delivered through Qt. That way clients can
199 // intercept the shortcut and override its effect.
200 [self handleKeyEvent:currentEvent];
201}
202
203- (void)flagsChanged:(NSEvent *)nsevent
204{
205 // FIXME: Why are we not checking isTransparentForUserInput here?
206
207 KeyEvent keyEvent(nsevent);
208 qCDebug(lcQpaKeys) << "Flags changed resulting in" << keyEvent.modifiers;
209
210 // Calculate the delta and remember the current modifiers for next time
211 static NSEventModifierFlags m_lastKnownModifiers;
212 NSEventModifierFlags lastKnownModifiers = m_lastKnownModifiers;
213 NSEventModifierFlags newModifiers = lastKnownModifiers ^ keyEvent.nativeModifiers;
214 m_lastKnownModifiers = keyEvent.nativeModifiers;
215
216 static constexpr std::tuple<NSEventModifierFlags, Qt::Key> modifierMap[] = {
217 { NSEventModifierFlagShift, Qt::Key_Shift },
218 { NSEventModifierFlagControl, Qt::Key_Meta },
219 { NSEventModifierFlagCommand, Qt::Key_Control },
220 { NSEventModifierFlagOption, Qt::Key_Alt },
221 { NSEventModifierFlagCapsLock, Qt::Key_CapsLock }
222 };
223
224 for (auto [macModifier, qtKey] : modifierMap) {
225 if (!(newModifiers & macModifier))
226 continue;
227
228 // FIXME: Use QAppleKeyMapper helper
229 if (qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta)) {
230 if (qtKey == Qt::Key_Meta)
231 qtKey = Qt::Key_Control;
232 else if (qtKey == Qt::Key_Control)
233 qtKey = Qt::Key_Meta;
234 }
235
236 KeyEvent modifierEvent = keyEvent;
237 modifierEvent.type = lastKnownModifiers & macModifier
238 ? QEvent::KeyRelease : QEvent::KeyPress;
239
240 modifierEvent.key = qtKey;
241
242 // FIXME: Shouldn't this be based on lastKnownModifiers?
243 modifierEvent.modifiers ^= QAppleKeyMapper::fromCocoaModifiers(macModifier);
244 modifierEvent.nativeModifiers ^= macModifier;
245
246 // FIXME: Why are we sending to m_platformWindow here, but not for key events?
247 QWindow *window = m_platformWindow->window();
248
249 qCDebug(lcQpaKeys) << "Sending" << modifierEvent;
250 modifierEvent.sendWindowSystemEvent(window);
251 }
252}
253
254#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(150000)
255- (void)contextMenuKeyDown:(NSEvent *)nsevent
256{
257 qCDebug(lcQpaKeys) << "Handling context menu key down for" << nsevent;
258
259 if ([self isTransparentForUserInput])
260 return [super contextMenuKeyDown:nsevent];
261
262 if ([self handleKeyEvent:nsevent]) {
263 qCDebug(lcQpaKeys) << "Accepted context menu event as regular key down";
264 m_acceptedKeyDowns.insert(nsevent.keyCode);
265 } else {
266 // Forward up the responder chain to trigger default system
267 // behavior of calling showContextMenuForSelection.
268 [super contextMenuKeyDown:nsevent];
269 }
270}
271#endif
272
273@end
274
275QT_BEGIN_NAMESPACE
276
277// -------------------------------------------------------------------------
278
279KeyEvent::KeyEvent(NSEvent *nsevent)
280{
281 timestamp = nsevent.timestamp * 1000;
282 nativeModifiers = nsevent.modifierFlags;
283 modifiers = QAppleKeyMapper::fromCocoaModifiers(nativeModifiers);
284
285 switch (nsevent.type) {
286 case NSEventTypeKeyDown: type = QEvent::KeyPress; break;
287 case NSEventTypeKeyUp: type = QEvent::KeyRelease; break;
288 default: break; // Must be manually set
289 }
290
291 switch (nsevent.type) {
292 case NSEventTypeKeyDown:
293 case NSEventTypeKeyUp:
294 case NSEventTypeFlagsChanged:
295 nativeVirtualKey = nsevent.keyCode;
296 default:
297 break;
298 }
299
300 if (nsevent.type == NSEventTypeKeyDown || nsevent.type == NSEventTypeKeyUp) {
301 NSString *charactersIgnoringModifiers = nsevent.charactersIgnoringModifiers;
302 NSString *characters = nsevent.characters;
303
304 QChar character = QChar::ReplacementCharacter;
305
306 // If a dead key occurs as a result of pressing a key combination then
307 // characters will have 0 length, but charactersIgnoringModifiers will
308 // have a valid character in it. This enables key combinations such as
309 // ALT+E to be used as a shortcut with an English keyboard even though
310 // pressing ALT+E will give a dead key while doing normal text input.
311 if (characters.length || charactersIgnoringModifiers.length) {
312 if (nativeModifiers & (NSEventModifierFlagControl | NSEventModifierFlagOption)
313 && charactersIgnoringModifiers.length)
314 character = QChar([charactersIgnoringModifiers characterAtIndex:0]);
315 else if (characters.length)
316 character = QChar([characters characterAtIndex:0]);
317 key = QAppleKeyMapper::fromCocoaKey(character);
318 }
319
320 text = QString::fromNSString(characters);
321
322 isRepeat = nsevent.ARepeat;
323 }
324}
325
326bool KeyEvent::sendWindowSystemEvent(QWindow *window) const
327{
328 switch (type) {
329 case QEvent::Shortcut: {
330 return QWindowSystemInterface::handleShortcutEvent(window, timestamp,
331 key, modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers,
332 text, isRepeat);
333 }
334 case QEvent::KeyPress:
335 case QEvent::KeyRelease: {
336 static const int count = 1;
337 QWindowSystemInterface::handleExtendedKeyEvent(window, timestamp,
338 type, key, modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers,
339 text, isRepeat, count);
340 // FIXME: Make handleExtendedKeyEvent synchronous
341 return QWindowSystemInterface::flushWindowSystemEvents();
342 }
343 default:
344 qCritical() << "KeyEvent can not send event type" << type;
345 return false;
346 }
347}
348
349QDebug operator<<(QDebug debug, const KeyEvent &e)
350{
351 QDebugStateSaver saver(debug);
352 debug.nospace().verbosity(0) << "KeyEvent("
353 << e.type << ", timestamp=" << e.timestamp
354 << ", key=" << e.key << ", modifiers=" << e.modifiers
355 << ", text="<< e.text << ", isRepeat=" << e.isRepeat
356 << ", nativeVirtualKey=" << e.nativeVirtualKey
357 << ", nativeModifiers=" << e.nativeModifiers
358 << ")";
359 return debug;
360}
361
362QT_END_NAMESPACE
QDebug operator<<(QDebug dbg, const QFileInfo &fi)