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 // Asking the input context to handle the event will involve both
118 // the current input method, as well as NSKeyBindingManager, which
119 // may result in action callbacks to doCommandBySelector.
120 if (![self.inputContext handleEvent:nsevent]) {
121 qCDebug(lcQpaKeys) << "Input context did not consume event";
122 m_sendKeyEvent = true;
123 }
124 m_currentlyInterpretedKeyEvent = 0;
125 didInterpretKeyEvent = true;
126
127 // If the last key we sent was dead, then pass the next
128 // key to the IM as well to complete composition.
129 m_lastKeyDead = isDeadKey;
130 }
131
132 }
133 }
134 }
135
136 bool accepted = true;
137 if (m_sendKeyEvent && m_composingText.isEmpty()) {
138 // Trust text input system on whether to send the event with text or not,
139 // or otherwise apply heuristics to filter out private use symbols.
140 if (didInterpretKeyEvent ? m_sendKeyEventWithoutText : isSpecialKey(keyEvent.text))
141 keyEvent.text = {};
142 qCDebug(lcQpaKeys) << "Sending as" << keyEvent;
143 accepted = keyEvent.sendWindowSystemEvent(window);
144 }
145 return accepted;
146}
147
148- (void)keyDown:(NSEvent *)nsevent
149{
150 if ([self isTransparentForUserInput])
151 return [super keyDown:nsevent];
152
153 const bool accepted = [self handleKeyEvent:nsevent];
154
155 // When Qt is used to implement a plugin for a native application we
156 // want to propagate unhandled events to other native views. However,
157 // Qt does not always set the accepted state correctly (in particular
158 // for return key events), so do this for plugin applications only
159 // to prevent incorrect forwarding in the general case.
160 const bool shouldPropagate = QCoreApplication::testAttribute(Qt::AA_PluginApplication) && !accepted;
161
162 // Track keyDown acceptance/forward state for later acceptance of the keyUp.
163 if (!shouldPropagate)
164 m_acceptedKeyDowns.insert(nsevent.keyCode);
165
166 if (shouldPropagate)
167 [super keyDown:nsevent];
168}
169
170- (void)keyUp:(NSEvent *)nsevent
171{
172 if ([self isTransparentForUserInput])
173 return [super keyUp:nsevent];
174
175 const bool keyUpAccepted = [self handleKeyEvent:nsevent];
176
177 // Propagate the keyUp if neither Qt accepted it nor the corresponding KeyDown was
178 // accepted. Qt text controls will often not use and ignore keyUp events, but we
179 // want to avoid propagating unmatched keyUps.
180 const bool keyDownAccepted = m_acceptedKeyDowns.remove(nsevent.keyCode);
181 if (!keyUpAccepted && !keyDownAccepted)
182 [super keyUp:nsevent];
183}
184
185- (void)cancelOperation:(id)sender
186{
187 Q_UNUSED(sender);
188
189 NSEvent *currentEvent = NSApp.currentEvent;
190 if (!currentEvent || currentEvent.type != NSEventTypeKeyDown)
191 return;
192
193 // Handling the key event may recurse back here through interpretKeyEvents
194 // (when IM is enabled), so we need to guard against that.
195 if (currentEvent == m_currentlyInterpretedKeyEvent) {
196 m_sendKeyEvent = true;
197 return;
198 }
199
200 // Send Command+Key_Period and Escape as normal keypresses so that
201 // the key sequence is delivered through Qt. That way clients can
202 // intercept the shortcut and override its effect.
203 [self handleKeyEvent:currentEvent];
204}
205
206- (void)flagsChanged:(NSEvent *)nsevent
207{
208 // FIXME: Why are we not checking isTransparentForUserInput here?
209
210 KeyEvent keyEvent(nsevent);
211 qCDebug(lcQpaKeys) << "Flags changed resulting in" << keyEvent.modifiers;
212
213 // Calculate the delta and remember the current modifiers for next time
214 static NSEventModifierFlags m_lastKnownModifiers;
215 NSEventModifierFlags lastKnownModifiers = m_lastKnownModifiers;
216 NSEventModifierFlags newModifiers = lastKnownModifiers ^ keyEvent.nativeModifiers;
217 m_lastKnownModifiers = keyEvent.nativeModifiers;
218
219 static constexpr std::tuple<NSEventModifierFlags, Qt::Key> modifierMap[] = {
220 { NSEventModifierFlagShift, Qt::Key_Shift },
221 { NSEventModifierFlagControl, Qt::Key_Meta },
222 { NSEventModifierFlagCommand, Qt::Key_Control },
223 { NSEventModifierFlagOption, Qt::Key_Alt },
224 { NSEventModifierFlagCapsLock, Qt::Key_CapsLock }
225 };
226
227 for (auto [macModifier, qtKey] : modifierMap) {
228 if (!(newModifiers & macModifier))
229 continue;
230
231 // FIXME: Use QAppleKeyMapper helper
232 if (qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta)) {
233 if (qtKey == Qt::Key_Meta)
234 qtKey = Qt::Key_Control;
235 else if (qtKey == Qt::Key_Control)
236 qtKey = Qt::Key_Meta;
237 }
238
239 KeyEvent modifierEvent = keyEvent;
240 modifierEvent.type = lastKnownModifiers & macModifier
241 ? QEvent::KeyRelease : QEvent::KeyPress;
242
243 modifierEvent.key = qtKey;
244
245 // FIXME: Shouldn't this be based on lastKnownModifiers?
246 modifierEvent.modifiers ^= QAppleKeyMapper::fromCocoaModifiers(macModifier);
247 modifierEvent.nativeModifiers ^= macModifier;
248
249 // FIXME: Why are we sending to m_platformWindow here, but not for key events?
250 QWindow *window = m_platformWindow->window();
251
252 qCDebug(lcQpaKeys) << "Sending" << modifierEvent;
253 modifierEvent.sendWindowSystemEvent(window);
254 }
255}
256
257#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(150000)
258- (void)contextMenuKeyDown:(NSEvent *)nsevent
259{
260 qCDebug(lcQpaKeys) << "Handling context menu key down for" << nsevent;
261
262 if ([self isTransparentForUserInput])
263 return [super contextMenuKeyDown:nsevent];
264
265 if ([self handleKeyEvent:nsevent]) {
266 qCDebug(lcQpaKeys) << "Accepted context menu event as regular key down";
267 m_acceptedKeyDowns.insert(nsevent.keyCode);
268 } else {
269 // Forward up the responder chain to trigger default system
270 // behavior of calling showContextMenuForSelection.
271 [super contextMenuKeyDown:nsevent];
272 }
273}
274#endif
275
276@end
277
278QT_BEGIN_NAMESPACE
279
280// -------------------------------------------------------------------------
281
282KeyEvent::KeyEvent(NSEvent *nsevent)
283{
284 timestamp = nsevent.timestamp * 1000;
285 nativeModifiers = nsevent.modifierFlags;
286 modifiers = QAppleKeyMapper::fromCocoaModifiers(nativeModifiers);
287
288 switch (nsevent.type) {
289 case NSEventTypeKeyDown: type = QEvent::KeyPress; break;
290 case NSEventTypeKeyUp: type = QEvent::KeyRelease; break;
291 default: break; // Must be manually set
292 }
293
294 switch (nsevent.type) {
295 case NSEventTypeKeyDown:
296 case NSEventTypeKeyUp:
297 case NSEventTypeFlagsChanged:
298 nativeVirtualKey = nsevent.keyCode;
299 default:
300 break;
301 }
302
303 if (nsevent.type == NSEventTypeKeyDown || nsevent.type == NSEventTypeKeyUp) {
304 NSString *charactersIgnoringModifiers = nsevent.charactersIgnoringModifiers;
305 NSString *characters = nsevent.characters;
306
307 QChar character = QChar::ReplacementCharacter;
308
309 // If a dead key occurs as a result of pressing a key combination then
310 // characters will have 0 length, but charactersIgnoringModifiers will
311 // have a valid character in it. This enables key combinations such as
312 // ALT+E to be used as a shortcut with an English keyboard even though
313 // pressing ALT+E will give a dead key while doing normal text input.
314 if (characters.length || charactersIgnoringModifiers.length) {
315 if (nativeModifiers & (NSEventModifierFlagControl | NSEventModifierFlagOption)
316 && charactersIgnoringModifiers.length)
317 character = QChar([charactersIgnoringModifiers characterAtIndex:0]);
318 else if (characters.length)
319 character = QChar([characters characterAtIndex:0]);
320 key = QAppleKeyMapper::fromCocoaKey(character);
321 }
322
323 text = QString::fromNSString(characters);
324
325 isRepeat = nsevent.ARepeat;
326 }
327}
328
329bool KeyEvent::sendWindowSystemEvent(QWindow *window) const
330{
331 switch (type) {
332 case QEvent::Shortcut: {
333 return QWindowSystemInterface::handleShortcutEvent(window, timestamp,
334 key, modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers,
335 text, isRepeat);
336 }
337 case QEvent::KeyPress:
338 case QEvent::KeyRelease: {
339 static const int count = 1;
340 QWindowSystemInterface::handleExtendedKeyEvent(window, timestamp,
341 type, key, modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers,
342 text, isRepeat, count);
343 // FIXME: Make handleExtendedKeyEvent synchronous
344 return QWindowSystemInterface::flushWindowSystemEvents();
345 }
346 default:
347 qCritical() << "KeyEvent can not send event type" << type;
348 return false;
349 }
350}
351
352QDebug operator<<(QDebug debug, const KeyEvent &e)
353{
354 QDebugStateSaver saver(debug);
355 debug.nospace().verbosity(0) << "KeyEvent("
356 << e.type << ", timestamp=" << e.timestamp
357 << ", key=" << e.key << ", modifiers=" << e.modifiers
358 << ", text="<< e.text << ", isRepeat=" << e.isRepeat
359 << ", nativeVirtualKey=" << e.nativeVirtualKey
360 << ", nativeModifiers=" << e.nativeModifiers
361 << ")";
362 return debug;
363}
364
365QT_END_NAMESPACE
QDebug operator<<(QDebug dbg, const QFileInfo &fi)