File indexing completed on 2024-11-24 04:55:40

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2001, 2002 Ellis Whitehead <ellis@kde.org>
0004     SPDX-FileCopyrightText: 2013 Martin Gräßlin <mgraesslin@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "kglobalaccel_x11.h"
0010 
0011 #include "logging_p.h"
0012 #include <KKeyServer>
0013 #include <netwm.h>
0014 
0015 #include <QDebug>
0016 #include <QSocketNotifier>
0017 
0018 #include <QApplication>
0019 #include <QWidget>
0020 #include <private/qtx11extras_p.h>
0021 
0022 #include <X11/keysym.h>
0023 
0024 // xcb
0025 
0026 // It uses "explicit" as a variable name, which is not allowed in C++
0027 #define explicit xcb_explicit
0028 #include <xcb/record.h>
0029 #include <xcb/xcb.h>
0030 #include <xcb/xcb_keysyms.h>
0031 #include <xcb/xcbext.h>
0032 #include <xcb/xkb.h>
0033 #undef explicit
0034 
0035 // g_keyModMaskXAccel
0036 //  mask of modifiers which can be used in shortcuts
0037 //  (meta, alt, ctrl, shift)
0038 // g_keyModMaskXOnOrOff
0039 //  mask of modifiers where we don't care whether they are on or off
0040 //  (caps lock, num lock, scroll lock)
0041 static uint g_keyModMaskXAccel = 0;
0042 static uint g_keyModMaskXOnOrOff = 0;
0043 
0044 static void calculateGrabMasks()
0045 {
0046     g_keyModMaskXAccel = KKeyServer::accelModMaskX();
0047     g_keyModMaskXOnOrOff = KKeyServer::modXLock() | KKeyServer::modXNumLock() | KKeyServer::modXScrollLock() | KKeyServer::modXModeSwitch();
0048     // qCDebug(KGLOBALACCELD) << "g_keyModMaskXAccel = " << g_keyModMaskXAccel
0049     //  << "g_keyModMaskXOnOrOff = " << g_keyModMaskXOnOrOff << endl;
0050 }
0051 
0052 //----------------------------------------------------
0053 
0054 KGlobalAccelImpl::KGlobalAccelImpl(QObject *parent)
0055     : KGlobalAccelInterface(parent)
0056     , m_keySymbols(nullptr)
0057     , m_xkb_first_event(0)
0058 {
0059     Q_ASSERT(QX11Info::connection());
0060 
0061     int events = XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE;
0062     xcb_change_window_attributes(QX11Info::connection(), QX11Info::appRootWindow(), XCB_CW_EVENT_MASK, &events);
0063 
0064     const xcb_query_extension_reply_t *reply = xcb_get_extension_data(QX11Info::connection(), &xcb_xkb_id);
0065     if (reply && reply->present) {
0066         m_xkb_first_event = reply->first_event;
0067     }
0068 
0069     // We use XRecord to get the released keys because we need a way to get notified about
0070     // them without needing to hold a grab
0071     // Holding a grab would be a problem for the cases when a process (looking at you KWin's
0072     // toolbox) replies to a global shortcut trigger with another grab
0073     m_display = XOpenDisplay(nullptr);
0074     auto connection = xcb_connect(XDisplayString((Display *)m_display), nullptr);
0075     auto context = xcb_generate_id(connection);
0076     xcb_record_range_t range;
0077     memset(&range, 0, sizeof(range));
0078     range.device_events.first = XCB_KEY_RELEASE;
0079     range.device_events.last = XCB_KEY_RELEASE;
0080     xcb_record_client_spec_t cs = XCB_RECORD_CS_ALL_CLIENTS;
0081     xcb_record_create_context(connection, context, 0, 1, 1, &cs, &range);
0082     auto cookie = xcb_record_enable_context(connection, context);
0083     xcb_flush(connection);
0084 
0085     m_xrecordCookieSequence = cookie.sequence;
0086 
0087     auto m_notifier = new QSocketNotifier(xcb_get_file_descriptor(connection), QSocketNotifier::Read, this);
0088     connect(m_notifier, &QSocketNotifier::activated, this, [this, connection] {
0089         xcb_generic_event_t *event;
0090         while ((event = xcb_poll_for_event(connection))) {
0091             std::free(event);
0092         }
0093 
0094         xcb_record_enable_context_reply_t *reply = nullptr;
0095         xcb_generic_error_t *error = nullptr;
0096         while (m_xrecordCookieSequence && xcb_poll_for_reply(connection, m_xrecordCookieSequence, (void **)&reply, &error)) {
0097             // xcb_poll_for_reply may set both reply and error to null if connection has error.
0098             // break if xcb_connection has error, no point to continue anyway.
0099             if (xcb_connection_has_error(connection)) {
0100                 break;
0101             }
0102 
0103             if (error) {
0104                 std::free(error);
0105                 break;
0106             }
0107 
0108             if (!reply) {
0109                 continue;
0110             }
0111 
0112             QScopedPointer<xcb_record_enable_context_reply_t, QScopedPointerPodDeleter> data(reinterpret_cast<xcb_record_enable_context_reply_t *>(reply));
0113             xcb_key_press_event_t *events = reinterpret_cast<xcb_key_press_event_t *>(xcb_record_enable_context_data(reply));
0114             int nEvents = xcb_record_enable_context_data_length(reply) / sizeof(xcb_key_press_event_t);
0115             for (xcb_key_press_event_t *e = events; e < events + nEvents; e++) {
0116                 Q_ASSERT(e->response_type == XCB_KEY_RELEASE);
0117                 qCDebug(KGLOBALACCELD) << "Got XKeyRelease event";
0118                 x11KeyRelease(e);
0119             }
0120         }
0121     });
0122     m_notifier->setEnabled(true);
0123 
0124     calculateGrabMasks();
0125 }
0126 
0127 KGlobalAccelImpl::~KGlobalAccelImpl()
0128 {
0129     XCloseDisplay((Display *)m_display);
0130     if (m_keySymbols) {
0131         xcb_key_symbols_free(m_keySymbols);
0132     }
0133 }
0134 
0135 bool KGlobalAccelImpl::grabKey(int keyQt, bool grab)
0136 {
0137     // grabKey is called during shutdown
0138     // shutdown might be due to the X server being killed
0139     // if so, fail immediately before trying to make other xcb calls
0140     if (!QX11Info::connection() || xcb_connection_has_error(QX11Info::connection())) {
0141         return false;
0142     }
0143 
0144     if (!m_keySymbols) {
0145         m_keySymbols = xcb_key_symbols_alloc(QX11Info::connection());
0146         if (!m_keySymbols) {
0147             return false;
0148         }
0149     }
0150 
0151     if (!keyQt) {
0152         qCDebug(KGLOBALACCELD) << "Tried to grab key with null code.";
0153         return false;
0154     }
0155 
0156     uint keyModX;
0157 
0158     // Resolve the modifier
0159     if (!KKeyServer::keyQtToModX(keyQt, &keyModX)) {
0160         qCDebug(KGLOBALACCELD) << "keyQt (0x" << Qt::hex << keyQt << ") failed to resolve to x11 modifier";
0161         return false;
0162     }
0163 
0164     // Resolve the X symbol
0165     const QList<int> keySymXs(KKeyServer::keyQtToSymXs(keyQt));
0166     if (keySymXs.empty()) {
0167         qCDebug(KGLOBALACCELD) << "keyQt (0x" << Qt::hex << keyQt << ") failed to resolve to x11 keycode";
0168         return false;
0169     }
0170     xcb_keycode_t *keyCodes = nullptr;
0171     xcb_keysym_t keySymX;
0172     for (xcb_keysym_t sym : keySymXs) {
0173         keyCodes = xcb_key_symbols_get_keycode(m_keySymbols, sym);
0174         if (keyCodes) {
0175             keySymX = sym;
0176             break;
0177         }
0178     }
0179 
0180     if (!keyCodes) {
0181         return false;
0182     }
0183     int i = 0;
0184     bool success = !grab;
0185     while (keyCodes[i] != XCB_NO_SYMBOL) {
0186         xcb_keycode_t keyCodeX = keyCodes[i++];
0187 
0188         // Check if shift needs to be added to the grab since KKeySequenceWidget
0189         // can remove shift for some keys. (all the %&* and such)
0190         /* clang-format off */
0191         if (!(keyQt & Qt::SHIFT)
0192             && !KKeyServer::isShiftAsModifierAllowed(keyQt)
0193             && !(keyQt & Qt::KeypadModifier)
0194             && keySymX != xcb_key_symbols_get_keysym(m_keySymbols, keyCodeX, 0)
0195             && keySymX == xcb_key_symbols_get_keysym(m_keySymbols, keyCodeX, 1)) { /* clang-format on */
0196             qCDebug(KGLOBALACCELD) << "adding shift to the grab";
0197             keyModX |= KKeyServer::modXShift();
0198         }
0199 
0200         keyModX &= g_keyModMaskXAccel; // Get rid of any non-relevant bits in mod
0201 
0202         if (!keyCodeX) {
0203             qCDebug(KGLOBALACCELD) << "keyQt (0x" << Qt::hex << keyQt << ") was resolved to x11 keycode 0";
0204             continue;
0205         }
0206 
0207         // We'll have to grab 8 key modifier combinations in order to cover all
0208         //  combinations of CapsLock, NumLock, ScrollLock.
0209         // Does anyone with more X-savvy know how to set a mask on QX11Info::appRootWindow so that
0210         //  the irrelevant bits are always ignored and we can just make one XGrabKey
0211         //  call per accelerator? -- ellis
0212 #ifndef NDEBUG
0213         QString sDebug = QStringLiteral("\tcode: 0x%1 state: 0x%2 | ").arg(keyCodeX, 0, 16).arg(keyModX, 0, 16);
0214 #endif
0215         uint keyModMaskX = ~g_keyModMaskXOnOrOff;
0216         QList<xcb_void_cookie_t> cookies;
0217         for (uint irrelevantBitsMask = 0; irrelevantBitsMask <= 0xff; irrelevantBitsMask++) {
0218             if ((irrelevantBitsMask & keyModMaskX) == 0) {
0219 #ifndef NDEBUG
0220                 sDebug += QStringLiteral("0x%3, ").arg(irrelevantBitsMask, 0, 16);
0221 #endif
0222                 if (grab) {
0223                     cookies << xcb_grab_key_checked(QX11Info::connection(),
0224                                                     true,
0225                                                     QX11Info::appRootWindow(),
0226                                                     keyModX | irrelevantBitsMask,
0227                                                     keyCodeX,
0228                                                     XCB_GRAB_MODE_ASYNC,
0229                                                     XCB_GRAB_MODE_SYNC);
0230                 } else {
0231                     /* clang-format off */
0232                     cookies << xcb_ungrab_key_checked(QX11Info::connection(),
0233                                                       keyCodeX, QX11Info::appRootWindow(),
0234                                                       keyModX | irrelevantBitsMask);
0235                     /* clang-format on */
0236                 }
0237             }
0238         }
0239 
0240         bool failed = false;
0241         if (grab) {
0242             for (int i = 0; i < cookies.size(); ++i) {
0243                 QScopedPointer<xcb_generic_error_t, QScopedPointerPodDeleter> error(xcb_request_check(QX11Info::connection(), cookies.at(i)));
0244                 if (!error.isNull()) {
0245                     failed = true;
0246                 }
0247             }
0248             if (failed) {
0249                 qCDebug(KGLOBALACCELD) << "grab failed!\n";
0250                 for (uint m = 0; m <= 0xff; m++) {
0251                     if ((m & keyModMaskX) == 0) {
0252                         xcb_ungrab_key(QX11Info::connection(), keyCodeX, QX11Info::appRootWindow(), keyModX | m);
0253                     }
0254                 }
0255             } else {
0256                 success = true;
0257             }
0258         }
0259     }
0260     free(keyCodes);
0261     return success;
0262 }
0263 
0264 bool KGlobalAccelImpl::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *)
0265 {
0266     if (eventType != "xcb_generic_event_t") {
0267         return false;
0268     }
0269     xcb_generic_event_t *event = reinterpret_cast<xcb_generic_event_t *>(message);
0270     const uint8_t responseType = event->response_type & ~0x80;
0271     if (responseType == XCB_MAPPING_NOTIFY) {
0272         x11MappingNotify();
0273 
0274         // Make sure to let Qt handle it as well
0275         return false;
0276     } else if (responseType == XCB_KEY_PRESS) {
0277         qCDebug(KGLOBALACCELD) << "Got XKeyPress event";
0278         return x11KeyPress(reinterpret_cast<xcb_key_press_event_t *>(event));
0279     } else if (m_xkb_first_event && responseType == m_xkb_first_event) {
0280         const uint8_t xkbEvent = event->pad0;
0281         switch (xkbEvent) {
0282         case XCB_XKB_MAP_NOTIFY:
0283             x11MappingNotify();
0284             break;
0285         case XCB_XKB_NEW_KEYBOARD_NOTIFY: {
0286             const xcb_xkb_new_keyboard_notify_event_t *ev = reinterpret_cast<xcb_xkb_new_keyboard_notify_event_t *>(event);
0287             if (ev->changed & XCB_XKB_NKN_DETAIL_KEYCODES) {
0288                 x11MappingNotify();
0289             }
0290             break;
0291         }
0292         default:
0293             break;
0294         }
0295 
0296         // Make sure to let Qt handle it as well
0297         return false;
0298     } else {
0299         // We get all XEvents. Just ignore them.
0300         return false;
0301     }
0302 }
0303 
0304 void KGlobalAccelImpl::x11MappingNotify()
0305 {
0306     qCDebug(KGLOBALACCELD) << "Got XMappingNotify event";
0307 
0308     // Maybe the X modifier map has been changed.
0309     // uint oldKeyModMaskXAccel = g_keyModMaskXAccel;
0310     // uint oldKeyModMaskXOnOrOff = g_keyModMaskXOnOrOff;
0311 
0312     // First ungrab all currently grabbed keys. This is needed because we
0313     // store the keys as qt keycodes and use KKeyServer to map them to x11 key
0314     // codes. After calling KKeyServer::initializeMods() they could map to
0315     // different keycodes.
0316     ungrabKeys();
0317 
0318     if (m_keySymbols) {
0319         // Force reloading of the keySym mapping
0320         xcb_key_symbols_free(m_keySymbols);
0321         m_keySymbols = nullptr;
0322     }
0323 
0324     KKeyServer::initializeMods();
0325     calculateGrabMasks();
0326 
0327     grabKeys();
0328 }
0329 
0330 bool KGlobalAccelImpl::x11KeyPress(xcb_key_press_event_t *pEvent)
0331 {
0332     if (QWidget::keyboardGrabber() || QApplication::activePopupWidget()) {
0333         qCWarning(KGLOBALACCELD) << "kglobalacceld should be popup and keyboard grabbing free!";
0334     }
0335 
0336     // Keyboard needs to be ungrabed after XGrabKey() activates the grab,
0337     // otherwise it becomes frozen.
0338     xcb_connection_t *c = QX11Info::connection();
0339     xcb_void_cookie_t cookie = xcb_ungrab_keyboard_checked(c, XCB_TIME_CURRENT_TIME);
0340     xcb_flush(c);
0341     // xcb_flush() only makes sure that the ungrab keyboard request has been
0342     // sent, but is not enough to make sure that request has been fulfilled. Use
0343     // xcb_request_check() to make sure that the request has been processed.
0344     xcb_request_check(c, cookie);
0345 
0346     int keyQt;
0347     if (!KKeyServer::xcbKeyPressEventToQt(pEvent, &keyQt)) {
0348         qCWarning(KGLOBALACCELD) << "KKeyServer::xcbKeyPressEventToQt failed";
0349         return false;
0350     }
0351     // qDebug() << "keyQt=" << QString::number(keyQt, 16);
0352 
0353     // All that work for this hey... argh...
0354     if (NET::timestampCompare(pEvent->time, QX11Info::appTime()) > 0) {
0355         QX11Info::setAppTime(pEvent->time);
0356     }
0357     return keyPressed(keyQt);
0358 }
0359 
0360 bool KGlobalAccelImpl::x11KeyRelease(xcb_key_press_event_t *pEvent)
0361 {
0362     if (QWidget::keyboardGrabber() || QApplication::activePopupWidget()) {
0363         qCWarning(KGLOBALACCELD) << "kglobalacceld should be popup and keyboard grabbing free!";
0364     }
0365 
0366     int keyQt;
0367     if (!KKeyServer::xcbKeyPressEventToQt(pEvent, &keyQt)) {
0368         return false;
0369     }
0370     return keyReleased(keyQt);
0371 }
0372 
0373 void KGlobalAccelImpl::setEnabled(bool enable)
0374 {
0375     if (enable && qApp->platformName() == QLatin1String("xcb")) {
0376         qApp->installNativeEventFilter(this);
0377     } else {
0378         qApp->removeNativeEventFilter(this);
0379     }
0380 }