KWin
Loading...
Searching...
No Matches
effectsmodel.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2013 Antonis Tsiapaliokas <kok3rs@gmail.com>
6 SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10#include "effectsmodel.h"
11
12#include <config-kwin.h>
13
14#include <kwin_effects_interface.h>
15
16#include <KAboutData>
17#include <KCMultiDialog>
18#include <KConfigGroup>
19#include <KLocalizedString>
20#include <KPackage/PackageLoader>
21#include <KPluginMetaData>
22
23#include <QDBusConnection>
24#include <QDBusInterface>
25#include <QDBusMessage>
26#include <QDBusPendingCall>
27#include <QDirIterator>
28#include <QStandardPaths>
29
30namespace KWin
31{
32
33static QString translatedCategory(const QString &category)
34{
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")};
44
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")};
54
55 const int index = knownCategories.indexOf(category);
56 if (index == -1) {
57 qDebug() << "Unknown category '" << category << "' and thus not translated";
58 return category;
59 }
60
61 return translatedCategories[index];
62}
63
64static EffectsModel::Status effectStatus(bool enabled)
65{
67}
68
70 : QAbstractItemModel(parent)
71{
72}
73
74QHash<int, QByteArray> EffectsModel::roleNames() const
75{
76 QHash<int, QByteArray> roleNames;
77 roleNames[NameRole] = "NameRole";
78 roleNames[DescriptionRole] = "DescriptionRole";
79 roleNames[AuthorNameRole] = "AuthorNameRole";
80 roleNames[AuthorEmailRole] = "AuthorEmailRole";
81 roleNames[LicenseRole] = "LicenseRole";
82 roleNames[VersionRole] = "VersionRole";
83 roleNames[CategoryRole] = "CategoryRole";
84 roleNames[ServiceNameRole] = "ServiceNameRole";
85 roleNames[IconNameRole] = "IconNameRole";
86 roleNames[StatusRole] = "StatusRole";
87 roleNames[VideoRole] = "VideoRole";
88 roleNames[WebsiteRole] = "WebsiteRole";
89 roleNames[SupportedRole] = "SupportedRole";
90 roleNames[ExclusiveRole] = "ExclusiveRole";
91 roleNames[ConfigurableRole] = "ConfigurableRole";
92 roleNames[EnabledByDefaultRole] = "EnabledByDefaultRole";
93 roleNames[EnabledByDefaultFunctionRole] = "EnabledByDefaultFunctionRole";
94 roleNames[ConfigModuleRole] = "ConfigModuleRole";
95 return roleNames;
96}
97
98QModelIndex EffectsModel::index(int row, int column, const QModelIndex &parent) const
99{
100 if (parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_effects.count()) {
101 return {};
102 }
103
104 return createIndex(row, column);
105}
106
107QModelIndex EffectsModel::parent(const QModelIndex &child) const
108{
109 return {};
110}
111
112int EffectsModel::columnCount(const QModelIndex &parent) const
113{
114 return 1;
115}
116
117int EffectsModel::rowCount(const QModelIndex &parent) const
118{
119 if (parent.isValid()) {
120 return 0;
121 }
122 return m_effects.count();
123}
124
125QVariant EffectsModel::data(const QModelIndex &index, int role) const
126{
127 if (!index.isValid()) {
128 return {};
129 }
130
131 const EffectData effect = m_effects.at(index.row());
132 switch (role) {
133 case Qt::DisplayRole:
134 case NameRole:
135 return effect.name;
136 case DescriptionRole:
137 return effect.description;
138 case AuthorNameRole:
139 return effect.authorName;
140 case AuthorEmailRole:
141 return effect.authorEmail;
142 case LicenseRole:
143 return effect.license;
144 case VersionRole:
145 return effect.version;
146 case CategoryRole:
147 return effect.category;
148 case ServiceNameRole:
149 return effect.serviceName;
150 case IconNameRole:
151 return effect.iconName;
152 case StatusRole:
153 return static_cast<int>(effect.status);
154 case VideoRole:
155 return effect.video;
156 case WebsiteRole:
157 return effect.website;
158 case SupportedRole:
159 return effect.supported;
160 case ExclusiveRole:
161 return effect.exclusiveGroup;
162 case InternalRole:
163 return effect.internal;
164 case ConfigurableRole:
165 return !effect.configModule.isEmpty();
167 return effect.enabledByDefault;
169 return effect.enabledByDefaultFunction;
170 case ConfigModuleRole:
171 return effect.configModule;
172 default:
173 return {};
174 }
175}
176
177bool EffectsModel::setData(const QModelIndex &index, const QVariant &value, int role)
178{
179 if (!index.isValid()) {
180 return QAbstractItemModel::setData(index, value, role);
181 }
182
183 if (role == StatusRole) {
184 // note: whenever the StatusRole is modified (even to the same value) the entry
185 // gets marked as changed and will get saved to the config file. This means the
186 // config file could get polluted
187 EffectData &data = m_effects[index.row()];
188 data.status = Status(value.toInt());
189 data.changed = data.status != data.originalStatus;
190 Q_EMIT dataChanged(index, index);
191
192 if (data.status == Status::Enabled && !data.exclusiveGroup.isEmpty()) {
193 // need to disable all other exclusive effects in the same category
194 for (int i = 0; i < m_effects.size(); ++i) {
195 if (i == index.row()) {
196 continue;
197 }
198 EffectData &otherData = m_effects[i];
199 if (otherData.exclusiveGroup == data.exclusiveGroup) {
200 otherData.status = Status::Disabled;
201 otherData.changed = otherData.status != otherData.originalStatus;
202 Q_EMIT dataChanged(this->index(i, 0), this->index(i, 0));
203 }
204 }
205 }
206
207 return true;
208 }
209
210 return QAbstractItemModel::setData(index, value, role);
211}
212
213void EffectsModel::loadBuiltInEffects(const KConfigGroup &kwinConfig)
214{
215 const QString rootDirectory = QStandardPaths::locate(QStandardPaths::GenericDataLocation,
216 QStringLiteral("kwin/builtin-effects"),
217 QStandardPaths::LocateDirectory);
218
219 const QStringList nameFilters{QStringLiteral("*.json")};
220 QDirIterator it(rootDirectory, nameFilters, QDir::Files);
221 while (it.hasNext()) {
222 it.next();
223
224 const KPluginMetaData metaData = KPluginMetaData::fromJsonFile(it.filePath());
225 if (!metaData.isValid()) {
226 continue;
227 }
228
229 EffectData effect;
230 effect.name = metaData.name();
231 effect.description = metaData.description();
232 effect.authorName = i18n("KWin development team");
233 effect.authorEmail = QString(); // not used at all
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());
246
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();
253 }
254
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) {
259 effect.status = Status::EnabledUndeterminded;
260 } else {
261 effect.status = effectStatus(effect.enabledByDefault);
262 }
263
264 effect.originalStatus = effect.status;
265
266 if (shouldStore(effect)) {
267 m_pendingEffects << effect;
268 }
269 }
270}
271
272void EffectsModel::loadJavascriptEffects(const KConfigGroup &kwinConfig)
273{
274 const auto plugins = KPackage::PackageLoader::self()->listPackages(
275 QStringLiteral("KWin/Effect"),
276 QStringLiteral("kwin/effects"));
277 for (const KPluginMetaData &plugin : plugins) {
278 EffectData effect;
279
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);
300
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")};
308 }
309 } else {
310 effect.configModule = configModule;
311 }
312 }
313
314 if (shouldStore(effect)) {
315 m_pendingEffects << effect;
316 }
317 }
318}
319
320void EffectsModel::loadPluginEffects(const KConfigGroup &kwinConfig)
321{
322 const auto pluginEffects = KPluginMetaData::findPlugins(QStringLiteral("kwin/effects/plugins"));
323 for (const KPluginMetaData &pluginEffect : pluginEffects) {
324 if (!pluginEffect.isValid()) {
325 continue;
326 }
327 EffectData effect;
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"));
341
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(", ");
348 }
349 }
350
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();
356 }
357
358 effect.website = QUrl(pluginEffect.website());
359
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) {
364 effect.status = Status::EnabledUndeterminded;
365 } else {
366 effect.status = effectStatus(effect.enabledByDefault);
367 }
368
369 effect.originalStatus = effect.status;
370
371 if (shouldStore(effect)) {
372 m_pendingEffects << effect;
373 }
374 }
375}
376
378{
379 KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins"));
380
381 m_pendingEffects.clear();
382 loadBuiltInEffects(kwinConfig);
383 loadJavascriptEffects(kwinConfig);
384 loadPluginEffects(kwinConfig);
385
386 std::sort(m_pendingEffects.begin(), m_pendingEffects.end(),
387 [](const EffectData &a, const EffectData &b) {
388 if (a.category == b.category) {
389 if (a.exclusiveGroup == b.exclusiveGroup) {
390 return a.name < b.name;
391 }
392 return a.exclusiveGroup < b.exclusiveGroup;
393 }
394 return a.category < b.category;
395 });
396
397 auto commit = [this, options] {
398 if (options == LoadOptions::KeepDirty) {
399 for (const EffectData &oldEffect : std::as_const(m_effects)) {
400 if (!oldEffect.changed) {
401 continue;
402 }
403 auto effectIt = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(),
404 [oldEffect](const EffectData &data) {
405 return data.serviceName == oldEffect.serviceName;
406 });
407 if (effectIt == m_pendingEffects.end()) {
408 continue;
409 }
410 effectIt->status = oldEffect.status;
411 effectIt->changed = effectIt->status != effectIt->originalStatus;
412 }
413 }
414
415 beginResetModel();
416 m_effects = m_pendingEffects;
417 m_pendingEffects.clear();
418 endResetModel();
419
420 Q_EMIT loaded();
421 };
422
423 OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"),
424 QStringLiteral("/Effects"),
425 QDBusConnection::sessionBus());
426
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);
432 }
433
434 const int serial = ++m_lastSerial;
435
436 QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(interface.areEffectsSupported(effectNames), this);
437 connect(watcher, &QDBusPendingCallWatcher::finished, this, [=, this](QDBusPendingCallWatcher *self) {
438 self->deleteLater();
439
440 if (m_lastSerial != serial) {
441 return;
442 }
443
444 const QDBusPendingReply<QList<bool>> reply = *self;
445 if (reply.isError()) {
446 commit();
447 return;
448 }
449
450 const QList<bool> supportedValues = reply.value();
451 if (supportedValues.count() != effectNames.count()) {
452 return;
453 }
454
455 for (int i = 0; i < effectNames.size(); ++i) {
456 const bool supported = supportedValues.at(i);
457 const QString effectName = effectNames.at(i);
458
459 auto it = std::find_if(m_pendingEffects.begin(), m_pendingEffects.end(),
460 [effectName](const EffectData &data) {
461 return data.serviceName == effectName;
462 });
463 if (it == m_pendingEffects.end()) {
464 continue;
465 }
466
467 if ((*it).supported != supported) {
468 (*it).supported = supported;
469 }
470 }
471
472 commit();
473 });
474 } else {
475 commit();
476 }
477}
478
479void EffectsModel::updateEffectStatus(const QModelIndex &rowIndex, Status effectState)
480{
481 setData(rowIndex, static_cast<int>(effectState), StatusRole);
482}
483
484void EffectsModel::save()
485{
486 KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), QStringLiteral("Plugins"));
487
488 QList<EffectData> dirtyEffects;
489
490 for (EffectData &effect : m_effects) {
491 if (!effect.changed) {
492 continue;
493 }
494
495 effect.changed = false;
496 effect.originalStatus = effect.status;
497
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);
505 } else {
506 kwinConfig.writeEntry(key, shouldEnable);
507 }
508
509 dirtyEffects.append(effect);
510 }
511
512 if (dirtyEffects.isEmpty()) {
513 return;
514 }
515
516 kwinConfig.sync();
517
518 OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"),
519 QStringLiteral("/Effects"),
520 QDBusConnection::sessionBus());
521
522 if (!interface.isValid()) {
523 return;
524 }
525
526 // Unload effects first, it's need to ensure that switching between mutually exclusive
527 // effects works as expected, for example so global shortcuts are handed over, etc.
528 auto split = std::partition(dirtyEffects.begin(), dirtyEffects.end(), [](const EffectData &data) {
529 return data.status == Status::Disabled;
530 });
531
532 for (auto it = dirtyEffects.begin(); it != split; ++it) {
533 interface.unloadEffect(it->serviceName);
534 }
535
536 for (auto it = split; it != dirtyEffects.end(); ++it) {
537 interface.loadEffect(it->serviceName);
538 }
539}
540
541void EffectsModel::defaults()
542{
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);
549 }
550 }
551}
552
553bool EffectsModel::isDefaults() const
554{
555 return std::all_of(m_effects.constBegin(), m_effects.constEnd(), [](const EffectData &effect) {
556 if (effect.enabledByDefaultFunction && effect.status != Status::EnabledUndeterminded) {
557 return false;
558 }
559 if (static_cast<bool>(effect.status) != effect.enabledByDefault) {
560 return false;
561 }
562 return true;
563 });
564}
565
566bool EffectsModel::needsSave() const
567{
568 return std::any_of(m_effects.constBegin(), m_effects.constEnd(),
569 [](const EffectData &data) {
570 return data.changed;
571 });
572}
573
574QModelIndex EffectsModel::findByPluginId(const QString &pluginId) const
575{
576 auto it = std::find_if(m_effects.constBegin(), m_effects.constEnd(),
577 [pluginId](const EffectData &data) {
578 return data.serviceName == pluginId;
579 });
580 if (it == m_effects.constEnd()) {
581 return {};
582 }
583 return index(std::distance(m_effects.constBegin(), it), 0);
584}
585
586void EffectsModel::requestConfigure(const QModelIndex &index, QWindow *transientParent)
587{
588 if (!index.isValid()) {
589 return;
590 }
591
592 const EffectData &effect = m_effects.at(index.row());
593 Q_ASSERT(!effect.configModule.isEmpty());
594
595 KCMultiDialog *dialog = new KCMultiDialog();
596 dialog->addModule(KPluginMetaData(QStringLiteral("kwin/effects/configs/") + effect.configModule), effect.configArgs);
597 dialog->setAttribute(Qt::WA_DeleteOnClose);
598 dialog->winId();
599 dialog->windowHandle()->setTransientParent(transientParent);
600 dialog->show();
601}
602
603bool EffectsModel::shouldStore(const EffectData &data) const
604{
605 return true;
606}
607
608}
609
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
Options * options
Definition main.cpp:73