File indexing completed on 2024-12-22 03:41:45

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 
0016 #include <utility>
0017 
0018 #include "kcmutilscore_debug.h"
0019 
0020 class KPluginModelPrivate
0021 {
0022 public:
0023     bool isDefaulted()
0024     {
0025         return std::all_of(m_plugins.cbegin(), m_plugins.cend(), [this](const KPluginMetaData &data) {
0026             return isPluginEnabled(data) == data.isEnabledByDefault();
0027         });
0028     }
0029     bool isPluginEnabled(const KPluginMetaData &plugin) const
0030     {
0031         auto pendingState = m_pendingStates.constFind(plugin.pluginId());
0032         if (pendingState != m_pendingStates.constEnd()) {
0033             return pendingState.value();
0034         }
0035 
0036         if (m_config.isValid()) {
0037             return m_config.readEntry(plugin.pluginId() + QLatin1String("Enabled"), plugin.isEnabledByDefault());
0038         }
0039         return plugin.isEnabledByDefault();
0040     }
0041     KPluginMetaData findConfig(const KPluginMetaData &plugin) const
0042     {
0043         const QString metaDataKCM = plugin.value(QStringLiteral("X-KDE-ConfigModule"));
0044 
0045         if (!metaDataKCM.isEmpty()) {
0046             const QString absoluteKCMPath = QPluginLoader(metaDataKCM).fileName();
0047             // If we have a static plugin the file does not exist on disk
0048             // instead we query in the plugin namespace
0049             if (absoluteKCMPath.isEmpty()) {
0050                 const int idx = metaDataKCM.lastIndexOf(QLatin1Char('/'));
0051                 const QString pluginNamespace = metaDataKCM.left(idx);
0052                 const QString pluginId = metaDataKCM.mid(idx + 1);
0053                 return KPluginMetaData::findPluginById(pluginNamespace, pluginId);
0054             } else {
0055                 return KPluginMetaData(plugin.rawData(), absoluteKCMPath);
0056             }
0057         }
0058 
0059         return KPluginMetaData();
0060     }
0061 
0062     QList<KPluginMetaData> m_plugins;
0063     QSet<KPluginMetaData> m_unsortablePlugins;
0064     QHash<QString, KPluginMetaData> m_pluginKcms;
0065     KConfigGroup m_config;
0066     QList<QString> m_orderedCategories; // Preserve order of categories in which they were added
0067     QHash<QString, QString> m_categoryLabels;
0068     QHash<QString, bool> m_pendingStates;
0069 };
0070 
0071 KPluginModel::KPluginModel(QObject *parent)
0072     : QAbstractListModel(parent)
0073     , d(new KPluginModelPrivate())
0074 {
0075 }
0076 
0077 KPluginModel::~KPluginModel() = default;
0078 
0079 QVariant KPluginModel::data(const QModelIndex &index, int role) const
0080 {
0081     const KPluginMetaData &plugin = d->m_plugins[index.row()];
0082 
0083     switch (role) {
0084     case Roles::NameRole:
0085         return plugin.name();
0086     case Roles::DescriptionRole:
0087         return plugin.description();
0088     case Roles::IconRole:
0089         return plugin.iconName();
0090     case Roles::EnabledRole:
0091         return d->isPluginEnabled(plugin);
0092     case Roles::IsChangeableRole:
0093         if (d->m_unsortablePlugins.contains(plugin)) {
0094             return false;
0095         }
0096         if (d->m_config.isValid()) {
0097             return !d->m_config.isEntryImmutable(plugin.pluginId() + QLatin1String("Enabled"));
0098         }
0099         return true;
0100     case MetaDataRole:
0101         return QVariant::fromValue(plugin);
0102     case KCategorizedSortFilterProxyModel::CategoryDisplayRole:
0103     case KCategorizedSortFilterProxyModel::CategorySortRole:
0104         return d->m_categoryLabels[plugin.pluginId()];
0105     case ConfigRole:
0106         return QVariant::fromValue(d->m_pluginKcms.value(plugin.pluginId()));
0107     case IdRole:
0108         return plugin.pluginId();
0109     case EnabledByDefaultRole:
0110         return plugin.isEnabledByDefault();
0111     case SortableRole:
0112         return !d->m_unsortablePlugins.contains(plugin);
0113     }
0114 
0115     return {};
0116 }
0117 
0118 bool KPluginModel::setData(const QModelIndex &index, const QVariant &value, int role)
0119 {
0120     if (role == Roles::EnabledRole) {
0121         const QString pluginId = d->m_plugins[index.row()].pluginId();
0122 
0123         // If we already have a pending state and the user reverts it remove it from the map
0124         auto pendingStateIt = d->m_pendingStates.constFind(pluginId);
0125         if (pendingStateIt != d->m_pendingStates.constEnd()) {
0126             if (pendingStateIt.value() != value.toBool()) {
0127                 d->m_pendingStates.erase(pendingStateIt);
0128             }
0129         } else {
0130             d->m_pendingStates[pluginId] = value.toBool();
0131         }
0132 
0133         Q_EMIT dataChanged(index, index, {Roles::EnabledRole});
0134         Q_EMIT defaulted(d->isDefaulted());
0135         Q_EMIT isSaveNeededChanged();
0136 
0137         return true;
0138     }
0139 
0140     return false;
0141 }
0142 
0143 int KPluginModel::rowCount(const QModelIndex & /*parent*/) const
0144 {
0145     return d->m_plugins.count();
0146 }
0147 
0148 QHash<int, QByteArray> KPluginModel::roleNames() const
0149 {
0150     return {
0151         {KCategorizedSortFilterProxyModel::CategoryDisplayRole, "category"},
0152         {Roles::NameRole, "name"},
0153         {Roles::IconRole, "icon"},
0154         {Roles::EnabledRole, "enabled"},
0155         {Roles::DescriptionRole, "description"},
0156         {Roles::IsChangeableRole, "changable"},
0157         {Roles::EnabledByDefaultRole, "enabledByDefault"},
0158         {Roles::MetaDataRole, "metaData"},
0159         {Roles::ConfigRole, "config"},
0160     };
0161 };
0162 
0163 void KPluginModel::addUnsortablePlugins(const QList<KPluginMetaData> &newPlugins, const QString &categoryLabel)
0164 {
0165     d->m_unsortablePlugins.unite(QSet(newPlugins.begin(), newPlugins.end()));
0166     addPlugins(newPlugins, categoryLabel);
0167 }
0168 
0169 bool KPluginModel::moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild)
0170 {
0171     if (sourceParent.isValid() || destinationParent.isValid()) {
0172         return false;
0173     }
0174     if ((sourceRow + count - 1) >= d->m_plugins.size()) {
0175         return false;
0176     }
0177 
0178     const bool isMoveDown = destinationChild > sourceRow;
0179     if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, isMoveDown ? destinationChild + 1 : destinationChild)) {
0180         return false;
0181     }
0182     for (int i = 0; i < count; i++) {
0183         d->m_plugins.insert(destinationChild, d->m_plugins.takeAt(sourceRow + i));
0184     }
0185     endMoveRows();
0186     return true;
0187 }
0188 
0189 void KPluginModel::addPlugins(const QList<KPluginMetaData> &newPlugins, const QString &categoryLabel)
0190 {
0191     beginInsertRows({}, d->m_plugins.size(), d->m_plugins.size() + newPlugins.size() - 1);
0192     d->m_orderedCategories << categoryLabel;
0193     d->m_plugins.append(newPlugins);
0194 
0195     for (const KPluginMetaData &plugin : newPlugins) {
0196         d->m_categoryLabels[plugin.pluginId()] = categoryLabel;
0197         d->m_pluginKcms.insert(plugin.pluginId(), d->findConfig(plugin));
0198     }
0199 
0200     endInsertRows();
0201 
0202     Q_EMIT defaulted(d->isDefaulted());
0203 }
0204 
0205 void KPluginModel::removePlugin(const KPluginMetaData &data)
0206 {
0207     if (const int index = d->m_plugins.indexOf(data); index != -1) {
0208         beginRemoveRows({}, index, index);
0209         d->m_plugins.removeAt(index);
0210         d->m_unsortablePlugins.remove(data);
0211         endRemoveRows();
0212     }
0213 }
0214 
0215 void KPluginModel::setConfig(const KConfigGroup &config)
0216 {
0217     d->m_config = config;
0218 
0219     if (!d->m_plugins.isEmpty()) {
0220         Q_EMIT dataChanged(index(0, 0), index(d->m_plugins.size() - 1, 0), {Roles::EnabledRole, Roles::IsChangeableRole});
0221     }
0222 }
0223 
0224 void KPluginModel::clear()
0225 {
0226     if (d->m_plugins.isEmpty()) {
0227         return;
0228     }
0229     beginRemoveRows({}, 0, d->m_plugins.size() - 1);
0230     d->m_plugins.clear();
0231     d->m_pluginKcms.clear();
0232     // In case of the "Reset"-button of the KCMs load is called again with the goal
0233     // of discarding all local changes. Consequently, the pending states have to be cleared here.
0234     d->m_pendingStates.clear();
0235     endRemoveRows();
0236 }
0237 
0238 void KPluginModel::save()
0239 {
0240     if (d->m_config.isValid()) {
0241         for (auto it = d->m_pendingStates.cbegin(); it != d->m_pendingStates.cend(); ++it) {
0242             d->m_config.writeEntry(it.key() + QLatin1String("Enabled"), it.value());
0243         }
0244 
0245         d->m_config.sync();
0246     }
0247     d->m_pendingStates.clear();
0248 }
0249 
0250 KPluginMetaData KPluginModel::findConfigForPluginId(const QString &pluginId) const
0251 {
0252     for (const KPluginMetaData &plugin : std::as_const(d->m_plugins)) {
0253         if (plugin.pluginId() == pluginId) {
0254             return d->findConfig(plugin);
0255         }
0256     }
0257     return KPluginMetaData();
0258 }
0259 
0260 void KPluginModel::load()
0261 {
0262     if (!d->m_config.isValid()) {
0263         return;
0264     }
0265 
0266     d->m_pendingStates.clear();
0267     Q_EMIT dataChanged(index(0, 0), index(d->m_plugins.size() - 1, 0), {Roles::EnabledRole});
0268 }
0269 
0270 void KPluginModel::defaults()
0271 {
0272     for (int pluginIndex = 0, count = d->m_plugins.count(); pluginIndex < count; ++pluginIndex) {
0273         const KPluginMetaData plugin = d->m_plugins.at(pluginIndex);
0274         const bool changed = d->isPluginEnabled(plugin) != plugin.isEnabledByDefault();
0275 
0276         if (changed) {
0277             // If the entry was marked as changed, but we flip the value it is unchanged again
0278             if (d->m_pendingStates.remove(plugin.pluginId()) == 0) {
0279                 // If the entry was not changed before, we have to mark it as changed
0280                 d->m_pendingStates.insert(plugin.pluginId(), plugin.isEnabledByDefault());
0281             }
0282             Q_EMIT dataChanged(index(pluginIndex, 0), index(pluginIndex, 0), {Roles::EnabledRole});
0283         }
0284     }
0285 
0286     Q_EMIT defaulted(true);
0287 }
0288 
0289 bool KPluginModel::isSaveNeeded()
0290 {
0291     return !d->m_pendingStates.isEmpty();
0292 }
0293 
0294 QStringList KPluginModel::getOrderedCategoryLabels()
0295 {
0296     return d->m_orderedCategories;
0297 }
0298 
0299 #include "moc_kpluginmodel.cpp"