File indexing completed on 2024-05-12 17:07:18

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