KWin
Loading...
Searching...
No Matches
kcmrules.cpp
Go to the documentation of this file.
1/*
2 SPDX-FileCopyrightText: 2004 Lubos Lunak <l.lunak@kde.org>
3 SPDX-FileCopyrightText: 2020 Ismael Asensio <isma.af@gmail.com>
4
5 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
6*/
7
8#include "kcmrules.h"
9#include "rulesettings.h"
10
11#include <QDBusConnection>
12#include <QDBusMessage>
13#include <QDBusPendingCallWatcher>
14#include <QDBusPendingReply>
15
16#include <KConfig>
17#include <KLocalizedString>
18#include <KPluginFactory>
19
20namespace KWin
21{
22
23KCMKWinRules::KCMKWinRules(QObject *parent, const KPluginMetaData &metaData, const QVariantList &arguments)
24 : KQuickConfigModule(parent, metaData)
25 , m_ruleBookModel(new RuleBookModel(this))
26 , m_rulesModel(new RulesModel(this))
27{
28 QStringList argList;
29 for (const QVariant &arg : arguments) {
30 argList << arg.toString();
31 }
32 parseArguments(argList);
33
34 connect(m_rulesModel, &RulesModel::descriptionChanged, this, [this] {
35 if (m_editIndex.isValid()) {
36 m_ruleBookModel->setDescriptionAt(m_editIndex.row(), m_rulesModel->description());
37 }
38 });
39 connect(m_rulesModel, &RulesModel::dataChanged, this, [this] {
40 Q_EMIT m_ruleBookModel->dataChanged(m_editIndex, m_editIndex, {});
41 });
42 connect(m_ruleBookModel, &RuleBookModel::dataChanged, this, &KCMKWinRules::updateNeedsSave);
43}
44
45void KCMKWinRules::parseArguments(const QStringList &args)
46{
47 // When called from window menu, "uuid" and "whole-app" are set in arguments list
48 bool nextArgIsUuid = false;
49 QUuid uuid = QUuid();
50
51 // TODO: Use a better argument parser
52 for (const QString &arg : args) {
53 if (arg == QLatin1String("uuid")) {
54 nextArgIsUuid = true;
55 } else if (nextArgIsUuid) {
56 uuid = QUuid(arg);
57 nextArgIsUuid = false;
58 } else if (arg.startsWith("uuid=")) {
59 uuid = QUuid(arg.mid(strlen("uuid=")));
60 } else if (arg == QLatin1String("whole-app")) {
61 m_wholeApp = true;
62 }
63 }
64
65 if (uuid.isNull()) {
66 qDebug() << "Invalid window uuid.";
67 return;
68 }
69
70 // Get the Window properties
71 QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
72 QStringLiteral("/KWin"),
73 QStringLiteral("org.kde.KWin"),
74 QStringLiteral("getWindowInfo"));
75 message.setArguments({uuid.toString()});
76 QDBusPendingReply<QVariantMap> async = QDBusConnection::sessionBus().asyncCall(message);
77
78 QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
79 connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, uuid](QDBusPendingCallWatcher *self) {
80 QDBusPendingReply<QVariantMap> reply = *self;
81 self->deleteLater();
82 if (!reply.isValid() || reply.value().isEmpty()) {
83 qDebug() << "Error retrieving properties for window" << uuid;
84 return;
85 }
86 qDebug() << "Retrieved properties for window" << uuid;
87 m_winProperties = reply.value();
88
89 if (m_alreadyLoaded) {
90 createRuleFromProperties();
91 }
92 });
93}
94
96{
97 m_ruleBookModel->load();
98
99 if (!m_winProperties.isEmpty() && !m_alreadyLoaded) {
100 createRuleFromProperties();
101 } else {
102 m_editIndex = QModelIndex();
103 Q_EMIT editIndexChanged();
104 }
105
106 m_alreadyLoaded = true;
107
108 updateNeedsSave();
109}
110
112{
113 m_ruleBookModel->save();
114
115 // Notify kwin to reload configuration
116 QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig");
117 QDBusConnection::sessionBus().send(message);
118}
119
120void KCMKWinRules::updateNeedsSave()
121{
122 setNeedsSave(m_ruleBookModel->isSaveNeeded());
123 Q_EMIT needsSaveChanged();
124}
125
126void KCMKWinRules::createRuleFromProperties()
127{
128 if (m_winProperties.isEmpty()) {
129 return;
130 }
131
132 QModelIndex matchedIndex = findRuleWithProperties(m_winProperties, m_wholeApp);
133 if (!matchedIndex.isValid()) {
134 m_ruleBookModel->insertRow(0);
135 fillSettingsFromProperties(m_ruleBookModel->ruleSettingsAt(0), m_winProperties, m_wholeApp);
136 matchedIndex = m_ruleBookModel->index(0);
137 updateNeedsSave();
138 }
139
140 editRule(matchedIndex.row());
141 m_rulesModel->setSuggestedProperties(m_winProperties);
142
143 m_winProperties.clear();
144}
145
146int KCMKWinRules::editIndex() const
147{
148 if (!m_editIndex.isValid()) {
149 return -1;
150 }
151 return m_editIndex.row();
152}
153
154void KCMKWinRules::setRuleDescription(int index, const QString &description)
155{
156 if (index < 0 || index >= m_ruleBookModel->rowCount()) {
157 return;
158 }
159
160 if (m_editIndex.row() == index) {
161 m_rulesModel->setDescription(description);
162 return;
163 }
164 m_ruleBookModel->setDescriptionAt(index, description);
165
166 updateNeedsSave();
167}
168
170{
171 if (index < 0 || index >= m_ruleBookModel->rowCount()) {
172 return;
173 }
174
175 m_editIndex = m_ruleBookModel->index(index);
176 Q_EMIT editIndexChanged();
177
178 m_rulesModel->setSettings(m_ruleBookModel->ruleSettingsAt(m_editIndex.row()));
179
180 // Set the active page to rules editor (0:RulesList, 1:RulesEditor)
181 setCurrentIndex(1);
182}
183
185{
186 const int newIndex = m_ruleBookModel->rowCount();
187 m_ruleBookModel->insertRow(newIndex);
188
189 updateNeedsSave();
190
191 editRule(newIndex);
192}
193
195{
196 if (index < 0 || index >= m_ruleBookModel->rowCount()) {
197 return;
198 }
199
200 m_ruleBookModel->removeRow(index);
201
202 Q_EMIT editIndexChanged();
203 updateNeedsSave();
204}
205
206void KCMKWinRules::moveRule(int sourceIndex, int destIndex)
207{
208 const int lastIndex = m_ruleBookModel->rowCount() - 1;
209 if (sourceIndex == destIndex
210 || (sourceIndex < 0 || sourceIndex > lastIndex)
211 || (destIndex < 0 || destIndex > lastIndex)) {
212 return;
213 }
214
215 m_ruleBookModel->moveRow(QModelIndex(), sourceIndex, QModelIndex(), destIndex);
216
217 Q_EMIT editIndexChanged();
218 updateNeedsSave();
219}
220
222{
223 if (index < 0 || index >= m_ruleBookModel->rowCount()) {
224 return;
225 }
226
227 const int newIndex = index + 1;
228 const QString newDescription = i18n("Copy of %1", m_ruleBookModel->descriptionAt(index));
229
230 m_ruleBookModel->insertRow(newIndex);
231 m_ruleBookModel->setRuleSettingsAt(newIndex, *(m_ruleBookModel->ruleSettingsAt(index)));
232 m_ruleBookModel->setDescriptionAt(newIndex, newDescription);
233
234 updateNeedsSave();
235}
236
237void KCMKWinRules::exportToFile(const QUrl &path, const QList<int> &indexes)
238{
239 if (indexes.isEmpty()) {
240 return;
241 }
242
243 const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig);
244
245 const QStringList groups = config->groupList();
246 for (const QString &groupName : groups) {
247 config->deleteGroup(groupName);
248 }
249
250 for (int index : indexes) {
251 if (index < 0 || index > m_ruleBookModel->rowCount()) {
252 continue;
253 }
254 const RuleSettings *origin = m_ruleBookModel->ruleSettingsAt(index);
255 RuleSettings exported(config, origin->description());
256
257 RuleBookModel::copySettingsTo(&exported, *origin);
258 exported.save();
259 }
260}
261
262void KCMKWinRules::importFromFile(const QUrl &path)
263{
264 const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig);
265 const QStringList groups = config->groupList();
266 if (groups.isEmpty()) {
267 return;
268 }
269
270 for (const QString &groupName : groups) {
271 RuleSettings settings(config, groupName);
272
273 const bool remove = settings.deleteRule();
274 const QString importDescription = settings.description();
275 if (importDescription.isEmpty()) {
276 continue;
277 }
278
279 // Try to find a rule with the same description to replace
280 int newIndex = -2;
281 for (int index = 0; index < m_ruleBookModel->rowCount(); index++) {
282 if (m_ruleBookModel->descriptionAt(index) == importDescription) {
283 newIndex = index;
284 break;
285 }
286 }
287
288 if (remove) {
289 m_ruleBookModel->removeRow(newIndex);
290 continue;
291 }
292
293 if (newIndex < 0) {
294 newIndex = m_ruleBookModel->rowCount();
295 m_ruleBookModel->insertRow(newIndex);
296 }
297
298 m_ruleBookModel->setRuleSettingsAt(newIndex, settings);
299
300 // Reset rule editor if the current rule changed when importing
301 if (m_editIndex.row() == newIndex) {
302 m_rulesModel->setSettings(m_ruleBookModel->ruleSettingsAt(newIndex));
303 }
304 }
305
306 updateNeedsSave();
307}
308
309// Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp`
310QModelIndex KCMKWinRules::findRuleWithProperties(const QVariantMap &info, bool wholeApp) const
311{
312 const QString wmclass_class = info.value("resourceClass").toString();
313 const QString wmclass_name = info.value("resourceName").toString();
314 const QString role = info.value("role").toString();
315 const NET::WindowType type = static_cast<NET::WindowType>(info.value("type").toInt());
316 const QString title = info.value("caption").toString();
317 const QString machine = info.value("clientMachine").toString();
318 const bool isLocalHost = info.value("localhost").toBool();
319
320 int bestMatchRow = -1;
321 int bestMatchScore = 0;
322
323 for (int row = 0; row < m_ruleBookModel->rowCount(); row++) {
324 const RuleSettings *settings = m_ruleBookModel->ruleSettingsAt(row);
325
326 // If the rule doesn't match try the next one
327 const Rules rule = Rules(settings);
328 /* clang-format off */
329 if (!rule.matchWMClass(wmclass_class, wmclass_name)
330 || !rule.matchType(type)
331 || !rule.matchRole(role)
332 || !rule.matchTitle(title)
333 || !rule.matchClientMachine(machine, isLocalHost)) {
334 continue;
335 }
336 /* clang-format on */
337
338 if (settings->wmclassmatch() != Rules::ExactMatch) {
339 continue; // too generic
340 }
341
342 // Now that the rule matches the window, check the quality of the match
343 // It stablishes a quality depending on the match policy of the rule
344 int score = 0;
345 bool generic = true;
346
347 // from now on, it matches the app - now try to match for a specific window
348 if (settings->wmclasscomplete()) {
349 score += 1;
350 generic = false; // this can be considered specific enough (old X apps)
351 }
352 if (!wholeApp) {
353 if (settings->windowrolematch() != Rules::UnimportantMatch) {
354 score += settings->windowrolematch() == Rules::ExactMatch ? 5 : 1;
355 generic = false;
356 }
357 if (settings->titlematch() != Rules::UnimportantMatch) {
358 score += settings->titlematch() == Rules::ExactMatch ? 3 : 1;
359 generic = false;
360 }
361 if (settings->types() != NET::AllTypesMask) {
362 // Checks that type fits the mask, and only one of the types
363 int bits = 0;
364 for (unsigned int bit = 1; bit < 1U << 31; bit <<= 1) {
365 if (settings->types() & bit) {
366 ++bits;
367 }
368 }
369 if (bits == 1) {
370 score += 2;
371 }
372 }
373 if (generic) { // ignore generic rules, use only the ones that are for this window
374 continue;
375 }
376 } else {
377 if (settings->types() == NET::AllTypesMask) {
378 score += 2;
379 }
380 }
381
382 if (score > bestMatchScore) {
383 bestMatchRow = row;
384 bestMatchScore = score;
385 }
386 }
387
388 if (bestMatchRow < 0) {
389 return QModelIndex();
390 }
391 return m_ruleBookModel->index(bestMatchRow);
392}
393
394// Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp`
395void KCMKWinRules::fillSettingsFromProperties(RuleSettings *settings, const QVariantMap &info, bool wholeApp) const
396{
397 const QString wmclass_class = info.value("resourceClass").toString();
398 const QString wmclass_name = info.value("resourceName").toString();
399 const QString role = info.value("role").toString();
400 const NET::WindowType type = static_cast<NET::WindowType>(info.value("type").toInt());
401 const QString title = info.value("caption").toString();
402 const QString machine = info.value("clientMachine").toString();
403
404 settings->setDefaults();
405
406 if (wholeApp) {
407 if (!wmclass_class.isEmpty()) {
408 settings->setDescription(i18n("Application settings for %1", wmclass_class));
409 }
410 // TODO maybe exclude some types? If yes, then also exclude them when searching.
411 settings->setTypes(NET::AllTypesMask);
412 settings->setTitlematch(Rules::UnimportantMatch);
413 settings->setClientmachine(machine); // set, but make unimportant
414 settings->setClientmachinematch(Rules::UnimportantMatch);
415 settings->setWindowrolematch(Rules::UnimportantMatch);
416 if (wmclass_name == wmclass_class) {
417 settings->setWmclasscomplete(false);
418 settings->setWmclass(wmclass_class);
419 settings->setWmclassmatch(Rules::ExactMatch);
420 } else {
421 // WM_CLASS components differ - perhaps the app got -name argument
422 settings->setWmclasscomplete(true);
423 settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class));
424 settings->setWmclassmatch(Rules::ExactMatch);
425 }
426 return;
427 }
428
429 if (!wmclass_class.isEmpty()) {
430 settings->setDescription(i18n("Window settings for %1", wmclass_class));
431 }
432 if (type == NET::Unknown) {
433 settings->setTypes(NET::NormalMask);
434 } else {
435 settings->setTypes(NET::WindowTypeMask(1 << type)); // convert type to its mask
436 }
437 settings->setTitle(title); // set, but make unimportant
438 settings->setTitlematch(Rules::UnimportantMatch);
439 settings->setClientmachine(machine); // set, but make unimportant
440 settings->setClientmachinematch(Rules::UnimportantMatch);
441 if (!role.isEmpty() && role != "unknown" && role != "unnamed") { // Qt sets this if not specified
442 settings->setWindowrole(role);
443 settings->setWindowrolematch(Rules::ExactMatch);
444 if (wmclass_name == wmclass_class) {
445 settings->setWmclasscomplete(false);
446 settings->setWmclass(wmclass_class);
447 settings->setWmclassmatch(Rules::ExactMatch);
448 } else {
449 // WM_CLASS components differ - perhaps the app got -name argument
450 settings->setWmclasscomplete(true);
451 settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class));
452 settings->setWmclassmatch(Rules::ExactMatch);
453 }
454 } else { // no role set
455 if (wmclass_name != wmclass_class) {
456 // WM_CLASS components differ - perhaps the app got -name argument
457 settings->setWmclasscomplete(true);
458 settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class));
459 settings->setWmclassmatch(Rules::ExactMatch);
460 } else {
461 // This is a window that has no role set, and both components of WM_CLASS
462 // match (possibly only differing in case), which most likely means either
463 // the application doesn't give a damn about distinguishing its various
464 // windows, or it's an app that uses role for that, but this window
465 // lacks it for some reason. Use non-complete WM_CLASS matching, also
466 // include window title in the matching, and pray it causes many more positive
467 // matches than negative matches.
468 // WM_CLASS components differ - perhaps the app got -name argument
469 settings->setTitlematch(Rules::ExactMatch);
470 settings->setWmclasscomplete(false);
471 settings->setWmclass(wmclass_class);
472 settings->setWmclassmatch(Rules::ExactMatch);
473 }
474 }
475}
476
478
479} // namespace
480
481#include "kcmrules.moc"
482
483#include "moc_kcmrules.cpp"
Q_INVOKABLE void setRuleDescription(int index, const QString &description)
Definition kcmrules.cpp:154
void save() override
Definition kcmrules.cpp:111
Q_INVOKABLE void moveRule(int sourceIndex, int destIndex)
Definition kcmrules.cpp:206
void load() override
Definition kcmrules.cpp:95
Q_INVOKABLE void importFromFile(const QUrl &path)
Definition kcmrules.cpp:262
Q_INVOKABLE void createRule()
Definition kcmrules.cpp:184
Q_INVOKABLE void removeRule(int index)
Definition kcmrules.cpp:194
Q_INVOKABLE void exportToFile(const QUrl &path, const QList< int > &indexes)
Definition kcmrules.cpp:237
Q_INVOKABLE void duplicateRule(int index)
Definition kcmrules.cpp:221
KCMKWinRules(QObject *parent, const KPluginMetaData &metaData, const QVariantList &arguments)
Definition kcmrules.cpp:23
Q_INVOKABLE void editRule(int index)
Definition kcmrules.cpp:169
RuleSettings * ruleSettingsAt(int row) const
void setDescriptionAt(int row, const QString &description)
static void copySettingsTo(RuleSettings *dest, const RuleSettings &source)
QString descriptionAt(int row) const
int rowCount(const QModelIndex &parent=QModelIndex()) const override
void setRuleSettingsAt(int row, const RuleSettings &settings)
@ UnimportantMatch
Definition rules.h:131
@ ExactMatch
Definition rules.h:132
void setDescription(const QString &description)
void descriptionChanged()
void setSuggestedProperties(const QVariantMap &info)
void setSettings(RuleSettings *settings)
Session::Type type
Definition session.cpp:17
K_PLUGIN_CLASS_WITH_JSON(KCMKWinRules, "kcm_kwinrules.json")