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"