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 }