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 }