Commit c2d07beb authored by David Redondo's avatar David Redondo 🏎

Integrate standard shortcuts into global shortcuts kcm and rename it to just "Shortcuts"

The standard shortcuts kcm is dropped and instead a new category "Standard Shortcuts" shown in the kcm. The standard shortcuts themselves are divided into some categries like navigation and edit to make finding a particular shortcut easier. The model specific behavior and changing of shortcuts of the model is moved into a base class. Both the new standard shortcuts model and the kglobalaccel model (renamed from ShortcutsModel) inherit from the base and implement their own saving and loading methods. ShortcutsModel now combines both of these model. It is a KConcatenateRowsProxyModel extended to support the tree models that are two levels deep.
parent c73dfd7d
......@@ -27,7 +27,6 @@ add_subdirectory( launch )
add_subdirectory( colors )
add_subdirectory( krdb )
add_subdirectory( style )
add_subdirectory( standard_actions )
add_subdirectory( keys )
add_subdirectory( ksmserver )
add_subdirectory( lookandfeel )
......
......@@ -2,9 +2,12 @@
add_definitions(-DTRANSLATION_DOMAIN=\"kcm_keys\")
set(kcm_keys_SRCS
basemodel.cpp
kcm_keys.cpp
filteredmodel.cpp
globalaccelmodel.cpp
shortcutsmodel.cpp
standardshortcutsmodel.cpp
)
set(kglobalaccel_xml ${KGLOBALACCEL_DBUS_INTERFACES_DIR}/kf5_org.kde.KGlobalAccel.xml)
......@@ -42,6 +45,7 @@ add_library(kcm_keys MODULE ${kcm_keys_SRCS})
target_link_libraries(kcm_keys
Qt5::DBus
KF5::ConfigGui
KF5::GlobalAccel
KF5::I18n
KF5::KIOWidgets
......
/*
* Copyright 2020 David Redondo <kde@david-redondo.de>
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include "basemodel.h"
#include "kcmkeys_debug.h"
BaseModel::BaseModel(QObject *parent)
: QAbstractItemModel(parent)
{
}
void BaseModel::toggleDefaultShortcut(const QModelIndex &index, const QKeySequence &shortcut, bool enabled)
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid) || !index.parent().isValid()) {
return;
}
qCDebug(KCMKEYS) << "Default shortcut" << index << shortcut << enabled;
Action &a = m_components[index.parent().row()].actions[index.row()];
if (enabled) {
a.activeShortcuts.insert(shortcut);
} else {
a.activeShortcuts.remove(shortcut);
}
Q_EMIT dataChanged(index, index, {ActiveShortcutsRole, DefaultShortcutsRole});
}
void BaseModel::addShortcut(const QModelIndex &index, const QKeySequence &shortcut)
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid) || !index.parent().isValid()) {
return;
}
if (shortcut.isEmpty()) {
return;
}
qCDebug(KCMKEYS) << "Adding shortcut" << index << shortcut;
Action &a = m_components[index.parent().row()].actions[index.row()];
a.activeShortcuts.insert(shortcut);
Q_EMIT dataChanged(index, index, {ActiveShortcutsRole, CustomShortcutsRole});
}
void BaseModel::disableShortcut(const QModelIndex &index, const QKeySequence &shortcut)
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid) || !index.parent().isValid()) {
return;
}
qCDebug(KCMKEYS) << "Disabling shortcut" << index << shortcut;
Action &a = m_components[index.parent().row()].actions[index.row()];
a.activeShortcuts.remove(shortcut);
Q_EMIT dataChanged(index, index, {ActiveShortcutsRole, CustomShortcutsRole});
}
void BaseModel::changeShortcut(const QModelIndex &index, const QKeySequence &oldShortcut, const QKeySequence &newShortcut)
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid) || !index.parent().isValid()) {
return;
}
if (newShortcut.isEmpty()) {
return;
}
qCDebug(KCMKEYS) << "Changing Shortcut" << index << oldShortcut << " to " << newShortcut;
Action &a = m_components[index.parent().row()].actions[index.row()];
a.activeShortcuts.remove(oldShortcut);
a.activeShortcuts.insert(newShortcut);
Q_EMIT dataChanged(index, index, {ActiveShortcutsRole, CustomShortcutsRole});
}
void BaseModel::defaults()
{
for (int i = 0; i < m_components.size(); ++i) {
const auto componentIndex = index(i, 0);
for (auto action_it = m_components[i].actions.begin(); action_it != m_components[i].actions.end(); ++action_it) {
action_it->activeShortcuts = action_it->defaultShortcuts;
}
Q_EMIT dataChanged(index(0, 0, componentIndex), index(m_components[i].actions.size() - 1, 0, componentIndex),
{ActiveShortcutsRole, CustomShortcutsRole});
}
}
bool BaseModel::needsSave() const
{
for (const auto& component : qAsConst(m_components)) {
if (component.pendingDeletion) {
return true;
}
for (const auto& action : qAsConst(component.actions)) {
if (action.initialShortcuts != action.activeShortcuts) {
return true;
}
}
}
return false;
}
bool BaseModel::isDefault() const
{
for (const auto& component : qAsConst(m_components)) {
for (const auto& action : qAsConst(component.actions)) {
if (action.defaultShortcuts != action.activeShortcuts) {
return false;
}
}
}
return true;
}
QModelIndex BaseModel::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 BaseModel::parent(const QModelIndex &child) const
{
if (child.internalId()) {
return createIndex(child.internalId() - 1, 0, nullptr);
}
return QModelIndex();
}
int BaseModel::rowCount(const QModelIndex &parent) const
{
if (parent.isValid()) {
if (parent.parent().isValid()) {
return 0;
}
return m_components[parent.row()].actions.size();
}
return m_components.size();
}
int BaseModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
QVariant BaseModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, QAbstractItemModel::CheckIndexOption::IndexIsValid)) {
return QVariant();
}
if (index.parent().isValid()) {
const Action &action = m_components[index.parent().row()].actions[index.row()];
switch (role) {
case Qt::DisplayRole:
return action.displayName.isEmpty() ? action.id : action.displayName;
case ActionRole:
return action.id;
case ActiveShortcutsRole:
return QVariant::fromValue(action.activeShortcuts);
case DefaultShortcutsRole:
return QVariant::fromValue(action.defaultShortcuts);
case CustomShortcutsRole: {
auto shortcuts = action.activeShortcuts;
return QVariant::fromValue(shortcuts.subtract(action.defaultShortcuts));
}
}
return QVariant();
}
const Component &component = m_components[index.row()];
switch(role) {
case Qt::DisplayRole:
return component.displayName;
case Qt::DecorationRole:
return component.icon;
case SectionRole:
return component.type;
case ComponentRole:
return component.id;
case CheckedRole:
return component.checked;
case PendingDeletionRole:
return component.pendingDeletion;
}
return QVariant();
}
bool BaseModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!checkIndex(index, QAbstractListModel::CheckIndexOption::IndexIsValid | QAbstractListModel::CheckIndexOption::ParentIsInvalid)) {
return false;
}
const bool boolValue = value.toBool();
switch (role) {
case CheckedRole:
if (m_components[index.row()].checked != boolValue) {
m_components[index.row()].checked = boolValue;
Q_EMIT dataChanged(index, index, {CheckedRole});
return true;
}
break;
case PendingDeletionRole:
if (m_components[index.row()].pendingDeletion != boolValue) {
m_components[index.row()].pendingDeletion = boolValue;
Q_EMIT dataChanged(index, index, {PendingDeletionRole});
return true;
}
break;
}
return false;
}
QHash<int, QByteArray> BaseModel::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")}
};
}
/*
* Copyright 2020 David Redondo <kde@david-redondo.de>
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef BASEMODEL_H
#define BASEMODEL_H
#include <QAbstractItemModel>
#include <QKeySequence>
#include <QSet>
#include <QVector>
class KConfigBase;
struct Action {
QString id;
QString displayName;
QSet<QKeySequence> activeShortcuts;
QSet<QKeySequence> defaultShortcuts;
QSet<QKeySequence> initialShortcuts;
};
struct Component {
QString id;
QString displayName;
QString type;
QString icon;
QVector<Action> actions;
bool checked;
bool pendingDeletion;
};
class BaseModel : public QAbstractItemModel
{
Q_OBJECT
public:
enum Roles {
SectionRole = Qt::UserRole,
ComponentRole,
ActionRole,
ActiveShortcutsRole,
DefaultShortcutsRole,
CustomShortcutsRole,
CheckedRole,
PendingDeletionRole
};
Q_ENUM(Roles)
BaseModel(QObject *parent = nullptr);
Q_INVOKABLE void toggleDefaultShortcut(const QModelIndex &index, const QKeySequence &shortcut, bool enabled);
Q_INVOKABLE void addShortcut(const QModelIndex &index, const QKeySequence &shortcut);
Q_INVOKABLE void disableShortcut(const QModelIndex &index, const QKeySequence &shortcut);
Q_INVOKABLE void changeShortcut(const QModelIndex &index, const QKeySequence &oldShortcut, const QKeySequence &newShortcut);
virtual void exportToConfig(const KConfigBase &config) = 0;
virtual void importConfig(const KConfigBase &config) = 0;
virtual void load() = 0;
virtual void save() = 0;
void defaults();
bool needsSave() const;
bool isDefault() const;
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
QHash<int, QByteArray> roleNames() const override;
protected:
QVector<Component> m_components;
};
#endif
......@@ -22,7 +22,7 @@
#include <QKeySequence>
#include "shortcutsmodel.h"
#include "basemodel.h"
FilteredShortcutsModel::FilteredShortcutsModel(QObject *parent)
: QSortFilterProxyModel(parent)
......@@ -46,7 +46,7 @@ bool FilteredShortcutsModel::filterAcceptsRow(int source_row, const QModelIndex
return true;
}
const auto &defaultShortcuts = index.data(ShortcutsModel::DefaultShortcutsRole).value<QSet<QKeySequence>>();
const auto &defaultShortcuts = index.data(BaseModel::DefaultShortcutsRole).value<QSet<QKeySequence>>();
for (const auto& shortcut : defaultShortcuts) {
if (shortcut.toString(QKeySequence::NativeText).contains(m_filter, Qt::CaseInsensitive)
|| shortcut.toString(QKeySequence::PortableText).contains(m_filter, Qt::CaseInsensitive)) {
......@@ -54,7 +54,7 @@ bool FilteredShortcutsModel::filterAcceptsRow(int source_row, const QModelIndex
}
}
const auto &shortcuts = index.data(ShortcutsModel::CustomShortcutsRole).value<QSet<QKeySequence>>();
const auto &shortcuts = index.data(BaseModel::CustomShortcutsRole).value<QSet<QKeySequence>>();
for (const auto& shortcut : shortcuts) {
if (shortcut.toString(QKeySequence::NativeText).contains(m_filter, Qt::CaseInsensitive)
|| shortcut.toString(QKeySequence::PortableText).contains(m_filter, Qt::CaseInsensitive)) {
......
/*
* Copyright (C) 2020 David Redondo <david@david-redondo.de>
*
* 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 "globalaccelmodel.h"
#include <QIcon>
#include <QDBusPendingCallWatcher>
#include <KConfigGroup>
#include <KGlobalAccel>
#include <kglobalaccel_interface.h>
#include <kglobalaccel_component_interface.h>
#include <KGlobalShortcutInfo>
/*
* Copyright 2020 David Redondo <kde@david-redondo.de>
*
* 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) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include <KLocalizedString>
#include <KService>
#include <KServiceTypeTrader>
#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;
}
GlobalAccelModel::GlobalAccelModel(KGlobalAccelInterface *interface, QObject *parent)
: BaseModel(parent)
, m_globalAccelInterface{interface}
{
}
void GlobalAccelModel::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<QList<QDBusObjectPath>> componentsReply = *componentsWatcher;
componentsWatcher->deleteLater();
if (componentsReply.isError()) {
genericErrorOccured(QStringLiteral("Error while calling allComponents()"), componentsReply.error());
endResetModel();
return;
}
const QList<QDBusObjectPath> 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<QList<KGlobalShortcutInfo>> 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;
collator.setCaseSensitivity(Qt::CaseInsensitive);
collator.setNumericMode(true);
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.displayName, c2.displayName) < 0;
});
endResetModel();
delete pendingCalls;
}
});
}
});
}
Component GlobalAccelModel::loadComponent(const QList<KGlobalShortcutInfo> &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");
QString icon;
static const QHash<QString, QString> hardCodedIcons = {
{"ActivityManager", "preferences-desktop-activities"},
{"KDE Keyboard Layout Switcher", "input-keyboard"},
{"krunner.desktop", "krunner"},
{"org_kde_powerdevil", "preferences-system-power-management"}
};
if(service && !service->icon().isEmpty()) {
icon = service->icon();
} else if (hardCodedIcons.contains(componentUnique)) {
icon = hardCodedIcons[componentUnique];
} else {
icon = componentUnique;
}
Component c{componentUnique, componentFriendly, type, icon, {}, false, false};
for (const auto &actionInfo : info) {
const QString &actionUnique = actionInfo.uniqueName();
const QString &actionFriendly = actionInfo.friendlyName();
Action action;
action.id = actionUnique;
action.displayName = actionFriendly;
const QList<QKeySequence> defaultShortcuts = actionInfo.defaultKeys();
for (const auto &keySequence : defaultShortcuts) {
if (!keySequence.isEmpty()) {
action.defaultShortcuts.insert(keySequence);
}
}
const QList<QKeySequence> activeShortcuts = actionInfo.keys();
for (const QKeySequence &keySequence : activeShortcuts) {
if (!keySequence.isEmpty()) {
action.activeShortcuts.insert(keySequence);
}
}
action.initialShortcuts = action.activeShortcuts;
c.actions.push_back(action);
}
QCollator collator;
collator.setCaseSensitivity(Qt::CaseInsensitive);
collator.setNumericMode(true);
std::sort(c.actions.begin(), c.actions.end(), [&] (const Action &s1, const Action &s2) {
return collator.compare(s1.displayName, s2.displayName) < 0;
});
return c;
}
void GlobalAccelModel::save()
{
for (auto it = m_components.rbegin(); it != m_components.rend(); ++it) {
if (it->pendingDeletion) {
removeComponent(*it);
continue;
}
for (auto& action : it->actions) {
if (action.initialShortcuts != action.activeShortcuts) {
const QStringList actionId = buildActionId(it->id, it->displayName,
action.id, action.displayName);
//operator int of QKeySequence
QList<int> keys(action.activeShortcuts.cbegin(), action.activeShortcuts.cend());
qCDebug(KCMKEYS) << "Saving" << actionId << action.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 w