File indexing completed on 2024-04-14 15:33:38
0001 /* 0002 SPDX-FileCopyrightText: 2015 Martin Gräßlin <mgraesslin@kde.org> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 #include "globalaccel.h" 0007 0008 #include <KKeyServer> 0009 #include <netwm.h> 0010 0011 #include <QDBusConnection> 0012 #include <QDBusMessage> 0013 #include <QDBusPendingReply> 0014 #include <QKeyEvent> 0015 #include <QRegularExpression> 0016 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0017 #include <private/qtx11extras_p.h> 0018 #else 0019 #include <QX11Info> 0020 #endif 0021 0022 #include <X11/keysym.h> 0023 #include <xcb/xcb.h> 0024 #include <xcb/xcb_keysyms.h> 0025 0026 static const QString s_kglobalAccelService = QStringLiteral("org.kde.kglobalaccel"); 0027 static const QString s_componentInterface = QStringLiteral("org.kde.kglobalaccel.Component"); 0028 0029 /** 0030 * Whitelist of the components which are allowed to get global shortcuts. 0031 * The DBus path of the component is the key for the whitelist. 0032 * The value for each key contains a regular expression matching unique shortcut names which are allowed. 0033 * This allows to not only restrict on component, but also restrict on the shortcuts. 0034 * E.g. plasmashell might accept media shortcuts, but not shortcuts for switching the activity. 0035 **/ 0036 static const QMap<QString, QRegularExpression> s_shortcutWhitelist{ 0037 {QStringLiteral("/component/mediacontrol"), QRegularExpression(QStringLiteral("stopmedia|nextmedia|previousmedia|playpausemedia"))}, 0038 {QStringLiteral("/component/kmix"), QRegularExpression(QStringLiteral("mute|decrease_volume|increase_volume"))}, 0039 {QStringLiteral("/component/org_kde_powerdevil"), 0040 QRegularExpression(QStringLiteral( 0041 "Increase Screen Brightness|Decrease Screen Brightness|Increase Keyboard Brightness|Decrease Keyboard Brightness|Turn Off Screen|Sleep|Hibernate"))}, 0042 {QStringLiteral("/component/KDE_Keyboard_Layout_Switcher"), 0043 QRegularExpression(QStringLiteral("Switch to Next Keyboard Layout|Switch keyboard layout to .*"))}, 0044 {QStringLiteral("/component/kcm_touchpad"), QRegularExpression(QStringLiteral("Toggle Touchpad|Enable Touchpad|Disable Touchpad"))}, 0045 {QStringLiteral("/component/kwin"), QRegularExpression(QStringLiteral("view_zoom_in|view_zoom_out|view_actual_size"))}, 0046 }; 0047 0048 static uint g_keyModMaskXAccel = 0; 0049 static uint g_keyModMaskXOnOrOff = 0; 0050 0051 static void calculateGrabMasks() 0052 { 0053 g_keyModMaskXAccel = KKeyServer::accelModMaskX(); 0054 g_keyModMaskXOnOrOff = KKeyServer::modXLock() | KKeyServer::modXNumLock() | KKeyServer::modXScrollLock() | KKeyServer::modXModeSwitch(); 0055 } 0056 0057 GlobalAccel::GlobalAccel(QObject *parent) 0058 : QObject(parent) 0059 { 0060 } 0061 0062 void GlobalAccel::prepare() 0063 { 0064 // recursion check 0065 if (m_updatingInformation) { 0066 return; 0067 } 0068 // first ensure that we don't have some left over 0069 release(); 0070 0071 if (QX11Info::isPlatformX11()) { 0072 m_keySymbols = xcb_key_symbols_alloc(QX11Info::connection()); 0073 calculateGrabMasks(); 0074 } 0075 0076 // fetch all components from KGlobalAccel 0077 m_updatingInformation++; 0078 auto message = QDBusMessage::createMethodCall(s_kglobalAccelService, 0079 QStringLiteral("/kglobalaccel"), 0080 QStringLiteral("org.kde.KGlobalAccel"), 0081 QStringLiteral("allComponents")); 0082 QDBusPendingReply<QList<QDBusObjectPath>> async = QDBusConnection::sessionBus().asyncCall(message); 0083 QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); 0084 connect(callWatcher, &QDBusPendingCallWatcher::finished, this, &GlobalAccel::components); 0085 } 0086 0087 void GlobalAccel::components(QDBusPendingCallWatcher *self) 0088 { 0089 QDBusPendingReply<QList<QDBusObjectPath>> reply = *self; 0090 self->deleteLater(); 0091 if (!reply.isValid()) { 0092 m_updatingInformation--; 0093 return; 0094 } 0095 // go through all components, check whether they are in our whitelist 0096 // if they are whitelisted we check whether they are active 0097 for (const auto &path : reply.value()) { 0098 const QString objectPath = path.path(); 0099 bool whitelisted = false; 0100 for (auto it = s_shortcutWhitelist.begin(); it != s_shortcutWhitelist.end(); ++it) { 0101 if (objectPath == it.key()) { 0102 whitelisted = true; 0103 break; 0104 } 0105 } 0106 if (!whitelisted) { 0107 continue; 0108 } 0109 auto message = QDBusMessage::createMethodCall(s_kglobalAccelService, objectPath, s_componentInterface, QStringLiteral("isActive")); 0110 QDBusPendingReply<bool> async = QDBusConnection::sessionBus().asyncCall(message); 0111 QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); 0112 m_updatingInformation++; 0113 connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, objectPath](QDBusPendingCallWatcher *self) { 0114 QDBusPendingReply<bool> reply = *self; 0115 self->deleteLater(); 0116 // filter out inactive components 0117 if (!reply.isValid() || !reply.value()) { 0118 m_updatingInformation--; 0119 return; 0120 } 0121 0122 // active, whitelisted component: get all shortcuts 0123 auto message = QDBusMessage::createMethodCall(s_kglobalAccelService, objectPath, s_componentInterface, QStringLiteral("allShortcutInfos")); 0124 QDBusPendingReply<QList<KGlobalShortcutInfo>> async = QDBusConnection::sessionBus().asyncCall(message); 0125 QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this); 0126 connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, objectPath](QDBusPendingCallWatcher *self) { 0127 m_updatingInformation--; 0128 QDBusPendingReply<QList<KGlobalShortcutInfo>> reply = *self; 0129 self->deleteLater(); 0130 if (!reply.isValid()) { 0131 return; 0132 } 0133 // restrict to whitelist 0134 QList<KGlobalShortcutInfo> infos; 0135 auto whitelist = s_shortcutWhitelist.constFind(objectPath); 0136 if (whitelist == s_shortcutWhitelist.constEnd()) { 0137 // this should not happen, just for safety 0138 return; 0139 } 0140 const auto s = reply.value(); 0141 for (auto it = s.begin(); it != s.end(); ++it) { 0142 auto matches = whitelist.value().match((*it).uniqueName()); 0143 if (matches.hasMatch()) { 0144 infos.append(*it); 0145 } 0146 } 0147 m_shortcuts.insert(objectPath, infos); 0148 }); 0149 }); 0150 } 0151 m_updatingInformation--; 0152 } 0153 0154 void GlobalAccel::release() 0155 { 0156 m_shortcuts.clear(); 0157 if (m_keySymbols) { 0158 xcb_key_symbols_free(m_keySymbols); 0159 m_keySymbols = nullptr; 0160 } 0161 } 0162 0163 bool GlobalAccel::keyEvent(QKeyEvent *event) 0164 { 0165 const int keyCodeQt = event->key(); 0166 Qt::KeyboardModifiers keyModQt = event->modifiers(); 0167 0168 if (keyModQt & Qt::SHIFT && !KKeyServer::isShiftAsModifierAllowed(keyCodeQt)) { 0169 keyModQt &= ~Qt::SHIFT; 0170 } 0171 0172 if ((keyModQt == 0 || keyModQt == Qt::SHIFT) && (keyCodeQt >= Qt::Key_Space && keyCodeQt <= Qt::Key_AsciiTilde)) { 0173 // security check: we don't allow shortcuts without modifier for "normal" keys 0174 // this is to prevent a malicious application to grab shortcuts for all keys 0175 // and by that being able to read out the keyboard 0176 return false; 0177 } 0178 0179 const QKeySequence seq(keyCodeQt | keyModQt); 0180 // let's check whether we have a mapping shortcut 0181 for (auto it = m_shortcuts.constBegin(); it != m_shortcuts.constEnd(); ++it) { 0182 for (const auto &info : it.value()) { 0183 if (info.keys().contains(seq)) { 0184 auto signal = QDBusMessage::createMethodCall(s_kglobalAccelService, it.key(), s_componentInterface, QStringLiteral("invokeShortcut")); 0185 signal.setArguments(QList<QVariant>{QVariant(info.uniqueName())}); 0186 QDBusConnection::sessionBus().asyncCall(signal); 0187 return true; 0188 } 0189 } 0190 } 0191 return false; 0192 } 0193 0194 bool GlobalAccel::checkKeyPress(xcb_key_press_event_t *event) 0195 { 0196 if (!m_keySymbols) { 0197 return false; 0198 } 0199 // based and inspired from code in kglobalaccel_x11.cpp 0200 xcb_keycode_t keyCodeX = event->detail; 0201 uint16_t keyModX = event->state & (g_keyModMaskXAccel | KKeyServer::MODE_SWITCH); 0202 0203 xcb_keysym_t keySymX = xcb_key_press_lookup_keysym(m_keySymbols, event, 0); 0204 0205 // If numlock is active and a keypad key is pressed, XOR the SHIFT state. 0206 // e.g., KP_4 => Shift+KP_Left, and Shift+KP_4 => KP_Left. 0207 if (event->state & KKeyServer::modXNumLock()) { 0208 xcb_keysym_t sym = xcb_key_symbols_get_keysym(m_keySymbols, keyCodeX, 0); 0209 // If this is a keypad key, 0210 if (sym >= XK_KP_Space && sym <= XK_KP_9) { 0211 switch (sym) { 0212 // Leave the following keys unaltered 0213 // FIXME: The proper solution is to see which keysyms don't change when shifted. 0214 case XK_KP_Multiply: 0215 case XK_KP_Add: 0216 case XK_KP_Subtract: 0217 case XK_KP_Divide: 0218 break; 0219 0220 default: 0221 keyModX ^= KKeyServer::modXShift(); 0222 } 0223 } 0224 } 0225 0226 int keyCodeQt; 0227 KKeyServer::symXModXToKeyQt(keySymX, keyModX, &keyCodeQt); 0228 // Split keycode and modifier 0229 int keyModQt = keyCodeQt & Qt::KeyboardModifierMask; 0230 keyCodeQt &= ~Qt::KeyboardModifierMask; 0231 0232 if (keyModQt & Qt::SHIFT && !KKeyServer::isShiftAsModifierAllowed(keyCodeQt)) { 0233 keyModQt &= ~Qt::SHIFT; 0234 } 0235 0236 if ((keyModQt == 0 || keyModQt == Qt::SHIFT) && (keyCodeQt >= Qt::Key_Space && keyCodeQt <= Qt::Key_AsciiTilde)) { 0237 // security check: we don't allow shortcuts without modifier for "normal" keys 0238 // this is to prevent a malicious application to grab shortcuts for all keys 0239 // and by that being able to read out the keyboard 0240 return false; 0241 } 0242 0243 const QKeySequence seq(keyCodeQt | keyModQt); 0244 // let's check whether we have a mapping shortcut 0245 for (auto it = m_shortcuts.begin(); it != m_shortcuts.end(); ++it) { 0246 for (const auto &info : it.value()) { 0247 if (info.keys().contains(seq)) { 0248 auto signal = QDBusMessage::createMethodCall(s_kglobalAccelService, it.key(), s_componentInterface, QStringLiteral("invokeShortcut")); 0249 signal.setArguments(QList<QVariant>{QVariant(info.uniqueName())}); 0250 QDBusConnection::sessionBus().asyncCall(signal); 0251 return true; 0252 } 0253 } 0254 } 0255 return false; 0256 }