Warning, file /frameworks/kglobalaccel/src/runtime/globalshortcutsregistry.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2008 Michael Jansen <kde@michael-jansen.biz>
0003     SPDX-FileCopyrightText: 2022 Ahmad Samir <a.samirh78@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "globalshortcutsregistry.h"
0009 #include "component.h"
0010 #include "globalshortcut.h"
0011 #include "globalshortcutcontext.h"
0012 #include "kglobalaccel_interface.h"
0013 #include "kglobalshortcutinfo_p.h"
0014 #include "kserviceactioncomponent.h"
0015 #include "logging_p.h"
0016 #include <config-kglobalaccel.h>
0017 
0018 #include <KDesktopFile>
0019 #include <KFileUtils>
0020 #include <KPluginMetaData>
0021 
0022 #include <QDBusConnection>
0023 #include <QDir>
0024 #include <QGuiApplication>
0025 #include <QJsonArray>
0026 #include <QPluginLoader>
0027 #include <QStandardPaths>
0028 
0029 static bool checkPlatform(const QJsonObject &metadata, const QString &platformName)
0030 {
0031     const QJsonArray platforms = metadata.value(QStringLiteral("MetaData")).toObject().value(QStringLiteral("platforms")).toArray();
0032     return std::any_of(platforms.begin(), platforms.end(), [&platformName](const QJsonValue &value) {
0033         return QString::compare(platformName, value.toString(), Qt::CaseInsensitive) == 0;
0034     });
0035 }
0036 
0037 static KGlobalAccelInterface *loadPlugin(GlobalShortcutsRegistry *parent)
0038 {
0039     QString platformName = QString::fromLocal8Bit(qgetenv("KGLOBALACCELD_PLATFORM"));
0040     if (platformName.isEmpty()) {
0041         platformName = QGuiApplication::platformName();
0042     }
0043 
0044     const QVector<QStaticPlugin> staticPlugins = QPluginLoader::staticPlugins();
0045     for (const QStaticPlugin &staticPlugin : staticPlugins) {
0046         const QJsonObject metadata = staticPlugin.metaData();
0047         if (metadata.value(QLatin1String("IID")) != QLatin1String(KGlobalAccelInterface_iid)) {
0048             continue;
0049         }
0050         if (checkPlatform(metadata, platformName)) {
0051             KGlobalAccelInterface *interface = qobject_cast<KGlobalAccelInterface *>(staticPlugin.instance());
0052             if (interface) {
0053                 qCDebug(KGLOBALACCELD) << "Loaded a static plugin for platform" << platformName;
0054                 interface->setRegistry(parent);
0055                 return interface;
0056             }
0057         }
0058     }
0059 
0060     const QVector<KPluginMetaData> candidates = KPluginMetaData::findPlugins(QStringLiteral("org.kde.kglobalaccel5.platforms"));
0061     for (const KPluginMetaData &candidate : candidates) {
0062         QPluginLoader loader(candidate.fileName());
0063         if (checkPlatform(loader.metaData(), platformName)) {
0064             KGlobalAccelInterface *interface = qobject_cast<KGlobalAccelInterface *>(loader.instance());
0065             if (interface) {
0066                 qCDebug(KGLOBALACCELD) << "Loaded plugin" << candidate.fileName() << "for platform" << platformName;
0067                 interface->setRegistry(parent);
0068                 return interface;
0069             }
0070         }
0071     }
0072 
0073     qCWarning(KGLOBALACCELD) << "Could not find any platform plugin";
0074     return nullptr;
0075 }
0076 
0077 static QString getConfigFile()
0078 {
0079     return qEnvironmentVariableIsSet("KGLOBALACCEL_TEST_MODE") ? QString() : QStringLiteral("kglobalshortcutsrc");
0080 }
0081 
0082 GlobalShortcutsRegistry::GlobalShortcutsRegistry()
0083     : QObject()
0084     , _manager(loadPlugin(this))
0085     , _config(getConfigFile(), KConfig::SimpleConfig)
0086 {
0087     if (_manager) {
0088         _manager->setEnabled(true);
0089     }
0090 }
0091 
0092 GlobalShortcutsRegistry::~GlobalShortcutsRegistry()
0093 {
0094     m_components.clear();
0095 
0096     if (_manager) {
0097         _manager->setEnabled(false);
0098 
0099         // Ungrab all keys. We don't go over GlobalShortcuts because
0100         // GlobalShortcutsRegistry::self() doesn't work anymore.
0101         const auto listKeys = _active_keys.keys();
0102         for (const QKeySequence &key : listKeys) {
0103             for (int i = 0; i < key.count(); i++) {
0104 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0105                 _manager->grabKey(key[i].toCombined(), false);
0106 #else
0107                 _manager->grabKey(key[i], false);
0108 #endif
0109             }
0110         }
0111     }
0112     _active_keys.clear();
0113     _keys_count.clear();
0114 }
0115 
0116 Component *GlobalShortcutsRegistry::registerComponent(ComponentPtr component)
0117 {
0118     m_components.push_back(std::move(component));
0119     auto *comp = m_components.back().get();
0120     QDBusConnection conn(QDBusConnection::sessionBus());
0121     conn.registerObject(comp->dbusPath().path(), comp, QDBusConnection::ExportScriptableContents);
0122     return comp;
0123 }
0124 
0125 void GlobalShortcutsRegistry::activateShortcuts()
0126 {
0127     for (auto &component : m_components) {
0128         component->activateShortcuts();
0129     }
0130 }
0131 
0132 QList<QDBusObjectPath> GlobalShortcutsRegistry::componentsDbusPaths() const
0133 {
0134     QList<QDBusObjectPath> dbusPaths;
0135     dbusPaths.reserve(m_components.size());
0136     std::transform(m_components.cbegin(), m_components.cend(), std::back_inserter(dbusPaths), [](const auto &comp) {
0137         return comp->dbusPath();
0138     });
0139     return dbusPaths;
0140 }
0141 
0142 QList<QStringList> GlobalShortcutsRegistry::allComponentNames() const
0143 {
0144     QList<QStringList> ret;
0145     ret.reserve(m_components.size());
0146     std::transform(m_components.cbegin(), m_components.cend(), std::back_inserter(ret), [](const auto &component) {
0147         // A string for each enumerator in KGlobalAccel::actionIdFields
0148         return QStringList{component->uniqueName(), component->friendlyName(), {}, {}};
0149     });
0150 
0151     return ret;
0152 }
0153 
0154 void GlobalShortcutsRegistry::clear()
0155 {
0156     m_components.clear();
0157 
0158     // The shortcuts should have deregistered themselves
0159     Q_ASSERT(_active_keys.isEmpty());
0160 }
0161 
0162 QDBusObjectPath GlobalShortcutsRegistry::dbusPath() const
0163 {
0164     return _dbusPath;
0165 }
0166 
0167 void GlobalShortcutsRegistry::deactivateShortcuts(bool temporarily)
0168 {
0169     for (ComponentPtr &component : m_components) {
0170         component->deactivateShortcuts(temporarily);
0171     }
0172 }
0173 
0174 Component *GlobalShortcutsRegistry::getComponent(const QString &uniqueName)
0175 {
0176     auto it = findByName(uniqueName);
0177     return it != m_components.cend() ? (*it).get() : nullptr;
0178 }
0179 
0180 GlobalShortcut *GlobalShortcutsRegistry::getShortcutByKey(const QKeySequence &key, KGlobalAccel::MatchType type) const
0181 {
0182     for (const ComponentPtr &component : m_components) {
0183         GlobalShortcut *rc = component->getShortcutByKey(key, type);
0184         if (rc) {
0185             return rc;
0186         }
0187     }
0188     return nullptr;
0189 }
0190 
0191 QList<GlobalShortcut *> GlobalShortcutsRegistry::getShortcutsByKey(const QKeySequence &key, KGlobalAccel::MatchType type) const
0192 {
0193     QList<GlobalShortcut *> rc;
0194     for (const ComponentPtr &component : m_components) {
0195         rc = component->getShortcutsByKey(key, type);
0196         if (!rc.isEmpty()) {
0197             return rc;
0198         }
0199     }
0200     return {};
0201 }
0202 
0203 bool GlobalShortcutsRegistry::isShortcutAvailable(const QKeySequence &shortcut, const QString &componentName, const QString &contextName) const
0204 {
0205     return std::all_of(m_components.cbegin(), m_components.cend(), [&shortcut, &componentName, &contextName](const ComponentPtr &component) {
0206         return component->isShortcutAvailable(shortcut, componentName, contextName);
0207     });
0208 }
0209 
0210 Q_GLOBAL_STATIC(GlobalShortcutsRegistry, _self)
0211 GlobalShortcutsRegistry *GlobalShortcutsRegistry::self()
0212 {
0213     return _self;
0214 }
0215 
0216 /**
0217  * When we are provided just a Shift key press, interpret it as "Shift" not as "Shift+Shift"
0218  */
0219 static void correctKeyEvent(int &keyQt)
0220 {
0221     switch (keyQt) {
0222     case Qt::ShiftModifier | Qt::Key_Shift:
0223         keyQt = Qt::Key_Shift;
0224         break;
0225     case Qt::ControlModifier | Qt::Key_Control:
0226         keyQt = Qt::Key_Control;
0227         break;
0228     case Qt::AltModifier | Qt::Key_Alt:
0229         keyQt = Qt::Key_Alt;
0230         break;
0231     case Qt::MetaModifier | Qt::Key_Meta:
0232         keyQt = Qt::Key_Meta;
0233         break;
0234     }
0235 }
0236 
0237 bool GlobalShortcutsRegistry::keyPressed(int keyQt)
0238 {
0239     correctKeyEvent(keyQt);
0240     int keys[maxSequenceLength] = {0, 0, 0, 0};
0241     int count = _active_sequence.count();
0242     if (count == maxSequenceLength) {
0243         // buffer is full, rotate it
0244         for (int i = 1; i < count; i++) {
0245 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0246             keys[i - 1] = _active_sequence[i].toCombined();
0247 #else
0248             keys[i - 1] = _active_sequence[i];
0249 #endif
0250         }
0251         keys[maxSequenceLength - 1] = keyQt;
0252     } else {
0253         // just append the new key
0254         for (int i = 0; i < count; i++) {
0255 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0256             keys[i] = _active_sequence[i].toCombined();
0257 #else
0258             keys[i] = _active_sequence[i];
0259 #endif
0260         }
0261         keys[count] = keyQt;
0262     }
0263 
0264     _active_sequence = QKeySequence(keys[0], keys[1], keys[2], keys[3]);
0265 
0266     GlobalShortcut *shortcut = nullptr;
0267     QKeySequence tempSequence;
0268     for (int length = 1; length <= _active_sequence.count(); length++) {
0269         // We have to check all possible matches from the end since we're rotating active sequence
0270         // instead of cleaning it when it's full
0271         int sequenceToCheck[maxSequenceLength] = {0, 0, 0, 0};
0272         for (int i = 0; i < length; i++) {
0273 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0274             sequenceToCheck[i] = _active_sequence[_active_sequence.count() - length + i].toCombined();
0275 #else
0276             sequenceToCheck[i] = _active_sequence[_active_sequence.count() - length + i];
0277 #endif
0278         }
0279         tempSequence = QKeySequence(sequenceToCheck[0], sequenceToCheck[1], sequenceToCheck[2], sequenceToCheck[3]);
0280         shortcut = getShortcutByKey(tempSequence);
0281 
0282         if (shortcut) {
0283             break;
0284         }
0285     }
0286 
0287     qCDebug(KGLOBALACCELD) << "Pressed key" << QKeySequence(keyQt).toString() << ", current sequence" << _active_sequence.toString() << "="
0288                            << (shortcut ? shortcut->uniqueName() : QStringLiteral("(no shortcut found)"));
0289     if (!shortcut) {
0290         // This can happen for example with the ALT-Print shortcut of kwin.
0291         // ALT+PRINT is SYSREQ on my keyboard. So we grab something we think
0292         // is ALT+PRINT but symXToKeyQt and modXToQt make ALT+SYSREQ of it
0293         // when pressed (correctly). We can't match that.
0294         qCDebug(KGLOBALACCELD) << "Got unknown key" << QKeySequence(keyQt).toString();
0295 
0296         // In production mode just do nothing.
0297         return false;
0298     } else if (!shortcut->isActive()) {
0299         qCDebug(KGLOBALACCELD) << "Got inactive key" << QKeySequence(keyQt).toString();
0300 
0301         // In production mode just do nothing.
0302         return false;
0303     }
0304 
0305     qCDebug(KGLOBALACCELD) << QKeySequence(keyQt).toString() << "=" << shortcut->uniqueName();
0306 
0307     // shortcut is found, reset active sequence
0308     _active_sequence = QKeySequence();
0309 
0310     QStringList data(shortcut->context()->component()->uniqueName());
0311     data.append(shortcut->uniqueName());
0312     data.append(shortcut->context()->component()->friendlyName());
0313     data.append(shortcut->friendlyName());
0314 
0315     // Make sure kglobalacceld has ungrabbed the keyboard after receiving the
0316     // keypress, otherwise actions in application that try to grab the
0317     // keyboard (e.g. in kwin) may fail to do so. There is still a small race
0318     // condition with this being out-of-process.
0319     if (_manager) {
0320         _manager->syncWindowingSystem();
0321     }
0322 
0323     if (m_lastShortcut && m_lastShortcut != shortcut) {
0324         m_lastShortcut->context()->component()->emitGlobalShortcutReleased(*m_lastShortcut);
0325     }
0326 
0327     // Invoke the action
0328     shortcut->context()->component()->emitGlobalShortcutPressed(*shortcut);
0329     m_lastShortcut = shortcut;
0330 
0331     return true;
0332 }
0333 
0334 bool GlobalShortcutsRegistry::keyReleased(int keyQt)
0335 {
0336     Q_UNUSED(keyQt)
0337     if (m_lastShortcut) {
0338         m_lastShortcut->context()->component()->emitGlobalShortcutReleased(*m_lastShortcut);
0339         m_lastShortcut = nullptr;
0340     }
0341     return false;
0342 }
0343 
0344 Component *GlobalShortcutsRegistry::createComponent(const QString &uniqueName, const QString &friendlyName)
0345 {
0346     auto it = findByName(uniqueName);
0347     if (it != m_components.cend()) {
0348         Q_ASSERT_X(false, //
0349                    "GlobalShortcutsRegistry::createComponent",
0350                    QLatin1String("A Component with the name: %1, already exists").arg(uniqueName).toUtf8().constData());
0351         return (*it).get();
0352     }
0353 
0354     auto *c = registerComponent(ComponentPtr(new Component(uniqueName, friendlyName), &unregisterComponent));
0355     return c;
0356 }
0357 
0358 void GlobalShortcutsRegistry::unregisterComponent(Component *component)
0359 {
0360     QDBusConnection::sessionBus().unregisterObject(component->dbusPath().path());
0361     delete component;
0362 }
0363 
0364 KServiceActionComponent *GlobalShortcutsRegistry::createServiceActionComponent(const QString &uniqueName, const QString &friendlyName)
0365 {
0366     auto it = findByName(uniqueName);
0367     if (it != m_components.cend()) {
0368         Q_ASSERT_X(false, //
0369                    "GlobalShortcutsRegistry::createServiceActionComponent",
0370                    QLatin1String("A KServiceActionComponent with the name: %1, already exists").arg(uniqueName).toUtf8().constData());
0371         return static_cast<KServiceActionComponent *>((*it).get());
0372     }
0373 
0374     auto *c = registerComponent(ComponentPtr(new KServiceActionComponent(uniqueName, friendlyName), &unregisterComponent));
0375     return static_cast<KServiceActionComponent *>(c);
0376 }
0377 
0378 void GlobalShortcutsRegistry::loadSettings()
0379 {
0380     if (!m_components.empty()) {
0381         qCDebug(KGLOBALACCELD) << "Registry settings already loaded. Skipped loading again.";
0382         return;
0383     }
0384 
0385     const auto groupList = _config.groupList();
0386     for (const QString &groupName : groupList) {
0387         qCDebug(KGLOBALACCELD) << "Loading group " << groupName;
0388 
0389         Q_ASSERT(groupName.indexOf(QLatin1Char('\x1d')) == -1);
0390 
0391         // loadSettings isn't designed to be called in between. Only at the
0392         // beginning.
0393         Q_ASSERT(!getComponent(groupName));
0394 
0395         KConfigGroup configGroup(&_config, groupName);
0396 
0397         const QString friendlyName = configGroup.readEntry("_k_friendly_name");
0398 
0399         const bool isDesktop = groupName.endsWith(QLatin1String(".desktop"));
0400         // Create the component
0401         Component *component = isDesktop ? createServiceActionComponent(groupName, friendlyName) //
0402                                          : createComponent(groupName, friendlyName);
0403 
0404         // Now load the contexts
0405         const auto groupList = configGroup.groupList();
0406         for (const QString &context : groupList) {
0407             // Skip the friendly name group, this was previously used instead of _k_friendly_name
0408             if (context == QLatin1String("Friendly Name")) {
0409                 continue;
0410             }
0411 
0412             KConfigGroup contextGroup(&configGroup, context);
0413             QString contextFriendlyName = contextGroup.readEntry("_k_friendly_name");
0414             component->createGlobalShortcutContext(context, contextFriendlyName);
0415             component->activateGlobalShortcutContext(context);
0416             component->loadSettings(contextGroup);
0417         }
0418 
0419         // Load the default context
0420         component->activateGlobalShortcutContext(QStringLiteral("default"));
0421         component->loadSettings(configGroup);
0422     }
0423 
0424     // Load the configured KServiceActions
0425     const QStringList desktopPaths =
0426         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kglobalaccel"), QStandardPaths::LocateDirectory);
0427 
0428     const QStringList desktopFiles = KFileUtils::findAllUniqueFiles(desktopPaths, {QStringLiteral("*.desktop")});
0429 
0430     for (const QString &file : desktopFiles) {
0431         const QString fileName = QFileInfo(file).fileName();
0432         auto it = findByName(fileName);
0433         if (it != m_components.cend()) {
0434             continue;
0435         }
0436 
0437         KDesktopFile deskF(file);
0438         if (deskF.noDisplay()) {
0439             continue;
0440         }
0441 
0442         auto *actionComp = createServiceActionComponent(fileName, deskF.readName());
0443         actionComp->activateGlobalShortcutContext(QStringLiteral("default"));
0444         actionComp->loadFromService();
0445     }
0446 }
0447 
0448 void GlobalShortcutsRegistry::grabKeys()
0449 {
0450     activateShortcuts();
0451 }
0452 
0453 bool GlobalShortcutsRegistry::registerKey(const QKeySequence &key, GlobalShortcut *shortcut)
0454 {
0455     if (!_manager) {
0456         return false;
0457     }
0458     if (key.isEmpty()) {
0459         qCDebug(KGLOBALACCELD) << shortcut->uniqueName() << ": Key '" << QKeySequence(key).toString() << "' already taken by "
0460                                << _active_keys.value(key)->uniqueName() << ".";
0461         return false;
0462     } else if (_active_keys.value(key)) {
0463         qCDebug(KGLOBALACCELD) << shortcut->uniqueName() << ": Attempt to register key 0.";
0464         return false;
0465     }
0466 
0467     qCDebug(KGLOBALACCELD) << "Registering key" << QKeySequence(key).toString() << "for" << shortcut->context()->component()->uniqueName() << ":"
0468                            << shortcut->uniqueName();
0469 
0470     bool error = false;
0471     int i;
0472     for (i = 0; i < key.count(); i++) {
0473 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0474         const int combined = key[i].toCombined();
0475 #else
0476         const int combined(key[i]);
0477 #endif
0478         if (!_manager->grabKey(combined, true)) {
0479             error = true;
0480             break;
0481         }
0482         ++_keys_count[combined];
0483     }
0484 
0485     if (error) {
0486         // Last key was not registered, rewind index by 1
0487         for (--i; i >= 0; i--) {
0488 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0489             const int combined = key[i].toCombined();
0490 #else
0491             const int combined(key[i]);
0492 #endif
0493             auto it = _keys_count.find(combined);
0494             if (it == _keys_count.end()) {
0495                 continue;
0496             }
0497 
0498             if (it.value() == 1) {
0499                 _keys_count.erase(it);
0500                 _manager->grabKey(combined, false);
0501             } else {
0502                 --(it.value());
0503             }
0504         }
0505         return false;
0506     }
0507 
0508     _active_keys.insert(key, shortcut);
0509 
0510     return true;
0511 }
0512 
0513 void GlobalShortcutsRegistry::setAccelManager(KGlobalAccelInterface *manager)
0514 {
0515     _manager = manager;
0516 }
0517 
0518 void GlobalShortcutsRegistry::setDBusPath(const QDBusObjectPath &path)
0519 {
0520     _dbusPath = path;
0521 }
0522 
0523 void GlobalShortcutsRegistry::ungrabKeys()
0524 {
0525     deactivateShortcuts();
0526 }
0527 
0528 bool GlobalShortcutsRegistry::unregisterKey(const QKeySequence &key, GlobalShortcut *shortcut)
0529 {
0530     if (!_manager) {
0531         return false;
0532     }
0533     if (_active_keys.value(key) != shortcut) {
0534         // The shortcut doesn't own the key or the key isn't grabbed
0535         return false;
0536     }
0537 
0538     for (int i = 0; i < key.count(); i++) {
0539 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0540         auto iter = _keys_count.find(key[i].toCombined());
0541 
0542 #else
0543         auto iter = _keys_count.find(key[i]);
0544 #endif
0545         if ((iter == _keys_count.end()) || (iter.value() <= 0)) {
0546             continue;
0547         }
0548 
0549         // Unregister if there's only one ref to given key
0550         // We should fail earlier when key is not registered
0551         if (iter.value() == 1) {
0552             qCDebug(KGLOBALACCELD) << "Unregistering key" << QKeySequence(key[i]).toString() << "for" << shortcut->context()->component()->uniqueName() << ":"
0553                                    << shortcut->uniqueName();
0554 
0555 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0556             _manager->grabKey(key[i].toCombined(), false);
0557 
0558 #else
0559             _manager->grabKey(key[i], false);
0560 #endif
0561             _keys_count.erase(iter);
0562         } else {
0563             qCDebug(KGLOBALACCELD) << "Refused to unregister key" << QKeySequence(key[i]).toString() << ": used by another global shortcut";
0564             --(iter.value());
0565         }
0566     }
0567 
0568     if (shortcut && shortcut == m_lastShortcut) {
0569         m_lastShortcut->context()->component()->emitGlobalShortcutReleased(*m_lastShortcut);
0570         m_lastShortcut = nullptr;
0571     }
0572 
0573     _active_keys.remove(key);
0574     return true;
0575 }
0576 
0577 void GlobalShortcutsRegistry::writeSettings()
0578 {
0579     auto it = std::remove_if(m_components.begin(), m_components.end(), [this](const ComponentPtr &component) {
0580         KConfigGroup configGroup(&_config, component->uniqueName());
0581         if (component->allShortcuts().isEmpty()) {
0582             configGroup.deleteGroup();
0583             return true;
0584         } else {
0585             component->writeSettings(configGroup);
0586             return false;
0587         }
0588     });
0589 
0590     m_components.erase(it, m_components.end());
0591     _config.sync();
0592 }
0593 
0594 #include "moc_globalshortcutsregistry.cpp"