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