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"