File indexing completed on 2026-07-12 11:20:43

0001 /*
0002     SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
0003     SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kpluginmodel.h"
0009 #include "kpluginproxymodel.h"
0010 
0011 #include <QPluginLoader>
0012 
0013 #include <KCategorizedSortFilterProxyModel>
0014 #include <KConfigGroup>
0015 #include <KServiceTypeTrader>
0016 
0017 #include <utility>
0018 
0019 #include "kcmutilscore_debug.h"
0020 
0021 class KPluginModelPrivate
0022 {
0023 public:
0024     bool isDefaulted()
0025     {
0026         return std::all_of(m_plugins.cbegin(), m_plugins.cend(), [this](const KPluginMetaData &data) {
0027             return isPluginEnabled(data) == data.isEnabledByDefault();
0028         });
0029     }
0030     bool isPluginEnabled(const KPluginMetaData &plugin) const
0031     {
0032         auto pendingState = m_pendingStates.constFind(plugin.pluginId());
0033         if (pendingState != m_pendingStates.constEnd()) {
0034             return pendingState.value();
0035         }
0036 
0037         if (m_config.isValid()) {
0038             return m_config.readEntry(plugin.pluginId() + QLatin1String("Enabled"), plugin.isEnabledByDefault());
0039         }
0040         return plugin.isEnabledByDefault();
0041     }
0042     KPluginMetaData findConfig(const KPluginMetaData &plugin) const
0043     {
0044         const QString metaDataKCM = plugin.value(QStringLiteral("X-KDE-ConfigModule"));
0045 
0046         if (!metaDataKCM.isEmpty()) {
0047             const QString absoluteKCMPath = QPluginLoader(metaDataKCM).fileName();
0048             // If we have a static plugin the file does not exist on disk
0049             // instead we query in the plugin namespace
0050             if (absoluteKCMPath.isEmpty()) {
0051                 const int idx = metaDataKCM.lastIndexOf(QLatin1Char('/'));
0052                 const QString pluginNamespace = metaDataKCM.left(idx);
0053                 const QString pluginId = metaDataKCM.mid(idx + 1);
0054                 return KPluginMetaData::findPluginById(pluginNamespace, pluginId);
0055             } else {
0056                 return KPluginMetaData(plugin.rawData(), absoluteKCMPath);
0057             }
0058         }
0059 
0060 #if KSERVICE_BUILD_DEPRECATED_SINCE(5, 89)
0061         QT_WARNING_PUSH
0062         QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations")
0063         QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations")
0064 
0065         const QString constraint = QLatin1Char('\'') + plugin.pluginId() + QLatin1String("' in [X-KDE-ParentComponents]");
0066         const auto services = KServiceTypeTrader::self()->query(QStringLiteral("KCModule"), constraint);
0067         if (!services.isEmpty()) {
0068             const KService::Ptr service = services.constFirst();
0069             qCWarning(KCMUTILS_CORE_LOG) << service->entryPath() << "Querying the KCMs associated to a plugin using the X-KDE-ParentComponents is deprecated."
0070                                          << "Instead define the X-KDE-ConfigModule with the namespace and plugin filen name.";
0071             QJsonObject obj;
0072             QJsonObject kplugin{{QLatin1String("Name"), service->name()}};
0073             QString pluginInfoName = service->property(QStringLiteral("X-KDE-PluginInfo-Name")).toString();
0074             if (!pluginInfoName.isEmpty()) {
0075                 kplugin.insert(QLatin1String("Id"), pluginInfoName);
0076             }
0077             obj.insert(QLatin1String("KPlugin"), kplugin);
0078             obj.insert(QLatin1String("X-KDE-PluginKeyword"), service->pluginKeyword());
0079             return KPluginMetaData(obj, service->library());
0080         }
0081         QT_WARNING_POP
0082 #endif
0083         return KPluginMetaData();
0084     }
0085 
0086     QVector<KPluginMetaData> m_plugins;
0087     QHash<QString, KPluginMetaData> m_pluginKcms;
0088     KConfigGroup m_config;
0089     QHash<QString, QString> m_categoryLabels;
0090     QHash<QString, bool> m_pendingStates;
0091 };
0092 
0093 KPluginModel::KPluginModel(QObject *parent)
0094     : QAbstractListModel(parent)
0095     , d(new KPluginModelPrivate())
0096 {
0097 }
0098 
0099 KPluginModel::~KPluginModel() = default;
0100 
0101 QVariant KPluginModel::data(const QModelIndex &index, int role) const
0102 {
0103     const KPluginMetaData &plugin = d->m_plugins[index.row()];
0104 
0105     switch (role) {
0106     case Roles::NameRole:
0107         return plugin.name();
0108     case Roles::DescriptionRole:
0109         return plugin.description();
0110     case Roles::IconRole:
0111         return plugin.iconName();
0112     case Roles::EnabledRole:
0113         return d->isPluginEnabled(plugin);
0114     case Roles::IsChangeableRole:
0115         if (d->m_config.isValid()) {
0116             return !d->m_config.isEntryImmutable(plugin.pluginId() + QLatin1String("Enabled"));
0117         }
0118         return true;
0119     case MetaDataRole:
0120         return QVariant::fromValue(plugin);
0121     case KCategorizedSortFilterProxyModel::CategoryDisplayRole:
0122     case KCategorizedSortFilterProxyModel::CategorySortRole:
0123         return d->m_categoryLabels[plugin.pluginId()];
0124     case ConfigRole:
0125         return QVariant::fromValue(d->m_pluginKcms.value(plugin.pluginId()));
0126     case IdRole:
0127         return plugin.pluginId();
0128     case EnabledByDefaultRole:
0129         return plugin.isEnabledByDefault();
0130     }
0131 
0132     return {};
0133 }
0134 
0135 bool KPluginModel::setData(const QModelIndex &index, const QVariant &value, int role)
0136 {
0137     if (role == Roles::EnabledRole) {
0138         const QString pluginId = d->m_plugins[index.row()].pluginId();
0139 
0140         // If we already have a pending state and the user reverts it remove it from the map
0141         auto pendingStateIt = d->m_pendingStates.constFind(pluginId);
0142         if (pendingStateIt != d->m_pendingStates.constEnd()) {
0143             if (pendingStateIt.value() != value.toBool()) {
0144                 d->m_pendingStates.erase(pendingStateIt);
0145             }
0146         } else {
0147             d->m_pendingStates[pluginId] = value.toBool();
0148         }
0149 
0150         Q_EMIT dataChanged(index, index, {Roles::EnabledRole});
0151         Q_EMIT defaulted(d->isDefaulted());
0152         Q_EMIT isSaveNeededChanged();
0153 
0154         return true;
0155     }
0156 
0157     return false;
0158 }
0159 
0160 int KPluginModel::rowCount(const QModelIndex & /*parent*/) const
0161 {
0162     return d->m_plugins.count();
0163 }
0164 
0165 QHash<int, QByteArray> KPluginModel::roleNames() const
0166 {
0167     return {
0168         {KCategorizedSortFilterProxyModel::CategoryDisplayRole, "category"},
0169         {Roles::NameRole, "name"},
0170         {Roles::IconRole, "icon"},
0171         {Roles::EnabledRole, "enabled"},
0172         {Roles::DescriptionRole, "description"},
0173         {Roles::IsChangeableRole, "changable"},
0174         {Roles::EnabledByDefaultRole, "enabledByDefault"},
0175         {Roles::MetaDataRole, "metaData"},
0176         {Roles::ConfigRole, "config"},
0177     };
0178 };
0179 
0180 void KPluginModel::addPlugins(const QVector<KPluginMetaData> &newPlugins, const QString &categoryLabel)
0181 {
0182     beginInsertRows({}, d->m_plugins.size(), d->m_plugins.size() + newPlugins.size() - 1);
0183     d->m_plugins.append(newPlugins);
0184 
0185     for (const KPluginMetaData &plugin : newPlugins) {
0186         d->m_categoryLabels[plugin.pluginId()] = categoryLabel;
0187         d->m_pluginKcms.insert(plugin.pluginId(), d->findConfig(plugin));
0188     }
0189 
0190     endInsertRows();
0191 
0192     Q_EMIT defaulted(d->isDefaulted());
0193 }
0194 
0195 void KPluginModel::setConfig(const KConfigGroup &config)
0196 {
0197     d->m_config = config;
0198 
0199     if (!d->m_plugins.isEmpty()) {
0200         Q_EMIT dataChanged(index(0, 0), index(d->m_plugins.size() - 1, 0), {Roles::EnabledRole, Roles::IsChangeableRole});
0201     }
0202 }
0203 
0204 void KPluginModel::clear()
0205 {
0206     if (d->m_plugins.isEmpty()) {
0207         return;
0208     }
0209     beginRemoveRows({}, 0, d->m_plugins.size() - 1);
0210     d->m_plugins.clear();
0211     d->m_pluginKcms.clear();
0212     // In case of the "Reset"-button of the KCMs load is called again with the goal
0213     // of discarding all local changes. Consequently, the pending states have to be cleared here.
0214     d->m_pendingStates.clear();
0215     endRemoveRows();
0216 }
0217 
0218 void KPluginModel::save()
0219 {
0220     if (d->m_config.isValid()) {
0221         for (auto it = d->m_pendingStates.cbegin(); it != d->m_pendingStates.cend(); ++it) {
0222             d->m_config.writeEntry(it.key() + QLatin1String("Enabled"), it.value());
0223         }
0224 
0225         d->m_config.sync();
0226     }
0227     d->m_pendingStates.clear();
0228 }
0229 
0230 KPluginMetaData KPluginModel::findConfigForPluginId(const QString &pluginId) const
0231 {
0232     for (const KPluginMetaData &plugin : std::as_const(d->m_plugins)) {
0233         if (plugin.pluginId() == pluginId) {
0234             return d->findConfig(plugin);
0235         }
0236     }
0237     return KPluginMetaData();
0238 }
0239 
0240 void KPluginModel::load()
0241 {
0242     if (!d->m_config.isValid()) {
0243         return;
0244     }
0245 
0246     d->m_pendingStates.clear();
0247     Q_EMIT dataChanged(index(0, 0), index(d->m_plugins.size() - 1, 0), {Roles::EnabledRole});
0248 }
0249 
0250 void KPluginModel::defaults()
0251 {
0252     for (int pluginIndex = 0, count = d->m_plugins.count(); pluginIndex < count; ++pluginIndex) {
0253         const KPluginMetaData plugin = d->m_plugins.at(pluginIndex);
0254         const bool changed = d->isPluginEnabled(plugin) != plugin.isEnabledByDefault();
0255 
0256         if (changed) {
0257             // If the entry was marked as changed, but we flip the value it is unchanged again
0258             if (d->m_pendingStates.remove(plugin.pluginId()) == 0) {
0259                 // If the entry was not changed before, we have to mark it as changed
0260                 d->m_pendingStates.insert(plugin.pluginId(), plugin.isEnabledByDefault());
0261             }
0262             Q_EMIT dataChanged(index(pluginIndex, 0), index(pluginIndex, 0), {Roles::EnabledRole});
0263         }
0264     }
0265 
0266     Q_EMIT defaulted(true);
0267 }
0268 
0269 bool KPluginModel::isSaveNeeded()
0270 {
0271     return !d->m_pendingStates.isEmpty();
0272 }
0273 
0274 #include "moc_kpluginmodel.cpp"