diff --git a/kcms/keys/shortcutsmodel.cpp b/kcms/keys/shortcutsmodel.cpp index c20038cb3..a45d00d4a 100644 --- a/kcms/keys/shortcutsmodel.cpp +++ b/kcms/keys/shortcutsmodel.cpp @@ -1,480 +1,481 @@ /* * Copyright (C) 2020 David Redondo * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "shortcutsmodel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kcmkeys_debug.h" static QStringList buildActionId(const QString &componentUnique, const QString &componentFriendly, const QString &actionUnique, const QString &actionFriendly) { QStringList actionId{"", "", "", ""}; actionId[KGlobalAccel::ComponentUnique] = componentUnique; actionId[KGlobalAccel::ComponentFriendly] = componentFriendly; actionId[KGlobalAccel::ActionUnique] = actionUnique; actionId[KGlobalAccel::ActionFriendly] = actionFriendly; return actionId; } ShortcutsModel::ShortcutsModel(KGlobalAccelInterface *interface, QObject *parent) : QAbstractItemModel(parent) , m_globalAccelInterface{interface} { } void ShortcutsModel::load() { if (!m_globalAccelInterface->isValid()) { return; } beginResetModel(); m_components.clear(); auto componentsWatcher = new QDBusPendingCallWatcher( m_globalAccelInterface->allComponents()); connect(componentsWatcher, &QDBusPendingCallWatcher::finished, this, [this] (QDBusPendingCallWatcher *componentsWatcher) { QDBusPendingReply> componentsReply = *componentsWatcher; componentsWatcher->deleteLater(); if (componentsReply.isError()) { genericErrorOccured(QStringLiteral("Error while calling allComponents()"), componentsReply.error()); endResetModel(); return; } const QList componentPaths = componentsReply.value(); int *pendingCalls = new int; *pendingCalls = componentPaths.size(); for (const auto &componentPath : componentPaths) { const QString path = componentPath.path(); KGlobalAccelComponentInterface component(m_globalAccelInterface->service(), path, m_globalAccelInterface->connection()); auto watcher = new QDBusPendingCallWatcher(component.allShortcutInfos()); connect(watcher, &QDBusPendingCallWatcher::finished, this, [path, pendingCalls, this] (QDBusPendingCallWatcher *watcher){ QDBusPendingReply> reply = *watcher; if (reply.isError()) { genericErrorOccured(QStringLiteral("Error while calling allShortCutInfos of") + path, reply.error()); } else { m_components.push_back(loadComponent(reply.value())); } watcher->deleteLater(); if (--*pendingCalls == 0) { QCollator collator; std::sort(m_components.begin(), m_components.end(), [&](const Component &c1, const Component &c2){ return c1.type != c2.type ? c1.type < c2.type : collator.compare(c1.friendlyName, c2.friendlyName) < 0; }); endResetModel(); delete pendingCalls; } }); } }); } Component ShortcutsModel::loadComponent(const QList &info) { const QString &componentUnique = info[0].componentUniqueName(); const QString &componentFriendly = info[0].componentFriendlyName(); KService::Ptr service = KService::serviceByStorageId(componentUnique); if (!service) { // Do we have an application with that name? const KService::List apps = KServiceTypeTrader::self()->query(QStringLiteral("Application"), QStringLiteral("Name == '%1' or Name == '%2'").arg(componentUnique, componentFriendly)); if(!apps.isEmpty()) { service = apps[0]; } } const QString type = service && service->isApplication() ? i18n("Applications") : i18n("System Services"); const QString icon = service && !service->icon().isEmpty() ? service->icon() : componentUnique; Component c{componentUnique, componentFriendly, type, icon, QVector(), false, false}; for (const auto &action : info) { const QString &actionUnique = action.uniqueName(); const QString &actionFriendly = action.friendlyName(); Shortcut shortcut; shortcut.uniqueName = actionUnique; shortcut.friendlyName = actionFriendly; const QList defaultShortcuts = action.defaultKeys(); for (const auto &keySequence : defaultShortcuts) { if (!keySequence.isEmpty()) { shortcut.defaultShortcuts.insert(keySequence); } } const QList activeShortcuts = action.keys(); for (const QKeySequence &keySequence : activeShortcuts) { if (!keySequence.isEmpty()) { shortcut.activeShortcuts.insert(keySequence); } } shortcut.initialShortcuts = shortcut.activeShortcuts; c.shortcuts.push_back(shortcut); } QCollator collator; std::sort(c.shortcuts.begin(), c.shortcuts.end(), [&] (const Shortcut &s1, const Shortcut &s2) { return collator.compare(s1.friendlyName, s2.friendlyName) < 0; }); return c; } void ShortcutsModel::save() { for (auto& component : m_components) { if (component.pendingDeletion) { removeComponent(component); } for (auto& shortcut : component.shortcuts) { if (shortcut.initialShortcuts != shortcut.activeShortcuts) { const QStringList actionId = buildActionId(component.uniqueName, component.friendlyName, shortcut.uniqueName, shortcut.friendlyName); //operator int of QKeySequence QList keys(shortcut.activeShortcuts.cbegin(), shortcut.activeShortcuts.cend()); qCDebug(KCMKEYS) << "Saving" << actionId << shortcut.activeShortcuts << keys; auto reply = m_globalAccelInterface->setForeignShortcut(actionId, keys); reply.waitForFinished(); if (!reply.isValid()) { qCCritical(KCMKEYS) << "Error while saving"; if (reply.error().isValid()) { qCCritical(KCMKEYS) << reply.error().name() << reply.error().message(); } emit errorOccured(i18nc("%1 is the name of the component, %2 is the action for which saving failed", "Error while saving shortcut %1: %2", component.friendlyName, shortcut.friendlyName)); } else { shortcut.initialShortcuts = shortcut.activeShortcuts; } } } } } void ShortcutsModel::defaults() { for (auto component = m_components.begin(); component != m_components.end(); ++component) { auto componentIndex = index(component - m_components.begin(), 0); for (auto shortcut = component->shortcuts.begin(); shortcut != component->shortcuts.end(); ++shortcut) { shortcut->activeShortcuts = shortcut->defaultShortcuts; } emit dataChanged(index(0, 0, componentIndex), index(component->shortcuts.size(), 0, componentIndex), {ActiveShortcutsRole, CustomShortcutsRole}); } } bool ShortcutsModel::needsSave() const { for (const auto& component : m_components) { if (component.pendingDeletion) { return true; } for (const auto& shortcut : component.shortcuts) { if (shortcut.initialShortcuts != shortcut.activeShortcuts) { return true; } } } return false; } bool ShortcutsModel::isDefault() const { for (const auto& component : m_components) { for (const auto& shortcut : component.shortcuts) { if (shortcut.defaultShortcuts != shortcut.activeShortcuts) { return false; } } } return true; } QModelIndex ShortcutsModel::index(int row, int column, const QModelIndex &parent) const { if (row < 0 || column != 0) { return QModelIndex(); } if (parent.isValid() && row < rowCount(parent) && column == 0) { return createIndex(row, column, parent.row() + 1); } else if (column == 0 && row < m_components.size()) { return createIndex(row, column, nullptr); } return QModelIndex(); } QModelIndex ShortcutsModel::parent(const QModelIndex &child) const { if (child.internalId()) { return createIndex(child.internalId() - 1, 0, nullptr); } return QModelIndex(); } int ShortcutsModel::rowCount(const QModelIndex &parent) const { if (parent.isValid()) { if (parent.parent().isValid()) { return 0; } return m_components[parent.row()].shortcuts.size(); } return m_components.size(); } int ShortcutsModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } QVariant ShortcutsModel::data(const QModelIndex &index, int role) const { if (!checkIndex(index)) { return QVariant(); } if (index.parent().isValid()) { const Shortcut &shortcut = m_components[index.parent().row()].shortcuts[index.row()]; switch (role) { case Qt::DisplayRole: return shortcut.friendlyName.isEmpty() ? shortcut.uniqueName : shortcut.friendlyName; case ActionRole: return shortcut.uniqueName; case ActiveShortcutsRole: return QVariant::fromValue(shortcut.activeShortcuts); case DefaultShortcutsRole: return QVariant::fromValue(shortcut.defaultShortcuts); case CustomShortcutsRole: { auto shortcuts = shortcut.activeShortcuts; return QVariant::fromValue(shortcuts.subtract(shortcut.defaultShortcuts)); } } return QVariant(); } const Component &component = m_components[index.row()]; switch(role) { case Qt::DisplayRole: return component.friendlyName; case Qt::DecorationRole: return component.icon; case SectionRole: return component.type; case ComponentRole: return component.uniqueName; case CheckedRole: return component.checked; case PendingDeletionRole: return component.pendingDeletion; } return QVariant(); } bool ShortcutsModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (!checkIndex(index) || index.parent().isValid()) { return false; } switch (role) { case CheckedRole: m_components[index.row()].checked = value.toBool(); emit dataChanged(index, index, {CheckedRole}); return true; case PendingDeletionRole: m_components[index.row()].pendingDeletion = value.toBool(); emit dataChanged(index, index, {PendingDeletionRole}); return true; } return false; } QHash ShortcutsModel::roleNames() const { return { {Qt::DisplayRole, QByteArrayLiteral("display")}, {Qt::DecorationRole, QByteArrayLiteral("decoration")}, {SectionRole, QByteArrayLiteral("section")}, {ComponentRole, QByteArrayLiteral("component")}, {ActiveShortcutsRole, QByteArrayLiteral("activeShortcuts")}, {DefaultShortcutsRole, QByteArrayLiteral("defaultShortcuts")}, {CustomShortcutsRole, QByteArrayLiteral("customShortcuts")}, {CheckedRole, QByteArrayLiteral("checked")}, {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")} }; } void ShortcutsModel::toggleDefaultShortcut(const QModelIndex &index, const QKeySequence &shortcut, bool enabled) { if (!checkIndex(index) || !index.parent().isValid()) { return; } qCDebug(KCMKEYS) << "Default shortcut" << index << shortcut << enabled; Shortcut &s = m_components[index.parent().row()].shortcuts[index.row()]; if (enabled) { s.activeShortcuts.insert(shortcut); } else { s.activeShortcuts.remove(shortcut); } emit dataChanged(index, index, {ActiveShortcutsRole, DefaultShortcutsRole}); } void ShortcutsModel::addShortcut(const QModelIndex &index, const QKeySequence &shortcut) { if (!checkIndex(index) || !index.parent().isValid()) { return; } if (shortcut.isEmpty()) { return; } qCDebug(KCMKEYS) << "Adding shortcut" << index << shortcut; Shortcut &s = m_components[index.parent().row()].shortcuts[index.row()]; s.activeShortcuts.insert(shortcut); emit dataChanged(index, index, {ActiveShortcutsRole, CustomShortcutsRole}); } void ShortcutsModel::disableShortcut(const QModelIndex &index, const QKeySequence &shortcut) { if (!checkIndex(index) || !index.parent().isValid()) { return; } qCDebug(KCMKEYS) << "Disabling shortcut" << index << shortcut; Shortcut &s = m_components[index.parent().row()].shortcuts[index.row()]; s.activeShortcuts.remove(shortcut); emit dataChanged(index, index, {ActiveShortcutsRole, CustomShortcutsRole}); } void ShortcutsModel::changeShortcut(const QModelIndex &index, const QKeySequence &oldShortcut, const QKeySequence &newShortcut) { if (!checkIndex(index) || !index.parent().isValid()) { return; } if (newShortcut.isEmpty()) { return; } qCDebug(KCMKEYS) << "Changing Shortcut" << index << oldShortcut << " to " << newShortcut; Shortcut &s = m_components[index.parent().row()].shortcuts[index.row()]; s.activeShortcuts.remove(oldShortcut); s.activeShortcuts.insert(newShortcut); emit dataChanged(index, index, {ActiveShortcutsRole, CustomShortcutsRole}); } void ShortcutsModel::setShortcuts(const KConfigBase &config) { for (const auto componentGroupName : config.groupList()) { auto component = std::find_if(m_components.begin(), m_components.end(), [&] (const Component &c) { return c.uniqueName == componentGroupName; }); if (component == m_components.end()) { qCWarning(KCMKEYS) << "Ignoring unknown component" << componentGroupName; continue; } KConfigGroup componentGroup(&config, componentGroupName); if (!componentGroup.hasGroup("Global Shortcuts")) { qCWarning(KCMKEYS) << "Group" << componentGroupName << "has no shortcuts group"; continue; } KConfigGroup shortcutsGroup(&componentGroup, "Global Shortcuts"); for (const auto& key : shortcutsGroup.keyList()) { auto shortcut = std::find_if(component->shortcuts.begin(), component->shortcuts.end(), [&] (const Shortcut &s) { return s.uniqueName == key; }); if (shortcut == component->shortcuts.end()) { qCWarning(KCMKEYS) << "Ignoring unknown action" << key; + continue; } const auto shortcuts = QKeySequence::listFromString(shortcutsGroup.readEntry(key)); shortcut->activeShortcuts = QSet(shortcuts.cbegin(), shortcuts.cend()); } } emit dataChanged(index(0, 0), index(0, rowCount()), {ActiveShortcutsRole, CustomShortcutsRole}); } void ShortcutsModel::addApplication(const QString &desktopFileName, const QString &displayName) { // Register a dummy action to trigger kglobalaccel to parse the desktop file QStringList actionId = buildActionId(desktopFileName, displayName, QString(), QString()); m_globalAccelInterface->doRegister(actionId); m_globalAccelInterface->unRegister(actionId); QCollator collator; auto pos = std::lower_bound(m_components.begin(), m_components.end(), displayName, [&] (const Component &c, const QString &name) { return c.type != i18n("System Services") && collator.compare(c.friendlyName, name) < 0; }); auto watcher = new QDBusPendingCallWatcher(m_globalAccelInterface->getComponent(desktopFileName)); connect(watcher, &QDBusPendingCallWatcher::finished, this, [=] { QDBusPendingReply reply = *watcher; watcher->deleteLater(); if (!reply.isValid()) { genericErrorOccured(QStringLiteral("Error while calling objectPath of added application") + desktopFileName, reply.error()); return; } KGlobalAccelComponentInterface component(m_globalAccelInterface->service(), reply.value().path(), m_globalAccelInterface->connection()); auto infoWatcher = new QDBusPendingCallWatcher(component.allShortcutInfos()); connect(infoWatcher, &QDBusPendingCallWatcher::finished, this, [=] { QDBusPendingReply> infoReply = *infoWatcher; infoWatcher->deleteLater(); if (!infoReply.isValid()) { genericErrorOccured(QStringLiteral("Error while calling allShortCutInfos on new component") + desktopFileName, infoReply.error()); return; } qCDebug(KCMKEYS) << "inserting at " << pos - m_components.begin(); emit beginInsertRows(QModelIndex(), pos - m_components.begin(), pos - m_components.begin()); Component c = loadComponent(infoReply.value()); m_components.insert(pos, c); emit endInsertRows(); }); }); } void ShortcutsModel::removeComponent(const Component &component) { const QString &uniqueName = component.uniqueName; auto componentReply = m_globalAccelInterface->getComponent(uniqueName); componentReply.waitForFinished(); if (!componentReply.isValid()) { genericErrorOccured(QStringLiteral("Error while calling objectPath of component") + uniqueName, componentReply.error()); return; } KGlobalAccelComponentInterface componentInterface(m_globalAccelInterface->service(), componentReply.value().path(), m_globalAccelInterface->connection()); qCDebug(KCMKEYS) << "Cleaning up component at" << componentReply.value(); auto cleanUpReply = componentInterface.cleanUp(); cleanUpReply.waitForFinished(); if (!cleanUpReply.isValid()) { genericErrorOccured(QStringLiteral("Error while calling cleanUp of component") + uniqueName, cleanUpReply.error()); return; } auto it = std::find_if(m_components.begin(), m_components.end(), [&](const Component &c) { return c.uniqueName == uniqueName; }); const int row = it - m_components.begin(); beginRemoveRows(QModelIndex(), row, row); m_components.remove(row); endRemoveRows(); } void ShortcutsModel::genericErrorOccured(const QString &description, const QDBusError &error) { qCCritical(KCMKEYS) << description; if (error.isValid()) { qCCritical(KCMKEYS) << error.name() << error.message(); } emit this->errorOccured(i18n("Error while communicating with the global shortcuts service")); }