diff --git a/kcms/colors/CMakeLists.txt b/kcms/colors/CMakeLists.txt --- a/kcms/colors/CMakeLists.txt +++ b/kcms/colors/CMakeLists.txt @@ -4,6 +4,8 @@ set(kcm_colors_SRCS ../krdb/krdb.cpp colors.cpp + colorsmodel.cpp + filterproxymodel.cpp ) # needed for krdb diff --git a/kcms/colors/colors.h b/kcms/colors/colors.h --- a/kcms/colors/colors.h +++ b/kcms/colors/colors.h @@ -38,13 +38,15 @@ class FileCopyJob; } +class ColorsModel; +class FilterProxyModel; + class KCMColors : public KQuickAddons::ConfigModule { Q_OBJECT - Q_PROPERTY(QStandardItemModel *colorsModel READ colorsModel CONSTANT) - Q_PROPERTY(QString selectedScheme READ selectedScheme WRITE setSelectedScheme NOTIFY selectedSchemeChanged) - Q_PROPERTY(int selectedSchemeIndex READ selectedSchemeIndex NOTIFY selectedSchemeIndexChanged) + Q_PROPERTY(ColorsModel *model READ model CONSTANT) + Q_PROPERTY(FilterProxyModel *filteredModel READ filteredModel CONSTANT) Q_PROPERTY(bool downloadingFile READ downloadingFile NOTIFY downloadingFileChanged) public: @@ -58,21 +60,22 @@ PendingDeletionRole }; - QStandardItemModel *colorsModel() const; - - QString selectedScheme() const; - void setSelectedScheme(const QString &scheme); + enum SchemeFilter { + AllSchemes, + LightSchemes, + DarkSchemes + }; + Q_ENUM(SchemeFilter) - int selectedSchemeIndex() const; + ColorsModel *model() const; + FilterProxyModel *filteredModel() const; bool downloadingFile() const; Q_INVOKABLE void getNewStuff(QQuickItem *ctx); Q_INVOKABLE void installSchemeFromFile(const QUrl &url); - Q_INVOKABLE void setPendingDeletion(int index, bool pending); - - Q_INVOKABLE void editScheme(int index, QQuickItem *ctx); + Q_INVOKABLE void editScheme(const QString &schemeName, QQuickItem *ctx); public Q_SLOTS: void load() override; @@ -90,18 +93,14 @@ void showSchemeNotInstalledWarning(const QString &schemeName); private: - void loadModel(); - void saveColors(); void processPendingDeletions(); - int indexOfScheme(const QString &schemeName) const; - void installSchemeFile(const QString &path); - QStandardItemModel *m_model; + ColorsModel *m_model; + FilterProxyModel *m_filteredModel; - QString m_selectedScheme; bool m_selectedSchemeDirty = false; bool m_applyToAlien = true; diff --git a/kcms/colors/colors.cpp b/kcms/colors/colors.cpp --- a/kcms/colors/colors.cpp +++ b/kcms/colors/colors.cpp @@ -49,142 +49,60 @@ #include "../krdb/krdb.h" +#include "colorsmodel.h" +#include "filterproxymodel.h" + static const QString s_defaultColorSchemeName = QStringLiteral("Breeze"); K_PLUGIN_FACTORY_WITH_JSON(KCMColorsFactory, "kcm_colors.json", registerPlugin();) KCMColors::KCMColors(QObject *parent, const QVariantList &args) : KQuickAddons::ConfigModule(parent, args) + , m_model(new ColorsModel(this)) + , m_filteredModel(new FilterProxyModel(this)) , m_config(KSharedConfig::openConfig(QStringLiteral("kdeglobals"))) { - qmlRegisterType(); + qmlRegisterUncreatableType("org.kde.private.kcms.colors", 1, 0, "KCM", QStringLiteral("Cannot create instances of KCM")); + qmlRegisterType(); + qmlRegisterType(); KAboutData *about = new KAboutData(QStringLiteral("kcm_colors"), i18n("Colors"), QStringLiteral("2.0"), QString(), KAboutLicense::GPL); about->addAuthor(i18n("Kai Uwe Broulik"), QString(), QStringLiteral("kde@privat.broulik.de")); setAboutData(about); - m_model = new QStandardItemModel(this); - m_model->setItemRoleNames({ - {Qt::DisplayRole, QByteArrayLiteral("display")}, - {SchemeNameRole, QByteArrayLiteral("schemeName")}, - {PaletteRole, QByteArrayLiteral("palette")}, - {RemovableRole, QByteArrayLiteral("removable")}, - {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")} + connect(m_model, &ColorsModel::selectedSchemeChanged, this, [this] { + m_selectedSchemeDirty = true; + setNeedsSave(true); }); + connect(m_model, &ColorsModel::pendingDeletionsChanged, this, [this] { + setNeedsSave(true); + }); + + connect(m_model, &ColorsModel::selectedSchemeChanged, m_filteredModel, &FilterProxyModel::setSelectedScheme); + m_filteredModel->setSourceModel(m_model); } KCMColors::~KCMColors() { m_config->markAsClean(); } -QStandardItemModel *KCMColors::colorsModel() const +ColorsModel *KCMColors::model() const { return m_model; } -QString KCMColors::selectedScheme() const +FilterProxyModel *KCMColors::filteredModel() const { - return m_selectedScheme; -} - -void KCMColors::setSelectedScheme(const QString &scheme) -{ - if (m_selectedScheme == scheme) { - return; - } - - const bool firstTime = m_selectedScheme.isNull(); - m_selectedScheme = scheme; - emit selectedSchemeChanged(); - emit selectedSchemeIndexChanged(); - - if (!firstTime) { - setNeedsSave(true); - m_selectedSchemeDirty = true; - } -} - -int KCMColors::selectedSchemeIndex() const -{ - return indexOfScheme(m_selectedScheme); -} - -int KCMColors::indexOfScheme(const QString &schemeName) const -{ - const auto results = m_model->match(m_model->index(0, 0), SchemeNameRole, schemeName); - if (results.count() == 1) { - return results.first().row(); - } - - return -1; + return m_filteredModel; } bool KCMColors::downloadingFile() const { return m_tempCopyJob; } -void KCMColors::setPendingDeletion(int index, bool pending) -{ - QModelIndex idx = m_model->index(index, 0); - - m_model->setData(idx, pending, PendingDeletionRole); - - if (pending && selectedSchemeIndex() == index) { - // move to the next non-pending theme - const auto nonPending = m_model->match(idx, PendingDeletionRole, false); - setSelectedScheme(nonPending.first().data(SchemeNameRole).toString()); - } - - setNeedsSave(true); -} - -void KCMColors::loadModel() -{ - m_model->clear(); - - QStringList schemeFiles; - - const QStringList schemeDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes"), QStandardPaths::LocateDirectory); - for (const QString &dir : schemeDirs) { - const QStringList fileNames = QDir(dir).entryList(QStringList{QStringLiteral("*.colors")}); - for (const QString &file : fileNames) { - const QString suffixedFileName = QStringLiteral("color-schemes/") + file; - // can't use QSet because of the transform below (passing const QString as this argument discards qualifiers) - if (!schemeFiles.contains(suffixedFileName)) { - schemeFiles.append(suffixedFileName); - } - } - } - - std::transform(schemeFiles.begin(), schemeFiles.end(), schemeFiles.begin(), [](const QString &item) { - return QStandardPaths::locate(QStandardPaths::GenericDataLocation, item); - }); - - for (const QString &schemeFile : schemeFiles) { - const QFileInfo fi(schemeFile); - const QString baseName = fi.baseName(); - - KSharedConfigPtr config = KSharedConfig::openConfig(schemeFile, KConfig::SimpleConfig); - KConfigGroup group(config, "General"); - const QString name = group.readEntry("Name", baseName); - - QStandardItem *item = new QStandardItem(name); - item->setData(baseName, SchemeNameRole); - item->setData(fi.isWritable(), RemovableRole); - item->setData(false, PendingDeletionRole); - - item->setData(KColorScheme::createApplicationPalette(config), PaletteRole); - - m_model->appendRow(item); - } - - m_model->sort(0 /*column*/); - emit selectedSchemeIndexChanged(); -} - void KCMColors::getNewStuff(QQuickItem *ctx) { if (!m_newStuffDialog) { @@ -194,7 +112,7 @@ m_newStuffDialog->winId(); // so it creates the windowHandle(); connect(m_newStuffDialog.data(), &KNS3::DownloadDialog::accepted, this, [this] { - loadModel(); + m_model->load(); const auto newEntries = m_newStuffDialog->installedEntries(); // If one new theme was installed, select the first color file in it @@ -219,7 +137,7 @@ // but that would require us parse every file, so this should be close enough std::sort(installedThemes.begin(), installedThemes.end()); - setSelectedScheme(installedThemes.constFirst()); + m_model->setSelectedScheme(installedThemes.constFirst()); } } }); @@ -314,23 +232,23 @@ group2.writeEntry("Name", newName); config2->sync(); - loadModel(); + m_model->load(); const auto results = m_model->match(m_model->index(0, 0), SchemeNameRole, newName); if (!results.isEmpty()) { - setSelectedScheme(newName); + m_model->setSelectedScheme(newName); } emit showSuccessMessage(i18n("Color scheme installed successfully.")); } -void KCMColors::editScheme(int index, QQuickItem *ctx) +void KCMColors::editScheme(const QString &schemeName, QQuickItem *ctx) { if (m_editDialogProcess) { return; } - QModelIndex idx = m_model->index(index, 0); + QModelIndex idx = m_model->index(m_model->indexOfScheme(schemeName), 0); m_editDialogProcess = new QProcess(this); connect(m_editDialogProcess, QOverload::of(&QProcess::finished), this, @@ -341,9 +259,9 @@ const auto savedThemes = QString::fromUtf8(m_editDialogProcess->readAllStandardOutput()).split(QLatin1Char('\n'), QString::SkipEmptyParts); if (!savedThemes.isEmpty()) { - loadModel(); // would be cool to just reload/add the changed/new ones + m_model->load(); // would be cool to just reload/add the changed/new ones - setSelectedScheme(savedThemes.last()); + m_model->setSelectedScheme(savedThemes.last()); } m_editDialogProcess->deleteLater(); @@ -374,20 +292,24 @@ void KCMColors::load() { - loadModel(); + m_model->load(); m_config->markAsClean(); m_config->reparseConfiguration(); KConfigGroup group(m_config, "General"); const QString schemeName = group.readEntry("ColorScheme", s_defaultColorSchemeName); // If the scheme named in kdeglobals doesn't exist, show a warning and use default scheme - if (indexOfScheme(schemeName) == -1) { - setSelectedScheme(s_defaultColorSchemeName); + if (m_model->indexOfScheme(schemeName) == -1) { + m_model->setSelectedScheme(s_defaultColorSchemeName); + // These are normally synced but initially the model doesn't emit a change to avoid the + // Apply button from being enabled without any user interaction. Sync manually here. + m_filteredModel->setSelectedScheme(s_defaultColorSchemeName); emit showSchemeNotInstalledWarning(schemeName); } else { - setSelectedScheme(schemeName); + m_model->setSelectedScheme(schemeName); + m_filteredModel->setSelectedScheme(schemeName); } { @@ -411,10 +333,10 @@ void KCMColors::saveColors() { KConfigGroup grp(m_config, "General"); - grp.writeEntry("ColorScheme", m_selectedScheme); + grp.writeEntry("ColorScheme", m_model->selectedScheme()); const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, - QStringLiteral("color-schemes/%1.colors").arg(m_selectedScheme)); + QStringLiteral("color-schemes/%1.colors").arg(m_model->selectedScheme())); KSharedConfigPtr config = KSharedConfig::openConfig(path); @@ -529,18 +451,10 @@ void KCMColors::processPendingDeletions() { - const auto pendingDeletions = m_model->match(m_model->index(0, 0), 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 schemeName = idx.data(SchemeNameRole).toString(); + const QStringList pendingDeletions = m_model->pendingDeletions(); - Q_ASSERT(schemeName != m_selectedScheme); + for (const QString &schemeName : pendingDeletions) { + Q_ASSERT(schemeName != m_model->selectedScheme()); const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes/%1.colors").arg(schemeName)); @@ -550,15 +464,12 @@ job->exec(); } - // remove them in a separate loop after all the delete jobs for a smoother animation - for (const QPersistentModelIndex &idx : persistentPendingDeletions) { - m_model->removeRow(idx.row()); - } + m_model->removeItemsPendingDeletion(); } void KCMColors::defaults() { - setSelectedScheme(s_defaultColorSchemeName); + m_model->setSelectedScheme(s_defaultColorSchemeName); setNeedsSave(true); } diff --git a/kcms/colors/colorsmodel.h b/kcms/colors/colorsmodel.h new file mode 100644 --- /dev/null +++ b/kcms/colors/colorsmodel.h @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2007 Matthew Woehlke + * Copyright (C) 2007 Jeremy Whiting + * Copyright (C) 2016 Olivier Churlaud + * Copyright (C) 2019 Kai Uwe Broulik + * + * 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 + +struct ColorsModelData +{ + QString display; + QString schemeName; + QPalette palette; + bool removable; + bool pendingDeletion; +}; +Q_DECLARE_TYPEINFO(ColorsModelData, Q_MOVABLE_TYPE); + +class ColorsModel : public QAbstractListModel +{ + Q_OBJECT + + Q_PROPERTY(QString selectedScheme READ selectedScheme WRITE setSelectedScheme NOTIFY selectedSchemeChanged) + Q_PROPERTY(int selectedSchemeIndex READ selectedSchemeIndex NOTIFY selectedSchemeIndexChanged) + +public: + ColorsModel(QObject *parent); + ~ColorsModel() override; + + enum Roles { + SchemeNameRole = Qt::UserRole + 1, + PaletteRole, + RemovableRole, + PendingDeletionRole + }; + + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QHash roleNames() const override; + + QString selectedScheme() const; + void setSelectedScheme(const QString &scheme); + + int indexOfScheme(const QString &scheme) const; + int selectedSchemeIndex() const; + + QStringList pendingDeletions() const; + void removeItemsPendingDeletion(); + + void load(); + +Q_SIGNALS: + void selectedSchemeChanged(const QString &scheme); + void selectedSchemeIndexChanged(); + + void pendingDeletionsChanged(); + +private: + QString m_selectedScheme; + + QVector m_data; + +}; diff --git a/kcms/colors/colorsmodel.cpp b/kcms/colors/colorsmodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/colors/colorsmodel.cpp @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2007 Matthew Woehlke + * Copyright (C) 2007 Jeremy Whiting + * Copyright (C) 2016 Olivier Churlaud + * Copyright (C) 2019 Kai Uwe Broulik + * + * 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 "colorsmodel.h" + +#include +#include +#include + +#include +#include +#include + +#include + +ColorsModel::ColorsModel(QObject *parent) : QAbstractListModel(parent) +{ + +} + +ColorsModel::~ColorsModel() = default; + +int ColorsModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_data.count(); +} + +QVariant ColorsModel::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 SchemeNameRole: return item.schemeName; + case PaletteRole: return item.palette; + case PendingDeletionRole: return item.pendingDeletion; + case RemovableRole: return item.removable; + } + + return QVariant(); +} + +bool ColorsModel::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}); + + // move to the next non-pending theme + const auto nonPending = match(index, PendingDeletionRole, false); + if (!nonPending.isEmpty()) { + setSelectedScheme(nonPending.first().data(SchemeNameRole).toString()); + } + + emit pendingDeletionsChanged(); + return true; + } + } + + return false; +} + +QHash ColorsModel::roleNames() const +{ + return { + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {SchemeNameRole, QByteArrayLiteral("schemeName")}, + {PaletteRole, QByteArrayLiteral("palette")}, + {RemovableRole, QByteArrayLiteral("removable")}, + {PendingDeletionRole, QByteArrayLiteral("pendingDeletion")} + }; +} + +QString ColorsModel::selectedScheme() const +{ + return m_selectedScheme; +} + +void ColorsModel::setSelectedScheme(const QString &scheme) +{ + if (m_selectedScheme == scheme) { + return; + } + + const bool firstTime = m_selectedScheme.isNull(); + m_selectedScheme = scheme; + + if (!firstTime) { + emit selectedSchemeChanged(scheme); + } + emit selectedSchemeIndexChanged(); +} + +int ColorsModel::indexOfScheme(const QString &scheme) const +{ + auto it = std::find_if(m_data.begin(), m_data.end(), [this, &scheme](const ColorsModelData &item) { + return item.schemeName == scheme; + }); + + if (it != m_data.end()) { + return std::distance(m_data.begin(), it); + } + + return -1; +} + +int ColorsModel::selectedSchemeIndex() const +{ + return indexOfScheme(m_selectedScheme); +} + +void ColorsModel::load() +{ + beginResetModel(); + + const int oldCount = m_data.count(); + + m_data.clear(); + + QStringList schemeFiles; + + const QStringList schemeDirs = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("color-schemes"), QStandardPaths::LocateDirectory); + for (const QString &dir : schemeDirs) { + const QStringList fileNames = QDir(dir).entryList(QStringList{QStringLiteral("*.colors")}); + for (const QString &file : fileNames) { + const QString suffixedFileName = QStringLiteral("color-schemes/") + file; + // can't use QSet because of the transform below (passing const QString as this argument discards qualifiers) + if (!schemeFiles.contains(suffixedFileName)) { + schemeFiles.append(suffixedFileName); + } + } + } + + std::transform(schemeFiles.begin(), schemeFiles.end(), schemeFiles.begin(), [](const QString &item) { + return QStandardPaths::locate(QStandardPaths::GenericDataLocation, item); + }); + + for (const QString &schemeFile : schemeFiles) { + const QFileInfo fi(schemeFile); + const QString baseName = fi.baseName(); + + KSharedConfigPtr config = KSharedConfig::openConfig(schemeFile, KConfig::SimpleConfig); + KConfigGroup group(config, "General"); + const QString name = group.readEntry("Name", baseName); + + ColorsModelData item{ + name, + baseName, + KColorScheme::createApplicationPalette(config), + fi.isWritable(), + false, // pending deletion + }; + + m_data.append(item); + } + + QCollator collator; + std::sort(m_data.begin(), m_data.end(), [&collator](const ColorsModelData &a, const ColorsModelData &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 selectedSchemeIndexChanged(); + } +} + +QStringList ColorsModel::pendingDeletions() const +{ + QStringList pendingDeletions; + + for (const auto &item : m_data) { + if (item.pendingDeletion) { + pendingDeletions.append(item.schemeName); + } + } + + return pendingDeletions; +} + +void ColorsModel::removeItemsPendingDeletion() +{ + for (int i = m_data.count() - 1; i >= 0; --i) { + if (m_data.at(i).pendingDeletion) { + beginRemoveRows(QModelIndex(), i, i); + m_data.remove(i); + endRemoveRows(); + } + } +} diff --git a/kcms/colors/filterproxymodel.h b/kcms/colors/filterproxymodel.h new file mode 100644 --- /dev/null +++ b/kcms/colors/filterproxymodel.h @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019 Kai Uwe Broulik + * + * 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 "colors.h" + +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QString selectedScheme READ selectedScheme WRITE setSelectedScheme NOTIFY selectedSchemeChanged) + Q_PROPERTY(int selectedSchemeIndex READ selectedSchemeIndex NOTIFY selectedSchemeIndexChanged) + + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(KCMColors::SchemeFilter filter READ filter WRITE setFilter NOTIFY filterChanged) + +public: + FilterProxyModel(QObject *parent = nullptr); + ~FilterProxyModel() override; + + QString selectedScheme() const; + void setSelectedScheme(const QString &scheme); + + int selectedSchemeIndex() const; + + QString query() const; + void setQuery(const QString &query); + + KCMColors::SchemeFilter filter() const; + void setFilter(KCMColors::SchemeFilter filter); + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +Q_SIGNALS: + void queryChanged(); + void filterChanged(); + + void selectedSchemeChanged(); + void selectedSchemeIndexChanged(); + +private: + void emitSelectedSchemeIndexChange(); + + QString m_selectedScheme; + + QString m_query; + KCMColors::SchemeFilter m_filter = KCMColors::AllSchemes; + +}; diff --git a/kcms/colors/filterproxymodel.cpp b/kcms/colors/filterproxymodel.cpp new file mode 100644 --- /dev/null +++ b/kcms/colors/filterproxymodel.cpp @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2019 Kai Uwe Broulik + * + * 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 "colorsmodel.h" + +FilterProxyModel::FilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + +} + +FilterProxyModel::~FilterProxyModel() = default; + +QString FilterProxyModel::selectedScheme() const +{ + return m_selectedScheme; +} + +void FilterProxyModel::setSelectedScheme(const QString &scheme) +{ + if (m_selectedScheme == scheme) { + return; + } + + const bool firstTime = m_selectedScheme.isNull(); + m_selectedScheme = scheme; + + if (!firstTime) { + emit selectedSchemeChanged(); + } + emit selectedSchemeIndexChanged(); +} + +int FilterProxyModel::selectedSchemeIndex() 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), ColorsModel::SchemeNameRole, m_selectedScheme); + + 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 = selectedSchemeIndex(); + + m_query = query; + invalidateFilter(); + + emit queryChanged(); + + if (selectedSchemeIndex() != oldIndex) { + emit selectedSchemeIndexChanged(); + } + } +} + +KCMColors::SchemeFilter FilterProxyModel::filter() const +{ + return m_filter; +} + +void FilterProxyModel::setFilter(KCMColors::SchemeFilter filter) +{ + if (m_filter != filter) { + const int oldIndex = selectedSchemeIndex(); + + m_filter = filter; + invalidateFilter(); + + emit filterChanged(); + + if (selectedSchemeIndex() != oldIndex) { + emit selectedSchemeIndexChanged(); + } + } +} + +bool FilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + const QModelIndex idx = sourceModel()->index(source_row, 0, source_parent); + + if (!m_query.isEmpty()) { + if (!idx.data(Qt::DisplayRole).toString().contains(m_query, Qt::CaseInsensitive) + && !idx.data(KCMColors::SchemeNameRole).toString().contains(m_query, Qt::CaseInsensitive)) { + return false; + } + } + + if (m_filter != KCMColors::AllSchemes) { + const QPalette palette = idx.data(KCMColors::PaletteRole).value(); + + const int windowBackgroundGray = qGray(palette.window().color().rgb()); + + if (m_filter == KCMColors::DarkSchemes) { + return windowBackgroundGray < 192; + } else if (m_filter == KCMColors::LightSchemes) { + return windowBackgroundGray >= 192; + } + } + + return true; +} diff --git a/kcms/colors/package/contents/ui/main.qml b/kcms/colors/package/contents/ui/main.qml --- a/kcms/colors/package/contents/ui/main.qml +++ b/kcms/colors/package/contents/ui/main.qml @@ -26,13 +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.colors 1.0 as Private KCM.GridViewKCM { id: root KCM.ConfigModule.quickHelp: i18n("This module lets you choose the color scheme.") - view.model: kcm.colorsModel - view.currentIndex: kcm.selectedSchemeIndex + view.model: kcm.filteredModel + view.currentIndex: kcm.filteredModel.selectedSchemeIndex + + Binding { + target: kcm.filteredModel + property: "query" + value: searchField.text + } + + Binding { + target: kcm.filteredModel + property: "filter" + value: filterCombo.model[filterCombo.currentIndex].filter + } enabled: !kcm.downloadingFile @@ -72,6 +85,71 @@ } } } + + 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 Schemes"), filter: Private.KCM.AllSchemes}, + {text: i18n("Light Schemes"), filter: Private.KCM.LightSchemes}, + {text: i18n("Dark Schemes"), filter: Private.KCM.DarkSchemes} + ] + + // 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 || {}; + props.currentIcon = "view-filter"; + props.iconColor = Kirigami.Theme.textColor; + background.properties = props; + } + } + } } view.delegate: KCM.GridDelegate { @@ -177,24 +255,24 @@ iconName: "document-edit" tooltip: i18n("Edit Color Scheme...") enabled: !model.pendingDeletion - onTriggered: kcm.editScheme(model.index, root) + onTriggered: kcm.editScheme(model.schemeName, root) }, Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Remove Color Scheme") enabled: model.removable visible: !model.pendingDeletion - onTriggered: kcm.setPendingDeletion(model.index, true) + onTriggered: model.pendingDeletion = true }, Kirigami.Action { iconName: "edit-undo" tooltip: i18n("Restore Color Scheme") visible: model.pendingDeletion - onTriggered: kcm.setPendingDeletion(model.index, false) + onTriggered: model.pendingDeletion = false } ] onClicked: { - kcm.selectedScheme = model.schemeName; + kcm.model.selectedScheme = model.schemeName; view.forceActiveFocus(); } onDoubleClicked: {