Warning, file /plasma/kwin/src/kcms/rules/kcmrules.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2004 Lubos Lunak <l.lunak@kde.org>
0003     SPDX-FileCopyrightText: 2020 Ismael Asensio <isma.af@gmail.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0006 */
0007 
0008 #include "kcmrules.h"
0009 #include "rulesettings.h"
0010 
0011 #include <QDBusConnection>
0012 #include <QDBusMessage>
0013 
0014 #include <KAboutData>
0015 #include <KConfig>
0016 #include <KLocalizedString>
0017 #include <KPluginFactory>
0018 
0019 namespace KWin
0020 {
0021 
0022 KCMKWinRules::KCMKWinRules(QObject *parent, const QVariantList &arguments)
0023     : KQuickAddons::ConfigModule(parent, arguments)
0024     , m_ruleBookModel(new RuleBookModel(this))
0025     , m_rulesModel(new RulesModel(this))
0026 {
0027     auto about = new KAboutData(QStringLiteral("kcm_kwinrules"),
0028                                 i18n("Window Rules"),
0029                                 QStringLiteral("1.0"),
0030                                 QString(),
0031                                 KAboutLicense::GPL);
0032     about->addAuthor(i18n("Ismael Asensio"),
0033                      i18n("Author"),
0034                      QStringLiteral("isma.af@gmail.com"));
0035     setAboutData(about);
0036 
0037     setQuickHelp(i18n("<p><h1>Window-specific Settings</h1> Here you can customize window settings specifically only"
0038                       " for some windows.</p>"
0039                       " <p>Please note that this configuration will not take effect if you do not use"
0040                       " KWin as your window manager. If you do use a different window manager, please refer to its documentation"
0041                       " for how to customize window behavior.</p>"));
0042 
0043     QStringList argList;
0044     for (const QVariant &arg : arguments) {
0045         argList << arg.toString();
0046     }
0047     parseArguments(argList);
0048 
0049     connect(m_rulesModel, &RulesModel::descriptionChanged, this, [this] {
0050         if (m_editIndex.isValid()) {
0051             m_ruleBookModel->setDescriptionAt(m_editIndex.row(), m_rulesModel->description());
0052         }
0053     });
0054     connect(m_rulesModel, &RulesModel::dataChanged, this, [this] {
0055         Q_EMIT m_ruleBookModel->dataChanged(m_editIndex, m_editIndex, {});
0056     });
0057     connect(m_ruleBookModel, &RuleBookModel::dataChanged, this, &KCMKWinRules::updateNeedsSave);
0058 }
0059 
0060 void KCMKWinRules::parseArguments(const QStringList &args)
0061 {
0062     // When called from window menu, "uuid" and "whole-app" are set in arguments list
0063     bool nextArgIsUuid = false;
0064     QUuid uuid = QUuid();
0065 
0066     // TODO: Use a better argument parser
0067     for (const QString &arg : args) {
0068         if (arg == QLatin1String("uuid")) {
0069             nextArgIsUuid = true;
0070         } else if (nextArgIsUuid) {
0071             uuid = QUuid(arg);
0072             nextArgIsUuid = false;
0073         } else if (arg.startsWith("uuid=")) {
0074             uuid = QUuid(arg.mid(strlen("uuid=")));
0075         } else if (arg == QLatin1String("whole-app")) {
0076             m_wholeApp = true;
0077         }
0078     }
0079 
0080     if (uuid.isNull()) {
0081         qDebug() << "Invalid window uuid.";
0082         return;
0083     }
0084 
0085     // Get the Window properties
0086     QDBusMessage message = QDBusMessage::createMethodCall(QStringLiteral("org.kde.KWin"),
0087                                                           QStringLiteral("/KWin"),
0088                                                           QStringLiteral("org.kde.KWin"),
0089                                                           QStringLiteral("getWindowInfo"));
0090     message.setArguments({uuid.toString()});
0091     QDBusPendingReply<QVariantMap> async = QDBusConnection::sessionBus().asyncCall(message);
0092 
0093     QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(async, this);
0094     connect(callWatcher, &QDBusPendingCallWatcher::finished, this, [this, uuid](QDBusPendingCallWatcher *self) {
0095         QDBusPendingReply<QVariantMap> reply = *self;
0096         self->deleteLater();
0097         if (!reply.isValid() || reply.value().isEmpty()) {
0098             qDebug() << "Error retrieving properties for window" << uuid;
0099             return;
0100         }
0101         qDebug() << "Retrieved properties for window" << uuid;
0102         m_winProperties = reply.value();
0103 
0104         if (m_alreadyLoaded) {
0105             createRuleFromProperties();
0106         }
0107     });
0108 }
0109 
0110 void KCMKWinRules::load()
0111 {
0112     m_ruleBookModel->load();
0113 
0114     if (!m_winProperties.isEmpty() && !m_alreadyLoaded) {
0115         createRuleFromProperties();
0116     } else {
0117         m_editIndex = QModelIndex();
0118         Q_EMIT editIndexChanged();
0119     }
0120 
0121     m_alreadyLoaded = true;
0122 
0123     updateNeedsSave();
0124 }
0125 
0126 void KCMKWinRules::save()
0127 {
0128     m_ruleBookModel->save();
0129 
0130     // Notify kwin to reload configuration
0131     QDBusMessage message = QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig");
0132     QDBusConnection::sessionBus().send(message);
0133 }
0134 
0135 void KCMKWinRules::updateNeedsSave()
0136 {
0137     setNeedsSave(m_ruleBookModel->isSaveNeeded());
0138     Q_EMIT needsSaveChanged();
0139 }
0140 
0141 void KCMKWinRules::createRuleFromProperties()
0142 {
0143     if (m_winProperties.isEmpty()) {
0144         return;
0145     }
0146 
0147     QModelIndex matchedIndex = findRuleWithProperties(m_winProperties, m_wholeApp);
0148     if (!matchedIndex.isValid()) {
0149         m_ruleBookModel->insertRow(0);
0150         fillSettingsFromProperties(m_ruleBookModel->ruleSettingsAt(0), m_winProperties, m_wholeApp);
0151         matchedIndex = m_ruleBookModel->index(0);
0152         updateNeedsSave();
0153     }
0154 
0155     editRule(matchedIndex.row());
0156     m_rulesModel->setSuggestedProperties(m_winProperties);
0157 
0158     m_winProperties.clear();
0159 }
0160 
0161 int KCMKWinRules::editIndex() const
0162 {
0163     if (!m_editIndex.isValid()) {
0164         return -1;
0165     }
0166     return m_editIndex.row();
0167 }
0168 
0169 void KCMKWinRules::setRuleDescription(int index, const QString &description)
0170 {
0171     if (index < 0 || index >= m_ruleBookModel->rowCount()) {
0172         return;
0173     }
0174 
0175     if (m_editIndex.row() == index) {
0176         m_rulesModel->setDescription(description);
0177         return;
0178     }
0179     m_ruleBookModel->setDescriptionAt(index, description);
0180 
0181     updateNeedsSave();
0182 }
0183 
0184 void KCMKWinRules::editRule(int index)
0185 {
0186     if (index < 0 || index >= m_ruleBookModel->rowCount()) {
0187         return;
0188     }
0189 
0190     m_editIndex = m_ruleBookModel->index(index);
0191     Q_EMIT editIndexChanged();
0192 
0193     m_rulesModel->setSettings(m_ruleBookModel->ruleSettingsAt(m_editIndex.row()));
0194 
0195     // Set the active page to rules editor (0:RulesList, 1:RulesEditor)
0196     setCurrentIndex(1);
0197 }
0198 
0199 void KCMKWinRules::createRule()
0200 {
0201     const int newIndex = m_ruleBookModel->rowCount();
0202     m_ruleBookModel->insertRow(newIndex);
0203 
0204     updateNeedsSave();
0205 
0206     editRule(newIndex);
0207 }
0208 
0209 void KCMKWinRules::removeRule(int index)
0210 {
0211     if (index < 0 || index >= m_ruleBookModel->rowCount()) {
0212         return;
0213     }
0214 
0215     m_ruleBookModel->removeRow(index);
0216 
0217     Q_EMIT editIndexChanged();
0218     updateNeedsSave();
0219 }
0220 
0221 void KCMKWinRules::moveRule(int sourceIndex, int destIndex)
0222 {
0223     const int lastIndex = m_ruleBookModel->rowCount() - 1;
0224     if (sourceIndex == destIndex
0225         || (sourceIndex < 0 || sourceIndex > lastIndex)
0226         || (destIndex < 0 || destIndex > lastIndex)) {
0227         return;
0228     }
0229 
0230     m_ruleBookModel->moveRow(QModelIndex(), sourceIndex, QModelIndex(), destIndex);
0231 
0232     Q_EMIT editIndexChanged();
0233     updateNeedsSave();
0234 }
0235 
0236 void KCMKWinRules::duplicateRule(int index)
0237 {
0238     if (index < 0 || index >= m_ruleBookModel->rowCount()) {
0239         return;
0240     }
0241 
0242     const int newIndex = index + 1;
0243     const QString newDescription = i18n("Copy of %1", m_ruleBookModel->descriptionAt(index));
0244 
0245     m_ruleBookModel->insertRow(newIndex);
0246     m_ruleBookModel->setRuleSettingsAt(newIndex, *(m_ruleBookModel->ruleSettingsAt(index)));
0247     m_ruleBookModel->setDescriptionAt(newIndex, newDescription);
0248 
0249     updateNeedsSave();
0250 }
0251 
0252 void KCMKWinRules::exportToFile(const QUrl &path, const QList<int> &indexes)
0253 {
0254     if (indexes.isEmpty()) {
0255         return;
0256     }
0257 
0258     const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig);
0259 
0260     const QStringList groups = config->groupList();
0261     for (const QString &groupName : groups) {
0262         config->deleteGroup(groupName);
0263     }
0264 
0265     for (int index : indexes) {
0266         if (index < 0 || index > m_ruleBookModel->rowCount()) {
0267             continue;
0268         }
0269         const RuleSettings *origin = m_ruleBookModel->ruleSettingsAt(index);
0270         RuleSettings exported(config, origin->description());
0271 
0272         RuleBookModel::copySettingsTo(&exported, *origin);
0273         exported.save();
0274     }
0275 }
0276 
0277 void KCMKWinRules::importFromFile(const QUrl &path)
0278 {
0279     const auto config = KSharedConfig::openConfig(path.toLocalFile(), KConfig::SimpleConfig);
0280     const QStringList groups = config->groupList();
0281     if (groups.isEmpty()) {
0282         return;
0283     }
0284 
0285     for (const QString &groupName : groups) {
0286         RuleSettings settings(config, groupName);
0287 
0288         const bool remove = settings.deleteRule();
0289         const QString importDescription = settings.description();
0290         if (importDescription.isEmpty()) {
0291             continue;
0292         }
0293 
0294         // Try to find a rule with the same description to replace
0295         int newIndex = -2;
0296         for (int index = 0; index < m_ruleBookModel->rowCount(); index++) {
0297             if (m_ruleBookModel->descriptionAt(index) == importDescription) {
0298                 newIndex = index;
0299                 break;
0300             }
0301         }
0302 
0303         if (remove) {
0304             m_ruleBookModel->removeRow(newIndex);
0305             continue;
0306         }
0307 
0308         if (newIndex < 0) {
0309             newIndex = m_ruleBookModel->rowCount();
0310             m_ruleBookModel->insertRow(newIndex);
0311         }
0312 
0313         m_ruleBookModel->setRuleSettingsAt(newIndex, settings);
0314 
0315         // Reset rule editor if the current rule changed when importing
0316         if (m_editIndex.row() == newIndex) {
0317             m_rulesModel->setSettings(m_ruleBookModel->ruleSettingsAt(newIndex));
0318         }
0319     }
0320 
0321     updateNeedsSave();
0322 }
0323 
0324 // Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp`
0325 QModelIndex KCMKWinRules::findRuleWithProperties(const QVariantMap &info, bool wholeApp) const
0326 {
0327     const QString wmclass_class = info.value("resourceClass").toString();
0328     const QString wmclass_name = info.value("resourceName").toString();
0329     const QString role = info.value("role").toString();
0330     const NET::WindowType type = static_cast<NET::WindowType>(info.value("type").toInt());
0331     const QString title = info.value("caption").toString();
0332     const QString machine = info.value("clientMachine").toString();
0333     const bool isLocalHost = info.value("localhost").toBool();
0334 
0335     int bestMatchRow = -1;
0336     int bestMatchScore = 0;
0337 
0338     for (int row = 0; row < m_ruleBookModel->rowCount(); row++) {
0339         const RuleSettings *settings = m_ruleBookModel->ruleSettingsAt(row);
0340 
0341         // If the rule doesn't match try the next one
0342         const Rules rule = Rules(settings);
0343         /* clang-format off */
0344         if (!rule.matchWMClass(wmclass_class, wmclass_name)
0345                 || !rule.matchType(type)
0346                 || !rule.matchRole(role)
0347                 || !rule.matchTitle(title)
0348                 || !rule.matchClientMachine(machine, isLocalHost)) {
0349             continue;
0350         }
0351         /* clang-format on */
0352 
0353         if (settings->wmclassmatch() != Rules::ExactMatch) {
0354             continue; // too generic
0355         }
0356 
0357         // Now that the rule matches the window, check the quality of the match
0358         // It stablishes a quality depending on the match policy of the rule
0359         int score = 0;
0360         bool generic = true;
0361 
0362         // from now on, it matches the app - now try to match for a specific window
0363         if (settings->wmclasscomplete()) {
0364             score += 1;
0365             generic = false; // this can be considered specific enough (old X apps)
0366         }
0367         if (!wholeApp) {
0368             if (settings->windowrolematch() != Rules::UnimportantMatch) {
0369                 score += settings->windowrolematch() == Rules::ExactMatch ? 5 : 1;
0370                 generic = false;
0371             }
0372             if (settings->titlematch() != Rules::UnimportantMatch) {
0373                 score += settings->titlematch() == Rules::ExactMatch ? 3 : 1;
0374                 generic = false;
0375             }
0376             if (settings->types() != NET::AllTypesMask) {
0377                 // Checks that type fits the mask, and only one of the types
0378                 int bits = 0;
0379                 for (unsigned int bit = 1; bit < 1U << 31; bit <<= 1) {
0380                     if (settings->types() & bit) {
0381                         ++bits;
0382                     }
0383                 }
0384                 if (bits == 1) {
0385                     score += 2;
0386                 }
0387             }
0388             if (generic) { // ignore generic rules, use only the ones that are for this window
0389                 continue;
0390             }
0391         } else {
0392             if (settings->types() == NET::AllTypesMask) {
0393                 score += 2;
0394             }
0395         }
0396 
0397         if (score > bestMatchScore) {
0398             bestMatchRow = row;
0399             bestMatchScore = score;
0400         }
0401     }
0402 
0403     if (bestMatchRow < 0) {
0404         return QModelIndex();
0405     }
0406     return m_ruleBookModel->index(bestMatchRow);
0407 }
0408 
0409 // Code adapted from original `findRule()` method in `kwin_rules_dialog::main.cpp`
0410 void KCMKWinRules::fillSettingsFromProperties(RuleSettings *settings, const QVariantMap &info, bool wholeApp) const
0411 {
0412     const QString wmclass_class = info.value("resourceClass").toString();
0413     const QString wmclass_name = info.value("resourceName").toString();
0414     const QString role = info.value("role").toString();
0415     const NET::WindowType type = static_cast<NET::WindowType>(info.value("type").toInt());
0416     const QString title = info.value("caption").toString();
0417     const QString machine = info.value("clientMachine").toString();
0418 
0419     settings->setDefaults();
0420 
0421     if (wholeApp) {
0422         if (!wmclass_class.isEmpty()) {
0423             settings->setDescription(i18n("Application settings for %1", wmclass_class));
0424         }
0425         // TODO maybe exclude some types? If yes, then also exclude them when searching.
0426         settings->setTypes(NET::AllTypesMask);
0427         settings->setTitlematch(Rules::UnimportantMatch);
0428         settings->setClientmachine(machine); // set, but make unimportant
0429         settings->setClientmachinematch(Rules::UnimportantMatch);
0430         settings->setWindowrolematch(Rules::UnimportantMatch);
0431         if (wmclass_name == wmclass_class) {
0432             settings->setWmclasscomplete(false);
0433             settings->setWmclass(wmclass_class);
0434             settings->setWmclassmatch(Rules::ExactMatch);
0435         } else {
0436             // WM_CLASS components differ - perhaps the app got -name argument
0437             settings->setWmclasscomplete(true);
0438             settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class));
0439             settings->setWmclassmatch(Rules::ExactMatch);
0440         }
0441         return;
0442     }
0443 
0444     if (!wmclass_class.isEmpty()) {
0445         settings->setDescription(i18n("Window settings for %1", wmclass_class));
0446     }
0447     if (type == NET::Unknown) {
0448         settings->setTypes(NET::NormalMask);
0449     } else {
0450         settings->setTypes(NET::WindowTypeMask(1 << type)); // convert type to its mask
0451     }
0452     settings->setTitle(title); // set, but make unimportant
0453     settings->setTitlematch(Rules::UnimportantMatch);
0454     settings->setClientmachine(machine); // set, but make unimportant
0455     settings->setClientmachinematch(Rules::UnimportantMatch);
0456     if (!role.isEmpty() && role != "unknown" && role != "unnamed") { // Qt sets this if not specified
0457         settings->setWindowrole(role);
0458         settings->setWindowrolematch(Rules::ExactMatch);
0459         if (wmclass_name == wmclass_class) {
0460             settings->setWmclasscomplete(false);
0461             settings->setWmclass(wmclass_class);
0462             settings->setWmclassmatch(Rules::ExactMatch);
0463         } else {
0464             // WM_CLASS components differ - perhaps the app got -name argument
0465             settings->setWmclasscomplete(true);
0466             settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class));
0467             settings->setWmclassmatch(Rules::ExactMatch);
0468         }
0469     } else { // no role set
0470         if (wmclass_name != wmclass_class) {
0471             // WM_CLASS components differ - perhaps the app got -name argument
0472             settings->setWmclasscomplete(true);
0473             settings->setWmclass(QStringLiteral("%1 %2").arg(wmclass_name, wmclass_class));
0474             settings->setWmclassmatch(Rules::ExactMatch);
0475         } else {
0476             // This is a window that has no role set, and both components of WM_CLASS
0477             // match (possibly only differing in case), which most likely means either
0478             // the application doesn't give a damn about distinguishing its various
0479             // windows, or it's an app that uses role for that, but this window
0480             // lacks it for some reason. Use non-complete WM_CLASS matching, also
0481             // include window title in the matching, and pray it causes many more positive
0482             // matches than negative matches.
0483             // WM_CLASS components differ - perhaps the app got -name argument
0484             settings->setTitlematch(Rules::ExactMatch);
0485             settings->setWmclasscomplete(false);
0486             settings->setWmclass(wmclass_class);
0487             settings->setWmclassmatch(Rules::ExactMatch);
0488         }
0489     }
0490 }
0491 
0492 K_PLUGIN_CLASS_WITH_JSON(KCMKWinRules, "kcm_kwinrules.json");
0493 
0494 } // namespace
0495 
0496 #include "kcmrules.moc"