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