Commit a1fbeb96 authored by Kai Uwe Broulik's avatar Kai Uwe Broulik 🍇

[Colors KCM] Add search and filter

This adds a search bar to search for schemes in the list as well as a filter to show only light or dark themes
using a heuristic on the theme's window color.

Differential Revision: https://phabricator.kde.org/D18646
parent d6550037
......@@ -4,6 +4,8 @@ add_definitions(-DTRANSLATION_DOMAIN=\"kcm_colors\")
set(kcm_colors_SRCS
../krdb/krdb.cpp
colors.cpp
colorsmodel.cpp
filterproxymodel.cpp
)
# needed for krdb
......
This diff is collapsed.
......@@ -38,13 +38,15 @@ namespace KIO
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 @@ public:
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 @@ Q_SIGNALS:
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;
......
/*
* Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
* Copyright (C) 2007 Jeremy Whiting <jpwhiting@kde.org>
* Copyright (C) 2016 Olivier Churlaud <olivier@churlaud.com>
* Copyright (C) 2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "colorsmodel.h"
#include <QCollator>
#include <QDir>
#include <QStandardPaths>
#include <KColorScheme>
#include <KConfigGroup>
#include <KSharedConfig>
#include <algorithm>
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<int, QByteArray> 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();
}
}
}
/*
* Copyright (C) 2007 Matthew Woehlke <mw_triad@users.sourceforge.net>
* Copyright (C) 2007 Jeremy Whiting <jpwhiting@kde.org>
* Copyright (C) 2016 Olivier Churlaud <olivier@churlaud.com>
* Copyright (C) 2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QAbstractListModel>
#include <QString>
#include <QPalette>
#include <QVector>
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<int, QByteArray> 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<ColorsModelData> m_data;
};
/*
* Copyright (C) 2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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<QPalette>();
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;
}
/*
* Copyright (c) 2019 Kai Uwe Broulik <kde@privat.broulik.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <QSortFilterProxyModel>
#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;
};
......@@ -26,13 +26,26 @@ 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.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 @@ KCM.GridViewKCM {
}
}
}
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