File indexing completed on 2024-11-10 04:56:46
0001 /* 0002 KWin - the KDE window manager 0003 This file is part of the KDE project. 0004 0005 SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas <kok3rs@gmail.com> 0006 SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org> 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 #include "effectsmodel.h" 0011 0012 #include <config-kwin.h> 0013 0014 #include <kwin_effects_interface.h> 0015 0016 #include <KAboutData> 0017 #include <KCMultiDialog> 0018 #include <KConfigGroup> 0019 #include <KLocalizedString> 0020 #include <KPackage/PackageLoader> 0021 #include <KPluginMetaData> 0022 0023 #include <QDBusConnection> 0024 #include <QDBusInterface> 0025 #include <QDBusMessage> 0026 #include <QDBusPendingCall> 0027 #include <QDirIterator> 0028 #include <QStandardPaths> 0029 0030 namespace KWin 0031 { 0032 0033 static QString translatedCategory(const QString &category) 0034 { 0035 static const QList<QString> knownCategories = { 0036 QStringLiteral("Accessibility"), 0037 QStringLiteral("Appearance"), 0038 QStringLiteral("Focus"), 0039 QStringLiteral("Show Desktop Animation"), 0040 QStringLiteral("Tools"), 0041 QStringLiteral("Virtual Desktop Switching Animation"), 0042 QStringLiteral("Window Management"), 0043 QStringLiteral("Window Open/Close Animation")}; 0044 0045 static const QList<QString> translatedCategories = { 0046 i18nc("Category of Desktop Effects, used as section header", "Accessibility"), 0047 i18nc("Category of Desktop Effects, used as section header", "Appearance"), 0048 i18nc("Category of Desktop Effects, used as section header", "Focus"), 0049 i18nc("Category of Desktop Effects, used as section header", "Peek at Desktop Animation"), 0050 i18nc("Category of Desktop Effects, used as section header", "Tools"), 0051 i18nc("Category of Desktop Effects, used as section header", "Virtual Desktop Switching Animation"), 0052 i18nc("Category of Desktop Effects, used as section header", "Window Management"), 0053 i18nc("Category of Desktop Effects, used as section header", "Window Open/Close Animation")}; 0054 0055 const int index = knownCategories.indexOf(category); 0056 if (index == -1) { 0057 qDebug() << "Unknown category '" << category << "' and thus not translated"; 0058 return category; 0059 } 0060 0061 return translatedCategories[index]; 0062 } 0063 0064 static EffectsModel::Status effectStatus(bool enabled) 0065 { 0066 return enabled ? EffectsModel::Status::Enabled : EffectsModel::Status::Disabled; 0067 } 0068 0069 EffectsModel::EffectsModel(QObject *parent) 0070 : QAbstractItemModel(parent) 0071 { 0072 } 0073 0074 QHash<int, QByteArray> EffectsModel::roleNames() const 0075 { 0076 QHash<int, QByteArray> roleNames; 0077 roleNames[NameRole] = "NameRole"; 0078 roleNames[DescriptionRole] = "DescriptionRole"; 0079 roleNames[AuthorNameRole] = "AuthorNameRole"; 0080 roleNames[AuthorEmailRole] = "AuthorEmailRole"; 0081 roleNames[LicenseRole] = "LicenseRole"; 0082 roleNames[VersionRole] = "VersionRole"; 0083 roleNames[CategoryRole] = "CategoryRole"; 0084 roleNames[ServiceNameRole] = "ServiceNameRole"; 0085 roleNames[IconNameRole] = "IconNameRole"; 0086 roleNames[StatusRole] = "StatusRole"; 0087 roleNames[VideoRole] = "VideoRole"; 0088 roleNames[WebsiteRole] = "WebsiteRole"; 0089 roleNames[SupportedRole] = "SupportedRole"; 0090 roleNames[ExclusiveRole] = "ExclusiveRole"; 0091 roleNames[ConfigurableRole] = "ConfigurableRole"; 0092 roleNames[EnabledByDefaultRole] = "EnabledByDefaultRole"; 0093 roleNames[EnabledByDefaultFunctionRole] = "EnabledByDefaultFunctionRole"; 0094 roleNames[ConfigModuleRole] = "ConfigModuleRole"; 0095 return roleNames; 0096 } 0097 0098 QModelIndex EffectsModel::index(int row, int column, const QModelIndex &parent) const 0099 { 0100 if (parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_effects.count()) { 0101 return {}; 0102 } 0103 0104 return createIndex(row, column); 0105 } 0106 0107 QModelIndex EffectsModel::parent(const QModelIndex &child) const 0108 { 0109 return {}; 0110 } 0111 0112 int EffectsModel::columnCount(const QModelIndex &parent) const 0113 { 0114 return 1; 0115 } 0116 0117 int EffectsModel::rowCount(const QModelIndex &parent) const 0118 { 0119 if (parent.isValid()) { 0120 return 0; 0121 } 0122 return m_effects.count(); 0123 } 0124 0125 QVariant EffectsModel::data(const QModelIndex &index, int role) const 0126 { 0127 if (!index.isValid()) { 0128 return {}; 0129 } 0130 0131 const EffectData effect = m_effects.at(index.row()); 0132 switch (role) { 0133 case Qt::DisplayRole: 0134 case NameRole: 0135 return effect.name; 0136 case DescriptionRole: 0137 return effect.description; 0138 case AuthorNameRole: 0139 return effect.authorName; 0140 case AuthorEmailRole: 0141 return effect.authorEmail; 0142 case LicenseRole: 0143 return effect.license; 0144 case VersionRole: 0145 return effect.version; 0146 case CategoryRole: 0147 return effect.category; 0148 case ServiceNameRole: 0149 return effect.serviceName; 0150 case IconNameRole: 0151 return effect.iconName; 0152 case StatusRole: 0153 return static_cast<int>(effect.status); 0154 case VideoRole: 0155 return effect.video; 0156 case WebsiteRole: 0157 return effect.website; 0158 case SupportedRole: 0159 return effect.supported; 0160 case ExclusiveRole: 0161 return effect.exclusiveGroup; 0162 case InternalRole: 0163 return effect.internal; 0164 case ConfigurableRole: 0165 return !effect.configModule.isEmpty(); 0166 case EnabledByDefaultRole: 0167 return effect.enabledByDefault; 0168 case EnabledByDefaultFunctionRole: 0169 return effect.enabledByDefaultFunction; 0170 case ConfigModuleRole: 0171 return effect.configModule; 0172 default: 0173 return {}; 0174 } 0175 } 0176 0177 bool EffectsModel::setData(const QModelIndex &index, const QVariant &value, int role) 0178 { 0179 if (!index.isValid()) { 0180 return QAbstractItemModel::setData(index, value, role); 0181 } 0182 0183 if (role == StatusRole) { 0184 // note: whenever the StatusRole is modified (even to the same value) the entry 0185 // gets marked as changed and will get saved to the config file. This means the 0186 // config file could get polluted 0187 EffectData &data = m_effects[index.row()]; 0188 data.status = Status(value.toInt()); 0189 data.changed = data.status != data.originalStatus; 0190 Q_EMIT dataChanged(index, index); 0191 0192 if (data.status == Status::Enabled && !data.exclusiveGroup.isEmpty()) { 0193 // need to disable all other exclusive effects in the same category 0194 for (int i = 0; i < m_effects.size(); ++i) { 0195 if (i == index.row()) { 0196 continue; 0197 } 0198 EffectData &otherData = m_effects[i]; 0199 if (otherData.exclusiveGroup == data.exclusiveGroup) { 0200 otherData.status = Status::Disabled; 0201 otherData.changed = otherData.status != otherData.originalStatus; 0202 Q_EMIT dataChanged(this->index(i, 0), this->index(i, 0)); 0203 } 0204 } 0205 } 0206 0207 return true; 0208 } 0209 0210 return QAbstractItemModel::setData(index, value, role); 0211 } 0212 0213 void EffectsModel::loadBuiltInEffects(const KConfigGroup &kwinConfig) 0214 { 0215 const QString rootDirectory = QStandardPaths::locate(QStandardPaths::GenericDataLocation, 0216 QStringLiteral("kwin/builtin-effects"), 0217 QStandardPaths::LocateDirectory); 0218 0219 const QStringList nameFilters{QStringLiteral("*.json")}; 0220 QDirIterator it(rootDirectory, nameFilters, QDir::Files); 0221 while (it.hasNext()) { 0222 it.next(); 0223 0224 const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(it.filePath()); 0225 if (!metaData.isValid()) { 0226 continue; 0227 } 0228 0229 EffectData effect; 0230 effect.name = metaData.name(); 0231 effect.description = metaData.description(); 0232 effect.authorName = i18n("KWin development team"); 0233 effect.authorEmail = QString(); // not used at all 0234 effect.license = metaData.license(); 0235 effect.version = metaData.version(); 0236 effect.untranslatedCategory = metaData.category(); 0237 effect.category = translatedCategory(metaData.category()); 0238 effect.serviceName = metaData.pluginId(); 0239 effect.iconName = metaData.iconName(); 0240 effect.enabledByDefault = metaData.isEnabledByDefault(); 0241 effect.supported = true; 0242 effect.enabledByDefaultFunction = false; 0243 effect.internal = false; 0244 effect.configModule = metaData.value(QStringLiteral("X-KDE-ConfigModule")); 0245 effect.website = QUrl(metaData.website()); 0246 0247 if (metaData.rawData().contains("org.kde.kwin.effect")) { 0248 const QJsonObject d(metaData.rawData().value("org.kde.kwin.effect").toObject()); 0249 effect.exclusiveGroup = d.value("exclusiveGroup").toString(); 0250 effect.video = QUrl::fromUserInput(d.value("video").toString()); 0251 effect.enabledByDefaultFunction = d.value("enabledByDefaultMethod").toBool(); 0252 effect.internal = d.value("internal").toBool(); 0253 } 0254 0255 const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); 0256 if (kwinConfig.hasKey(enabledKey)) { 0257 effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); 0258 } else if (effect.enabledByDefaultFunction) { 0259 effect.status = Status::EnabledUndeterminded; 0260 } else { 0261 effect.status = effectStatus(effect.enabledByDefault); 0262 } 0263 0264 effect.originalStatus = effect.status; 0265 0266 if (shouldStore(effect)) { 0267 m_pendingEffects << effect; 0268 } 0269 } 0270 } 0271 0272 void EffectsModel::loadJavascriptEffects(const KConfigGroup &kwinConfig) 0273 { 0274 const auto plugins = KPackage::PackageLoader::self()->listPackages( 0275 QStringLiteral("KWin/Effect"), 0276 QStringLiteral("kwin/effects")); 0277 for (const KPluginMetaData &plugin : plugins) { 0278 EffectData effect; 0279 0280 effect.name = plugin.name(); 0281 effect.description = plugin.description(); 0282 const auto authors = plugin.authors(); 0283 effect.authorName = !authors.isEmpty() ? authors.first().name() : QString(); 0284 effect.authorEmail = !authors.isEmpty() ? authors.first().emailAddress() : QString(); 0285 effect.license = plugin.license(); 0286 effect.version = plugin.version(); 0287 effect.untranslatedCategory = plugin.category(); 0288 effect.category = translatedCategory(plugin.category()); 0289 effect.serviceName = plugin.pluginId(); 0290 effect.iconName = plugin.iconName(); 0291 effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", plugin.isEnabledByDefault())); 0292 effect.originalStatus = effect.status; 0293 effect.enabledByDefault = plugin.isEnabledByDefault(); 0294 effect.enabledByDefaultFunction = false; 0295 effect.video = QUrl(plugin.value(QStringLiteral("X-KWin-Video-Url"))); 0296 effect.website = QUrl(plugin.website()); 0297 effect.supported = true; 0298 effect.exclusiveGroup = plugin.value(QStringLiteral("X-KWin-Exclusive-Category")); 0299 effect.internal = plugin.value(QStringLiteral("X-KWin-Internal"), false); 0300 0301 if (const QString configModule = plugin.value(QStringLiteral("X-KDE-ConfigModule")); !configModule.isEmpty()) { 0302 if (configModule == QStringLiteral("kcm_kwin4_genericscripted")) { 0303 const QString xmlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + plugin.pluginId() + QLatin1String("/contents/config/main.xml")); 0304 const QString uiFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("kwin/effects/") + plugin.pluginId() + QLatin1String("/contents/ui/config.ui")); 0305 if (QFileInfo::exists(xmlFile) && QFileInfo::exists(uiFile)) { 0306 effect.configModule = configModule; 0307 effect.configArgs = QVariantList{plugin.pluginId(), QStringLiteral("KWin/Effect")}; 0308 } 0309 } else { 0310 effect.configModule = configModule; 0311 } 0312 } 0313 0314 if (shouldStore(effect)) { 0315 m_pendingEffects << effect; 0316 } 0317 } 0318 } 0319 0320 void EffectsModel::loadPluginEffects(const KConfigGroup &kwinConfig) 0321 { 0322 const auto pluginEffects = KPluginMetaData::findPlugins(QStringLiteral("kwin/effects/plugins")); 0323 for (const KPluginMetaData &pluginEffect : pluginEffects) { 0324 if (!pluginEffect.isValid()) { 0325 continue; 0326 } 0327 EffectData effect; 0328 effect.name = pluginEffect.name(); 0329 effect.description = pluginEffect.description(); 0330 effect.license = pluginEffect.license(); 0331 effect.version = pluginEffect.version(); 0332 effect.untranslatedCategory = pluginEffect.category(); 0333 effect.category = translatedCategory(pluginEffect.category()); 0334 effect.serviceName = pluginEffect.pluginId(); 0335 effect.iconName = pluginEffect.iconName(); 0336 effect.enabledByDefault = pluginEffect.isEnabledByDefault(); 0337 effect.supported = true; 0338 effect.enabledByDefaultFunction = false; 0339 effect.internal = false; 0340 effect.configModule = pluginEffect.value(QStringLiteral("X-KDE-ConfigModule")); 0341 0342 for (int i = 0; i < pluginEffect.authors().count(); ++i) { 0343 effect.authorName.append(pluginEffect.authors().at(i).name()); 0344 effect.authorEmail.append(pluginEffect.authors().at(i).emailAddress()); 0345 if (i + 1 < pluginEffect.authors().count()) { 0346 effect.authorName.append(", "); 0347 effect.authorEmail.append(", "); 0348 } 0349 } 0350 0351 if (pluginEffect.rawData().contains("org.kde.kwin.effect")) { 0352 const QJsonObject d(pluginEffect.rawData().value("org.kde.kwin.effect").toObject()); 0353 effect.exclusiveGroup = d.value("exclusiveGroup").toString(); 0354 effect.video = QUrl::fromUserInput(d.value("video").toString()); 0355 effect.enabledByDefaultFunction = d.value("enabledByDefaultMethod").toBool(); 0356 } 0357 0358 effect.website = QUrl(pluginEffect.website()); 0359 0360 const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); 0361 if (kwinConfig.hasKey(enabledKey)) { 0362 effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); 0363 } else if (effect.enabledByDefaultFunction) { 0364 effect.status = Status::EnabledUndeterminded; 0365 } else { 0366 effect.status = effectStatus(effect.enabledByDefault); 0367 } 0368 0369 effect.originalStatus = effect.status; 0370 0371 if (shouldStore(effect)) { 0372 m_pendingEffects << effect; 0373 } 0374 } 0375 } 0376 0377 void EffectsModel::load(LoadOptions options) 0378 { 0379 KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins")); 0380 0381 m_pendingEffects.clear(); 0382 loadBuiltInEffects(kwinConfig); 0383 loadJavascriptEffects(kwinConfig); 0384 loadPluginEffects(kwinConfig); 0385 0386 std::sort(m_pendingEffects.begin(), m_pendingEffects.end(), 0387 [](const EffectData &a, const EffectData &b) { 0388 if (a.category == b.category) { 0389 if (a.exclusiveGroup == b.exclusiveGroup) { 0390 return a.name < b.name; 0391 } 0392 return a.exclusiveGroup < b.exclusiveGroup; 0393 } 0394 return a.category < b.category; 0395 }); 0396 0397 auto commit = [this, options] { 0398 if (options == LoadOptions::KeepDirty) { 0399 for (const EffectData &oldEffect : std::as_const(m_effects)) { 0400 if (!oldEffect.changed) { 0401 continue; 0402 } 0403 auto effectIt = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(), 0404 [oldEffect](const EffectData &data) { 0405 return data.serviceName == oldEffect.serviceName; 0406 }); 0407 if (effectIt == m_pendingEffects.end()) { 0408 continue; 0409 } 0410 effectIt->status = oldEffect.status; 0411 effectIt->changed = effectIt->status != effectIt->originalStatus; 0412 } 0413 } 0414 0415 beginResetModel(); 0416 m_effects = m_pendingEffects; 0417 m_pendingEffects.clear(); 0418 endResetModel(); 0419 0420 Q_EMIT loaded(); 0421 }; 0422 0423 OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), 0424 QStringLiteral("/Effects"), 0425 QDBusConnection::sessionBus()); 0426 0427 if (interface.isValid()) { 0428 QStringList effectNames; 0429 effectNames.reserve(m_pendingEffects.count()); 0430 for (const EffectData &data : std::as_const(m_pendingEffects)) { 0431 effectNames.append(data.serviceName); 0432 } 0433 0434 const int serial = ++m_lastSerial; 0435 0436 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(interface.areEffectsSupported(effectNames), this); 0437 connect(watcher, &QDBusPendingCallWatcher::finished, this, [=, this](QDBusPendingCallWatcher *self) { 0438 self->deleteLater(); 0439 0440 if (m_lastSerial != serial) { 0441 return; 0442 } 0443 0444 const QDBusPendingReply<QList<bool>> reply = *self; 0445 if (reply.isError()) { 0446 commit(); 0447 return; 0448 } 0449 0450 const QList<bool> supportedValues = reply.value(); 0451 if (supportedValues.count() != effectNames.count()) { 0452 return; 0453 } 0454 0455 for (int i = 0; i < effectNames.size(); ++i) { 0456 const bool supported = supportedValues.at(i); 0457 const QString effectName = effectNames.at(i); 0458 0459 auto it = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(), 0460 [effectName](const EffectData &data) { 0461 return data.serviceName == effectName; 0462 }); 0463 if (it == m_pendingEffects.end()) { 0464 continue; 0465 } 0466 0467 if ((*it).supported != supported) { 0468 (*it).supported = supported; 0469 } 0470 } 0471 0472 commit(); 0473 }); 0474 } else { 0475 commit(); 0476 } 0477 } 0478 0479 void EffectsModel::updateEffectStatus(const QModelIndex &rowIndex, Status effectState) 0480 { 0481 setData(rowIndex, static_cast<int>(effectState), StatusRole); 0482 } 0483 0484 void EffectsModel::save() 0485 { 0486 KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins")); 0487 0488 QList<EffectData> dirtyEffects; 0489 0490 for (EffectData &effect : m_effects) { 0491 if (!effect.changed) { 0492 continue; 0493 } 0494 0495 effect.changed = false; 0496 effect.originalStatus = effect.status; 0497 0498 const QString key = effect.serviceName + QStringLiteral("Enabled"); 0499 const bool shouldEnable = (effect.status != Status::Disabled); 0500 const bool restoreToDefault = effect.enabledByDefaultFunction 0501 ? effect.status == Status::EnabledUndeterminded 0502 : shouldEnable == effect.enabledByDefault; 0503 if (restoreToDefault) { 0504 kwinConfig.deleteEntry(key); 0505 } else { 0506 kwinConfig.writeEntry(key, shouldEnable); 0507 } 0508 0509 dirtyEffects.append(effect); 0510 } 0511 0512 if (dirtyEffects.isEmpty()) { 0513 return; 0514 } 0515 0516 kwinConfig.sync(); 0517 0518 OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), 0519 QStringLiteral("/Effects"), 0520 QDBusConnection::sessionBus()); 0521 0522 if (!interface.isValid()) { 0523 return; 0524 } 0525 0526 // Unload effects first, it's need to ensure that switching between mutually exclusive 0527 // effects works as expected, for example so global shortcuts are handed over, etc. 0528 auto split = std::partition(dirtyEffects.begin(), dirtyEffects.end(), [](const EffectData &data) { 0529 return data.status == Status::Disabled; 0530 }); 0531 0532 for (auto it = dirtyEffects.begin(); it != split; ++it) { 0533 interface.unloadEffect(it->serviceName); 0534 } 0535 0536 for (auto it = split; it != dirtyEffects.end(); ++it) { 0537 interface.loadEffect(it->serviceName); 0538 } 0539 } 0540 0541 void EffectsModel::defaults() 0542 { 0543 for (int i = 0; i < m_effects.count(); ++i) { 0544 const auto &effect = m_effects.at(i); 0545 if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) { 0546 updateEffectStatus(index(i, 0), Status::EnabledUndeterminded); 0547 } else if (static_cast<bool>(effect.status) != effect.enabledByDefault) { 0548 updateEffectStatus(index(i, 0), effect.enabledByDefault ? Status::Enabled : Status::Disabled); 0549 } 0550 } 0551 } 0552 0553 bool EffectsModel::isDefaults() const 0554 { 0555 return std::all_of(m_effects.constBegin(), m_effects.constEnd(), [](const EffectData &effect) { 0556 if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) { 0557 return false; 0558 } 0559 if (static_cast<bool>(effect.status) != effect.enabledByDefault) { 0560 return false; 0561 } 0562 return true; 0563 }); 0564 } 0565 0566 bool EffectsModel::needsSave() const 0567 { 0568 return std::any_of(m_effects.constBegin(), m_effects.constEnd(), 0569 [](const EffectData &data) { 0570 return data.changed; 0571 }); 0572 } 0573 0574 QModelIndex EffectsModel::findByPluginId(const QString &pluginId) const 0575 { 0576 auto it = std::find_if(m_effects.constBegin(), m_effects.constEnd(), 0577 [pluginId](const EffectData &data) { 0578 return data.serviceName == pluginId; 0579 }); 0580 if (it == m_effects.constEnd()) { 0581 return {}; 0582 } 0583 return index(std::distance(m_effects.constBegin(), it), 0); 0584 } 0585 0586 void EffectsModel::requestConfigure(const QModelIndex &index, QWindow *transientParent) 0587 { 0588 if (!index.isValid()) { 0589 return; 0590 } 0591 0592 const EffectData &effect = m_effects.at(index.row()); 0593 Q_ASSERT(!effect.configModule.isEmpty()); 0594 0595 KCMultiDialog *dialog = new KCMultiDialog(); 0596 dialog->addModule(KPluginMetaData(QStringLiteral("kwin/effects/configs/") + effect.configModule), effect.configArgs); 0597 dialog->setAttribute(Qt::WA_DeleteOnClose); 0598 dialog->winId(); 0599 dialog->windowHandle()->setTransientParent(transientParent); 0600 dialog->show(); 0601 } 0602 0603 bool EffectsModel::shouldStore(const EffectData &data) const 0604 { 0605 return true; 0606 } 0607 0608 } 0609 0610 #include "moc_effectsmodel.cpp"