File indexing completed on 2024-05-26 05:33:04

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