File indexing completed on 2024-05-19 05:38:05

0001 /*
0002     SPDX-FileCopyrightText: 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
0003     SPDX-FileCopyrightText: 2007 Jeremy Whiting <jpwhiting@kde.org>
0004     SPDX-FileCopyrightText: 2016 Olivier Churlaud <olivier@churlaud.com>
0005     SPDX-FileCopyrightText: 2019 Kai Uwe Broulik <kde@privat.broulik.de>
0006     SPDX-FileCopyrightText: 2019 David Redondo <kde@david-redondo.de>
0007 
0008     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0009 */
0010 
0011 #include "themesmodel.h"
0012 
0013 #include <QCollator>
0014 #include <QDir>
0015 #include <QStandardPaths>
0016 
0017 #include <KColorScheme>
0018 #include <KDesktopFile>
0019 #include <KPluginMetaData>
0020 
0021 #include <KConfigGroup>
0022 #include <KSharedConfig>
0023 
0024 #include <algorithm>
0025 
0026 ThemesModel::ThemesModel(QObject *parent)
0027     : QAbstractListModel(parent)
0028 {
0029 }
0030 
0031 ThemesModel::~ThemesModel() = default;
0032 
0033 int ThemesModel::rowCount(const QModelIndex &parent) const
0034 {
0035     if (parent.isValid()) {
0036         return 0;
0037     }
0038 
0039     return m_data.count();
0040 }
0041 
0042 QVariant ThemesModel::data(const QModelIndex &index, int role) const
0043 {
0044     if (!index.isValid() || index.row() >= m_data.count()) {
0045         return QVariant();
0046     }
0047 
0048     const auto &item = m_data.at(index.row());
0049 
0050     switch (role) {
0051     case Qt::DisplayRole:
0052         return item.display;
0053     case PluginNameRole:
0054         return item.pluginName;
0055     case DescriptionRole:
0056         return item.description;
0057     case ColorTypeRole:
0058         return item.type;
0059     case IsLocalRole:
0060         return item.isLocal;
0061     case PendingDeletionRole:
0062         return item.pendingDeletion;
0063     }
0064     return QVariant();
0065 }
0066 
0067 bool ThemesModel::setData(const QModelIndex &index, const QVariant &value, int role)
0068 {
0069     if (!index.isValid() || index.row() >= m_data.count()) {
0070         return false;
0071     }
0072 
0073     if (role == PendingDeletionRole) {
0074         auto &item = m_data[index.row()];
0075 
0076         const bool pendingDeletion = value.toBool();
0077 
0078         if (item.pendingDeletion != pendingDeletion) {
0079             item.pendingDeletion = pendingDeletion;
0080             Q_EMIT dataChanged(index, index, {PendingDeletionRole});
0081 
0082             if (index.row() == selectedThemeIndex() && pendingDeletion) {
0083                 // move to the next non-pending theme
0084                 const auto nonPending = match(index, PendingDeletionRole, false);
0085                 if (!nonPending.isEmpty()) {
0086                     setSelectedTheme(nonPending.first().data(PluginNameRole).toString());
0087                 }
0088             }
0089 
0090             Q_EMIT pendingDeletionsChanged();
0091             return true;
0092         }
0093     }
0094 
0095     return false;
0096 }
0097 
0098 QHash<int, QByteArray> ThemesModel::roleNames() const
0099 {
0100     return {
0101         {Qt::DisplayRole, QByteArrayLiteral("display")},
0102         {PluginNameRole, QByteArrayLiteral("pluginName")},
0103         {DescriptionRole, QByteArrayLiteral("description")},
0104         {ColorTypeRole, QByteArrayLiteral("colorType")},
0105         {IsLocalRole, QByteArrayLiteral("isLocal")},
0106         {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")},
0107     };
0108 }
0109 
0110 QString ThemesModel::selectedTheme() const
0111 {
0112     return m_selectedTheme;
0113 }
0114 
0115 void ThemesModel::setSelectedTheme(const QString &pluginName)
0116 {
0117     if (m_selectedTheme == pluginName) {
0118         return;
0119     }
0120 
0121     m_selectedTheme = pluginName;
0122 
0123     Q_EMIT selectedThemeChanged(pluginName);
0124 
0125     Q_EMIT selectedThemeIndexChanged();
0126 }
0127 
0128 int ThemesModel::pluginIndex(const QString &pluginName) const
0129 {
0130     const auto results = match(index(0, 0), PluginNameRole, pluginName, 1, Qt::MatchExactly);
0131     if (results.count() == 1) {
0132         return results.first().row();
0133     }
0134 
0135     return -1;
0136 }
0137 
0138 int ThemesModel::selectedThemeIndex() const
0139 {
0140     return pluginIndex(m_selectedTheme);
0141 }
0142 
0143 void ThemesModel::load()
0144 {
0145     beginResetModel();
0146 
0147     const int oldCount = m_data.count();
0148 
0149     m_data.clear();
0150 
0151     // Get all desktop themes
0152     QStringList themes;
0153     const QStringList packs =
0154         QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/desktoptheme"), QStandardPaths::LocateDirectory);
0155     for (const QString &ppath : packs) {
0156         const QDir cd(ppath);
0157         const QStringList &entries = cd.entryList(QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot);
0158         for (const QString &pack : entries) {
0159             const QString prefix = QStringLiteral("%1%2%3%4metadata.").arg(ppath, QDir::separator(), pack, QDir::separator());
0160 
0161             QString _metadata = QStringLiteral("%1json").arg(prefix);
0162             if (QFile::exists(_metadata)) {
0163                 themes << _metadata;
0164                 continue;
0165             }
0166 
0167             _metadata = QStringLiteral("%1desktop").arg(prefix);
0168             if (QFile::exists(_metadata)) {
0169                 themes << _metadata;
0170             }
0171         }
0172     }
0173 
0174     for (const QString &theme : std::as_const(themes)) {
0175         int themeSepIndex = theme.lastIndexOf(QLatin1Char('/'), -1);
0176         const QString themeRoot = theme.left(themeSepIndex);
0177         int themeNameSepIndex = themeRoot.lastIndexOf(QLatin1Char('/'), -1);
0178         const QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1);
0179 
0180         QString name;
0181         QString comment;
0182 
0183         if (theme.endsWith(QLatin1String(".json"))) {
0184             KPluginMetaData data = KPluginMetaData::fromJsonFile(theme);
0185             name = data.name();
0186             comment = data.description();
0187         } else {
0188             KDesktopFile df(theme);
0189 
0190             if (df.noDisplay()) {
0191                 continue;
0192             }
0193 
0194             name = df.readName();
0195             if (name.isEmpty()) {
0196                 name = packageName;
0197             }
0198             comment = df.readComment();
0199         }
0200         const bool isLocal = QFileInfo(theme).isWritable();
0201         bool hasPluginName = std::any_of(m_data.begin(), m_data.end(), [&](const ThemesModelData &item) {
0202             return item.pluginName == packageName;
0203         });
0204         if (!hasPluginName) {
0205             // Plasma Theme creates a KColorScheme out of the "color" file and falls back to system colors if there is none
0206             const QString colorsPath = themeRoot + QStringLiteral("/colors");
0207             const bool followsSystemColors = !QFileInfo::exists(colorsPath);
0208             ColorType type = FollowsColorTheme;
0209             if (!followsSystemColors) {
0210                 const KSharedConfig::Ptr config = KSharedConfig::openConfig(colorsPath);
0211                 const QPalette palette = KColorScheme::createApplicationPalette(config);
0212                 const int windowBackgroundGray = qGray(palette.window().color().rgb());
0213                 if (windowBackgroundGray < 192) {
0214                     type = DarkTheme;
0215                 } else {
0216                     type = LightTheme;
0217                 }
0218             }
0219             ThemesModelData item{name, packageName, comment, type, isLocal, false};
0220             m_data.append(item);
0221         }
0222     }
0223 
0224     // Sort case-insensitively
0225     QCollator collator;
0226     collator.setCaseSensitivity(Qt::CaseInsensitive);
0227     std::sort(m_data.begin(), m_data.end(), [&collator](const ThemesModelData &a, const ThemesModelData &b) {
0228         return collator.compare(a.display, b.display) < 0;
0229     });
0230 
0231     endResetModel();
0232 
0233     // an item might have been added before the currently selected one
0234     if (oldCount != m_data.count()) {
0235         Q_EMIT selectedThemeIndexChanged();
0236     }
0237 }
0238 
0239 QStringList ThemesModel::pendingDeletions() const
0240 {
0241     QStringList pendingDeletions;
0242 
0243     for (const auto &item : std::as_const(m_data)) {
0244         if (item.pendingDeletion) {
0245             pendingDeletions.append(item.pluginName);
0246         }
0247     }
0248 
0249     return pendingDeletions;
0250 }
0251 
0252 void ThemesModel::removeRow(int row)
0253 {
0254     beginRemoveRows(QModelIndex(), row, row);
0255     m_data.erase(m_data.begin() + row);
0256     endRemoveRows();
0257 }