File indexing completed on 2024-05-12 05:35:45

0001 /*
0002     SPDX-FileCopyrightText: 2020 David Redondo <david@david-redondo.de>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "globalaccelmodel.h"
0008 
0009 #include <QDBusPendingCallWatcher>
0010 #include <QFile>
0011 #include <QIcon>
0012 
0013 #include <KApplicationTrader>
0014 #include <KConfigGroup>
0015 #include <KDesktopFile>
0016 #include <KGlobalAccel>
0017 #include <KGlobalShortcutInfo>
0018 #include <KLocalizedString>
0019 #include <KService>
0020 #include <kdesktopfile.h>
0021 #include <kglobalaccel_component_interface.h>
0022 #include <kglobalaccel_interface.h>
0023 
0024 #include "basemodel.h"
0025 #include "kcmkeys_debug.h"
0026 
0027 static QStringList buildActionId(const QString &componentUnique, const QString &componentFriendly, const QString &actionUnique, const QString &actionFriendly)
0028 {
0029     QStringList actionId{"", "", "", ""};
0030     actionId[KGlobalAccel::ComponentUnique] = componentUnique;
0031     actionId[KGlobalAccel::ComponentFriendly] = componentFriendly;
0032     actionId[KGlobalAccel::ActionUnique] = actionUnique;
0033     actionId[KGlobalAccel::ActionFriendly] = actionFriendly;
0034     return actionId;
0035 }
0036 
0037 GlobalAccelModel::GlobalAccelModel(KGlobalAccelInterface *interface, QObject *parent)
0038     : BaseModel(parent)
0039     , m_globalAccelInterface{interface}
0040 {
0041 }
0042 
0043 QVariant GlobalAccelModel::data(const QModelIndex &index, int role) const
0044 {
0045     if (role == SupportsMultipleKeysRole) {
0046         return false;
0047     }
0048     return BaseModel::data(index, role);
0049 }
0050 
0051 void GlobalAccelModel::load()
0052 {
0053     if (!m_globalAccelInterface->isValid()) {
0054         return;
0055     }
0056 
0057     auto componentsWatcher = new QDBusPendingCallWatcher(m_globalAccelInterface->allComponents());
0058     connect(componentsWatcher, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *componentsWatcher) {
0059         QDBusPendingReply<QList<QDBusObjectPath>> componentsReply = *componentsWatcher;
0060         componentsWatcher->deleteLater();
0061         if (componentsReply.isError()) {
0062             genericErrorOccured(QStringLiteral("Error while calling allComponents()"), componentsReply.error());
0063 
0064             beginResetModel();
0065             m_components.clear();
0066             m_pendingComponents.clear();
0067             endResetModel();
0068 
0069             return;
0070         }
0071         const QList<QDBusObjectPath> componentPaths = componentsReply.value();
0072         int *pendingCalls = new int;
0073         *pendingCalls = componentPaths.size();
0074         for (const auto &componentPath : componentPaths) {
0075             const QString path = componentPath.path();
0076             KGlobalAccelComponentInterface component(m_globalAccelInterface->service(), path, m_globalAccelInterface->connection());
0077             auto watcher = new QDBusPendingCallWatcher(component.allShortcutInfos());
0078             connect(watcher, &QDBusPendingCallWatcher::finished, this, [path, pendingCalls, this](QDBusPendingCallWatcher *watcher) {
0079                 QDBusPendingReply<QList<KGlobalShortcutInfo>> reply = *watcher;
0080                 if (reply.isError()) {
0081                     genericErrorOccured(QStringLiteral("Error while calling allShortCutInfos of") + path, reply.error());
0082                 } else if (!reply.value().isEmpty()) {
0083                     m_pendingComponents.push_back(loadComponent(reply.value()));
0084                 }
0085                 watcher->deleteLater();
0086                 if (--*pendingCalls == 0) {
0087                     beginResetModel();
0088                     QCollator collator;
0089                     collator.setCaseSensitivity(Qt::CaseInsensitive);
0090                     collator.setNumericMode(true);
0091                     m_components = m_pendingComponents;
0092                     m_pendingComponents.clear();
0093                     std::sort(m_components.begin(), m_components.end(), [&](const Component &c1, const Component &c2) {
0094                         return c1.type != c2.type ? c1.type < c2.type : collator.compare(c1.displayName, c2.displayName) < 0;
0095                     });
0096                     endResetModel();
0097                     delete pendingCalls;
0098                 }
0099             });
0100         }
0101     });
0102 }
0103 
0104 Component GlobalAccelModel::loadComponent(const QList<KGlobalShortcutInfo> &info)
0105 {
0106     const QString &componentUnique = info[0].componentUniqueName();
0107     const QString &componentFriendly = info[0].componentFriendlyName();
0108 
0109     KService::Ptr service = KService::serviceByStorageId(componentUnique);
0110     // Not a normal desktop file but maybe specific file in kglobalaccel dir
0111     if (!service && componentUnique.endsWith(QLatin1String(".desktop"))) {
0112         QString path = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, componentUnique);
0113 
0114         if (path.isEmpty()) {
0115             path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kglobalaccel/") + componentUnique);
0116         }
0117 
0118         if (!path.isEmpty()) {
0119             service = new KService(path);
0120         }
0121     }
0122 
0123     if (!service) {
0124         // Do we have a service with that name?
0125         auto filter = [componentUnique, componentFriendly](const KService::Ptr service) {
0126             return service->name() == componentUnique || service->name() == componentFriendly;
0127         };
0128 
0129         const KService::List services = KApplicationTrader::query(filter);
0130         service = services.value(0, KService::Ptr());
0131     }
0132 
0133     ComponentType type;
0134 
0135     if (service && service->isApplication()) {
0136         if (service->property<bool>(QStringLiteral("X-KDE-GlobalAccel-CommandShortcut"))) {
0137             type = ComponentType::Command;
0138         } else {
0139             if (service->noDisplay() || service->property<QString>(QStringLiteral("X-KDE-GlobalShortcutType")) == QLatin1String("Service")) {
0140                 // services with noDisplay are typically KCMs or implementation details
0141                 // don't show them as "Application"
0142                 type = ComponentType::SystemService;
0143             } else {
0144                 type = ComponentType::Application;
0145             }
0146         }
0147     } else {
0148         type = ComponentType::SystemService;
0149     }
0150 
0151     QString icon;
0152 
0153     static const QHash<QString, QString> hardCodedIcons = {
0154         {"ActivityManager", "preferences-desktop-activities"},
0155         {"KDE Keyboard Layout Switcher", "input-keyboard"},
0156         {"org_kde_powerdevil", "preferences-system-power-management"},
0157         {"wacomtablet", "preferences-desktop-tablet"},
0158     };
0159 
0160     if (service && !service->icon().isEmpty()) {
0161         icon = service->icon();
0162     } else if (hardCodedIcons.contains(componentUnique)) {
0163         icon = hardCodedIcons[componentUnique];
0164     } else if (type == ComponentType::Command) {
0165         icon = QStringLiteral("system-run");
0166     } else {
0167         icon = componentUnique;
0168     }
0169 
0170     Component c{componentUnique, componentFriendly, type, icon, {}, false, false};
0171     for (const auto &actionInfo : info) {
0172         const QString &actionUnique = actionInfo.uniqueName();
0173         const QString &actionFriendly = actionInfo.friendlyName();
0174         Action action;
0175         action.id = actionUnique;
0176         action.displayName = actionFriendly;
0177         const QList<QKeySequence> defaultShortcuts = actionInfo.defaultKeys();
0178         for (const auto &keySequence : defaultShortcuts) {
0179             if (!keySequence.isEmpty()) {
0180                 action.defaultShortcuts.insert(keySequence);
0181             }
0182         }
0183         const QList<QKeySequence> activeShortcuts = actionInfo.keys();
0184         for (const QKeySequence &keySequence : activeShortcuts) {
0185             if (!keySequence.isEmpty()) {
0186                 action.activeShortcuts.insert(keySequence);
0187             }
0188         }
0189         action.initialShortcuts = action.activeShortcuts;
0190         c.actions.push_back(action);
0191     }
0192     QCollator collator;
0193     collator.setCaseSensitivity(Qt::CaseInsensitive);
0194     collator.setNumericMode(true);
0195     std::sort(c.actions.begin(), c.actions.end(), [&](const Action &s1, const Action &s2) {
0196         return collator.compare(s1.displayName, s2.displayName) < 0;
0197     });
0198     return c;
0199 }
0200 
0201 void GlobalAccelModel::save()
0202 {
0203     QList<Component *> componentsToRemove;
0204 
0205     for (auto it = m_components.rbegin(); it != m_components.rend(); ++it) {
0206         if (it->pendingDeletion) {
0207             componentsToRemove.append(&(*it));
0208         }
0209     }
0210 
0211     for (Component *component : std::as_const(componentsToRemove)) {
0212         removeComponent(*component);
0213     }
0214 
0215     for (auto it = m_components.rbegin(); it != m_components.rend(); ++it) {
0216         for (auto &action : it->actions) {
0217             if (action.initialShortcuts != action.activeShortcuts) {
0218                 const QStringList actionId = buildActionId(it->id, it->displayName, action.id, action.displayName);
0219                 // TODO: pass action.activeShortcuts to m_globalAccelInterface->setForeignShortcut() as a QSet<QKeySequence>
0220                 // or QList<QKeySequence>?
0221                 QList<QKeySequence> keys;
0222                 keys.reserve(action.activeShortcuts.size());
0223                 for (const QKeySequence &key : std::as_const(action.activeShortcuts)) {
0224                     keys.append(key);
0225                 }
0226                 qCDebug(KCMKEYS) << "Saving" << actionId << action.activeShortcuts << keys;
0227                 auto reply = m_globalAccelInterface->setForeignShortcutKeys(actionId, keys);
0228                 reply.waitForFinished();
0229                 if (!reply.isValid()) {
0230                     qCCritical(KCMKEYS) << "Error while saving";
0231                     if (reply.error().isValid()) {
0232                         qCCritical(KCMKEYS) << reply.error().name() << reply.error().message();
0233                     }
0234                     Q_EMIT errorOccured(i18nc("%1 is the name of the component, %2 is the action for which saving failed",
0235                                               "Error while saving shortcut %1: %2",
0236                                               it->displayName,
0237                                               it->displayName));
0238                 } else {
0239                     action.initialShortcuts = action.activeShortcuts;
0240                 }
0241             }
0242         }
0243     }
0244 }
0245 
0246 void GlobalAccelModel::exportToConfig(const KConfigBase &config)
0247 {
0248     for (const auto &component : std::as_const(m_components)) {
0249         if (component.checked) {
0250             KConfigGroup mainGroup(&config, component.id);
0251             KConfigGroup group(&mainGroup, QStringLiteral("Global Shortcuts"));
0252             for (const auto &action : component.actions) {
0253                 const QList<QKeySequence> shortcutsList(action.activeShortcuts.cbegin(), action.activeShortcuts.cend());
0254                 group.writeEntry(action.id, QKeySequence::listToString(shortcutsList));
0255             }
0256         }
0257     }
0258 }
0259 
0260 void GlobalAccelModel::importConfig(const KConfigBase &config)
0261 {
0262     const auto groupList = config.groupList();
0263     for (const auto &componentGroupName : groupList) {
0264         auto component = std::find_if(m_components.begin(), m_components.end(), [&](const Component &c) {
0265             return c.id == componentGroupName;
0266         });
0267         if (component == m_components.end()) {
0268             qCWarning(KCMKEYS) << "Ignoring unknown component" << componentGroupName;
0269             continue;
0270         }
0271         KConfigGroup componentGroup(&config, componentGroupName);
0272         if (!componentGroup.hasGroup("Global Shortcuts")) {
0273             qCWarning(KCMKEYS) << "Group" << componentGroupName << "has no shortcuts group";
0274             continue;
0275         }
0276         KConfigGroup shortcutsGroup(&componentGroup, QStringLiteral("Global Shortcuts"));
0277         const QStringList keys = shortcutsGroup.keyList();
0278         for (const auto &key : keys) {
0279             auto action = std::find_if(component->actions.begin(), component->actions.end(), [&](const Action &a) {
0280                 return a.id == key;
0281             });
0282             if (action == component->actions.end()) {
0283                 qCWarning(KCMKEYS) << "Ignoring unknown action" << key;
0284                 continue;
0285             }
0286             const auto shortcuts = QKeySequence::listFromString(shortcutsGroup.readEntry(key));
0287             const QSet<QKeySequence> shortcutsSet(shortcuts.cbegin(), shortcuts.cend());
0288             if (shortcutsSet != action->activeShortcuts) {
0289                 action->activeShortcuts = shortcutsSet;
0290                 const QModelIndex i = index(action - component->actions.begin(), 0, index(component - m_components.begin(), 0));
0291                 Q_EMIT dataChanged(i, i, {CustomShortcutsRole, ActiveShortcutsRole});
0292             }
0293         }
0294     }
0295 }
0296 
0297 void GlobalAccelModel::addApplication(const QString &desktopFileName, const QString &displayName)
0298 {
0299     if (desktopFileName.isEmpty()) {
0300         qCWarning(KCMKEYS()) << "Tried to add empty application" << displayName;
0301         return;
0302     }
0303 
0304     // In certain cases, we can get an absolute file name as desktopFileName,
0305     // but the rest of the code assumes we're using a relative filename. So in
0306     // that case, strip the paths off and only use the file name.
0307     QFileInfo info(desktopFileName);
0308 
0309     QString desktopName = desktopFileName;
0310     if (info.isAbsolute()) {
0311         desktopName = info.fileName();
0312     }
0313 
0314     KDesktopFile desktopFile(desktopName);
0315     KConfigGroup cg = desktopFile.desktopGroup();
0316     ComponentType type = cg.readEntry<bool>(QStringLiteral("X-KDE-GlobalAccel-CommandShortcut"), false) ? ComponentType::Command : ComponentType::Application;
0317 
0318     // Register a dummy action to trigger kglobalaccel to parse the desktop file
0319     QStringList actionId = buildActionId(desktopName, displayName, QString(), QString());
0320     m_globalAccelInterface->doRegister(actionId);
0321     m_globalAccelInterface->unRegister(actionId);
0322     QCollator collator;
0323     collator.setCaseSensitivity(Qt::CaseInsensitive);
0324     collator.setNumericMode(true);
0325     auto pos = std::lower_bound(m_components.begin(), m_components.end(), displayName, [&](const Component &c, const QString &name) {
0326         return c.type != ComponentType::SystemService && (c.type != type ? c.type < type : collator.compare(c.displayName, name) < 0);
0327     });
0328     auto watcher = new QDBusPendingCallWatcher(m_globalAccelInterface->getComponent(desktopName));
0329     connect(watcher, &QDBusPendingCallWatcher::finished, this, [=, this] {
0330         QDBusPendingReply<QDBusObjectPath> reply = *watcher;
0331         watcher->deleteLater();
0332         if (!reply.isValid()) {
0333             genericErrorOccured(QStringLiteral("Error while calling objectPath of added application") + desktopName, reply.error());
0334             return;
0335         }
0336         KGlobalAccelComponentInterface component(m_globalAccelInterface->service(), reply.value().path(), m_globalAccelInterface->connection());
0337         auto infoWatcher = new QDBusPendingCallWatcher(component.allShortcutInfos());
0338         connect(infoWatcher, &QDBusPendingCallWatcher::finished, this, [=, this] {
0339             QDBusPendingReply<QList<KGlobalShortcutInfo>> infoReply = *infoWatcher;
0340             infoWatcher->deleteLater();
0341             if (!infoReply.isValid()) {
0342                 genericErrorOccured(QStringLiteral("Error while calling allShortCutInfos on new component") + desktopName, infoReply.error());
0343                 return;
0344             }
0345             if (infoReply.value().isEmpty()) {
0346                 qCWarning(KCMKEYS()) << "New component has no shortcuts:" << reply.value().path();
0347                 Q_EMIT errorOccured(i18nc("%1 is the name of an application", "Error while adding %1, it seems it has no actions."));
0348             }
0349             qCDebug(KCMKEYS) << "inserting at " << pos - m_components.begin();
0350             beginInsertRows(QModelIndex(), pos - m_components.begin(), pos - m_components.begin());
0351             auto c = loadComponent(infoReply.value());
0352             m_components.insert(pos, c);
0353             endInsertRows();
0354         });
0355     });
0356 }
0357 
0358 void GlobalAccelModel::removeComponent(const Component &component)
0359 {
0360     const QString &uniqueName = component.id;
0361     auto componentReply = m_globalAccelInterface->getComponent(uniqueName);
0362     componentReply.waitForFinished();
0363     if (!componentReply.isValid()) {
0364         genericErrorOccured(QStringLiteral("Error while calling objectPath of component") + uniqueName, componentReply.error());
0365         return;
0366     }
0367     if (component.type == ComponentType::Command) {
0368         KService::Ptr service = KService::serviceByStorageId(component.id);
0369         if (service) {
0370             qCDebug(KCMKEYS) << "Removing " << service->entryPath();
0371             QFile::remove(service->entryPath());
0372         }
0373     }
0374     KGlobalAccelComponentInterface componentInterface(m_globalAccelInterface->service(), componentReply.value().path(), m_globalAccelInterface->connection());
0375     qCDebug(KCMKEYS) << "Cleaning up component at" << componentReply.value().path();
0376     auto cleanUpReply = componentInterface.cleanUp();
0377     cleanUpReply.waitForFinished();
0378     if (!cleanUpReply.isValid()) {
0379         genericErrorOccured(QStringLiteral("Error while calling cleanUp of component") + uniqueName, cleanUpReply.error());
0380         return;
0381     }
0382     auto it = std::find_if(m_components.begin(), m_components.end(), [&](const Component &c) {
0383         return c.id == uniqueName;
0384     });
0385     const int row = it - m_components.begin();
0386     beginRemoveRows(QModelIndex(), row, row);
0387     m_components.remove(row);
0388     endRemoveRows();
0389 }
0390 
0391 void GlobalAccelModel::genericErrorOccured(const QString &description, const QDBusError &error)
0392 {
0393     qCCritical(KCMKEYS) << description;
0394     if (error.isValid()) {
0395         qCCritical(KCMKEYS) << error.name() << error.message();
0396     }
0397     Q_EMIT this->errorOccured(i18n("Error while communicating with the global shortcuts service"));
0398 }