diff --git a/kcms/desktoptheme/CMakeLists.txt b/kcms/desktoptheme/CMakeLists.txt --- a/kcms/desktoptheme/CMakeLists.txt +++ b/kcms/desktoptheme/CMakeLists.txt @@ -3,6 +3,8 @@ set(kcm_desktoptheme_SRCS kcm.cpp + themesmodel.cpp + filterproxymodel.cpp ) kconfig_add_kcfg_files(kcm_desktoptheme_SRCS desktopthemesettings.kcfgc GENERATE_MOC) diff --git a/kcms/desktoptheme/filterproxymodel.h b/kcms/desktoptheme/filterproxymodel.h new file mode 100644 --- /dev/null +++ b/kcms/desktoptheme/filterproxymodel.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019 Kai Uwe Broulik + * Copyright (c) 2019 David Redondo + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include "kcm.h" + +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + + enum ThemeFilter { + AllThemes, + LightThemes, + DarkThemes, + ThemesFollowingColors + }; + Q_ENUM(ThemeFilter) + + Q_PROPERTY(QString selectedTheme READ selectedTheme WRITE setSelectedTheme NOTIFY selectedThemeChanged) + Q_PROPERTY(int selectedThemeIndex READ selectedThemeIndex NOTIFY selectedThemeIndexChanged) + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(ThemeFilter filter READ filter WRITE setFilter NOTIFY filterChanged) + + FilterProxyModel(QObject *parent = nullptr); + ~FilterProxyModel() override; + + QString selectedTheme() const; + void setSelectedTheme(const QString &pluginName); + + int selectedThemeIndex() const; + + QString query() const; + void setQuery(const QString &query); + + ThemeFilter filter() const; + void setFilter(ThemeFilter filter); + + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + +Q_SIGNALS: + void filterChanged(); + void queryChanged(); + + void selectedThemeChanged(); + void selectedThemeIndexChanged(); + +private: + QString m_selectedTheme; + QString m_query; + ThemeFilter m_filter = AllThemes; +}; diff --git a/kcms/desktoptheme/filterproxymodel.cpp b/kcms/desktoptheme/filterproxymodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/desktoptheme/filterproxymodel.cpp @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2019 Kai Uwe Broulik + * Copyright (c) 2019 David Redondo + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "filterproxymodel.h" + +#include "themesmodel.h" + +FilterProxyModel::FilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + +} + +FilterProxyModel::~FilterProxyModel() = default; + +QString FilterProxyModel::selectedTheme() const +{ + return m_selectedTheme; +} + +void FilterProxyModel::setSelectedTheme(const QString &pluginName) +{ + if (m_selectedTheme == pluginName) { + return; + } + + const bool firstTime = m_selectedTheme.isNull(); + m_selectedTheme = pluginName; + + if (!firstTime) { + emit selectedThemeChanged(); + } + emit selectedThemeIndexChanged(); +} + +int FilterProxyModel::selectedThemeIndex() const +{ + // We must search in the source model and then map the index to our proxy model. + const auto results = sourceModel()->match(sourceModel()->index(0, 0), ThemesModel::PluginNameRole, m_selectedTheme); + + if (results.count() == 1) { + const QModelIndex result = mapFromSource(results.first()); + if (result.isValid()) { + return result.row(); + } + } + + return -1; +} + +QString FilterProxyModel::query() const +{ + return m_query; +} + +void FilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + const int oldIndex = selectedThemeIndex(); + + m_query = query; + invalidateFilter(); + + emit queryChanged(); + + if (selectedThemeIndex() != oldIndex) { + emit selectedThemeIndexChanged(); + } + } +} + +FilterProxyModel::ThemeFilter FilterProxyModel::filter() const +{ + return m_filter; +} + +void FilterProxyModel::setFilter(ThemeFilter filter) +{ + if (m_filter != filter) { + const int oldIndex = selectedThemeIndex(); + + m_filter = filter; + invalidateFilter(); + + emit filterChanged(); + + if (selectedThemeIndex() != oldIndex) { + emit selectedThemeIndexChanged(); + } + } +} + +bool FilterProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + + if (!m_query.isEmpty()) { + if (!idx.data(Qt::DisplayRole).toString().contains(m_query, Qt::CaseInsensitive) + && !idx.data(ThemesModel::PluginNameRole).toString().contains(m_query, Qt::CaseInsensitive)) { + return false; + } + } + + const auto type = idx.data(ThemesModel::ColorTypeRole).value(); + switch (m_filter) { + case AllThemes: + return true; + case LightThemes: + return type == ThemesModel::LightTheme; + case DarkThemes: + return type == ThemesModel::DarkTheme; + case ThemesFollowingColors: + return type == ThemesModel::FollowsColorTheme; + } + + return true; +} diff --git a/kcms/desktoptheme/kcm.h b/kcms/desktoptheme/kcm.h --- a/kcms/desktoptheme/kcm.h +++ b/kcms/desktoptheme/kcm.h @@ -39,45 +39,36 @@ } class QQuickItem; -class QStandardItemModel; class DesktopThemeSettings; +class FilterProxyModel; +class ThemesModel; class KCMDesktopTheme : public KQuickAddons::ManagedConfigModule { Q_OBJECT + Q_PROPERTY(DesktopThemeSettings *desktopThemeSettings READ desktopThemeSettings CONSTANT) - Q_PROPERTY(QStandardItemModel *desktopThemeModel READ desktopThemeModel CONSTANT) + Q_PROPERTY(FilterProxyModel *filteredModel READ filteredModel CONSTANT) + Q_PROPERTY(ThemesModel *desktopThemeModel READ desktopThemeModel CONSTANT) Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) Q_PROPERTY(bool canEditThemes READ canEditThemes CONSTANT) public: - enum Roles { - PluginNameRole = Qt::UserRole + 1, - ThemeNameRole, - DescriptionRole, - FollowsSystemColorsRole, - IsLocalRole, - PendingDeletionRole - }; - Q_ENUM(Roles) KCMDesktopTheme(QObject *parent, const QVariantList &args); ~KCMDesktopTheme() override; DesktopThemeSettings *desktopThemeSettings() const; - QStandardItemModel *desktopThemeModel() const; - - Q_INVOKABLE int pluginIndex(const QString &pluginName) const; + ThemesModel *desktopThemeModel() const; + FilterProxyModel *filteredModel() const; bool downloadingFile() const; bool canEditThemes() const; Q_INVOKABLE void getNewStuff(QQuickItem *ctx); Q_INVOKABLE void installThemeFromFile(const QUrl &url); - Q_INVOKABLE void setPendingDeletion(int index, bool pending); - Q_INVOKABLE void applyPlasmaTheme(QQuickItem *item, const QString &themeName); Q_INVOKABLE void editTheme(const QString &themeName); @@ -102,8 +93,8 @@ DesktopThemeSettings *m_settings; - QStandardItemModel *m_model; - QStringList m_pendingRemoval; + ThemesModel *m_model; + FilterProxyModel *m_filteredModel; QHash m_themes; bool m_haveThemeExplorerInstalled; diff --git a/kcms/desktoptheme/kcm.cpp b/kcms/desktoptheme/kcm.cpp --- a/kcms/desktoptheme/kcm.cpp +++ b/kcms/desktoptheme/kcm.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include @@ -44,36 +43,45 @@ #include #include "desktopthemesettings.h" +#include "filterproxymodel.h" +#include "themesmodel.h" Q_LOGGING_CATEGORY(KCM_DESKTOP_THEME, "kcm_desktoptheme") K_PLUGIN_FACTORY_WITH_JSON(KCMDesktopThemeFactory, "kcm_desktoptheme.json", registerPlugin();) KCMDesktopTheme::KCMDesktopTheme(QObject *parent, const QVariantList &args) : KQuickAddons::ManagedConfigModule(parent, args) , m_settings(new DesktopThemeSettings(this)) + , m_model(new ThemesModel(this)) + , m_filteredModel(new FilterProxyModel(this)) , m_haveThemeExplorerInstalled(false) { qmlRegisterType(); - qmlRegisterType(); + qmlRegisterUncreatableType("org.kde.private.kcms.desktoptheme", 1, 0, "ThemesModel", "Cannot create ThemesModel"); + qmlRegisterUncreatableType("org.kde.private.kcms.desktoptheme", 1, 0, "FilterProxyModel", "Cannot create FilterProxyModel"); KAboutData* about = new KAboutData(QStringLiteral("kcm_desktoptheme"), i18n("Plasma Style"), QStringLiteral("0.1"), QString(), KAboutLicense::LGPL); about->addAuthor(i18n("David Rosca"), QString(), QStringLiteral("nowrep@gmail.com")); setAboutData(about); setButtons(Apply | Default | Help); - m_model = new QStandardItemModel(this); - QHash roles = m_model->roleNames(); - roles[PluginNameRole] = QByteArrayLiteral("pluginName"); - roles[ThemeNameRole] = QByteArrayLiteral("themeName"); - roles[DescriptionRole] = QByteArrayLiteral("description"); - roles[FollowsSystemColorsRole] = QByteArrayLiteral("followsSystemColors"); - roles[IsLocalRole] = QByteArrayLiteral("isLocal"); - roles[PendingDeletionRole] = QByteArrayLiteral("pendingDeletion"); - m_model->setItemRoleNames(roles); - m_haveThemeExplorerInstalled = !QStandardPaths::findExecutable(QStringLiteral("plasmathemeexplorer")).isEmpty(); + + connect(m_model, &ThemesModel::pendingDeletionsChanged, this, &KCMDesktopTheme::settingsChanged); + + connect(m_model, &ThemesModel::selectedThemeChanged, this, [this](const QString &pluginName) { + m_settings->setName(pluginName); + }); + + connect(m_settings, &DesktopThemeSettings::nameChanged, this, [this] { + m_model->setSelectedTheme(m_settings->name()); + }); + + connect(m_model, &ThemesModel::selectedThemeChanged, m_filteredModel, &FilterProxyModel::setSelectedTheme); + + m_filteredModel->setSourceModel(m_model); } KCMDesktopTheme::~KCMDesktopTheme() @@ -85,41 +93,21 @@ return m_settings; } -QStandardItemModel *KCMDesktopTheme::desktopThemeModel() const +ThemesModel *KCMDesktopTheme::desktopThemeModel() const { return m_model; } -int KCMDesktopTheme::pluginIndex(const QString &pluginName) const +FilterProxyModel *KCMDesktopTheme::filteredModel() const { - const auto results = m_model->match(m_model->index(0, 0), PluginNameRole, pluginName); - if (results.count() == 1) { - return results.first().row(); - } - - return -1; + return m_filteredModel; } bool KCMDesktopTheme::downloadingFile() const { return m_tempCopyJob; } -void KCMDesktopTheme::setPendingDeletion(int index, bool pending) -{ - QModelIndex idx = m_model->index(index, 0); - - m_model->setData(idx, pending, PendingDeletionRole); - - if (pending && pluginIndex(m_settings->name()) == index) { - // move to the next non-pending theme - const auto nonPending = m_model->match(idx, PendingDeletionRole, false); - m_settings->setName(nonPending.first().data(PluginNameRole).toString()); - } - - settingsChanged(); -} - void KCMDesktopTheme::getNewStuff(QQuickItem *ctx) { if (!m_newStuffDialog) { @@ -223,63 +211,8 @@ void KCMDesktopTheme::load() { ManagedConfigModule::load(); - - m_pendingRemoval.clear(); - - // Get all desktop themes - QStringList themes; - const QStringList &packs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/desktoptheme"), QStandardPaths::LocateDirectory); - Q_FOREACH (const QString &ppath, packs) { - const QDir cd(ppath); - const QStringList &entries = cd.entryList(QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot); - Q_FOREACH (const QString &pack, entries) { - const QString _metadata = ppath + QLatin1Char('/') + pack + QStringLiteral("/metadata.desktop"); - if (QFile::exists(_metadata)) { - themes << _metadata; - } - } - } - - m_model->clear(); - - Q_FOREACH (const QString &theme, themes) { - int themeSepIndex = theme.lastIndexOf(QLatin1Char('/'), -1); - const QString themeRoot = theme.left(themeSepIndex); - int themeNameSepIndex = themeRoot.lastIndexOf(QLatin1Char('/'), -1); - const QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1); - - KDesktopFile df(theme); - - if (df.noDisplay()) { - continue; - } - - QString name = df.readName(); - if (name.isEmpty()) { - name = packageName; - } - const bool isLocal = QFileInfo(theme).isWritable(); - // Plasma Theme creates a KColorScheme out of the "color" file and falls back to system colors if there is none - const bool followsSystemColors = !QFileInfo::exists(themeRoot + QLatin1String("/colors")); - - if (m_model->findItems(packageName).isEmpty()) { - QStandardItem *item = new QStandardItem; - item->setText(packageName); - item->setData(packageName, PluginNameRole); - item->setData(name, ThemeNameRole); - item->setData(df.readComment(), DescriptionRole); - item->setData(followsSystemColors, FollowsSystemColorsRole); - item->setData(isLocal, IsLocalRole); - item->setData(false, PendingDeletionRole); - m_model->appendRow(item); - } - } - - m_model->setSortRole(ThemeNameRole); // FIXME the model should really be just using Qt::DisplayRole - m_model->sort(0 /*column*/); - - // Model has been cleared so pretend the theme name changed to force view update - emit m_settings->nameChanged(); + m_model->load(); + m_model->setSelectedTheme(m_settings->name()); } void KCMDesktopTheme::save() @@ -294,9 +227,9 @@ ManagedConfigModule::defaults(); // can this be done more elegantly? - const auto pendingDeletions = m_model->match(m_model->index(0, 0), PendingDeletionRole, true); + const auto pendingDeletions = m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true); for (const QModelIndex &idx : pendingDeletions) { - m_model->setData(idx, false, PendingDeletionRole); + m_model->setData(idx, false, ThemesModel::PendingDeletionRole); } } @@ -312,23 +245,23 @@ bool KCMDesktopTheme::isSaveNeeded() const { - return !m_model->match(m_model->index(0, 0), PendingDeletionRole, true).isEmpty(); + return !m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true).isEmpty(); } void KCMDesktopTheme::processPendingDeletions() { const QString program = QStringLiteral("plasmapkg2"); - const auto pendingDeletions = m_model->match(m_model->index(0, 0), PendingDeletionRole, true, -1 /*all*/); + const auto pendingDeletions = m_model->match(m_model->index(0, 0), ThemesModel::PendingDeletionRole, true, -1 /*all*/); QVector persistentPendingDeletions; // turn into persistent model index so we can delete as we go std::transform(pendingDeletions.begin(), pendingDeletions.end(), std::back_inserter(persistentPendingDeletions), [](const QModelIndex &idx) { return QPersistentModelIndex(idx); }); for (const QPersistentModelIndex &idx : persistentPendingDeletions) { - const QString pluginName = idx.data(PluginNameRole).toString(); + const QString pluginName = idx.data(ThemesModel::PluginNameRole).toString(); const QString displayName = idx.data(Qt::DisplayRole).toString(); Q_ASSERT(pluginName != m_settings->name()); @@ -344,7 +277,7 @@ } else { emit showErrorMessage(i18n("Removing theme failed: %1", QString::fromLocal8Bit(process->readAllStandardOutput().trimmed()))); - m_model->setData(idx, false, PendingDeletionRole); + m_model->setData(idx, false, ThemesModel::PendingDeletionRole); } process->deleteLater(); }); diff --git a/kcms/desktoptheme/package/contents/ui/ThemePreview.qml b/kcms/desktoptheme/package/contents/ui/ThemePreview.qml --- a/kcms/desktoptheme/package/contents/ui/ThemePreview.qml +++ b/kcms/desktoptheme/package/contents/ui/ThemePreview.qml @@ -20,6 +20,7 @@ import org.kde.plasma.core 2.0 as PlasmaCore import org.kde.kirigami 2.4 as Kirigami import org.kde.plasma.components 2.0 as PlasmaComponents +import org.kde.private.kcms.desktoptheme 1.0 as Private Item { id: root @@ -176,7 +177,7 @@ } } Kirigami.Icon { - visible: model.followsSystemColors + visible: model.colorType === Private.ThemesModel.FollowsColorTheme source: "color-profile" width: Kirigami.Units.iconSizes.smallMedium height: width diff --git a/kcms/desktoptheme/package/contents/ui/main.qml b/kcms/desktoptheme/package/contents/ui/main.qml --- a/kcms/desktoptheme/package/contents/ui/main.qml +++ b/kcms/desktoptheme/package/contents/ui/main.qml @@ -26,12 +26,26 @@ import org.kde.kirigami 2.4 as Kirigami import org.kde.kconfig 1.0 // for KAuthorized import org.kde.kcm 1.1 as KCM +import org.kde.private.kcms.desktoptheme 1.0 as Private + KCM.GridViewKCM { KCM.ConfigModule.quickHelp: i18n("This module lets you choose the Plasma style.") - view.model: kcm.desktopThemeModel - view.currentIndex: kcm.pluginIndex(kcm.desktopThemeSettings.name) + view.model: kcm.filteredModel + view.currentIndex: kcm.filteredModel.selectedThemeIndex + + Binding { + target: kcm.filteredModel + property: "query" + value: searchField.text + } + + Binding { + target: kcm.filteredModel + property: "filter" + value: filterCombo.model[filterCombo.currentIndex].filter + } enabled: !kcm.downloadingFile && !kcm.desktopThemeSettings.isImmutable("name") @@ -44,13 +58,81 @@ } onDropped: kcm.installThemeFromFile(drop.urls[0]) } + header: RowLayout { + Layout.fillWidth: true + + QtControls.TextField { + id: searchField + Layout.fillWidth: true + placeholderText: i18n("Search...") + leftPadding: LayoutMirroring.enabled ? clearButton.width : undefined + rightPadding: LayoutMirroring.enabled ? undefined : clearButton.width + // this could be useful as a component + MouseArea { + id: clearButton + anchors { + top: parent.top + topMargin: parent.topPadding + right: parent.right + // the TextField's padding is taking into account the clear button's size + // so we just use the opposite one for positioning the clear button + rightMargin: LayoutMirroring.enabled ? parent.rightPadding: parent.leftPadding + bottom: parent.bottom + bottomMargin: parent.bottomPadding + } + width: height + + opacity: searchField.length > 0 ? 1 : 0 + onClicked: searchField.clear() + + Kirigami.Icon { + anchors.fill: parent + active: parent.pressed + source: "edit-clear-locationbar-" + (LayoutMirroring.enabled ? "ltr" : "rtl") + } + + Behavior on opacity { + NumberAnimation { duration: Kirigami.Units.longDuration } + } + } + } + QtControls.ComboBox { + id: filterCombo + textRole: "text" + model: [ + {text: i18n("All Themes"), filter: Private.FilterProxyModel.AllThemes}, + {text: i18n("Light Themes"), filter: Private.FilterProxyModel.LightThemes}, + {text: i18n("Dark Themes"), filter: Private.FilterProxyModel.DarkThemes}, + {text: i18n("Color scheme compatible"), filter: Private.FilterProxyModel.ThemesFollowingColors} + ] + + // HACK QQC2 doesn't support icons, so we just tamper with the desktop style ComboBox's background + // and inject a nice little filter icon. + Component.onCompleted: { + if (!background || !background.hasOwnProperty("properties")) { + // not a KQuickStyleItem + return; + } + + var props = background.properties || {}; + + background.properties = Qt.binding(function() { + var newProps = props; + newProps.currentIcon = "view-filter"; + newProps.iconColor = Kirigami.Theme.textColor; + return newProps; + }); + } + } + } view.delegate: KCM.GridDelegate { id: delegate - text: model.themeName - subtitle: model.followsSystemColors ? i18n("Follows color scheme") : undefined - toolTip: model.description || model.themeName + text: model.display + subtitle: model.colorType == Private.ThemesModel.FollowsColorTheme + && view.model.filter != Private.FilterProxyModel.ThemesFollowingColors ? i18n("Follows color scheme") : "" + toolTip: model.description || model.display opacity: model.pendingDeletion ? 0.3 : 1 Behavior on opacity { @@ -77,13 +159,13 @@ tooltip: i18n("Remove Theme") enabled: model.isLocal visible: !model.pendingDeletion - onTriggered: kcm.setPendingDeletion(model.index, true); + onTriggered: model.pendingDeletion = true; }, Kirigami.Action { iconName: "edit-undo" tooltip: i18n("Restore Theme") visible: model.pendingDeletion - onTriggered: kcm.setPendingDeletion(model.index, false); + onTriggered: model.pendingDeletion = false; } ] diff --git a/kcms/desktoptheme/themesmodel.h b/kcms/desktoptheme/themesmodel.h new file mode 100644 --- /dev/null +++ b/kcms/desktoptheme/themesmodel.h @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2007 Matthew Woehlke + * Copyright (C) 2007 Jeremy Whiting + * Copyright (C) 2016 Olivier Churlaud + * Copyright (C) 2019 Kai Uwe Broulik + * Copyright (C) 2019 David Redondo + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include +#include + +#include + +struct ThemesModelData; + +class ThemesModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString selectedTheme READ selectedTheme WRITE setSelectedTheme NOTIFY selectedThemeChanged) + Q_PROPERTY(int selectedThemeIndex READ selectedThemeIndex NOTIFY selectedThemeChanged) + +public: + ThemesModel(QObject *parent); + ~ThemesModel() override; + + enum Roles { + PluginNameRole = Qt::UserRole + 1, + ThemeNameRole, + DescriptionRole, + FollowsSystemColorsRole, + ColorTypeRole, + IsLocalRole, + PendingDeletionRole + }; + Q_ENUM(Roles) + enum ColorType { + LightTheme, + DarkTheme, + FollowsColorTheme + }; + Q_ENUM(ColorType) + + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QHash roleNames() const override; + + QString selectedTheme() const; + void setSelectedTheme(const QString &pluginName); + + int pluginIndex(const QString &pluginName) const; + int selectedThemeIndex() const; + + QStringList pendingDeletions() const; + void removeRow(int row); + + void load(); + +Q_SIGNALS: + void selectedThemeChanged(const QString &pluginName); + void selectedThemeIndexChanged(); + + void pendingDeletionsChanged(); + +private: + QString m_selectedTheme; + //Can't use QVector because unique_ptr causes deletion of copy-ctor + QVector m_data; + +}; + +struct ThemesModelData +{ + QString display; + QString pluginName; + QString description; + ThemesModel::ColorType type; + bool isLocal; + bool pendingDeletion; +}; diff --git a/kcms/desktoptheme/themesmodel.cpp b/kcms/desktoptheme/themesmodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/desktoptheme/themesmodel.cpp @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2007 Matthew Woehlke + * Copyright (C) 2007 Jeremy Whiting + * Copyright (C) 2016 Olivier Churlaud + * Copyright (C) 2019 Kai Uwe Broulik + * Copyright (C) 2019 David Redondo + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "themesmodel.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#include +#include + +ThemesModel::ThemesModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +ThemesModel::~ThemesModel() = default; + +int ThemesModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant ThemesModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return QVariant(); + } + + const auto &item = m_data.at(index.row()); + + switch (role) { + case Qt::DisplayRole: return item.display; + case PluginNameRole: return item.pluginName; + case DescriptionRole: return item.description; + case ColorTypeRole: return item.type; + case IsLocalRole: return item.isLocal; + case PendingDeletionRole: return item.pendingDeletion; + } + return QVariant(); +} + +bool ThemesModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || index.row() >= m_data.count()) { + return false; + } + + if (role == PendingDeletionRole) { + auto &item = m_data[index.row()]; + + const bool pendingDeletion = value.toBool(); + + if (item.pendingDeletion != pendingDeletion) { + item.pendingDeletion = pendingDeletion; + emit dataChanged(index, index, {PendingDeletionRole}); + + if (index.row() == selectedThemeIndex() && pendingDeletion) { + // move to the next non-pending theme + const auto nonPending = match(index, PendingDeletionRole, false); + if (!nonPending.isEmpty()) { + setSelectedTheme(nonPending.first().data(PluginNameRole).toString()); + } + } + + emit pendingDeletionsChanged(); + return true; + } + } + + return false; +} + +QHash ThemesModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {PluginNameRole, QByteArrayLiteral("pluginName")}, + {DescriptionRole, QByteArrayLiteral("description")}, + {ColorTypeRole, QByteArrayLiteral("colorType")}, + {IsLocalRole, QByteArrayLiteral("isLocal")}, + {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")} + }; +} + +QString ThemesModel::selectedTheme() const +{ + return m_selectedTheme; +} + +void ThemesModel::setSelectedTheme(const QString &pluginName) +{ + if (m_selectedTheme == pluginName) { + return; + } + + m_selectedTheme = pluginName; + + emit selectedThemeChanged(pluginName); + + emit selectedThemeIndexChanged(); +} + +int ThemesModel::pluginIndex(const QString &pluginName) const +{ + const auto results = match(index(0, 0), PluginNameRole, pluginName); + if (results.count() == 1) { + return results.first().row(); + } + + return -1; +} + +int ThemesModel::selectedThemeIndex() const +{ + return pluginIndex(m_selectedTheme); +} + +void ThemesModel::load() +{ + beginResetModel(); + + const int oldCount = m_data.count(); + + m_data.clear(); + + // Get all desktop themes + QStringList themes; + const QStringList packs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("plasma/desktoptheme"), QStandardPaths::LocateDirectory); + for(const QString &ppath : packs) { + const QDir cd(ppath); + const QStringList &entries = cd.entryList(QDir::Dirs | QDir::Hidden | QDir::NoDotAndDotDot); + for (const QString &pack : entries) { + const QString _metadata = ppath + QLatin1Char('/') + pack + QStringLiteral("/metadata.desktop"); + if (QFile::exists(_metadata)) { + themes << _metadata; + } + } + } + + for (const QString &theme : qAsConst(themes)) { + int themeSepIndex = theme.lastIndexOf(QLatin1Char('/'), -1); + const QString themeRoot = theme.left(themeSepIndex); + int themeNameSepIndex = themeRoot.lastIndexOf(QLatin1Char('/'), -1); + const QString packageName = themeRoot.right(themeRoot.length() - themeNameSepIndex - 1); + + KDesktopFile df(theme); + + if (df.noDisplay()) { + continue; + } + + QString name = df.readName(); + if (name.isEmpty()) { + name = packageName; + } + const bool isLocal = QFileInfo(theme).isWritable(); + bool hasPluginName = std::any_of(m_data.begin(), m_data.end(), [&] (const ThemesModelData &item) { + return item.pluginName == packageName; + }); + if (!hasPluginName) { + // Plasma Theme creates a KColorScheme out of the "color" file and falls back to system colors if there is none + const QString colorsPath = themeRoot + QLatin1String("/colors"); + const bool followsSystemColors = !QFileInfo::exists(colorsPath); + ColorType type = FollowsColorTheme; + if (!followsSystemColors) { + const KSharedConfig::Ptr config = KSharedConfig::openConfig(colorsPath); + const QPalette palette = KColorScheme::createApplicationPalette(config); + const int windowBackgroundGray = qGray(palette.window().color().rgb()); + if (windowBackgroundGray < 192) { + type = DarkTheme; + } else { + type = LightTheme; + } + } + ThemesModelData item { + name, + packageName, + df.readComment(), + type, + isLocal, + false + }; + m_data.append(item); + } + } + + QCollator collator; + std::sort(m_data.begin(), m_data.end(), [&collator](const ThemesModelData &a, const ThemesModelData &b) { + return collator.compare(a.display, b.display) < 0; + }); + + endResetModel(); + + // an item might have been added before the currently selected one + if (oldCount != m_data.count()) { + emit selectedThemeIndexChanged(); + } +} + +QStringList ThemesModel::pendingDeletions() const +{ + QStringList pendingDeletions; + + for (const auto &item : qAsConst(m_data)) { + if (item.pendingDeletion) { + pendingDeletions.append(item.pluginName); + } + } + + return pendingDeletions; +} + +void ThemesModel::removeRow(int row) +{ + beginRemoveRows(QModelIndex(), row, row); + m_data.erase(m_data.begin() + row); + endRemoveRows(); +}