File indexing completed on 2022-10-04 14:08:45

0001 /*
0002     SPDX-FileCopyrightText: 2008 Michael Jansen <kde@michael-jansen.biz>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "component.h"
0008 
0009 #include "globalshortcutcontext.h"
0010 #include "globalshortcutsregistry.h"
0011 #include "kglobalaccel_interface.h"
0012 #include "logging_p.h"
0013 #include <config-kglobalaccel.h>
0014 
0015 #include <QKeySequence>
0016 #include <QStringList>
0017 
0018 #if HAVE_X11
0019 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0020 #include <private/qtx11extras_p.h>
0021 #else
0022 #include <QX11Info>
0023 #endif
0024 #endif
0025 
0026 static QList<QKeySequence> keysFromString(const QString &str)
0027 {
0028     QList<QKeySequence> ret;
0029     if (str == QLatin1String("none")) {
0030         return ret;
0031     }
0032     const QStringList strList = str.split(QLatin1Char('\t'));
0033     for (const QString &s : strList) {
0034         QKeySequence key = QKeySequence::fromString(s, QKeySequence::PortableText);
0035         if (!key.isEmpty()) { // sanity check just in case
0036             ret.append(key);
0037         }
0038     }
0039     return ret;
0040 }
0041 
0042 static QString stringFromKeys(const QList<QKeySequence> &keys)
0043 {
0044     if (keys.isEmpty()) {
0045         return QStringLiteral("none");
0046     }
0047     QString ret;
0048     for (const QKeySequence &key : keys) {
0049         ret.append(key.toString(QKeySequence::PortableText));
0050         ret.append(QLatin1Char('\t'));
0051     }
0052     ret.chop(1);
0053     return ret;
0054 }
0055 
0056 Component::Component(const QString &uniqueName, const QString &friendlyName)
0057     : _uniqueName(uniqueName)
0058     , _friendlyName(friendlyName)
0059     , _registry(GlobalShortcutsRegistry::self())
0060 {
0061     // Make sure we do no get uniquenames still containing the context
0062     Q_ASSERT(uniqueName.indexOf(QLatin1Char('|')) == -1);
0063 
0064     const QString DEFAULT(QStringLiteral("default"));
0065     createGlobalShortcutContext(DEFAULT, QStringLiteral("Default Context"));
0066     _current = _contexts.value(DEFAULT);
0067 }
0068 
0069 Component::~Component()
0070 {
0071     // We delete all shortcuts from all contexts
0072     qDeleteAll(_contexts);
0073 }
0074 
0075 bool Component::activateGlobalShortcutContext(const QString &uniqueName)
0076 {
0077     if (!_contexts.value(uniqueName)) {
0078         createGlobalShortcutContext(uniqueName, QStringLiteral("TODO4"));
0079         return false;
0080     }
0081 
0082     // Deactivate the current contexts shortcuts
0083     deactivateShortcuts();
0084 
0085     // Switch the context
0086     _current = _contexts.value(uniqueName);
0087 
0088     return true;
0089 }
0090 
0091 void Component::activateShortcuts()
0092 {
0093     for (GlobalShortcut *shortcut : std::as_const(_current->_actionsMap)) {
0094         shortcut->setActive();
0095     }
0096 }
0097 
0098 QList<GlobalShortcut *> Component::allShortcuts(const QString &contextName) const
0099 {
0100     GlobalShortcutContext *context = _contexts.value(contextName);
0101     return context ? context->_actionsMap.values() : QList<GlobalShortcut *>{};
0102 }
0103 
0104 QList<KGlobalShortcutInfo> Component::allShortcutInfos(const QString &contextName) const
0105 {
0106     GlobalShortcutContext *context = _contexts.value(contextName);
0107     return context ? context->allShortcutInfos() : QList<KGlobalShortcutInfo>{};
0108 }
0109 
0110 bool Component::cleanUp()
0111 {
0112     bool changed = false;
0113 
0114     const auto actions = _current->_actionsMap;
0115     for (GlobalShortcut *shortcut : actions) {
0116         qCDebug(KGLOBALACCELD) << _current->_actionsMap.size();
0117         if (!shortcut->isPresent()) {
0118             changed = true;
0119             shortcut->unRegister();
0120         }
0121     }
0122 
0123     if (changed) {
0124         _registry->writeSettings();
0125         // We could be destroyed after this call!
0126     }
0127 
0128     return changed;
0129 }
0130 
0131 bool Component::createGlobalShortcutContext(const QString &uniqueName, const QString &friendlyName)
0132 {
0133     if (_contexts.value(uniqueName)) {
0134         qCDebug(KGLOBALACCELD) << "Shortcut Context " << uniqueName << "already exists for component " << _uniqueName;
0135         return false;
0136     }
0137     _contexts.insert(uniqueName, new GlobalShortcutContext(uniqueName, friendlyName, this));
0138     return true;
0139 }
0140 
0141 GlobalShortcutContext *Component::currentContext()
0142 {
0143     return _current;
0144 }
0145 
0146 QDBusObjectPath Component::dbusPath() const
0147 {
0148     auto isNonAscii = [](QChar ch) {
0149         const char c = ch.unicode();
0150         const bool isAscii = c == '_' //
0151             || (c >= 'A' && c <= 'Z') //
0152             || (c >= 'a' && c <= 'z') //
0153             || (c >= '0' && c <= '9');
0154         return !isAscii;
0155     };
0156 
0157     QString dbusPath = _uniqueName;
0158     // DBus path can only contain ASCII characters, any non-alphanumeric char should
0159     // be turned into '_'
0160     std::replace_if(dbusPath.begin(), dbusPath.end(), isNonAscii, QLatin1Char('_'));
0161 
0162     // QDBusObjectPath could be a little bit easier to handle :-)
0163     return QDBusObjectPath(_registry->dbusPath().path() + QLatin1String("component/") + dbusPath);
0164 }
0165 
0166 void Component::deactivateShortcuts(bool temporarily)
0167 {
0168     for (GlobalShortcut *shortcut : std::as_const(_current->_actionsMap)) {
0169         if (temporarily //
0170             && _uniqueName == QLatin1String("kwin") //
0171             && shortcut->uniqueName() == QLatin1String("Block Global Shortcuts")) {
0172             continue;
0173         }
0174         shortcut->setInactive();
0175     }
0176 }
0177 
0178 void Component::emitGlobalShortcutPressed(const GlobalShortcut &shortcut)
0179 {
0180 #if HAVE_X11
0181     // pass X11 timestamp
0182     const long timestamp = QX11Info::appTime();
0183     // Make sure kglobalacceld has ungrabbed the keyboard after receiving the
0184     // keypress, otherwise actions in application that try to grab the
0185     // keyboard (e.g. in kwin) may fail to do so. There is still a small race
0186     // condition with this being out-of-process.
0187     if (_registry->_manager) {
0188         _registry->_manager->syncWindowingSystem();
0189     }
0190 #else
0191     const long timestamp = 0;
0192 #endif
0193 
0194     if (shortcut.context()->component() != this) {
0195         return;
0196     }
0197 
0198     Q_EMIT globalShortcutPressed(shortcut.context()->component()->uniqueName(), shortcut.uniqueName(), timestamp);
0199 }
0200 
0201 void Component::emitGlobalShortcutReleased(const GlobalShortcut &shortcut)
0202 {
0203 #if HAVE_X11
0204     // pass X11 timestamp
0205     const long timestamp = QX11Info::appTime();
0206     // Make sure kglobalacceld has ungrabbed the keyboard after receiving the
0207     // keypress, otherwise actions in application that try to grab the
0208     // keyboard (e.g. in kwin) may fail to do so. There is still a small race
0209     // condition with this being out-of-process.
0210     if (_registry->_manager) {
0211         _registry->_manager->syncWindowingSystem();
0212     }
0213 #else
0214     const long timestamp = 0;
0215 #endif
0216 
0217     if (shortcut.context()->component() != this) {
0218         return;
0219     }
0220 
0221     Q_EMIT globalShortcutReleased(shortcut.context()->component()->uniqueName(), shortcut.uniqueName(), timestamp);
0222 }
0223 
0224 void Component::invokeShortcut(const QString &shortcutName, const QString &context)
0225 {
0226     GlobalShortcut *shortcut = getShortcutByName(shortcutName, context);
0227     if (shortcut) {
0228         emitGlobalShortcutPressed(*shortcut);
0229     }
0230 }
0231 
0232 QString Component::friendlyName() const
0233 {
0234     return !_friendlyName.isEmpty() ? _friendlyName : _uniqueName;
0235 }
0236 
0237 GlobalShortcut *Component::getShortcutByKey(const QKeySequence &key, KGlobalAccel::MatchType type) const
0238 {
0239     return _current->getShortcutByKey(key, type);
0240 }
0241 
0242 QList<GlobalShortcut *> Component::getShortcutsByKey(const QKeySequence &key, KGlobalAccel::MatchType type) const
0243 {
0244     QList<GlobalShortcut *> rc;
0245     for (GlobalShortcutContext *context : std::as_const(_contexts)) {
0246         GlobalShortcut *sc = context->getShortcutByKey(key, type);
0247         if (sc) {
0248             rc.append(sc);
0249         }
0250     }
0251     return rc;
0252 }
0253 
0254 GlobalShortcut *Component::getShortcutByName(const QString &uniqueName, const QString &context) const
0255 {
0256     const GlobalShortcutContext *shortcutContext = _contexts.value(context);
0257     return shortcutContext ? shortcutContext->_actionsMap.value(uniqueName) : nullptr;
0258 }
0259 
0260 QStringList Component::getShortcutContexts() const
0261 {
0262     return _contexts.keys();
0263 }
0264 
0265 bool Component::isActive() const
0266 {
0267     // The component is active if at least one of it's global shortcuts is
0268     // present.
0269     for (GlobalShortcut *shortcut : std::as_const(_current->_actionsMap)) {
0270         if (shortcut->isPresent()) {
0271             return true;
0272         }
0273     }
0274     return false;
0275 }
0276 
0277 bool Component::isShortcutAvailable(const QKeySequence &key, const QString &component, const QString &context) const
0278 {
0279     qCDebug(KGLOBALACCELD) << key.toString() << component;
0280 
0281     // if this component asks for the key. only check the keys in the same
0282     // context
0283     if (component == uniqueName()) {
0284         return shortcutContext(context)->isShortcutAvailable(key);
0285     } else {
0286         for (auto it = _contexts.cbegin(), endIt = _contexts.cend(); it != endIt; ++it) {
0287             const GlobalShortcutContext *ctx = it.value();
0288             if (!ctx->isShortcutAvailable(key)) {
0289                 return false;
0290             }
0291         }
0292     }
0293     return true;
0294 }
0295 
0296 GlobalShortcut *
0297 Component::registerShortcut(const QString &uniqueName, const QString &friendlyName, const QString &shortcutString, const QString &defaultShortcutString)
0298 {
0299     // The shortcut will register itself with us
0300     GlobalShortcut *shortcut = new GlobalShortcut(uniqueName, friendlyName, currentContext());
0301 
0302     const QList<QKeySequence> keys = keysFromString(shortcutString);
0303     shortcut->setDefaultKeys(keysFromString(defaultShortcutString));
0304     shortcut->setIsFresh(false);
0305     QList<QKeySequence> newKeys = keys;
0306     for (const QKeySequence &key : keys) {
0307         if (!key.isEmpty()) {
0308             if (_registry->getShortcutByKey(key)) {
0309                 // The shortcut is already used. The config file is
0310                 // broken. Ignore the request.
0311                 newKeys.removeAll(key);
0312                 qCWarning(KGLOBALACCELD) << "Shortcut found twice in kglobalshortcutsrc." << key;
0313             }
0314         }
0315     }
0316     shortcut->setKeys(keys);
0317     return shortcut;
0318 }
0319 
0320 void Component::loadSettings(KConfigGroup &configGroup)
0321 {
0322     // GlobalShortcutsRegistry::loadSettings handles contexts.
0323     const auto listKeys = configGroup.keyList();
0324     for (const QString &confKey : listKeys) {
0325         const QStringList entry = configGroup.readEntry(confKey, QStringList());
0326         if (entry.size() != 3) {
0327             continue;
0328         }
0329 
0330         GlobalShortcut *shortcut = registerShortcut(confKey, entry[2], entry[0], entry[1]);
0331         if (configGroup.name().endsWith(QLatin1String(".desktop"))) {
0332             shortcut->setIsPresent(true);
0333         }
0334     }
0335 }
0336 
0337 void Component::setFriendlyName(const QString &name)
0338 {
0339     _friendlyName = name;
0340 }
0341 
0342 GlobalShortcutContext *Component::shortcutContext(const QString &contextName)
0343 {
0344     return _contexts.value(contextName);
0345 }
0346 
0347 GlobalShortcutContext const *Component::shortcutContext(const QString &contextName) const
0348 {
0349     return _contexts.value(contextName);
0350 }
0351 
0352 QStringList Component::shortcutNames(const QString &contextName) const
0353 {
0354     const GlobalShortcutContext *context = _contexts.value(contextName);
0355     return context ? context->_actionsMap.keys() : QStringList{};
0356 }
0357 
0358 QString Component::uniqueName() const
0359 {
0360     return _uniqueName;
0361 }
0362 
0363 void Component::unregisterShortcut(const QString &uniqueName)
0364 {
0365     // Now wrote all contexts
0366     for (GlobalShortcutContext *context : std::as_const(_contexts)) {
0367         if (context->_actionsMap.value(uniqueName)) {
0368             delete context->takeShortcut(context->_actionsMap.value(uniqueName));
0369         }
0370     }
0371 }
0372 
0373 void Component::writeSettings(KConfigGroup &configGroup) const
0374 {
0375     // If we don't delete the current content global shortcut
0376     // registrations will never not deleted after forgetGlobalShortcut()
0377     configGroup.deleteGroup();
0378 
0379     // Now write all contexts
0380     for (GlobalShortcutContext *context : std::as_const(_contexts)) {
0381         KConfigGroup contextGroup;
0382 
0383         if (context->uniqueName() == QLatin1String("default")) {
0384             contextGroup = configGroup;
0385             // Write the friendly name
0386             contextGroup.writeEntry("_k_friendly_name", friendlyName());
0387         } else {
0388             contextGroup = KConfigGroup(&configGroup, context->uniqueName());
0389             // Write the friendly name
0390             contextGroup.writeEntry("_k_friendly_name", context->friendlyName());
0391         }
0392 
0393         // qCDebug(KGLOBALACCELD) << "writing group " << _uniqueName << ":" << context->uniqueName();
0394 
0395         for (const GlobalShortcut *shortcut : std::as_const(context->_actionsMap)) {
0396             // qCDebug(KGLOBALACCELD) << "writing" << shortcut->uniqueName();
0397 
0398             // We do not write fresh shortcuts.
0399             // We do not write session shortcuts
0400             if (shortcut->isFresh() || shortcut->isSessionShortcut()) {
0401                 continue;
0402             }
0403             // qCDebug(KGLOBALACCELD) << "really writing" << shortcut->uniqueName();
0404 
0405             QStringList entry(stringFromKeys(shortcut->keys()));
0406             entry.append(stringFromKeys(shortcut->defaultKeys()));
0407             entry.append(shortcut->friendlyName());
0408 
0409             contextGroup.writeEntry(shortcut->uniqueName(), entry);
0410         }
0411     }
0412 }