diff --git a/kcms/desktoptheme/CMakeLists.txt b/kcms/desktoptheme/CMakeLists.txt index 12ea347fd..c8dec1fea 100644 --- a/kcms/desktoptheme/CMakeLists.txt +++ b/kcms/desktoptheme/CMakeLists.txt @@ -1,31 +1,33 @@ # KI18N Translation Domain for this library add_definitions(-DTRANSLATION_DOMAIN=\"kcm_desktoptheme\") set(kcm_desktoptheme_SRCS kcm.cpp + themesmodel.cpp + filterproxymodel.cpp ) kconfig_add_kcfg_files(kcm_desktoptheme_SRCS desktopthemesettings.kcfgc GENERATE_MOC) add_library(kcm_desktoptheme MODULE ${kcm_desktoptheme_SRCS}) target_link_libraries(kcm_desktoptheme KF5::CoreAddons KF5::KCMUtils KF5::KIOCore KF5::KIOWidgets KF5::I18n KF5::Plasma KF5::Declarative KF5::QuickAddons KF5::NewStuff ) kcoreaddons_desktop_to_json(kcm_desktoptheme "kcm_desktoptheme.desktop" SERVICE_TYPES kcmodule.desktop) #this desktop file is installed only for retrocompatibility with sycoca install(FILES kcm_desktoptheme.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES plasma-themes.knsrc DESTINATION ${KDE_INSTALL_KNSRCDIR}) install(TARGETS kcm_desktoptheme DESTINATION ${KDE_INSTALL_PLUGINDIR}/kcms) kpackage_install_package(package kcm_desktoptheme kcms) diff --git a/kcms/desktoptheme/filterproxymodel.cpp b/kcms/desktoptheme/filterproxymodel.cpp new file mode 100644 index 000000000..5f6211863 --- /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/filterproxymodel.h b/kcms/desktoptheme/filterproxymodel.h new file mode 100644 index 000000000..a6a04020f --- /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/kcm.cpp b/kcms/desktoptheme/kcm.cpp index d56b16988..49679a8be 100644 --- a/kcms/desktoptheme/kcm.cpp +++ b/kcms/desktoptheme/kcm.cpp @@ -1,357 +1,290 @@ /* This file is part of the KDE Project Copyright (c) 2014 Marco Martin Copyright (c) 2014 Vishesh Handa Copyright (c) 2016 David Rosca Copyright (c) 2018 Kai Uwe Broulik Copyright (c) 2019 Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kcm.h" #include #include #include -#include #include #include #include #include #include #include #include #include #include #include #include #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() { } DesktopThemeSettings *KCMDesktopTheme::desktopThemeSettings() const { 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) { m_newStuffDialog = new KNS3::DownloadDialog(QStringLiteral("plasma-themes.knsrc")); m_newStuffDialog.data()->setWindowTitle(i18n("Download New Plasma Styles")); m_newStuffDialog->setWindowModality(Qt::WindowModal); m_newStuffDialog->winId(); // so it creates the windowHandle(); connect(m_newStuffDialog.data(), &KNS3::DownloadDialog::accepted, this, &KCMDesktopTheme::load); } if (ctx && ctx->window()) { m_newStuffDialog->windowHandle()->setTransientParent(ctx->window()); } m_newStuffDialog.data()->show(); } void KCMDesktopTheme::installThemeFromFile(const QUrl &url) { if (url.isLocalFile()) { installTheme(url.toLocalFile()); return; } if (m_tempCopyJob) { return; } m_tempInstallFile.reset(new QTemporaryFile()); if (!m_tempInstallFile->open()) { emit showErrorMessage(i18n("Unable to create a temporary file.")); m_tempInstallFile.reset(); return; } m_tempCopyJob = KIO::file_copy(url, QUrl::fromLocalFile(m_tempInstallFile->fileName()), -1, KIO::Overwrite); m_tempCopyJob->uiDelegate()->setAutoErrorHandlingEnabled(true); emit downloadingFileChanged(); connect(m_tempCopyJob, &KIO::FileCopyJob::result, this, [this, url](KJob *job) { if (job->error() != KJob::NoError) { emit showErrorMessage(i18n("Unable to download the theme: %1", job->errorText())); return; } installTheme(m_tempInstallFile->fileName()); m_tempInstallFile.reset(); }); connect(m_tempCopyJob, &QObject::destroyed, this, &KCMDesktopTheme::downloadingFileChanged); } void KCMDesktopTheme::installTheme(const QString &path) { qCDebug(KCM_DESKTOP_THEME) << "Installing ... " << path; const QString program = QStringLiteral("kpackagetool5"); const QStringList arguments = { QStringLiteral("--type"), QStringLiteral("Plasma/Theme"), QStringLiteral("--install"), path}; qCDebug(KCM_DESKTOP_THEME) << program << arguments.join(QLatin1Char(' ')); QProcess *myProcess = new QProcess(this); connect(myProcess, static_cast(&QProcess::finished), this, [this](int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitStatus) if (exitCode == 0) { emit showSuccessMessage(i18n("Theme installed successfully.")); load(); } else { Q_EMIT showErrorMessage(i18n("Theme installation failed.")); } }); connect(myProcess, static_cast(&QProcess::error), this, [this](QProcess::ProcessError e) { qCWarning(KCM_DESKTOP_THEME) << "Theme installation failed: " << e; Q_EMIT showErrorMessage(i18n("Theme installation failed.")); }); myProcess->start(program, arguments); } void KCMDesktopTheme::applyPlasmaTheme(QQuickItem *item, const QString &themeName) { if (!item) { return; } Plasma::Theme *theme = m_themes[themeName]; if (!theme) { theme = new Plasma::Theme(themeName, this); m_themes[themeName] = theme; } Q_FOREACH (Plasma::Svg *svg, item->findChildren()) { svg->setTheme(theme); svg->setUsingRenderingCache(false); } } 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() { ManagedConfigModule::save(); Plasma::Theme().setThemeName(m_settings->name()); processPendingDeletions(); } void KCMDesktopTheme::defaults() { 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); } } bool KCMDesktopTheme::canEditThemes() const { return m_haveThemeExplorerInstalled; } void KCMDesktopTheme::editTheme(const QString &theme) { QProcess::startDetached(QStringLiteral("plasmathemeexplorer -t ") % theme); } 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()); const QStringList arguments = {QStringLiteral("-t"), QStringLiteral("theme"), QStringLiteral("-r"), pluginName}; QProcess *process = new QProcess(this); connect(process, static_cast(&QProcess::finished), this, [this, process, idx, pluginName, displayName](int exitCode, QProcess::ExitStatus exitStatus) { Q_UNUSED(exitStatus) if (exitCode == 0) { m_model->removeRow(idx.row()); } 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(); }); process->start(program, arguments); process->waitForFinished(); // needed so it deletes fine when "OK" is clicked and the dialog destroyed } } #include "kcm.moc" diff --git a/kcms/desktoptheme/kcm.h b/kcms/desktoptheme/kcm.h index a2e163279..8575eb034 100644 --- a/kcms/desktoptheme/kcm.h +++ b/kcms/desktoptheme/kcm.h @@ -1,118 +1,109 @@ /* Copyright (c) 2014 Marco Martin Copyright (c) 2014 Vishesh Handa Copyright (c) 2016 David Rosca Copyright (c) 2018 Kai Uwe Broulik Copyright (c) 2019 Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KCM_DESKTOPTHEME_H #define _KCM_DESKTOPTHEME_H #include #include class QTemporaryFile; namespace Plasma { class Theme; } namespace KIO { class FileCopyJob; } 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); Q_SIGNALS: void downloadingFileChanged(); void showSuccessMessage(const QString &message); void showErrorMessage(const QString &message); public Q_SLOTS: void load() override; void save() override; void defaults() override; private: bool isSaveNeeded() const override; void processPendingDeletions(); void installTheme(const QString &path); DesktopThemeSettings *m_settings; - QStandardItemModel *m_model; - QStringList m_pendingRemoval; + ThemesModel *m_model; + FilterProxyModel *m_filteredModel; QHash m_themes; bool m_haveThemeExplorerInstalled; QPointer m_newStuffDialog; QScopedPointer m_tempInstallFile; QPointer m_tempCopyJob; }; Q_DECLARE_LOGGING_CATEGORY(KCM_DESKTOP_THEME) #endif // _KCM_DESKTOPTHEME_H diff --git a/kcms/desktoptheme/package/contents/ui/ThemePreview.qml b/kcms/desktoptheme/package/contents/ui/ThemePreview.qml index 479b5e05d..278ab5fbe 100644 --- a/kcms/desktoptheme/package/contents/ui/ThemePreview.qml +++ b/kcms/desktoptheme/package/contents/ui/ThemePreview.qml @@ -1,190 +1,191 @@ /* Copyright (c) 2016 David Rosca This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.4 import QtQuick.Layouts 1.1 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 property string themeName Item { id: backgroundMask anchors.fill: parent clip: true PlasmaCore.FrameSvgItem { id: background // Normalize margins around background. // Some themes like "Air" have huge transparent margins which would result in too small container area. // Sadly all of the breathing, shadow and border sizes are in one single margin value, // but for typical themes the border is the smaller part the margin and should be in the size of // Units.largeSpacing, to which we add another Units.largeSpacing for margin of the visual content // Ideally Plasma::FrameSvg exposes the transparent margins one day. readonly property int generalMargin: 2 * Kirigami.Units.largeSpacing anchors { fill: parent topMargin: -margins.top + generalMargin bottomMargin: -margins.bottom + generalMargin leftMargin: -margins.left + generalMargin rightMargin: -margins.right + generalMargin } imagePath: "widgets/background" } } RowLayout { id: contents spacing: 0 anchors { fill: parent topMargin: background.generalMargin bottomMargin: background.generalMargin leftMargin: background.generalMargin rightMargin: background.generalMargin } // Icons ColumnLayout { id: icons Layout.fillHeight: true PlasmaCore.IconItem { id: computerIcon Layout.fillHeight: true source: "computer" } PlasmaCore.IconItem { id: applicationsIcon Layout.fillHeight: true source: "applications-other" } PlasmaCore.IconItem { id: logoutIcon Layout.fillHeight: true source: "system-log-out" } } // Analog clock Item { id: clock Layout.fillHeight: true Layout.fillWidth: true Layout.preferredWidth: height Layout.alignment: Qt.AlignHCenter property int hours: 9 property int minutes: 5 readonly property double svgScale: face.width / face.naturalSize.width readonly property double horizontalShadowOffset: Math.round(clockSvg.naturalHorizontalHandShadowOffset * svgScale) + Math.round(clockSvg.naturalHorizontalHandShadowOffset * svgScale) % 2 readonly property double verticalShadowOffset: Math.round(clockSvg.naturalVerticalHandShadowOffset * svgScale) + Math.round(clockSvg.naturalVerticalHandShadowOffset * svgScale) % 2 PlasmaCore.Svg { id: clockSvg imagePath: "widgets/clock" function estimateHorizontalHandShadowOffset() { var id = "hint-hands-shadow-offset-to-west"; if (hasElement(id)) { return -elementSize(id).width; } id = "hint-hands-shadows-offset-to-east"; if (hasElement(id)) { return elementSize(id).width; } return 0; } function estimateVerticalHandShadowOffset() { var id = "hint-hands-shadow-offset-to-north"; if (hasElement(id)) { return -elementSize(id).height; } id = "hint-hands-shadow-offset-to-south"; if (hasElement(id)) { return elementSize(id).height; } return 0; } property double naturalHorizontalHandShadowOffset: estimateHorizontalHandShadowOffset() property double naturalVerticalHandShadowOffset: estimateVerticalHandShadowOffset() onRepaintNeeded: { naturalHorizontalHandShadowOffset = estimateHorizontalHandShadowOffset(); naturalVerticalHandShadowOffset = estimateVerticalHandShadowOffset(); } } PlasmaCore.SvgItem { id: face anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: Math.min(parent.width, parent.height) svg: clockSvg elementId: "ClockFace" } Hand { elementId: "HourHand" rotationCenterHintId: "hint-hourhand-rotation-center-offset" rotation: 180 + clock.hours * 30 + (clock.minutes/2) svgScale: clock.svgScale } Hand { elementId: "MinuteHand" rotationCenterHintId: "hint-minutehand-rotation-center-offset" rotation: 180 + clock.minutes * 6 svgScale: clock.svgScale } PlasmaCore.SvgItem { id: center width: naturalSize.width * clock.svgScale height: naturalSize.height * clock.svgScale anchors.centerIn: clock svg: clockSvg elementId: "HandCenterScrew" z: 1000 } PlasmaCore.SvgItem { anchors.fill: face svg: clockSvg elementId: "Glass" width: naturalSize.width * clock.svgScale height: naturalSize.height * clock.svgScale } } Kirigami.Icon { - visible: model.followsSystemColors + visible: model.colorType === Private.ThemesModel.FollowsColorTheme source: "color-profile" width: Kirigami.Units.iconSizes.smallMedium height: width Layout.alignment: Qt.AlignRight && Qt.AlignTop } } Component.onCompleted: { kcm.applyPlasmaTheme(root, themeName); } } diff --git a/kcms/desktoptheme/package/contents/ui/main.qml b/kcms/desktoptheme/package/contents/ui/main.qml index abbb49842..5581119b3 100644 --- a/kcms/desktoptheme/package/contents/ui/main.qml +++ b/kcms/desktoptheme/package/contents/ui/main.qml @@ -1,153 +1,235 @@ /* Copyright (c) 2014 Marco Martin Copyright (c) 2016 David Rosca Copyright (c) 2018 Kai Uwe Broulik Copyright (c) 2019 Kevin Ottens This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2 as published by the Free Software Foundation. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import QtQuick 2.1 import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.0 import QtQuick.Controls 2.3 as QtControls 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") DropArea { anchors.fill: parent onEntered: { if (!drag.hasUrls) { drag.accepted = false; } } 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 { NumberAnimation { duration: Kirigami.Units.longDuration } } thumbnailAvailable: true thumbnail: ThemePreview { id: preview anchors.fill: parent themeName: model.pluginName } actions: [ Kirigami.Action { iconName: "document-edit" tooltip: i18n("Edit Theme...") enabled: !model.pendingDeletion visible: kcm.canEditThemes onTriggered: kcm.editTheme(model.pluginName) }, Kirigami.Action { iconName: "edit-delete" 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; } ] onClicked: { kcm.desktopThemeSettings.name = model.pluginName; view.forceActiveFocus(); } } footer: ColumnLayout { Kirigami.InlineMessage { id: infoLabel Layout.fillWidth: true showCloseButton: true Connections { target: kcm onShowSuccessMessage: { infoLabel.type = Kirigami.MessageType.Positive; infoLabel.text = message; infoLabel.visible = true; } onShowErrorMessage: { infoLabel.type = Kirigami.MessageType.Error; infoLabel.text = message; infoLabel.visible = true; } } } RowLayout { Layout.alignment: Qt.AlignRight QtControls.Button { text: i18n("Install from File...") icon.name: "document-import" onClicked: fileDialogLoader.active = true; } QtControls.Button { text: i18n("Get New Plasma Styles...") icon.name: "get-hot-new-stuff" onClicked: kcm.getNewStuff(this) visible: KAuthorized.authorize("ghns") } } } Loader { id: fileDialogLoader active: false sourceComponent: FileDialog { title: i18n("Open Theme") folder: shortcuts.home nameFilters: [ i18n("Theme Files (*.zip *.tar.gz *.tar.bz2)") ] Component.onCompleted: open() onAccepted: { kcm.installThemeFromFile(fileUrls[0]) fileDialogLoader.active = false } onRejected: { fileDialogLoader.active = false } } } } diff --git a/kcms/desktoptheme/themesmodel.cpp b/kcms/desktoptheme/themesmodel.cpp new file mode 100644 index 000000000..f2b3fa8dd --- /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(); +} diff --git a/kcms/desktoptheme/themesmodel.h b/kcms/desktoptheme/themesmodel.h new file mode 100644 index 000000000..9b893b40a --- /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; +};