From 91d3e81a768fb533d0c2dc7fd027946ede464f53 Mon Sep 17 00:00:00 2001 From: Ilya Fedin Date: Tue, 12 Mar 2024 03:22:32 +0400 Subject: [PATCH] Add KDE theming support KDE applications are a notable part of the popular Qt software in the Linux world. Sadly, they have their own theming add-ons that are applied through QPlatformTheme. This adds support for all aspects of the KDE applications theming present in the plasma-integration QPlatformTheme: * KDE's QtQuick style bridging the currently set QtWidgets style (you may want to get QtQuick style choice in the settings UI in the future instead) * KDE's color schemes (the most important part as KDE applications don't fallback to QtQuick style/QtWidgets style/QPalette for colors) * KDE's icon engine applying KDE's color schemes to icons (to not to have black icons on black background) This also writes widget style and icon theme to kdeglobals as various KDE applications read only from there --- CMakeLists.txt | 5 +++- src/qt6ct-common/CMakeLists.txt | 2 +- src/qt6ct-common/qt6ct.cpp | 11 +++++++ src/qt6ct-common/qt6ct.h | 1 + src/qt6ct-qtplugin/CMakeLists.txt | 2 +- src/qt6ct-qtplugin/qt6ctplatformtheme.cpp | 36 +++++++++++++++++++++-- src/qt6ct-qtplugin/qt6ctplatformtheme.h | 7 +++-- src/qt6ct/CMakeLists.txt | 2 +- src/qt6ct/appearancepage.cpp | 26 ++++++++++++++-- src/qt6ct/appearancepage.h | 2 +- src/qt6ct/iconthemepage.cpp | 13 ++++++-- 11 files changed, 92 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d6b608c..357d428 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,10 @@ set(CMAKE_BUILD_RPATH_USE_ORIGIN ON) add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000 -DUSE_WIDGETS) -find_package(Qt6 REQUIRED COMPONENTS BuildInternals Core Widgets OPTIONAL_COMPONENTS LinguistTools) +find_package(Qt6 REQUIRED COMPONENTS BuildInternals Core Widgets QuickControls2 OPTIONAL_COMPONENTS LinguistTools) +find_package(KF6Config REQUIRED) +find_package(KF6ColorScheme REQUIRED) +find_package(KF6IconThemes REQUIRED) get_target_property(QT_QTPATHS_EXECUTABLE Qt6::qtpaths IMPORTED_LOCATION) diff --git a/src/qt6ct-common/CMakeLists.txt b/src/qt6ct-common/CMakeLists.txt index f748167..bc883b2 100644 --- a/src/qt6ct-common/CMakeLists.txt +++ b/src/qt6ct-common/CMakeLists.txt @@ -26,5 +26,5 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../) add_library(qt6ct-common SHARED ${app_SRCS}) set_target_properties(qt6ct-common PROPERTIES VERSION ${QT6CT_VERSION}) -target_link_libraries(qt6ct-common PRIVATE Qt6::Gui) +target_link_libraries(qt6ct-common PRIVATE Qt6::Gui KF6::ConfigCore KF6::ColorScheme) install(TARGETS qt6ct-common DESTINATION ${CMAKE_INSTALL_LIBDIR}) diff --git a/src/qt6ct-common/qt6ct.cpp b/src/qt6ct-common/qt6ct.cpp index 825ba67..6aa3864 100644 --- a/src/qt6ct-common/qt6ct.cpp +++ b/src/qt6ct-common/qt6ct.cpp @@ -34,6 +34,8 @@ #include #include #include +#include +#include #include "qt6ct.h" #ifndef QT6CT_DATADIR @@ -116,6 +118,7 @@ QStringList Qt6CT::sharedColorSchemePaths() for(const QString &p : QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation)) { paths << (p + QLatin1String("/qt6ct/colors")); + paths << (p + QLatin1String("/color-schemes")); } paths << QLatin1String(QT6CT_DATADIR"/qt6ct/colors"); paths.removeDuplicates(); @@ -146,8 +149,16 @@ QString Qt6CT::resolvePath(const QString &path) return tmp; } +bool Qt6CT::isKColorScheme(const QString &filePath) +{ + return filePath.toLower().endsWith(".colors"); +} + QPalette Qt6CT::loadColorScheme(const QString &filePath, const QPalette &fallback) { + if(isKColorScheme(filePath)) + return KColorScheme::createApplicationPalette(KSharedConfig::openConfig(filePath)); + QPalette customPalette; QSettings settings(filePath, QSettings::IniFormat); settings.beginGroup("ColorScheme"); diff --git a/src/qt6ct-common/qt6ct.h b/src/qt6ct-common/qt6ct.h index 3608c11..78f2076 100644 --- a/src/qt6ct-common/qt6ct.h +++ b/src/qt6ct-common/qt6ct.h @@ -67,6 +67,7 @@ class QT6CT_EXPORT Qt6CT static QString userColorSchemePath(); static QStringList sharedColorSchemePaths(); static QString resolvePath(const QString &path); + static bool isKColorScheme(const QString &filePath); static QPalette loadColorScheme(const QString &filePath, const QPalette &fallback); static void registerStyleInstance(StyleInstance *instance); diff --git a/src/qt6ct-qtplugin/CMakeLists.txt b/src/qt6ct-qtplugin/CMakeLists.txt index c3d7498..60b2599 100644 --- a/src/qt6ct-qtplugin/CMakeLists.txt +++ b/src/qt6ct-qtplugin/CMakeLists.txt @@ -9,5 +9,5 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../) add_library(qt6ct-qtplugin MODULE ${app_SRCS}) set_target_properties(qt6ct-qtplugin PROPERTIES OUTPUT_NAME qt6ct) -target_link_libraries(qt6ct-qtplugin PRIVATE Qt6::Widgets Qt6::GuiPrivate qt6ct-common) +target_link_libraries(qt6ct-qtplugin PRIVATE Qt6::Widgets Qt6::GuiPrivate Qt6::QuickControls2 KF6::IconThemes qt6ct-common) install(TARGETS qt6ct-qtplugin DESTINATION ${PLUGINDIR}/platformthemes) diff --git a/src/qt6ct-qtplugin/qt6ctplatformtheme.cpp b/src/qt6ct-qtplugin/qt6ctplatformtheme.cpp index 81ce4f9..50f8aaa 100644 --- a/src/qt6ct-qtplugin/qt6ctplatformtheme.cpp +++ b/src/qt6ct-qtplugin/qt6ctplatformtheme.cpp @@ -44,6 +44,7 @@ #endif #include #include +#include #include #include "qt6ct.h" @@ -52,6 +53,9 @@ #include #include +#include +#include + Q_LOGGING_CATEGORY(lqt6ct, "qt6ct", QtWarningMsg) //QT_QPA_PLATFORMTHEME=qt6ct @@ -67,12 +71,16 @@ Qt6CTPlatformTheme::Qt6CTPlatformTheme() QMetaObject::invokeMethod(this, "createFSWatcher", Qt::QueuedConnection); #endif QGuiApplication::setFont(m_generalFont); + //don't override the value explicitly set by the user + if(QQuickStyle::name().isEmpty() || QQuickStyle::name() == QLatin1String("Fusion")) + QQuickStyle::setStyle(QLatin1String("org.kde.desktop")); } qCDebug(lqt6ct) << "using qt6ct plugin"; #ifdef QT_WIDGETS_LIB if(!QStyleFactory::keys().contains("qt6ct-style")) qCCritical(lqt6ct) << "unable to find qt6ct proxy style"; #endif + QCoreApplication::instance()->installEventFilter(this); } Qt6CTPlatformTheme::~Qt6CTPlatformTheme() @@ -146,6 +154,11 @@ QIcon Qt6CTPlatformTheme::fileIcon(const QFileInfo &fileInfo, QPlatformTheme::Ic return QIcon::fromTheme(type.iconName()); } +QIconEngine *Qt6CTPlatformTheme::createIconEngine(const QString &iconName) const +{ + return new KIconEngine(iconName, KIconLoader::global()); +} + void Qt6CTPlatformTheme::applySettings() { if(!QGuiApplication::desktopSettingsAware() || m_isIgnored) @@ -167,6 +180,9 @@ void Qt6CTPlatformTheme::applySettings() QGuiApplication::setFont(m_generalFont); //apply font + if(Qt6CT::isKColorScheme(m_schemePath)) + qApp->setProperty("KDE_COLOR_SCHEME_PATH", m_schemePath); + if(!m_palette) m_palette = std::make_unique(*QGenericUnixTheme::palette(QPlatformTheme::SystemPalette)); @@ -252,11 +268,11 @@ void Qt6CTPlatformTheme::readSettings() settings.beginGroup("Appearance"); m_style = settings.value("style", "Fusion").toString(); - QString schemePath = settings.value("custom_palette", false).toBool() + m_schemePath = settings.value("custom_palette", false).toBool() ? Qt6CT::resolvePath(settings.value("color_scheme_path").toString()) //replace environment variables : QString(); - m_palette = !schemePath.isEmpty() - ? std::make_unique(Qt6CT::loadColorScheme(schemePath, *QPlatformTheme::palette(SystemPalette))) + m_palette = !m_schemePath.isEmpty() + ? std::make_unique(Qt6CT::loadColorScheme(m_schemePath, *QPlatformTheme::palette(SystemPalette))) : nullptr; m_iconTheme = settings.value("icon_theme").toString(); //load dialogs @@ -365,3 +381,17 @@ QString Qt6CTPlatformTheme::loadStyleSheets(const QStringList &paths) content.replace(regExp, "\n"); return content; } + +//There's such a thing as KColorSchemeManager that lets the user to change the color scheme +//application-wide and we should re-apply the color scheme if KCSM resets it to the default +//which leads KColorScheme to get the color scheme from kdeglobals which won't help us. +bool Qt6CTPlatformTheme::eventFilter(QObject *obj, QEvent *e) +{ + if(obj == qApp && + e->type() == QEvent::DynamicPropertyChange && + static_cast(e)->propertyName() == "KDE_COLOR_SCHEME_PATH" && + qApp->property("KDE_COLOR_SCHEME_PATH").toString().isEmpty() && + Qt6CT::isKColorScheme(m_schemePath)) + applySettings(); + return QObject::eventFilter(obj, e); +} diff --git a/src/qt6ct-qtplugin/qt6ctplatformtheme.h b/src/qt6ct-qtplugin/qt6ctplatformtheme.h index 6ad245b..ea1918b 100644 --- a/src/qt6ct-qtplugin/qt6ctplatformtheme.h +++ b/src/qt6ct-qtplugin/qt6ctplatformtheme.h @@ -62,10 +62,13 @@ class Qt6CTPlatformTheme : public QObject, public QGenericUnixTheme //virtual QPixmap fileIconPixmap(const QFileInfo &fileInfo, const QSizeF &size, // QPlatformTheme::IconOptions iconOptions = 0) const; - //virtual QIconEngine *createIconEngine(const QString &iconName) const; + virtual QIconEngine *createIconEngine(const QString &iconName) const; //virtual QList keyBindings(QKeySequence::StandardKey key) const; //virtual QString standardButtonText(int button) const; +protected: + bool eventFilter(QObject *obj, QEvent *e) override; + private slots: void applySettings(); #ifdef QT_WIDGETS_LIB @@ -79,7 +82,7 @@ private slots: bool hasWidgets(); #endif QString loadStyleSheets(const QStringList &paths); - QString m_style, m_iconTheme, m_userStyleSheet, m_prevStyleSheet; + QString m_style, m_schemePath, m_iconTheme, m_userStyleSheet, m_prevStyleSheet; std::unique_ptr m_palette; QFont m_generalFont, m_fixedFont; int m_doubleClickInterval; diff --git a/src/qt6ct/CMakeLists.txt b/src/qt6ct/CMakeLists.txt index fb0e1f7..acd8313 100644 --- a/src/qt6ct/CMakeLists.txt +++ b/src/qt6ct/CMakeLists.txt @@ -31,6 +31,6 @@ if(Qt6LinguistTools_FOUND) endif() add_executable(qt6ct ${app_SRCS}) -target_link_libraries(qt6ct PRIVATE Qt6::Widgets Qt6::WidgetsPrivate qt6ct-common) +target_link_libraries(qt6ct PRIVATE Qt6::Widgets Qt6::WidgetsPrivate KF6::ConfigCore qt6ct-common) install(TARGETS qt6ct DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES qt6ct.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) diff --git a/src/qt6ct/appearancepage.cpp b/src/qt6ct/appearancepage.cpp index c0ddc40..de9753e 100644 --- a/src/qt6ct/appearancepage.cpp +++ b/src/qt6ct/appearancepage.cpp @@ -35,6 +35,8 @@ #include #include #include +#include +#include #include #include "qt6ct.h" #include "appearancepage.h" @@ -71,7 +73,7 @@ AppearancePage::AppearancePage(QWidget *parent) : QMenu *menu = new QMenu(this); menu->addAction(QIcon::fromTheme("document-new"), tr("Create"), this, SLOT(createColorScheme())); m_changeColorSchemeAction = menu->addAction(QIcon::fromTheme("accessories-text-editor"), tr("Edit"), this, SLOT(changeColorScheme())); - menu->addAction(QIcon::fromTheme("edit-copy"), tr("Create a Copy"), this, SLOT(copyColorScheme())); + m_copyColorSchemeAction = menu->addAction(QIcon::fromTheme("edit-copy"), tr("Create a Copy"), this, SLOT(copyColorScheme())); m_renameColorSchemeAction = menu->addAction(tr("Rename"), this, SLOT(renameColorScheme())); menu->addSeparator(); m_removeColorSchemeAction = menu->addAction(QIcon::fromTheme("edit-delete"), tr("Remove"), this, SLOT(removeColorScheme())); @@ -110,6 +112,11 @@ void AppearancePage::writeSettings(QSettings *settings) settings->setValue("color_scheme_path", m_ui->colorSchemeComboBox->currentData().toString()); settings->setValue("standard_dialogs", m_ui->dialogComboBox->currentData().toString()); settings->endGroup(); + + KSharedConfigPtr config = KSharedConfig::openConfig("kdeglobals"); + KConfigGroup group(config, "KDE"); + group.writeEntry("widgetStyle", "qt6ct-style"); + group.sync(); } void AppearancePage::on_styleComboBox_textActivated(const QString &text) @@ -303,6 +310,7 @@ void AppearancePage::setPreviewPalette(const QPalette &p) void AppearancePage::updateActions() { + m_copyColorSchemeAction->setVisible(!Qt6CT::isKColorScheme(m_ui->colorSchemeComboBox->currentData().toString())); if(m_ui->colorSchemeComboBox->count() == 0 || !QFileInfo(m_ui->colorSchemeComboBox->currentData().toString()).isWritable()) { @@ -380,11 +388,23 @@ void AppearancePage::findColorSchemes(const QString &path) { QDir dir(path); dir.setFilter(QDir::Files); - dir.setNameFilters(QStringList() << "*.conf"); + dir.setNameFilters(QStringList() << "*.conf" << "*.colors"); for(const QFileInfo &info : dir.entryInfoList()) { - m_ui->colorSchemeComboBox->addItem(info.baseName(), info.filePath()); + QString name; + QString path = info.filePath(); + if(Qt6CT::isKColorScheme(path)) + { + KSharedConfigPtr config = KSharedConfig::openConfig(path, KConfig::SimpleConfig); + KConfigGroup group(config, "General"); + name = group.readEntry("Name", info.baseName()) + " (KColorScheme)"; + } + else + { + name = info.baseName(); + } + m_ui->colorSchemeComboBox->addItem(name, path); } } diff --git a/src/qt6ct/appearancepage.h b/src/qt6ct/appearancepage.h index a5c1a88..3404fdc 100644 --- a/src/qt6ct/appearancepage.h +++ b/src/qt6ct/appearancepage.h @@ -72,7 +72,7 @@ private slots: QStyle *m_selectedStyle = nullptr; QPalette m_customPalette; QWidget *m_previewWidget; - QAction *m_changeColorSchemeAction, *m_renameColorSchemeAction, *m_removeColorSchemeAction; + QAction *m_changeColorSchemeAction, *m_copyColorSchemeAction, *m_renameColorSchemeAction, *m_removeColorSchemeAction; Ui::PreviewForm *m_previewUi; }; diff --git a/src/qt6ct/iconthemepage.cpp b/src/qt6ct/iconthemepage.cpp index 2f7621b..b5666c0 100644 --- a/src/qt6ct/iconthemepage.cpp +++ b/src/qt6ct/iconthemepage.cpp @@ -34,6 +34,8 @@ #include #include #include +#include +#include #include "qt6ct.h" #include "iconthemepage.h" #include "ui_iconthemepage.h" @@ -67,8 +69,15 @@ IconThemePage::~IconThemePage() void IconThemePage::writeSettings(QSettings *settings) { QTreeWidgetItem *item = m_ui->treeWidget->currentItem(); - if(item) - settings->setValue("Appearance/icon_theme", item->data(3, Qt::UserRole)); + if(!item) + return; + + settings->setValue("Appearance/icon_theme", item->data(3, Qt::UserRole)); + + KSharedConfigPtr config = KSharedConfig::openConfig("kdeglobals"); + KConfigGroup group(config, "Icons"); + group.writeEntry("Theme", item->data(3, Qt::UserRole)); + group.sync(); } void IconThemePage::onFinished()