diff --git a/kcms/colors/colorsmodel.cpp b/kcms/colors/colorsmodel.cpp index d9b7c8d39..786f9398b 100644 --- a/kcms/colors/colorsmodel.cpp +++ b/kcms/colors/colorsmodel.cpp @@ -1,229 +1,242 @@ /* * 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 ActiveTitleBarBackgroundRole: return item.activeTitleBarBackground; + case ActiveTitleBarForegroundRole: return item.activeTitleBarForeground; 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")}, + {ActiveTitleBarBackgroundRole, QByteArrayLiteral("activeTitleBarBackground")}, + {ActiveTitleBarForegroundRole, QByteArrayLiteral("activeTitleBarForeground")}, {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); + const QPalette palette = KColorScheme::createApplicationPalette(config); + + // from kwin/decorations/decorationpalette.cpp + KConfigGroup wmConfig(config, QStringLiteral("WM")); + const QColor activeTitleBarBackground = wmConfig.readEntry("activeBackground", palette.color(QPalette::Active, QPalette::Highlight)); + const QColor activeTitleBarForeground = wmConfig.readEntry("activeForeground", palette.color(QPalette::Active, QPalette::HighlightedText)); + ColorsModelData item{ name, baseName, - KColorScheme::createApplicationPalette(config), + palette, + activeTitleBarBackground, + activeTitleBarForeground, 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/colorsmodel.h b/kcms/colors/colorsmodel.h index 5368d5269..82424d11f 100644 --- a/kcms/colors/colorsmodel.h +++ b/kcms/colors/colorsmodel.h @@ -1,86 +1,91 @@ /* * 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; + QColor activeTitleBarBackground; + QColor activeTitleBarForeground; 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, + // Colors which aren't in QPalette + ActiveTitleBarBackgroundRole, + ActiveTitleBarForegroundRole, 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/package/contents/ui/main.qml b/kcms/colors/package/contents/ui/main.qml index 4cea6a90b..96fb4c6ad 100644 --- a/kcms/colors/package/contents/ui/main.qml +++ b/kcms/colors/package/contents/ui/main.qml @@ -1,348 +1,378 @@ /* * Copyright 2018 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 . */ import QtQuick 2.6 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import QtQuick.Dialogs 1.0 as QtDialogs 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.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 Component.onCompleted: { // The thumbnails are a bit more elaborate and need more room, especially when translated view.implicitCellWidth = Kirigami.Units.gridUnit * 13; + view.implicitCellHeight = Kirigami.Units.gridUnit * 11; } DropArea { anchors.fill: parent onEntered: { if (!drag.hasUrls) { drag.accepted = false; } } onDropped: { infoLabel.visible = false; kcm.installSchemeFromFile(drop.urls[0]); } } // putting the InlineMessage as header item causes it to show up initially despite visible false header: ColumnLayout { Kirigami.InlineMessage { id: notInstalledWarning Layout.fillWidth: true type: Kirigami.MessageType.Warning showCloseButton: true visible: false Connections { target: kcm onShowSchemeNotInstalledWarning: { notInstalledWarning.text = i18n("The color scheme '%1' is not installed. Selecting the default theme instead.", schemeName) notInstalledWarning.visible = true; } } } 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 || {}; 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.display thumbnailAvailable: true thumbnail: Rectangle { anchors.fill: parent opacity: model.pendingDeletion ? 0.3 : 1 Behavior on opacity { NumberAnimation { duration: Kirigami.Units.longDuration } } color: model.palette.window Kirigami.Theme.highlightColor: model.palette.highlight Kirigami.Theme.highlightedTextColor: model.palette.highlightedText Kirigami.Theme.linkColor: model.palette.link Kirigami.Theme.textColor: model.palette.text + Rectangle { + id: windowTitleBar + width: parent.width + height: Math.round(Kirigami.Units.gridUnit * 1.5) + gradient: Gradient { + // from Breeze Decoration::paintTitleBar + GradientStop { position: 0.0; color: Qt.lighter(model.activeTitleBarBackground, 1.2) } + GradientStop { position: 0.8; color: model.activeTitleBarBackground } + } + + color: model.activeTitleBarBackground + + QtControls.Label { + anchors { + fill: parent + leftMargin: Kirigami.Units.smallSpacing + rightMargin: Kirigami.Units.smallSpacing + } + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + color: model.activeTitleBarForeground + text: i18n("Window Title") + elide: Text.ElideRight + } + } + ColumnLayout { anchors { - fill: parent + left: parent.left + right: parent.right + top: windowTitleBar.bottom + bottom: parent.bottom margins: Kirigami.Units.smallSpacing } RowLayout { Layout.fillWidth: true QtControls.Label { Layout.fillWidth: true Layout.fillHeight: true verticalAlignment: Text.AlignVCenter text: i18n("Window text") elide: Text.ElideRight color: model.palette.windowText } QtControls.Button { Layout.alignment: Qt.AlignBottom text: i18n("Button") Kirigami.Theme.backgroundColor: model.palette.button Kirigami.Theme.textColor: model.palette.buttonText activeFocusOnTab: false } } QtControls.Frame { Layout.fillWidth: true Layout.fillHeight: true Kirigami.Theme.backgroundColor: model.palette.base // FIXME Make Frame still visible but without any inner padding padding: 1 activeFocusOnTab: false Column { id: listPreviewColumn readonly property string demoText: " %2 %4" .arg(i18nc("Hyperlink", "link")) .arg(model.palette.linkVisited) .arg(i18nc("Visited hyperlink", "visited")) width: parent.width QtControls.ItemDelegate { width: parent.width text: i18n("Normal text") + listPreviewColumn.demoText activeFocusOnTab: false } QtControls.ItemDelegate { width: parent.width highlighted: true // TODO use proper highlighted link color text: i18n("Highlighted text") + listPreviewColumn.demoText activeFocusOnTab: false } QtControls.ItemDelegate { width: parent.width enabled: false text: i18n("Disabled text") + listPreviewColumn.demoText activeFocusOnTab: false } } } } // Make the preview non-clickable but still reacting to hover MouseArea { anchors.fill: parent onClicked: delegate.clicked() onDoubleClicked: delegate.doubleClicked() } } actions: [ Kirigami.Action { iconName: "document-edit" tooltip: i18n("Edit Color Scheme...") enabled: !model.pendingDeletion onTriggered: kcm.editScheme(model.schemeName, root) }, Kirigami.Action { iconName: "edit-delete" tooltip: i18n("Remove Color Scheme") enabled: model.removable visible: !model.pendingDeletion onTriggered: model.pendingDeletion = true }, Kirigami.Action { iconName: "edit-undo" tooltip: i18n("Restore Color Scheme") visible: model.pendingDeletion onTriggered: model.pendingDeletion = false } ] onClicked: { kcm.model.selectedScheme = model.schemeName; view.forceActiveFocus(); } onDoubleClicked: { kcm.save(); } } 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; // Avoid dual message widgets notInstalledWarning.visible = false; } onShowErrorMessage: { infoLabel.type = Kirigami.MessageType.Error; infoLabel.text = message; infoLabel.visible = true; notInstalledWarning.visible = false; } } } 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 Color Schemes...") icon.name: "get-hot-new-stuff" onClicked: kcm.getNewStuff(this) visible: KAuthorized.authorize("ghns") } } } Loader { id: fileDialogLoader active: false sourceComponent: QtDialogs.FileDialog { title: i18n("Open Color Scheme") folder: shortcuts.home nameFilters: [ i18n("Color Scheme Files (*.colors)") ] Component.onCompleted: open() onAccepted: { infoLabel.visible = false; kcm.installSchemeFromFile(fileUrls[0]) fileDialogLoader.active = false } onRejected: { fileDialogLoader.active = false } } } }