Skip to content

Commit

Permalink
Add KDE theming support
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ilya-fedin committed Oct 1, 2024
1 parent 4558fe2 commit 91d3e81
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 15 deletions.
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion src/qt6ct-common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
11 changes: 11 additions & 0 deletions src/qt6ct-common/qt6ct.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include <QFile>
#include <QSettings>
#include <QtDebug>
#include <KSharedConfig>
#include <KColorScheme>
#include "qt6ct.h"

#ifndef QT6CT_DATADIR
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions src/qt6ct-common/qt6ct.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/qt6ct-qtplugin/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
36 changes: 33 additions & 3 deletions src/qt6ct-qtplugin/qt6ctplatformtheme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#endif
#include <QFile>
#include <QFileSystemWatcher>
#include <QQuickStyle>
#include <private/qiconloader_p.h>

#include "qt6ct.h"
Expand All @@ -52,6 +53,9 @@
#include <QStringList>
#include <qpa/qplatformthemefactory_p.h>

#include <KIconEngine>
#include <KIconLoader>

Q_LOGGING_CATEGORY(lqt6ct, "qt6ct", QtWarningMsg)

//QT_QPA_PLATFORMTHEME=qt6ct
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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<QPalette>(*QGenericUnixTheme::palette(QPlatformTheme::SystemPalette));

Expand Down Expand Up @@ -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<QPalette>(Qt6CT::loadColorScheme(schemePath, *QPlatformTheme::palette(SystemPalette)))
m_palette = !m_schemePath.isEmpty()
? std::make_unique<QPalette>(Qt6CT::loadColorScheme(m_schemePath, *QPlatformTheme::palette(SystemPalette)))
: nullptr;
m_iconTheme = settings.value("icon_theme").toString();
//load dialogs
Expand Down Expand Up @@ -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<QDynamicPropertyChangeEvent*>(e)->propertyName() == "KDE_COLOR_SCHEME_PATH" &&
qApp->property("KDE_COLOR_SCHEME_PATH").toString().isEmpty() &&
Qt6CT::isKColorScheme(m_schemePath))
applySettings();
return QObject::eventFilter(obj, e);
}
7 changes: 5 additions & 2 deletions src/qt6ct-qtplugin/qt6ctplatformtheme.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<QKeySequence> 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
Expand All @@ -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<QPalette> m_palette;
QFont m_generalFont, m_fixedFont;
int m_doubleClickInterval;
Expand Down
2 changes: 1 addition & 1 deletion src/qt6ct/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
26 changes: 23 additions & 3 deletions src/qt6ct/appearancepage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
#include <QMenu>
#include <QIcon>
#include <QStringList>
#include <KSharedConfig>
#include <KConfigGroup>
#include <qpa/qplatformthemefactory_p.h>
#include "qt6ct.h"
#include "appearancepage.h"
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
{
Expand Down Expand Up @@ -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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/qt6ct/appearancepage.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
13 changes: 11 additions & 2 deletions src/qt6ct/iconthemepage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
#include <QProgressBar>
#include <QMetaObject>
#include <QThread>
#include <KSharedConfig>
#include <KConfigGroup>
#include "qt6ct.h"
#include "iconthemepage.h"
#include "ui_iconthemepage.h"
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 91d3e81

Please sign in to comment.