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 "rulesmodel.h"
9 
10 #ifdef KWIN_BUILD_ACTIVITIES
11 #include "activities.h"
12 #endif
13 
14 #include <QIcon>
15 #include <QQmlEngine>
16 #include <QtDBus>
17 #include <QX11Info>
18 
19 #include <KColorSchemeManager>
20 #include <KConfig>
21 #include <KLocalizedString>
22 #include <KWindowSystem>
23 
24 
25 namespace KWin
26 {
27 
RulesModel(QObject * parent)28 RulesModel::RulesModel(QObject *parent)
29     : QAbstractListModel(parent)
30 {
31     qmlRegisterUncreatableType<RuleItem>("org.kde.kcms.kwinrules", 1, 0, "RuleItem",
32                                          QStringLiteral("Do not create objects of type RuleItem"));
33     qmlRegisterUncreatableType<RulesModel>("org.kde.kcms.kwinrules", 1, 0, "RulesModel",
34                                                  QStringLiteral("Do not create objects of type RulesModel"));
35 
36     qDBusRegisterMetaType<KWin::DBusDesktopDataStruct>();
37     qDBusRegisterMetaType<KWin::DBusDesktopDataVector>();
38 
39     populateRuleList();
40 }
41 
~RulesModel()42 RulesModel::~RulesModel()
43 {
44 }
45 
roleNames() const46 QHash< int, QByteArray > RulesModel::roleNames() const
47 {
48     return {
49         {KeyRole,            QByteArrayLiteral("key")},
50         {NameRole,           QByteArrayLiteral("name")},
51         {IconRole,           QByteArrayLiteral("icon")},
52         {IconNameRole,       QByteArrayLiteral("iconName")},
53         {SectionRole,        QByteArrayLiteral("section")},
54         {DescriptionRole,    QByteArrayLiteral("description")},
55         {EnabledRole,        QByteArrayLiteral("enabled")},
56         {SelectableRole,     QByteArrayLiteral("selectable")},
57         {ValueRole,          QByteArrayLiteral("value")},
58         {TypeRole,           QByteArrayLiteral("type")},
59         {PolicyRole,         QByteArrayLiteral("policy")},
60         {PolicyModelRole,    QByteArrayLiteral("policyModel")},
61         {OptionsModelRole,   QByteArrayLiteral("options")},
62         {OptionsMaskRole,    QByteArrayLiteral("optionsMask")},
63         {SuggestedValueRole, QByteArrayLiteral("suggested")},
64     };
65 }
66 
rowCount(const QModelIndex & parent) const67 int RulesModel::rowCount(const QModelIndex &parent) const
68 {
69     if (parent.isValid()) {
70         return 0;
71     }
72     return m_ruleList.size();
73 }
74 
data(const QModelIndex & index,int role) const75 QVariant RulesModel::data(const QModelIndex &index, int role) const
76 {
77     if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) {
78         return QVariant();
79     }
80 
81     const RuleItem *rule = m_ruleList.at(index.row());
82 
83     switch (role) {
84     case KeyRole:
85         return rule->key();
86     case NameRole:
87         return rule->name();
88     case IconRole:
89         return rule->icon();
90     case IconNameRole:
91         return rule->iconName();
92     case DescriptionRole:
93         return rule->description();
94     case SectionRole:
95         return rule->section();
96     case EnabledRole:
97         return rule->isEnabled();
98     case SelectableRole:
99         return !rule->hasFlag(RuleItem::AlwaysEnabled) && !rule->hasFlag(RuleItem::SuggestionOnly);
100     case ValueRole:
101         return rule->value();
102     case TypeRole:
103         return rule->type();
104     case PolicyRole:
105         return rule->policy();
106     case PolicyModelRole:
107         return rule->policyModel();
108     case OptionsModelRole:
109         return rule->options();
110     case OptionsMaskRole:
111         return rule->optionsMask();
112     case SuggestedValueRole:
113         return rule->suggestedValue();
114     }
115     return QVariant();
116 }
117 
setData(const QModelIndex & index,const QVariant & value,int role)118 bool RulesModel::setData(const QModelIndex &index, const QVariant &value, int role)
119 {
120     if (!checkIndex(index, CheckIndexOption::IndexIsValid | CheckIndexOption::ParentIsInvalid)) {
121         return false;
122     }
123 
124     RuleItem *rule = m_ruleList.at(index.row());
125 
126     switch (role) {
127     case EnabledRole:
128         if (value.toBool() == rule->isEnabled()) {
129             return true;
130         }
131         rule->setEnabled(value.toBool());
132         break;
133     case ValueRole:
134         if (rule->hasFlag(RuleItem::SuggestionOnly)) {
135             processSuggestion(rule->key(), value);
136         }
137         if (value == rule->value()) {
138             return true;
139         }
140         rule->setValue(value);
141         break;
142     case PolicyRole:
143         if (value.toInt() == rule->policy()) {
144             return true;
145         }
146         rule->setPolicy(value.toInt());
147         break;
148     case SuggestedValueRole:
149         if (value == rule->suggestedValue()) {
150             return true;
151         }
152         rule->setSuggestedValue(value);
153         break;
154     default:
155         return false;
156     }
157 
158     writeToSettings(rule);
159 
160     Q_EMIT dataChanged(index, index, QVector<int>{role});
161     if (rule->hasFlag(RuleItem::AffectsDescription)) {
162         Q_EMIT descriptionChanged();
163     }
164     if (rule->hasFlag(RuleItem::AffectsWarning)) {
165         Q_EMIT warningMessagesChanged();
166     }
167 
168     return true;
169 }
170 
indexOf(const QString & key) const171 QModelIndex RulesModel::indexOf(const QString& key) const
172 {
173     const QModelIndexList indexes = match(index(0), RulesModel::KeyRole, key, 1, Qt::MatchFixedString);
174     if (indexes.isEmpty()) {
175         return QModelIndex();
176     }
177     return indexes.at(0);
178 }
179 
addRule(RuleItem * rule)180 RuleItem *RulesModel::addRule(RuleItem *rule)
181 {
182     m_ruleList << rule;
183     m_rules.insert(rule->key(), rule);
184 
185     return rule;
186 }
187 
hasRule(const QString & key) const188 bool RulesModel::hasRule(const QString& key) const
189 {
190     return m_rules.contains(key);
191 }
192 
193 
ruleItem(const QString & key) const194 RuleItem *RulesModel::ruleItem(const QString& key) const
195 {
196     return m_rules.value(key);
197 }
198 
description() const199 QString RulesModel::description() const
200 {
201     const QString desc = m_rules["description"]->value().toString();
202     if (!desc.isEmpty()) {
203         return desc;
204     }
205     return defaultDescription();
206 }
207 
setDescription(const QString & description)208 void RulesModel::setDescription(const QString &description)
209 {
210     setData(indexOf("description"), description, RulesModel::ValueRole);
211 }
212 
defaultDescription() const213 QString RulesModel::defaultDescription() const
214 {
215     const QString wmclass = m_rules["wmclass"]->value().toString();
216     const QString title = m_rules["title"]->isEnabled() ? m_rules["title"]->value().toString() : QString();
217 
218     if (!title.isEmpty()) {
219         return i18n("Window settings for %1", title);
220     }
221     if (!wmclass.isEmpty()) {
222         return i18n("Settings for %1", wmclass);
223     }
224 
225     return i18n("New window settings");
226 }
227 
processSuggestion(const QString & key,const QVariant & value)228 void RulesModel::processSuggestion(const QString &key, const QVariant &value)
229 {
230     if (key == QLatin1String("wmclasshelper")) {
231         setData(indexOf("wmclass"), value, RulesModel::ValueRole);
232         setData(indexOf("wmclasscomplete"), true, RulesModel::ValueRole);
233     }
234 }
235 
warningMessages() const236 QStringList RulesModel::warningMessages() const
237 {
238     QStringList messages;
239 
240     if (wmclassWarning()) {
241         messages << i18n("You have specified the window class as unimportant.\n"
242                          "This means the settings will possibly apply to windows from all applications."
243                          " If you really want to create a generic setting, it is recommended"
244                          " you at least limit the window types to avoid special window types.");
245     }
246 
247     if (geometryWarning()) {
248         messages << i18n("Some applications set their own geometry after starting,"
249                          " overriding your initial settings for size and position. "
250                          "To enforce these settings, also force the property \"%1\" to \"Yes\".",
251                          m_rules["ignoregeometry"]->name());
252     }
253 
254     return messages;
255 }
256 
wmclassWarning() const257 bool RulesModel::wmclassWarning() const
258 {
259     const bool no_wmclass = !m_rules["wmclass"]->isEnabled()
260                                 || m_rules["wmclass"]->policy() == Rules::UnimportantMatch;
261     const bool alltypes = !m_rules["types"]->isEnabled()
262                               || (m_rules["types"]->value() == 0)
263                               || (m_rules["types"]->value() == NET::AllTypesMask)
264                               || ((m_rules["types"]->value().toInt() | (1 << NET::Override)) == 0x3FF);
265 
266     return (no_wmclass && alltypes);
267 }
268 
geometryWarning() const269 bool RulesModel::geometryWarning() const
270 {
271     const bool ignoregeometry = m_rules["ignoregeometry"]->isEnabled()
272                                     && m_rules["ignoregeometry"]->policy() == Rules::Force
273                                     && m_rules["ignoregeometry"]->value() == true;
274 
275     const bool initialPos = m_rules["position"]->isEnabled()
276                                 && (m_rules["position"]->policy() == Rules::Apply
277                                     || m_rules["position"]->policy() == Rules::Remember);
278 
279     const bool initialSize = m_rules["size"]->isEnabled()
280                                 && (m_rules["size"]->policy() == Rules::Apply
281                                     || m_rules["size"]->policy() == Rules::Remember);
282 
283     const bool initialPlacement = m_rules["placement"]->isEnabled()
284                                     && m_rules["placement"]->policy() == Rules::Force;
285 
286     return (!ignoregeometry && (initialPos || initialSize || initialPlacement));
287 }
288 
settings() const289 RuleSettings *RulesModel::settings() const
290 {
291     return m_settings;
292 }
293 
setSettings(RuleSettings * settings)294 void RulesModel::setSettings(RuleSettings *settings)
295 {
296     if (m_settings == settings) {
297         return;
298     }
299 
300     beginResetModel();
301 
302     m_settings = settings;
303 
304     for (RuleItem *rule : qAsConst(m_ruleList)) {
305         const KConfigSkeletonItem *configItem = m_settings->findItem(rule->key());
306         const KConfigSkeletonItem *configPolicyItem = m_settings->findItem(rule->policyKey());
307 
308         rule->reset();
309 
310         if (!configItem) {
311             continue;
312         }
313 
314         const bool isEnabled = configPolicyItem ? configPolicyItem->property() != Rules::Unused
315                                                 : !configItem->property().toString().isEmpty();
316         rule->setEnabled(isEnabled);
317 
318         const QVariant value = configItem->property();
319         rule->setValue(value);
320 
321         if (configPolicyItem) {
322             const int policy = configPolicyItem->property().toInt();
323             rule->setPolicy(policy);
324         }
325     }
326 
327     endResetModel();
328 
329     Q_EMIT descriptionChanged();
330     Q_EMIT warningMessagesChanged();
331 }
332 
writeToSettings(RuleItem * rule)333 void RulesModel::writeToSettings(RuleItem *rule)
334 {
335     KConfigSkeletonItem *configItem = m_settings->findItem(rule->key());
336     KConfigSkeletonItem *configPolicyItem = m_settings->findItem(rule->policyKey());
337 
338     if (!configItem) {
339         return;
340     }
341 
342     if (rule->isEnabled()) {
343         configItem->setProperty(rule->value());
344         if (configPolicyItem) {
345             configPolicyItem->setProperty(rule->policy());
346         }
347     } else {
348         configItem->setDefault();
349         if (configPolicyItem) {
350             configPolicyItem->setDefault();
351         }
352     }
353 }
354 
populateRuleList()355 void RulesModel::populateRuleList()
356 {
357     qDeleteAll(m_ruleList);
358     m_ruleList.clear();
359 
360     //Rule description
361     auto description = addRule(new RuleItem(QLatin1String("description"),
362                                             RulePolicy::NoPolicy, RuleItem::String,
363                                             i18n("Description"), i18n("Window matching"),
364                                             QIcon::fromTheme("entry-edit")));
365     description->setFlag(RuleItem::AlwaysEnabled);
366     description->setFlag(RuleItem::AffectsDescription);
367 
368     // Window matching
369     auto wmclass = addRule(new RuleItem(QLatin1String("wmclass"),
370                                         RulePolicy::StringMatch, RuleItem::String,
371                                         i18n("Window class (application)"), i18n("Window matching"),
372                                         QIcon::fromTheme("window")));
373     wmclass->setFlag(RuleItem::AlwaysEnabled);
374     wmclass->setFlag(RuleItem::AffectsDescription);
375     wmclass->setFlag(RuleItem::AffectsWarning);
376 
377     auto wmclasscomplete = addRule(new RuleItem(QLatin1String("wmclasscomplete"),
378                                                 RulePolicy::NoPolicy, RuleItem::Boolean,
379                                                 i18n("Match whole window class"), i18n("Window matching"),
380                                                 QIcon::fromTheme("window")));
381     wmclasscomplete->setFlag(RuleItem::AlwaysEnabled);
382 
383     // Helper item to store the detected whole window class when detecting properties
384     auto wmclasshelper = addRule(new RuleItem(QLatin1String("wmclasshelper"),
385                                               RulePolicy::NoPolicy, RuleItem::String,
386                                               i18n("Whole window class"), i18n("Window matching"),
387                                               QIcon::fromTheme("window")));
388     wmclasshelper->setFlag(RuleItem::SuggestionOnly);
389 
390     auto types = addRule(new RuleItem(QLatin1String("types"),
391                                       RulePolicy::NoPolicy, RuleItem::NetTypes,
392                                       i18n("Window types"), i18n("Window matching"),
393                                       QIcon::fromTheme("window-duplicate")));
394     types->setOptionsData(windowTypesModelData());
395     types->setFlag(RuleItem::AlwaysEnabled);
396     types->setFlag(RuleItem::AffectsWarning);
397 
398     addRule(new RuleItem(QLatin1String("windowrole"),
399                          RulePolicy::StringMatch, RuleItem::String,
400                          i18n("Window role"), i18n("Window matching"),
401                          QIcon::fromTheme("dialog-object-properties")));
402 
403     auto title = addRule(new RuleItem(QLatin1String("title"),
404                                       RulePolicy::StringMatch, RuleItem::String,
405                                       i18n("Window title"), i18n("Window matching"),
406                                       QIcon::fromTheme("edit-comment")));
407     title->setFlag(RuleItem::AffectsDescription);
408 
409     addRule(new RuleItem(QLatin1String("clientmachine"),
410                          RulePolicy::StringMatch, RuleItem::String,
411                          i18n("Machine (hostname)"), i18n("Window matching"),
412                          QIcon::fromTheme("computer")));
413 
414     // Size & Position
415     auto position = addRule(new RuleItem(QLatin1String("position"),
416                                 RulePolicy::SetRule, RuleItem::Point,
417                                 i18n("Position"), i18n("Size & Position"),
418                                 QIcon::fromTheme("transform-move")));
419     position->setFlag(RuleItem::AffectsWarning);
420 
421     auto size = addRule(new RuleItem(QLatin1String("size"),
422                                      RulePolicy::SetRule, RuleItem::Size,
423                                      i18n("Size"), i18n("Size & Position"),
424                                      QIcon::fromTheme("transform-scale")));
425     size->setFlag(RuleItem::AffectsWarning);
426 
427     addRule(new RuleItem(QLatin1String("maximizehoriz"),
428                          RulePolicy::SetRule, RuleItem::Boolean,
429                          i18n("Maximized horizontally"), i18n("Size & Position"),
430                          QIcon::fromTheme("resizecol")));
431 
432     addRule(new RuleItem(QLatin1String("maximizevert"),
433                          RulePolicy::SetRule, RuleItem::Boolean,
434                          i18n("Maximized vertically"), i18n("Size & Position"),
435                          QIcon::fromTheme("resizerow")));
436 
437     RuleItem *desktops;
438     if (QX11Info::isPlatformX11()) {
439         // Single selection of Virtual Desktop on X11
440         desktops = new RuleItem(QLatin1String("desktops"),
441                                 RulePolicy::SetRule, RuleItem::Option,
442                                 i18n("Virtual Desktop"), i18n("Size & Position"),
443                                 QIcon::fromTheme("virtual-desktops"));
444     } else {
445         // Multiple selection on Wayland
446         desktops = new RuleItem(QLatin1String("desktops"),
447                                 RulePolicy::SetRule, RuleItem::OptionList,
448                                 i18n("Virtual Desktops"), i18n("Size & Position"),
449                                 QIcon::fromTheme("virtual-desktops"));
450     }
451     addRule(desktops);
452     desktops->setOptionsData(virtualDesktopsModelData());
453 
454     connect(this, &RulesModel::virtualDesktopsUpdated,
455             this, [this] { m_rules["desktops"]->setOptionsData(virtualDesktopsModelData()); });
456 
457     updateVirtualDesktops();
458 
459 #ifdef KWIN_BUILD_ACTIVITIES
460     m_activities = new KActivities::Consumer(this);
461 
462     auto activity = addRule(new RuleItem(QLatin1String("activity"),
463                                          RulePolicy::SetRule, RuleItem::OptionList,
464                                          i18n("Activities"), i18n("Size & Position"),
465                                          QIcon::fromTheme("activities")));
466     activity->setOptionsData(activitiesModelData());
467 
468     // Activites consumer may update the available activities later
469     connect(m_activities, &KActivities::Consumer::activitiesChanged,
470             this, [this] { m_rules["activity"]->setOptionsData(activitiesModelData()); });
471     connect(m_activities, &KActivities::Consumer::serviceStatusChanged,
472             this, [this] { m_rules["activity"]->setOptionsData(activitiesModelData()); });
473 
474 #endif
475 
476     addRule(new RuleItem(QLatin1String("screen"),
477                          RulePolicy::SetRule, RuleItem::Integer,
478                          i18n("Screen"), i18n("Size & Position"),
479                          QIcon::fromTheme("osd-shutd-screen")));
480 
481     addRule(new RuleItem(QLatin1String("fullscreen"),
482                          RulePolicy::SetRule, RuleItem::Boolean,
483                          i18n("Fullscreen"), i18n("Size & Position"),
484                          QIcon::fromTheme("view-fullscreen")));
485 
486     addRule(new RuleItem(QLatin1String("minimize"),
487                          RulePolicy::SetRule, RuleItem::Boolean,
488                          i18n("Minimized"), i18n("Size & Position"),
489                          QIcon::fromTheme("window-minimize")));
490 
491     addRule(new RuleItem(QLatin1String("shade"),
492                          RulePolicy::SetRule, RuleItem::Boolean,
493                          i18n("Shaded"), i18n("Size & Position"),
494                          QIcon::fromTheme("window-shade")));
495 
496     auto placement = addRule(new RuleItem(QLatin1String("placement"),
497                                           RulePolicy::ForceRule, RuleItem::Option,
498                                           i18n("Initial placement"), i18n("Size & Position"),
499                                           QIcon::fromTheme("region")));
500     placement->setOptionsData(placementModelData());
501     placement->setFlag(RuleItem::AffectsWarning);
502 
503     auto ignoregeometry = addRule(new RuleItem(QLatin1String("ignoregeometry"),
504                                                RulePolicy::SetRule, RuleItem::Boolean,
505                                                i18n("Ignore requested geometry"), i18n("Size & Position"),
506                                                QIcon::fromTheme("view-time-schedule-baselined-remove"),
507                                                i18n("Windows can ask to appear in a certain position.\n"
508                                                     "By default this overrides the placement strategy\n"
509                                                     "what might be nasty if the client abuses the feature\n"
510                                                     "to unconditionally popup in the middle of your screen.")));
511     ignoregeometry->setFlag(RuleItem::AffectsWarning);
512 
513     addRule(new RuleItem(QLatin1String("minsize"),
514                          RulePolicy::ForceRule, RuleItem::Size,
515                          i18n("Minimum Size"), i18n("Size & Position"),
516                          QIcon::fromTheme("transform-scale")));
517 
518     addRule(new RuleItem(QLatin1String("maxsize"),
519                          RulePolicy::ForceRule, RuleItem::Size,
520                          i18n("Maximum Size"), i18n("Size & Position"),
521                          QIcon::fromTheme("transform-scale")));
522 
523     addRule(new RuleItem(QLatin1String("strictgeometry"),
524                          RulePolicy::ForceRule, RuleItem::Boolean,
525                          i18n("Obey geometry restrictions"), i18n("Size & Position"),
526                          QIcon::fromTheme("transform-crop-and-resize"),
527                          i18n("Eg. terminals or video players can ask to keep a certain aspect ratio\n"
528                               "or only grow by values larger than one\n"
529                               "(eg. by the dimensions of one character).\n"
530                               "This may be pointless and the restriction prevents arbitrary dimensions\n"
531                               "like your complete screen area.")));
532 
533     // Arrangement & Access
534     addRule(new RuleItem(QLatin1String("above"),
535                          RulePolicy::SetRule, RuleItem::Boolean,
536                          i18n("Keep above other windows"), i18n("Arrangement & Access"),
537                          QIcon::fromTheme("window-keep-above")));
538 
539     addRule(new RuleItem(QLatin1String("below"),
540                          RulePolicy::SetRule, RuleItem::Boolean,
541                          i18n("Keep below other windows"), i18n("Arrangement & Access"),
542                          QIcon::fromTheme("window-keep-below")));
543 
544     addRule(new RuleItem(QLatin1String("skiptaskbar"),
545                          RulePolicy::SetRule, RuleItem::Boolean,
546                          i18n("Skip taskbar"), i18n("Arrangement & Access"),
547                          QIcon::fromTheme("kt-show-statusbar"),
548                          i18n("Window shall (not) appear in the taskbar.")));
549 
550     addRule(new RuleItem(QLatin1String("skippager"),
551                          RulePolicy::SetRule, RuleItem::Boolean,
552                          i18n("Skip pager"), i18n("Arrangement & Access"),
553                          QIcon::fromTheme("org.kde.plasma.pager"),
554                          i18n("Window shall (not) appear in the manager for virtual desktops")));
555 
556     addRule(new RuleItem(QLatin1String("skipswitcher"),
557                          RulePolicy::SetRule, RuleItem::Boolean,
558                          i18n("Skip switcher"), i18n("Arrangement & Access"),
559                          QIcon::fromTheme("preferences-system-windows-effect-flipswitch"),
560                          i18n("Window shall (not) appear in the Alt+Tab list")));
561 
562     addRule(new RuleItem(QLatin1String("shortcut"),
563                          RulePolicy::SetRule, RuleItem::Shortcut,
564                          i18n("Shortcut"), i18n("Arrangement & Access"),
565                          QIcon::fromTheme("configure-shortcuts")));
566 
567     // Appearance & Fixes
568     addRule(new RuleItem(QLatin1String("noborder"),
569                          RulePolicy::SetRule, RuleItem::Boolean,
570                          i18n("No titlebar and frame"), i18n("Appearance & Fixes"),
571                          QIcon::fromTheme("dialog-cancel")));
572 
573     auto decocolor = addRule(new RuleItem(QLatin1String("decocolor"),
574                                           RulePolicy::ForceRule, RuleItem::Option,
575                                           i18n("Titlebar color scheme"), i18n("Appearance & Fixes"),
576                                           QIcon::fromTheme("preferences-desktop-theme")));
577     decocolor->setOptionsData(colorSchemesModelData());
578 
579     addRule(new RuleItem(QLatin1String("opacityactive"),
580                          RulePolicy::ForceRule, RuleItem::Percentage,
581                          i18n("Active opacity"), i18n("Appearance & Fixes"),
582                          QIcon::fromTheme("edit-opacity")));
583 
584     addRule(new RuleItem(QLatin1String("opacityinactive"),
585                          RulePolicy::ForceRule, RuleItem::Percentage,
586                          i18n("Inactive opacity"), i18n("Appearance & Fixes"),
587                          QIcon::fromTheme("edit-opacity")));
588 
589     auto fsplevel = addRule(new RuleItem(QLatin1String("fsplevel"),
590                                          RulePolicy::ForceRule, RuleItem::Option,
591                                          i18n("Focus stealing prevention"), i18n("Appearance & Fixes"),
592                                          QIcon::fromTheme("preferences-system-windows-effect-glide"),
593                                          i18n("KWin tries to prevent windows from taking the focus\n"
594                                               "(\"activate\") while you're working in another window,\n"
595                                               "but this may sometimes fail or superact.\n"
596                                               "\"None\" will unconditionally allow this window to get the focus while\n"
597                                               "\"Extreme\" will completely prevent it from taking the focus.")));
598     fsplevel->setOptionsData(focusModelData());
599 
600     auto fpplevel = addRule(new RuleItem(QLatin1String("fpplevel"),
601                                          RulePolicy::ForceRule, RuleItem::Option,
602                                          i18n("Focus protection"), i18n("Appearance & Fixes"),
603                                          QIcon::fromTheme("preferences-system-windows-effect-minimize"),
604                                          i18n("This controls the focus protection of the currently active window.\n"
605                                               "None will always give the focus away,\n"
606                                               "Extreme will keep it.\n"
607                                               "Otherwise it's interleaved with the stealing prevention\n"
608                                               "assigned to the window that wants the focus.")));
609     fpplevel->setOptionsData(focusModelData());
610 
611     addRule(new RuleItem(QLatin1String("acceptfocus"),
612                          RulePolicy::ForceRule, RuleItem::Boolean,
613                          i18n("Accept focus"), i18n("Appearance & Fixes"),
614                          QIcon::fromTheme("preferences-desktop-cursors"),
615                          i18n("Windows may prevent to get the focus (activate) when being clicked.\n"
616                               "On the other hand you might wish to prevent a window\n"
617                               "from getting focused on a mouse click.")));
618 
619     addRule(new RuleItem(QLatin1String("disableglobalshortcuts"),
620                          RulePolicy::ForceRule, RuleItem::Boolean,
621                          i18n("Ignore global shortcuts"), i18n("Appearance & Fixes"),
622                          QIcon::fromTheme("input-keyboard-virtual-off"),
623                          i18n("When used, a window will receive\n"
624                               "all keyboard inputs while it is active, including Alt+Tab etc.\n"
625                               "This is especially interesting for emulators or virtual machines.\n"
626                               "\n"
627                               "Be warned:\n"
628                               "you won't be able to Alt+Tab out of the window\n"
629                               "nor use any other global shortcut (such as Alt+F2 to show KRunner)\n"
630                               "while it's active!")));
631 
632     addRule(new RuleItem(QLatin1String("closeable"),
633                          RulePolicy::ForceRule, RuleItem::Boolean,
634                          i18n("Closeable"), i18n("Appearance & Fixes"),
635                          QIcon::fromTheme("dialog-close")));
636 
637     auto type = addRule(new RuleItem(QLatin1String("type"),
638                                      RulePolicy::ForceRule, RuleItem::Option,
639                                      i18n("Set window type"), i18n("Appearance & Fixes"),
640                                      QIcon::fromTheme("window-duplicate")));
641     type->setOptionsData(windowTypesModelData());
642 
643     addRule(new RuleItem(QLatin1String("desktopfile"),
644                          RulePolicy::SetRule, RuleItem::String,
645                          i18n("Desktop file name"), i18n("Appearance & Fixes"),
646                          QIcon::fromTheme("application-x-desktop")));
647 
648     addRule(new RuleItem(QLatin1String("blockcompositing"),
649                          RulePolicy::ForceRule, RuleItem::Boolean,
650                          i18n("Block compositing"), i18n("Appearance & Fixes"),
651                          QIcon::fromTheme("composite-track-on")));
652 }
653 
654 
x11PropertyHash()655 const QHash<QString, QString> RulesModel::x11PropertyHash()
656 {
657     static const auto propertyToRule = QHash<QString, QString> {
658         { "caption",            "title"         },
659         { "role",               "windowrole"    },
660         { "clientMachine",      "clientmachine" },
661         { "maximizeHorizontal", "maximizehoriz" },
662         { "maximizeVertical",   "maximizevert"  },
663         { "minimized",          "minimize"      },
664         { "shaded",             "shade"         },
665         { "fullscreen",         "fullscreen"    },
666         { "keepAbove",          "above"         },
667         { "keepBelow",          "below"         },
668         { "noBorder",           "noborder"      },
669         { "skipTaskbar",        "skiptaskbar"   },
670         { "skipPager",          "skippager"     },
671         { "skipSwitcher",       "skipswitcher"  },
672         { "type",               "type"          },
673         { "desktopFile",        "desktopfile"   },
674         { "desktops",           "desktops"      },
675     };
676     return propertyToRule;
677 };
678 
setSuggestedProperties(const QVariantMap & info)679 void RulesModel::setSuggestedProperties(const QVariantMap &info)
680 {
681     // Properties that cannot be directly applied via x11PropertyHash
682     const QPoint position = QPoint(info.value("x").toInt(), info.value("y").toInt());
683     const QSize size = QSize(info.value("width").toInt(), info.value("height").toInt());
684 
685     m_rules["position"]->setSuggestedValue(position);
686     m_rules["size"]->setSuggestedValue(size);
687     m_rules["minsize"]->setSuggestedValue(size);
688     m_rules["maxsize"]->setSuggestedValue(size);
689 
690     NET::WindowType window_type = static_cast<NET::WindowType>(info.value("type", 0).toInt());
691     if (window_type == NET::Unknown) {
692         window_type = NET::Normal;
693     }
694     m_rules["types"]->setSuggestedValue(1 << window_type);
695 
696     const QString wmsimpleclass = info.value("resourceClass").toString();
697     const QString wmcompleteclass = QStringLiteral("%1 %2").arg(info.value("resourceName").toString(),
698                                                                 info.value("resourceClass").toString());
699 
700     m_rules["wmclass"]->setSuggestedValue(wmsimpleclass);
701     m_rules["wmclasshelper"]->setSuggestedValue(wmcompleteclass);
702 
703 #ifdef KWIN_BUILD_ACTIVITIES
704     const QStringList activities = info.value("activities").toStringList();
705     m_rules["activity"]->setSuggestedValue(activities.isEmpty() ? QStringList{ Activities::nullUuid() }
706                                                                 : activities);
707 #endif
708 
709     const auto ruleForProperty = x11PropertyHash();
710     for (QString &property : info.keys()) {
711         if (!ruleForProperty.contains(property)) {
712             continue;
713         }
714         const QString ruleKey = ruleForProperty.value(property, QString());
715         Q_ASSERT(hasRule(ruleKey));
716 
717         m_rules[ruleKey]->setSuggestedValue(info.value(property));
718     }
719 
720     Q_EMIT dataChanged(index(0), index(rowCount()-1), {RulesModel::SuggestedValueRole});
721 }
722 
723 
windowTypesModelData() const724 QList<OptionsModel::Data> RulesModel::windowTypesModelData() const
725 {
726     static const auto modelData = QList<OptionsModel::Data> {
727         //TODO: Find/create better icons
728         { NET::Normal,  i18n("Normal Window")     , QIcon::fromTheme("window")                   },
729         { NET::Dialog,  i18n("Dialog Window")     , QIcon::fromTheme("window-duplicate")         },
730         { NET::Utility, i18n("Utility Window")    , QIcon::fromTheme("dialog-object-properties") },
731         { NET::Dock,    i18n("Dock (panel)")      , QIcon::fromTheme("list-remove")              },
732         { NET::Toolbar, i18n("Toolbar")           , QIcon::fromTheme("tools")                    },
733         { NET::Menu,    i18n("Torn-Off Menu")     , QIcon::fromTheme("overflow-menu-left")       },
734         { NET::Splash,  i18n("Splash Screen")     , QIcon::fromTheme("embosstool")               },
735         { NET::Desktop, i18n("Desktop")           , QIcon::fromTheme("desktop")                  },
736         // { NET::Override, i18n("Unmanaged Window")   },  deprecated
737         { NET::TopMenu, i18n("Standalone Menubar"), QIcon::fromTheme("application-menu")       },
738         { NET::OnScreenDisplay, i18n("On Screen Display"), QIcon::fromTheme("osd-duplicate")     }
739     };
740     return modelData;
741 }
742 
virtualDesktopsModelData() const743 QList<OptionsModel::Data> RulesModel::virtualDesktopsModelData() const
744 {
745     QList<OptionsModel::Data> modelData = { {QString(), i18n("All Desktops"), QIcon::fromTheme("window-pin")} };
746     for (const DBusDesktopDataStruct &desktop : m_virtualDesktops) {
747         modelData << OptionsModel::Data{
748             desktop.id,
749             QString::number(desktop.position + 1).rightJustified(2) + QStringLiteral(": ") + desktop.name,
750             QIcon::fromTheme("virtual-desktops")
751         };
752     }
753     return modelData;
754 }
755 
756 
activitiesModelData() const757 QList<OptionsModel::Data> RulesModel::activitiesModelData() const
758 {
759 #ifdef KWIN_BUILD_ACTIVITIES
760     QList<OptionsModel::Data> modelData;
761 
762     modelData << OptionsModel::Data{
763         Activities::nullUuid(),
764         i18n("All Activities"),
765         QIcon::fromTheme("activities")
766     };
767 
768     const auto activities = m_activities->activities(KActivities::Info::Running);
769     if (m_activities->serviceStatus() == KActivities::Consumer::Running) {
770         for (const QString &activityId : activities) {
771             const KActivities::Info info(activityId);
772             modelData << OptionsModel::Data{ activityId, info.name(), QIcon::fromTheme(info.icon()) };
773         }
774     }
775 
776     return modelData;
777 #else
778     return {};
779 #endif
780 }
781 
placementModelData() const782 QList<OptionsModel::Data> RulesModel::placementModelData() const
783 {
784     static const auto modelData = QList<OptionsModel::Data> {
785         { Placement::Default,      i18n("Default")             },
786         { Placement::NoPlacement,  i18n("No Placement")        },
787         { Placement::Smart,        i18n("Minimal Overlapping") },
788         { Placement::Maximizing,   i18n("Maximized")           },
789         { Placement::Cascade,      i18n("Cascaded")            },
790         { Placement::Centered,     i18n("Centered")            },
791         { Placement::Random,       i18n("Random")              },
792         { Placement::ZeroCornered, i18n("In Top-Left Corner")  },
793         { Placement::UnderMouse,   i18n("Under Mouse")         },
794         { Placement::OnMainWindow, i18n("On Main Window")      }
795     };
796     return modelData;
797 }
798 
focusModelData() const799 QList<OptionsModel::Data> RulesModel::focusModelData() const
800 {
801     static const auto modelData = QList<OptionsModel::Data> {
802         { 0, i18n("None")    },
803         { 1, i18n("Low")     },
804         { 2, i18n("Normal")  },
805         { 3, i18n("High")    },
806         { 4, i18n("Extreme") }
807     };
808     return modelData;
809 }
810 
colorSchemesModelData() const811 QList<OptionsModel::Data> RulesModel::colorSchemesModelData() const
812 {
813     QList<OptionsModel::Data> modelData;
814 
815     KColorSchemeManager schemes;
816     QAbstractItemModel *schemesModel = schemes.model();
817 
818     // Skip row 0, which is Default scheme
819     for (int r = 1; r < schemesModel->rowCount(); r++) {
820         const QModelIndex index = schemesModel->index(r, 0);
821         modelData << OptionsModel::Data{
822             QFileInfo(index.data(Qt::UserRole).toString()).baseName(),
823             index.data(Qt::DisplayRole).toString(),
824             index.data(Qt::DecorationRole).value<QIcon>()
825         };
826     }
827 
828     return modelData;
829 }
830 
detectWindowProperties(int miliseconds)831 void RulesModel::detectWindowProperties(int miliseconds)
832 {
833     QTimer::singleShot(miliseconds, this, &RulesModel::selectX11Window);
834 }
835 
selectX11Window()836 void RulesModel::selectX11Window()
837 {
838     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
839                                                           QStringLiteral("/KWin"),
840                                                           QStringLiteral("org.kde.KWin"),
841                                                           QStringLiteral("queryWindowInfo"));
842 
843     QDBusPendingReply<QVariantMap> async = QDBusConnection::sessionBus().asyncCall(message);
844 
845     QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
846     connect(callWatcher, &QDBusPendingCallWatcher::finished, this,
847             [this](QDBusPendingCallWatcher *self) {
848                 QDBusPendingReply<QVariantMap> reply = *self;
849                 self->deleteLater();
850                 if (!reply.isValid()) {
851                     if (reply.error().name() == QLatin1String("org.kde.KWin.Error.InvalidWindow")) {
852                         Q_EMIT showErrorMessage(i18n("Could not detect window properties. The window is not managed by KWin."));
853                     }
854                     return;
855                 }
856                 const QVariantMap windowInfo = reply.value();
857                 setSuggestedProperties(windowInfo);
858                 Q_EMIT showSuggestions();
859             }
860     );
861 }
862 
updateVirtualDesktops()863 void RulesModel::updateVirtualDesktops()
864 {
865     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
866                                                           QStringLiteral("/VirtualDesktopManager"),
867                                                           QStringLiteral("org.freedesktop.DBus.Properties"),
868                                                           QStringLiteral("Get"));
869     message.setArguments(QVariantList{
870         QStringLiteral("org.kde.KWin.VirtualDesktopManager"),
871         QStringLiteral("desktops")
872     });
873 
874     QDBusPendingReply<QVariant> async = QDBusConnection::sessionBus().asyncCall(message);
875 
876     QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
877     connect(callWatcher, &QDBusPendingCallWatcher::finished, this,
878             [this](QDBusPendingCallWatcher *self) {
879                 QDBusPendingReply<QVariant> reply = *self;
880                 self->deleteLater();
881                 if (!reply.isValid()) {
882                     return;
883                 }
884                 m_virtualDesktops = qdbus_cast<KWin::DBusDesktopDataVector>(reply.value());
885                 Q_EMIT virtualDesktopsUpdated();
886             }
887     );
888 }
889 
890 
891 } //namespace
892