1/****************************************************************************
2**
3** Copyright (C) 2018 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the plugins of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40// This file is included from qnsview.mm, and only used to organize the code
41
42@implementation QNSView (KeysAPI)
43
44+ (Qt::KeyboardModifiers)convertKeyModifiers:(ulong)modifierFlags
45{
46    const bool dontSwapCtrlAndMeta = qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta);
47    Qt::KeyboardModifiers qtMods =Qt::NoModifier;
48    if (modifierFlags & NSEventModifierFlagShift)
49        qtMods |= Qt::ShiftModifier;
50    if (modifierFlags & NSEventModifierFlagControl)
51        qtMods |= dontSwapCtrlAndMeta ? Qt::ControlModifier : Qt::MetaModifier;
52    if (modifierFlags & NSEventModifierFlagOption)
53        qtMods |= Qt::AltModifier;
54    if (modifierFlags & NSEventModifierFlagCommand)
55        qtMods |= dontSwapCtrlAndMeta ? Qt::MetaModifier : Qt::ControlModifier;
56    if (modifierFlags & NSEventModifierFlagNumericPad)
57        qtMods |= Qt::KeypadModifier;
58    return qtMods;
59}
60
61@end
62
63@implementation QNSView (Keys)
64
65- (int)convertKeyCode:(QChar)keyChar
66{
67    return qt_mac_cocoaKey2QtKey(keyChar);
68}
69
70- (bool)handleKeyEvent:(NSEvent *)nsevent eventType:(int)eventType
71{
72    ulong timestamp = [nsevent timestamp] * 1000;
73    ulong nativeModifiers = [nsevent modifierFlags];
74    Qt::KeyboardModifiers modifiers = [QNSView convertKeyModifiers: nativeModifiers];
75    NSString *charactersIgnoringModifiers = [nsevent charactersIgnoringModifiers];
76    NSString *characters = [nsevent characters];
77    if (m_inputSource != characters) {
78        [m_inputSource release];
79        m_inputSource = [characters retain];
80    }
81
82    // There is no way to get the scan code from carbon/cocoa. But we cannot
83    // use the value 0, since it indicates that the event originates from somewhere
84    // else than the keyboard.
85    quint32 nativeScanCode = 1;
86    quint32 nativeVirtualKey = [nsevent keyCode];
87
88    QChar ch = QChar::ReplacementCharacter;
89    int keyCode = Qt::Key_unknown;
90
91    // If a dead key occurs as a result of pressing a key combination then
92    // characters will have 0 length, but charactersIgnoringModifiers will
93    // have a valid character in it. This enables key combinations such as
94    // ALT+E to be used as a shortcut with an English keyboard even though
95    // pressing ALT+E will give a dead key while doing normal text input.
96    if ([characters length] != 0 || [charactersIgnoringModifiers length] != 0) {
97        auto ctrlOrMetaModifier = qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta) ? Qt::ControlModifier : Qt::MetaModifier;
98        if (((modifiers & ctrlOrMetaModifier) || (modifiers & Qt::AltModifier)) && ([charactersIgnoringModifiers length] != 0))
99            ch = QChar([charactersIgnoringModifiers characterAtIndex:0]);
100        else if ([characters length] != 0)
101            ch = QChar([characters characterAtIndex:0]);
102        keyCode = [self convertKeyCode:ch];
103    }
104
105    // we will send a key event unless the input method sets m_sendKeyEvent to false
106    m_sendKeyEvent = true;
107    QString text;
108    // ignore text for the U+F700-U+F8FF range. This is used by Cocoa when
109    // delivering function keys (e.g. arrow keys, backspace, F1-F35, etc.)
110    if (!(modifiers & (Qt::ControlModifier | Qt::MetaModifier)) && (ch.unicode() < 0xf700 || ch.unicode() > 0xf8ff))
111        text = QString::fromNSString(characters);
112
113    QWindow *window = [self topLevelWindow];
114
115    // Popups implicitly grab key events; forward to the active popup if there is one.
116    // This allows popups to e.g. intercept shortcuts and close the popup in response.
117    if (QCocoaWindow *popup = QCocoaIntegration::instance()->activePopupWindow()) {
118        if (!popup->window()->flags().testFlag(Qt::ToolTip))
119            window = popup->window();
120    }
121
122    if (eventType == QEvent::KeyPress) {
123
124        if (m_composingText.isEmpty()) {
125            m_sendKeyEvent = !QWindowSystemInterface::handleShortcutEvent(window, timestamp, keyCode,
126                modifiers, nativeScanCode, nativeVirtualKey, nativeModifiers, text, [nsevent isARepeat], 1);
127
128            // Handling a shortcut may result in closing the window
129            if (!m_platformWindow)
130                return true;
131        }
132
133        QObject *fo = m_platformWindow->window()->focusObject();
134        if (m_sendKeyEvent && fo) {
135            QInputMethodQueryEvent queryEvent(Qt::ImEnabled | Qt::ImHints);
136            if (QCoreApplication::sendEvent(fo, &queryEvent)) {
137                bool imEnabled = queryEvent.value(Qt::ImEnabled).toBool();
138                Qt::InputMethodHints hints = static_cast<Qt::InputMethodHints>(queryEvent.value(Qt::ImHints).toUInt());
139                if (imEnabled && !(hints & Qt::ImhDigitsOnly || hints & Qt::ImhFormattedNumbersOnly || hints & Qt::ImhHiddenText)) {
140                    // pass the key event to the input method. note that m_sendKeyEvent may be set to false during this call
141                    m_currentlyInterpretedKeyEvent = nsevent;
142                    [self interpretKeyEvents:@[nsevent]];
143                    m_currentlyInterpretedKeyEvent = 0;
144                }
145            }
146        }
147        if (m_resendKeyEvent)
148            m_sendKeyEvent = true;
149    }
150
151    bool accepted = true;
152    if (m_sendKeyEvent && m_composingText.isEmpty()) {
153        QWindowSystemInterface::handleExtendedKeyEvent(window, timestamp, QEvent::Type(eventType), keyCode, modifiers,
154                                                       nativeScanCode, nativeVirtualKey, nativeModifiers, text, [nsevent isARepeat], 1, false);
155        accepted = QWindowSystemInterface::flushWindowSystemEvents();
156    }
157    m_sendKeyEvent = false;
158    m_resendKeyEvent = false;
159    return accepted;
160}
161
162- (void)keyDown:(NSEvent *)nsevent
163{
164    if ([self isTransparentForUserInput])
165        return [super keyDown:nsevent];
166
167    const bool accepted = [self handleKeyEvent:nsevent eventType:int(QEvent::KeyPress)];
168
169    // When Qt is used to implement a plugin for a native application we
170    // want to propagate unhandled events to other native views. However,
171    // Qt does not always set the accepted state correctly (in particular
172    // for return key events), so do this for plugin applications only
173    // to prevent incorrect forwarding in the general case.
174    const bool shouldPropagate = QCoreApplication::testAttribute(Qt::AA_PluginApplication) && !accepted;
175
176    // Track keyDown acceptance/forward state for later acceptance of the keyUp.
177    if (!shouldPropagate)
178        m_acceptedKeyDowns.insert([nsevent keyCode]);
179
180    if (shouldPropagate)
181        [super keyDown:nsevent];
182}
183
184- (void)keyUp:(NSEvent *)nsevent
185{
186    if ([self isTransparentForUserInput])
187        return [super keyUp:nsevent];
188
189    const bool keyUpAccepted = [self handleKeyEvent:nsevent eventType:int(QEvent::KeyRelease)];
190
191    // Propagate the keyUp if neither Qt accepted it nor the corresponding KeyDown was
192    // accepted. Qt text controls wil often not use and ignore keyUp events, but we
193    // want to avoid propagating unmatched keyUps.
194    const bool keyDownAccepted = m_acceptedKeyDowns.remove([nsevent keyCode]);
195    if (!keyUpAccepted && !keyDownAccepted)
196        [super keyUp:nsevent];
197}
198
199- (void)cancelOperation:(id)sender
200{
201    Q_UNUSED(sender);
202
203    NSEvent *currentEvent = [NSApp currentEvent];
204    if (!currentEvent || currentEvent.type != NSEventTypeKeyDown)
205        return;
206
207    // Handling the key event may recurse back here through interpretKeyEvents
208    // (when IM is enabled), so we need to guard against that.
209    if (currentEvent == m_currentlyInterpretedKeyEvent)
210        return;
211
212    // Send Command+Key_Period and Escape as normal keypresses so that
213    // the key sequence is delivered through Qt. That way clients can
214    // intercept the shortcut and override its effect.
215    [self handleKeyEvent:currentEvent eventType:int(QEvent::KeyPress)];
216}
217
218- (void)flagsChanged:(NSEvent *)nsevent
219{
220    ulong timestamp = [nsevent timestamp] * 1000;
221    ulong nativeModifiers = [nsevent modifierFlags];
222    Qt::KeyboardModifiers modifiers = [QNSView convertKeyModifiers:nativeModifiers];
223
224    // There is no way to get the scan code from carbon/cocoa. But we cannot
225    // use the value 0, since it indicates that the event originates from somewhere
226    // else than the keyboard.
227    quint32 nativeScanCode = 1;
228    quint32 nativeVirtualKey = [nsevent keyCode];
229
230    // calculate the delta and remember the current modifiers for next time
231    static ulong m_lastKnownModifiers;
232    ulong lastKnownModifiers = m_lastKnownModifiers;
233    ulong delta = lastKnownModifiers ^ nativeModifiers;
234    m_lastKnownModifiers = nativeModifiers;
235
236    struct qt_mac_enum_mapper
237    {
238        ulong mac_mask;
239        Qt::Key qt_code;
240    };
241    static qt_mac_enum_mapper modifier_key_symbols[] = {
242        { NSEventModifierFlagShift, Qt::Key_Shift },
243        { NSEventModifierFlagControl, Qt::Key_Meta },
244        { NSEventModifierFlagCommand, Qt::Key_Control },
245        { NSEventModifierFlagOption, Qt::Key_Alt },
246        { NSEventModifierFlagCapsLock, Qt::Key_CapsLock },
247        { 0ul, Qt::Key_unknown } };
248    for (int i = 0; modifier_key_symbols[i].mac_mask != 0u; ++i) {
249        uint mac_mask = modifier_key_symbols[i].mac_mask;
250        if ((delta & mac_mask) == 0u)
251            continue;
252
253        Qt::Key qtCode = modifier_key_symbols[i].qt_code;
254        if (qApp->testAttribute(Qt::AA_MacDontSwapCtrlAndMeta)) {
255            if (qtCode == Qt::Key_Meta)
256                qtCode = Qt::Key_Control;
257            else if (qtCode == Qt::Key_Control)
258                qtCode = Qt::Key_Meta;
259        }
260        QWindowSystemInterface::handleExtendedKeyEvent(m_platformWindow->window(),
261                                                       timestamp,
262                                                       (lastKnownModifiers & mac_mask) ? QEvent::KeyRelease
263                                                                                       : QEvent::KeyPress,
264                                                       qtCode,
265                                                       modifiers ^ [QNSView convertKeyModifiers:mac_mask],
266                                                       nativeScanCode, nativeVirtualKey,
267                                                       nativeModifiers ^ mac_mask);
268    }
269}
270
271@end
272