File indexing completed on 2024-11-10 04:56:49
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"