1 /*
2     SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>
3 
4     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6 
7 #include "kcm_keys.h"
8 
9 #include <QDBusMetaType>
10 #include <QFile>
11 #include <QQuickItem>
12 #include <QQuickRenderControl>
13 #include <QWindow>
14 
15 #include <KAboutData>
16 #include <KConfig>
17 #include <KConfigGroup>
18 #include <KGlobalShortcutInfo>
19 #include <KLocalizedString>
20 #include <KMessageBox>
21 #include <KOpenWithDialog>
22 #include <KPluginFactory>
23 #include <kglobalaccel_interface.h>
24 
25 #include "basemodel.h"
26 #include "filteredmodel.h"
27 #include "globalaccelmodel.h"
28 #include "kcmkeys_debug.h"
29 #include "keysdata.h"
30 #include "shortcutsmodel.h"
31 #include "standardshortcutsmodel.h"
32 
33 K_PLUGIN_FACTORY_WITH_JSON(KCMKeysFactory, "kcm_keys.json", registerPlugin<KCMKeys>(); registerPlugin<KeysData>();)
34 
KCMKeys(QObject * parent,const QVariantList & args)35 KCMKeys::KCMKeys(QObject *parent, const QVariantList &args)
36     : KQuickAddons::ConfigModule(parent, args)
37 {
38     constexpr char uri[] = "org.kde.private.kcms.keys";
39     qmlRegisterUncreatableType<BaseModel>(uri, 2, 0, "BaseModel", "Can't create BaseModel");
40     qmlRegisterAnonymousType<ShortcutsModel>(uri, 2);
41     qmlRegisterAnonymousType<FilteredShortcutsModel>(uri, 2);
42     qmlProtectModule(uri, 2);
43     qDBusRegisterMetaType<KGlobalShortcutInfo>();
44     qDBusRegisterMetaType<QList<KGlobalShortcutInfo>>();
45     qDBusRegisterMetaType<QList<QStringList>>();
46     KAboutData *about = new KAboutData(QStringLiteral("kcm_keys"), i18n("Shortcuts"), QStringLiteral("2.0"), QString(), KAboutLicense::GPL);
47     about->addAuthor(i18n("David Redondo"), QString(), QStringLiteral("kde@david-redondo.de"));
48     setAboutData(about);
49     m_globalAccelInterface = new KGlobalAccelInterface(QStringLiteral("org.kde.kglobalaccel"), //
50                                                        QStringLiteral("/kglobalaccel"),
51                                                        QDBusConnection::sessionBus(),
52                                                        this);
53     if (!m_globalAccelInterface->isValid()) {
54         setError(i18n("Failed to communicate with global shortcuts daemon"));
55         qCCritical(KCMKEYS) << "Interface is not valid";
56         if (m_globalAccelInterface->lastError().isValid()) {
57             qCCritical(KCMKEYS) << m_globalAccelInterface->lastError().name() << m_globalAccelInterface->lastError().message();
58         }
59     }
60     m_globalAccelModel = new GlobalAccelModel(m_globalAccelInterface, this);
61     m_standardShortcutsModel = new StandardShortcutsModel(this);
62     m_shortcutsModel = new ShortcutsModel(this);
63     m_shortcutsModel->addSourceModel(m_globalAccelModel);
64     m_shortcutsModel->addSourceModel(m_standardShortcutsModel);
65     m_filteredModel = new FilteredShortcutsModel(this);
66     m_filteredModel->setSourceModel(m_shortcutsModel);
67 
68     connect(m_shortcutsModel, &QAbstractItemModel::dataChanged, this, [this] {
69         setNeedsSave(m_globalAccelModel->needsSave() || m_standardShortcutsModel->needsSave());
70         setRepresentsDefaults(m_globalAccelModel->isDefault() && m_standardShortcutsModel->isDefault());
71     });
72     connect(m_shortcutsModel, &QAbstractItemModel::modelReset, this, [this] {
73         setNeedsSave(false);
74         setRepresentsDefaults(m_globalAccelModel->isDefault() && m_standardShortcutsModel->isDefault());
75     });
76 
77     connect(m_globalAccelModel, &GlobalAccelModel::errorOccured, this, &KCMKeys::setError);
78 }
79 
load()80 void KCMKeys::load()
81 {
82     m_globalAccelModel->load();
83     m_standardShortcutsModel->load();
84 }
85 
save()86 void KCMKeys::save()
87 {
88     m_globalAccelModel->save();
89     m_standardShortcutsModel->save();
90 }
91 
defaults()92 void KCMKeys::defaults()
93 {
94     m_globalAccelModel->defaults();
95     m_standardShortcutsModel->defaults();
96 }
97 
shortcutsModel() const98 ShortcutsModel *KCMKeys::shortcutsModel() const
99 {
100     return m_shortcutsModel;
101 }
102 
filteredModel() const103 FilteredShortcutsModel *KCMKeys::filteredModel() const
104 {
105     return m_filteredModel;
106 }
107 
setError(const QString & errorMessage)108 void KCMKeys::setError(const QString &errorMessage)
109 {
110     m_lastError = errorMessage;
111     Q_EMIT this->errorOccured();
112 }
113 
lastError() const114 QString KCMKeys::lastError() const
115 {
116     return m_lastError;
117 }
118 
writeScheme(const QUrl & url)119 void KCMKeys::writeScheme(const QUrl &url)
120 {
121     qCDebug(KCMKEYS) << "Exporting to " << url.toLocalFile();
122     KConfig file(url.toLocalFile(), KConfig::SimpleConfig);
123     m_globalAccelModel->exportToConfig(file);
124     m_standardShortcutsModel->exportToConfig(file);
125     file.sync();
126 }
127 
loadScheme(const QUrl & url)128 void KCMKeys::loadScheme(const QUrl &url)
129 {
130     qCDebug(KCMKEYS) << "Loading scheme" << url.toLocalFile();
131     KConfig file(url.toLocalFile(), KConfig::SimpleConfig);
132     m_globalAccelModel->importConfig(file);
133     m_standardShortcutsModel->importConfig(file);
134 }
135 
defaultSchemes() const136 QVariantList KCMKeys::defaultSchemes() const
137 {
138     QVariantList schemes;
139     const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("kcmkeys"), QStandardPaths::LocateDirectory);
140     for (const QString &dir : dirs) {
141         const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.kksrc"));
142         for (const QString &file : fileNames) {
143             const QString path = dir + QLatin1Char('/') + file;
144             KConfig scheme(path, KConfig::SimpleConfig);
145             const QString name = KConfigGroup(&scheme, "Settings").readEntry("Name", file);
146             schemes.append(QVariantMap({{"name", name}, {"url", QUrl::fromLocalFile(path)}}));
147         }
148     }
149     return schemes;
150 }
151 
addApplication(QQuickItem * ctx)152 void KCMKeys::addApplication(QQuickItem *ctx)
153 {
154     auto dialog = new KOpenWithDialog;
155     if (ctx && ctx->window()) {
156         dialog->winId(); // so it creates windowHandle
157         dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(ctx->window()));
158         dialog->setWindowModality(Qt::WindowModal);
159     }
160     dialog->hideRunInTerminal();
161     dialog->open();
162     connect(dialog, &KOpenWithDialog::finished, this, [this, dialog](int result) {
163         if (result == QDialog::Accepted && dialog->service()) {
164             const KService::Ptr service = dialog->service();
165             const QString desktopFileName = service->storageId();
166             if (m_globalAccelModel->match(m_shortcutsModel->index(0, 0), BaseModel::ComponentRole, desktopFileName, 1, Qt::MatchExactly).isEmpty()) {
167                 m_globalAccelModel->addApplication(desktopFileName, service->name());
168             } else {
169                 qCDebug(KCMKEYS) << "Already have component" << service->storageId();
170             }
171         }
172         dialog->deleteLater();
173     });
174 }
175 
keySequenceToString(const QKeySequence & keySequence) const176 QString KCMKeys::keySequenceToString(const QKeySequence &keySequence) const
177 {
178     return keySequence.toString(QKeySequence::NativeText);
179 }
180 
urlFilename(const QUrl & url)181 QString KCMKeys::urlFilename(const QUrl &url)
182 {
183     return url.fileName();
184 }
185 
conflictingIndex(const QKeySequence & keySequence)186 QModelIndex KCMKeys::conflictingIndex(const QKeySequence &keySequence)
187 {
188     for (int i = 0; i < m_shortcutsModel->rowCount(); ++i) {
189         const QModelIndex componentIndex = m_shortcutsModel->index(i, 0);
190         for (int j = 0; j < m_shortcutsModel->rowCount(componentIndex); ++j) {
191             const QModelIndex actionIndex = m_shortcutsModel->index(j, 0, componentIndex);
192             if (m_shortcutsModel->data(actionIndex, BaseModel::ActiveShortcutsRole).value<QSet<QKeySequence>>().contains(keySequence)) {
193                 return m_shortcutsModel->mapToSource(actionIndex);
194             }
195         }
196     }
197     return QModelIndex();
198 }
199 
requestKeySequence(QQuickItem * context,const QModelIndex & index,const QKeySequence & newSequence,const QKeySequence & oldSequence)200 void KCMKeys::requestKeySequence(QQuickItem *context, const QModelIndex &index, const QKeySequence &newSequence, const QKeySequence &oldSequence)
201 {
202     qCDebug(KCMKEYS) << index << "wants" << newSequence << "instead of" << oldSequence;
203     const QModelIndex conflict = conflictingIndex(newSequence);
204     if (!conflict.isValid()) {
205         auto model = const_cast<BaseModel *>(static_cast<const BaseModel *>(index.model()));
206         if (!oldSequence.isEmpty()) {
207             model->changeShortcut(index, oldSequence, newSequence);
208         } else {
209             model->addShortcut(index, newSequence);
210         }
211         return;
212     }
213 
214     qCDebug(KCMKEYS) << "Found conflict for" << newSequence << conflict;
215     const bool isStandardAction = conflict.parent().data(BaseModel::SectionRole).toString() == i18n("Common Actions");
216     const QString actionName = conflict.data().toString();
217     const QString componentName = conflict.parent().data().toString();
218     const QString keysString = newSequence.toString(QKeySequence::NativeText);
219     const QString message = isStandardAction
220         ? i18nc("%2 is the name of a category inside the 'Common Actions' section",
221                 "Shortcut %1 is already assigned to the common %2 action '%3'.\nDo you want to reassign it?",
222                 keysString,
223                 componentName,
224                 actionName)
225         : i18n("Shortcut %1 is already assigned to action '%2' of %3.\nDo you want to reassign it?", keysString, actionName, componentName);
226     const QString title = i18nc("@title:window", "Found conflict");
227     auto dialog = new QDialog;
228     dialog->setWindowTitle(title);
229     if (context && context->window()) {
230         dialog->winId(); // so it creates windowHandle
231         dialog->windowHandle()->setTransientParent(QQuickRenderControl::renderWindowFor(context->window()));
232     }
233     dialog->setWindowModality(Qt::WindowModal);
234     dialog->setAttribute(Qt::WA_DeleteOnClose);
235     KMessageBox::createKMessageBox(dialog,
236                                    new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::No, dialog),
237                                    QMessageBox::Question,
238                                    message,
239                                    {},
240                                    QString(),
241                                    nullptr,
242                                    KMessageBox::NoExec);
243     dialog->show();
244 
245     connect(dialog, &QDialog::finished, this, [index, conflict, newSequence, oldSequence](int result) {
246         auto model = const_cast<BaseModel *>(static_cast<const BaseModel *>(index.model()));
247         if (result != QDialogButtonBox::Yes) {
248             // Also Q_EMIT if we are not changing anything, to force the frontend to update and be consistent
249             // with the model. It is currently out of sync because it reflects the user input that
250             // was rejected now.
251             Q_EMIT model->dataChanged(index, index, {BaseModel::ActiveShortcutsRole, BaseModel::CustomShortcutsRole});
252             return;
253         }
254         const_cast<BaseModel *>(static_cast<const BaseModel *>(conflict.model()))->disableShortcut(conflict, newSequence);
255         if (!oldSequence.isEmpty()) {
256             model->changeShortcut(index, oldSequence, newSequence);
257         } else {
258             model->addShortcut(index, newSequence);
259         }
260     });
261 }
262 
263 #include "kcm_keys.moc"
264