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,7 @@ set(kcm_colors_SRCS ../krdb/krdb.cpp colors.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 @@ -70,9 +70,9 @@ Q_INVOKABLE void getNewStuff(QQuickItem *ctx); Q_INVOKABLE void installSchemeFromFile(const QUrl &url); - Q_INVOKABLE void setPendingDeletion(int index, bool pending); + Q_INVOKABLE void setPendingDeletion(const QString &schemeName, 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; diff --git a/kcms/colors/colors.cpp b/kcms/colors/colors.cpp --- a/kcms/colors/colors.cpp +++ b/kcms/colors/colors.cpp @@ -49,6 +49,8 @@ #include "../krdb/krdb.h" +#include "filterproxymodel.h" + static const QString s_defaultColorSchemeName = QStringLiteral("Breeze"); K_PLUGIN_FACTORY_WITH_JSON(KCMColorsFactory, "kcm_colors.json", registerPlugin();) @@ -58,6 +60,7 @@ , m_config(KSharedConfig::openConfig(QStringLiteral("kdeglobals"))) { qmlRegisterType(); + qmlRegisterType("org.kde.private.kcms.colors", 1, 0, "FilterProxyModel"); KAboutData *about = new KAboutData(QStringLiteral("kcm_colors"), i18n("Choose the color scheme"), QStringLiteral("2.0"), QString(), KAboutLicense::GPL); @@ -126,13 +129,13 @@ return m_tempCopyJob; } -void KCMColors::setPendingDeletion(int index, bool pending) +void KCMColors::setPendingDeletion(const QString &schemeName, bool pending) { - QModelIndex idx = m_model->index(index, 0); + QModelIndex idx = m_model->index(indexOfScheme(schemeName), 0); m_model->setData(idx, pending, PendingDeletionRole); - if (pending && selectedSchemeIndex() == index) { + if (pending && selectedSchemeIndex() == idx.row()) { // move to the next non-pending theme const auto nonPending = m_model->match(idx, PendingDeletionRole, false); setSelectedScheme(nonPending.first().data(SchemeNameRole).toString()); @@ -324,13 +327,13 @@ 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(indexOfScheme(schemeName), 0); m_editDialogProcess = new QProcess(this); connect(m_editDialogProcess, QOverload::of(&QProcess::finished), this, 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,65 @@ +/* + * 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 + +class FilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setSourceModelProxy NOTIFY sourceModelChanged) + + Q_PROPERTY(QString query READ query WRITE setQuery NOTIFY queryChanged) + Q_PROPERTY(SchemeFilter filter READ filter WRITE setFilter NOTIFY filterChanged) + +public: + FilterProxyModel(QObject *parent = nullptr); + ~FilterProxyModel() override; + + enum SchemeFilter { + AllSchemes, + LightSchemes, + DarkSchemes + }; + Q_ENUM(SchemeFilter) + + void setSourceModelProxy(QAbstractItemModel *sourceModel); + + QString query() const; + void setQuery(const QString &query); + + SchemeFilter filter() const; + void setFilter(SchemeFilter filter); + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + +Q_SIGNALS: + void sourceModelChanged(); + + void queryChanged(); + void filterChanged(); + +private: + QString m_query; + SchemeFilter m_filter = 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,93 @@ +/* + * 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 "colors.h" + +FilterProxyModel::FilterProxyModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + +} + +FilterProxyModel::~FilterProxyModel() = default; + +void FilterProxyModel::setSourceModelProxy(QAbstractItemModel *sourceModel) +{ + setSourceModel(sourceModel); + emit sourceModelChanged(); +} + +QString FilterProxyModel::query() const +{ + return m_query; +} + +void FilterProxyModel::setQuery(const QString &query) +{ + if (m_query != query) { + m_query = query; + + invalidateFilter(); + + emit queryChanged(); + } +} + +FilterProxyModel::SchemeFilter FilterProxyModel::filter() const +{ + return m_filter; +} + +void FilterProxyModel::setFilter(SchemeFilter filter) +{ + if (m_filter != filter) { + m_filter = filter; + + invalidateFilter(); + + emit filterChanged(); + } +} + +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 != AllSchemes) { + const QPalette palette = idx.data(KCMColors::PaletteRole).value(); + + const int windowBackgroundGray = qGray(palette.window().color().rgb()); + + if (m_filter == DarkSchemes) { + return windowBackgroundGray < 192; + } else if (m_filter == 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,11 +26,16 @@ 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 { KCM.ConfigModule.quickHelp: i18n("This module lets you choose the color scheme.") - view.model: kcm.colorsModel + view.model: Private.FilterProxyModel { + query: searchField.text + filter: filterCombo.model[filterCombo.currentIndex].filter + sourceModel: kcm.colorsModel + } view.currentIndex: kcm.selectedSchemeIndex enabled: !kcm.downloadingFile @@ -71,6 +76,70 @@ } } } + + 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.FilterProxyModel.AllSchemes}, + {text: i18n("Light Schemes"), filter: Private.FilterProxyModel.LightSchemes}, + {text: i18n("Dark Schemes"), filter: Private.FilterProxyModel.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"; + background.properties = props; + } + } + } } view.delegate: KCM.GridDelegate { @@ -176,20 +245,20 @@ iconName: "document-edit" tooltip: i18n("Edit Color Scheme") enabled: !model.pendingDeletion - onTriggered: kcm.editScheme(model.index, parent) + onTriggered: kcm.editScheme(model.schemeName, parent) }, Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Remove Color Scheme") enabled: model.removable visible: !model.pendingDeletion - onTriggered: kcm.setPendingDeletion(model.index, true) + onTriggered: kcm.setPendingDeletion(model.schemeName, true) }, Kirigami.Action { iconName: "edit-undo" tooltip: i18n("Restore Color Scheme") visible: model.pendingDeletion - onTriggered: kcm.setPendingDeletion(model.index, false) + onTriggered: kcm.setPendingDeletion(model.schemeName, false) } ] onClicked: {