12#include <config-kwin.h>
14#include <kwin_effects_interface.h>
17#include <KCMultiDialog>
18#include <KConfigGroup>
19#include <KLocalizedString>
20#include <KPackage/PackageLoader>
21#include <KPluginMetaData>
23#include <QDBusConnection>
24#include <QDBusInterface>
25#include <QDBusMessage>
26#include <QDBusPendingCall>
27#include <QDirIterator>
28#include <QStandardPaths>
33static QString translatedCategory(
const QString &category)
35 static const QList<QString> knownCategories = {
36 QStringLiteral(
"Accessibility"),
37 QStringLiteral(
"Appearance"),
38 QStringLiteral(
"Focus"),
39 QStringLiteral(
"Show Desktop Animation"),
40 QStringLiteral(
"Tools"),
41 QStringLiteral(
"Virtual Desktop Switching Animation"),
42 QStringLiteral(
"Window Management"),
43 QStringLiteral(
"Window Open/Close Animation")};
45 static const QList<QString> translatedCategories = {
46 i18nc(
"Category of Desktop Effects, used as section header",
"Accessibility"),
47 i18nc(
"Category of Desktop Effects, used as section header",
"Appearance"),
48 i18nc(
"Category of Desktop Effects, used as section header",
"Focus"),
49 i18nc(
"Category of Desktop Effects, used as section header",
"Peek at Desktop Animation"),
50 i18nc(
"Category of Desktop Effects, used as section header",
"Tools"),
51 i18nc(
"Category of Desktop Effects, used as section header",
"Virtual Desktop Switching Animation"),
52 i18nc(
"Category of Desktop Effects, used as section header",
"Window Management"),
53 i18nc(
"Category of Desktop Effects, used as section header",
"Window Open/Close Animation")};
55 const int index = knownCategories.indexOf(category);
57 qDebug() <<
"Unknown category '" << category <<
"' and thus not translated";
61 return translatedCategories[index];
70 : QAbstractItemModel(parent)
100 if (
parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_effects.count()) {
104 return createIndex(row, column);
122 return m_effects.count();
127 if (!
index.isValid()) {
133 case Qt::DisplayRole:
153 return static_cast<int>(effect.
status);
179 if (!
index.isValid()) {
180 return QAbstractItemModel::setData(
index, value, role);
194 for (
int i = 0; i < m_effects.size(); ++i) {
195 if (i ==
index.row()) {
202 Q_EMIT dataChanged(this->
index(i, 0), this->
index(i, 0));
210 return QAbstractItemModel::setData(
index, value, role);
213void EffectsModel::loadBuiltInEffects(
const KConfigGroup &kwinConfig)
215 const QString rootDirectory = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
216 QStringLiteral(
"kwin/builtin-effects"),
217 QStandardPaths::LocateDirectory);
219 const QStringList nameFilters{QStringLiteral(
"*.json")};
220 QDirIterator it(rootDirectory, nameFilters, QDir::Files);
221 while (it.hasNext()) {
224 const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(it.filePath());
225 if (!metaData.isValid()) {
230 effect.name = metaData.name();
231 effect.description = metaData.description();
232 effect.authorName = i18n(
"KWin development team");
233 effect.authorEmail = QString();
234 effect.license = metaData.license();
235 effect.version = metaData.version();
236 effect.untranslatedCategory = metaData.category();
237 effect.category = translatedCategory(metaData.category());
238 effect.serviceName = metaData.pluginId();
239 effect.iconName = metaData.iconName();
240 effect.enabledByDefault = metaData.isEnabledByDefault();
241 effect.supported =
true;
242 effect.enabledByDefaultFunction =
false;
243 effect.internal =
false;
244 effect.configModule = metaData.value(QStringLiteral(
"X-KDE-ConfigModule"));
245 effect.website = QUrl(metaData.website());
247 if (metaData.rawData().contains(
"org.kde.kwin.effect")) {
248 const QJsonObject d(metaData.rawData().value(
"org.kde.kwin.effect").toObject());
249 effect.exclusiveGroup = d.value(
"exclusiveGroup").toString();
250 effect.video = QUrl::fromUserInput(d.value(
"video").toString());
251 effect.enabledByDefaultFunction = d.value(
"enabledByDefaultMethod").toBool();
252 effect.internal = d.value(
"internal").toBool();
255 const QString enabledKey = QStringLiteral(
"%1Enabled").arg(effect.serviceName);
256 if (kwinConfig.hasKey(enabledKey)) {
257 effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName +
"Enabled", effect.enabledByDefault));
258 }
else if (effect.enabledByDefaultFunction) {
261 effect.status = effectStatus(effect.enabledByDefault);
264 effect.originalStatus = effect.status;
267 m_pendingEffects << effect;
272void EffectsModel::loadJavascriptEffects(
const KConfigGroup &kwinConfig)
274 const auto plugins = KPackage::PackageLoader::self()->listPackages(
275 QStringLiteral(
"KWin/Effect"),
276 QStringLiteral(
"kwin/effects"));
277 for (
const KPluginMetaData &plugin : plugins) {
280 effect.name = plugin.name();
281 effect.description = plugin.description();
282 const auto authors = plugin.authors();
283 effect.authorName = !authors.isEmpty() ? authors.first().name() : QString();
284 effect.authorEmail = !authors.isEmpty() ? authors.first().emailAddress() : QString();
285 effect.license = plugin.license();
286 effect.version = plugin.version();
287 effect.untranslatedCategory = plugin.category();
288 effect.category = translatedCategory(plugin.category());
289 effect.serviceName = plugin.pluginId();
290 effect.iconName = plugin.iconName();
291 effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName +
"Enabled", plugin.isEnabledByDefault()));
292 effect.originalStatus = effect.status;
293 effect.enabledByDefault = plugin.isEnabledByDefault();
294 effect.enabledByDefaultFunction =
false;
295 effect.video = QUrl(plugin.value(QStringLiteral(
"X-KWin-Video-Url")));
296 effect.website = QUrl(plugin.website());
297 effect.supported =
true;
298 effect.exclusiveGroup = plugin.value(QStringLiteral(
"X-KWin-Exclusive-Category"));
299 effect.internal = plugin.value(QStringLiteral(
"X-KWin-Internal"),
false);
301 if (
const QString configModule = plugin.value(QStringLiteral(
"X-KDE-ConfigModule")); !configModule.isEmpty()) {
302 if (configModule == QStringLiteral(
"kcm_kwin4_genericscripted")) {
303 const QString xmlFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String(
"kwin/effects/") + plugin.pluginId() + QLatin1String(
"/contents/config/main.xml"));
304 const QString uiFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String(
"kwin/effects/") + plugin.pluginId() + QLatin1String(
"/contents/ui/config.ui"));
305 if (QFileInfo::exists(xmlFile) && QFileInfo::exists(uiFile)) {
306 effect.configModule = configModule;
307 effect.configArgs = QVariantList{plugin.pluginId(), QStringLiteral(
"KWin/Effect")};
310 effect.configModule = configModule;
315 m_pendingEffects << effect;
320void EffectsModel::loadPluginEffects(
const KConfigGroup &kwinConfig)
322 const auto pluginEffects = KPluginMetaData::findPlugins(QStringLiteral(
"kwin/effects/plugins"));
323 for (
const KPluginMetaData &pluginEffect : pluginEffects) {
324 if (!pluginEffect.isValid()) {
328 effect.name = pluginEffect.name();
329 effect.description = pluginEffect.description();
330 effect.license = pluginEffect.license();
331 effect.version = pluginEffect.version();
332 effect.untranslatedCategory = pluginEffect.category();
333 effect.category = translatedCategory(pluginEffect.category());
334 effect.serviceName = pluginEffect.pluginId();
335 effect.iconName = pluginEffect.iconName();
336 effect.enabledByDefault = pluginEffect.isEnabledByDefault();
337 effect.supported =
true;
338 effect.enabledByDefaultFunction =
false;
339 effect.internal =
false;
340 effect.configModule = pluginEffect.value(QStringLiteral(
"X-KDE-ConfigModule"));
342 for (
int i = 0; i < pluginEffect.authors().count(); ++i) {
343 effect.authorName.append(pluginEffect.authors().at(i).name());
344 effect.authorEmail.append(pluginEffect.authors().at(i).emailAddress());
345 if (i + 1 < pluginEffect.authors().count()) {
346 effect.authorName.append(
", ");
347 effect.authorEmail.append(
", ");
351 if (pluginEffect.rawData().contains(
"org.kde.kwin.effect")) {
352 const QJsonObject d(pluginEffect.rawData().value(
"org.kde.kwin.effect").toObject());
353 effect.exclusiveGroup = d.value(
"exclusiveGroup").toString();
354 effect.video = QUrl::fromUserInput(d.value(
"video").toString());
355 effect.enabledByDefaultFunction = d.value(
"enabledByDefaultMethod").toBool();
358 effect.website = QUrl(pluginEffect.website());
360 const QString enabledKey = QStringLiteral(
"%1Enabled").arg(effect.serviceName);
361 if (kwinConfig.hasKey(enabledKey)) {
362 effect.status = effectStatus(kwinConfig.readEntry(effect.serviceName +
"Enabled", effect.enabledByDefault));
363 }
else if (effect.enabledByDefaultFunction) {
366 effect.status = effectStatus(effect.enabledByDefault);
369 effect.originalStatus = effect.status;
372 m_pendingEffects << effect;
379 KConfigGroup kwinConfig(KSharedConfig::openConfig(
"kwinrc"), QStringLiteral(
"Plugins"));
381 m_pendingEffects.clear();
382 loadBuiltInEffects(kwinConfig);
383 loadJavascriptEffects(kwinConfig);
384 loadPluginEffects(kwinConfig);
386 std::sort(m_pendingEffects.begin(), m_pendingEffects.end(),
388 if (a.category == b.category) {
389 if (a.exclusiveGroup == b.exclusiveGroup) {
390 return a.name < b.name;
392 return a.exclusiveGroup < b.exclusiveGroup;
397 auto commit = [
this,
options] {
398 if (
options == LoadOptions::KeepDirty) {
399 for (
const EffectData &oldEffect : std::as_const(m_effects)) {
400 if (!oldEffect.changed) {
403 auto effectIt = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(),
404 [oldEffect](
const EffectData &data) {
405 return data.serviceName == oldEffect.serviceName;
407 if (effectIt == m_pendingEffects.end()) {
410 effectIt->status = oldEffect.status;
411 effectIt->changed = effectIt->status != effectIt->originalStatus;
416 m_effects = m_pendingEffects;
417 m_pendingEffects.clear();
423 OrgKdeKwinEffectsInterface interface(QStringLiteral(
"org.kde.KWin"),
424 QStringLiteral(
"/Effects"),
425 QDBusConnection::sessionBus());
427 if (interface.isValid()) {
428 QStringList effectNames;
429 effectNames.reserve(m_pendingEffects.count());
430 for (
const EffectData &data : std::as_const(m_pendingEffects)) {
431 effectNames.append(data.serviceName);
434 const int serial = ++m_lastSerial;
436 QDBusPendingCallWatcher *watcher =
new QDBusPendingCallWatcher(interface.areEffectsSupported(effectNames),
this);
437 connect(watcher, &QDBusPendingCallWatcher::finished,
this, [=,
this](QDBusPendingCallWatcher *self) {
440 if (m_lastSerial != serial) {
444 const QDBusPendingReply<QList<bool>> reply = *self;
445 if (reply.isError()) {
450 const QList<bool> supportedValues = reply.value();
451 if (supportedValues.count() != effectNames.count()) {
455 for (
int i = 0; i < effectNames.size(); ++i) {
456 const bool supported = supportedValues.at(i);
457 const QString effectName = effectNames.at(i);
459 auto it = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(),
460 [effectName](
const EffectData &data) {
461 return data.serviceName == effectName;
463 if (it == m_pendingEffects.end()) {
467 if ((*it).supported != supported) {
468 (*it).supported = supported;
479void EffectsModel::updateEffectStatus(
const QModelIndex &rowIndex,
Status effectState)
481 setData(rowIndex,
static_cast<int>(effectState), StatusRole);
484void EffectsModel::save()
486 KConfigGroup kwinConfig(KSharedConfig::openConfig(
"kwinrc"), QStringLiteral(
"Plugins"));
488 QList<EffectData> dirtyEffects;
491 if (!effect.changed) {
495 effect.changed =
false;
496 effect.originalStatus = effect.status;
498 const QString key = effect.serviceName + QStringLiteral(
"Enabled");
499 const bool shouldEnable = (effect.status != Status::Disabled);
500 const bool restoreToDefault = effect.enabledByDefaultFunction
501 ? effect.status == Status::EnabledUndeterminded
502 : shouldEnable == effect.enabledByDefault;
503 if (restoreToDefault) {
504 kwinConfig.deleteEntry(key);
506 kwinConfig.writeEntry(key, shouldEnable);
509 dirtyEffects.append(effect);
512 if (dirtyEffects.isEmpty()) {
518 OrgKdeKwinEffectsInterface interface(QStringLiteral(
"org.kde.KWin"),
519 QStringLiteral(
"/Effects"),
520 QDBusConnection::sessionBus());
522 if (!interface.isValid()) {
528 auto split = std::partition(dirtyEffects.begin(), dirtyEffects.end(), [](
const EffectData &data) {
529 return data.status == Status::Disabled;
532 for (
auto it = dirtyEffects.begin(); it != split; ++it) {
533 interface.unloadEffect(it->serviceName);
536 for (
auto it = split; it != dirtyEffects.end(); ++it) {
537 interface.loadEffect(it->serviceName);
541void EffectsModel::defaults()
543 for (
int i = 0; i < m_effects.count(); ++i) {
544 const auto &effect = m_effects.at(i);
545 if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) {
546 updateEffectStatus(index(i, 0), Status::EnabledUndeterminded);
547 }
else if (
static_cast<bool>(effect.status) != effect.enabledByDefault) {
548 updateEffectStatus(index(i, 0), effect.enabledByDefault ? Status::Enabled : Status::Disabled);
553bool EffectsModel::isDefaults()
const
555 return std::all_of(m_effects.constBegin(), m_effects.constEnd(), [](
const EffectData &effect) {
556 if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) {
566bool EffectsModel::needsSave()
const
568 return std::any_of(m_effects.constBegin(), m_effects.constEnd(),
574QModelIndex EffectsModel::findByPluginId(
const QString &pluginId)
const
576 auto it = std::find_if(m_effects.constBegin(), m_effects.constEnd(),
578 return data.serviceName == pluginId;
580 if (it == m_effects.constEnd()) {
583 return index(std::distance(m_effects.constBegin(), it), 0);
586void EffectsModel::requestConfigure(
const QModelIndex &index, QWindow *transientParent)
588 if (!index.isValid()) {
592 const EffectData &effect = m_effects.at(index.row());
595 KCMultiDialog *dialog =
new KCMultiDialog();
596 dialog->addModule(KPluginMetaData(QStringLiteral(
"kwin/effects/configs/") + effect.
configModule), effect.
configArgs);
597 dialog->setAttribute(Qt::WA_DeleteOnClose);
599 dialog->windowHandle()->setTransientParent(transientParent);
610#include "moc_effectsmodel.cpp"
int rowCount(const QModelIndex &parent={}) const override
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
QHash< int, QByteArray > roleNames() const override
int columnCount(const QModelIndex &parent={}) const override
virtual bool shouldStore(const EffectData &data) const
QModelIndex index(int row, int column, const QModelIndex &parent={}) const override
EffectsModel(QObject *parent=nullptr)
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override
void load(LoadOptions options=LoadOptions::None)
QModelIndex parent(const QModelIndex &child) const override
@ EnabledByDefaultFunctionRole
bool enabledByDefaultFunction