Re-implement palette, standardPixmap, file icons, fonts in QGtk3Theme

Read theme colors from GTK3 style context and build platform theme
palettes in Qt.
React to runtime theme changes.
Re-implement methods to retrieve GTK3 styled standardPixmaps, fonts
and file icons.

Task-number: QTBUG-62510
Change-Id: Ib58d1614850764f2e88cca33883fd6c426b35aee
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
Reviewed-by: Oliver Eftevaag <oliver.eftevaag@qt.io>
This commit is contained in:
Axel Spoerl 2022-10-12 21:38:25 +02:00
parent 96628b1254
commit 4ca8a68408
9 changed files with 1960 additions and 3 deletions

View File

@ -22,6 +22,9 @@ qt_internal_add_plugin(QGtk3ThemePlugin
qgtk3dialoghelpers.cpp qgtk3dialoghelpers.h
qgtk3menu.cpp qgtk3menu.h
qgtk3theme.cpp qgtk3theme.h
qgtk3interface.cpp qgtk3interface_p.h
qgtk3storage.cpp qgtk3storage_p.h
qgtk3json.cpp qgtk3json_p.h
DEFINES
GDK_VERSION_MIN_REQUIRED=GDK_VERSION_3_6
LIBRARIES # special case

View File

@ -0,0 +1,539 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qgtk3interface_p.h"
#include "qgtk3storage_p.h"
#include <QtCore/QMetaEnum>
#include <QtCore/QSettings>
#include <QtCore/QFileInfo>
#include <QtGui/QFontDatabase>
QT_BEGIN_NAMESPACE
Q_LOGGING_CATEGORY(lcQGtk3Interface, "qt.qpa.gtk");
// Callback for gnome event loop has to be static
static QGtk3Storage *m_storage = nullptr;
QGtk3Interface::QGtk3Interface(QGtk3Storage *s)
{
initColorMap();
if (!s) {
qCDebug(lcQGtk3Interface) << "QGtk3Interface instantiated without QGtk3Storage."
<< "No reaction to runtime theme changes.";
return;
}
// Connect to the GTK settings changed signal
auto handleThemeChange = [] {
if (m_storage)
m_storage->handleThemeChange();
};
GtkSettings *settings = gtk_settings_get_default();
const gboolean success = g_signal_connect(settings, "notify::gtk-theme-name",
G_CALLBACK(handleThemeChange), nullptr);
if (success == FALSE) {
qCDebug(lcQGtk3Interface) << "Connection to theme change signal failed."
<< "No reaction to runtime theme changes.";
} else {
m_storage = s;
}
}
QGtk3Interface::~QGtk3Interface()
{
// Ignore theme changes when destructor is reached
m_storage = nullptr;
// QGtkWidgets have to be destroyed manually
for (auto v : cache)
gtk_widget_destroy(v.second);
}
int QGtk3Interface::toGtkState(const QString &state)
{
#define CASE(x) \
if (QLatin1String(QByteArray(state.toLatin1())) == #x ##_L1) \
return GTK_STATE_FLAG_ ##x
#define CONVERT\
CASE(NORMAL);\
CASE(ACTIVE);\
CASE(PRELIGHT);\
CASE(SELECTED);\
CASE(INSENSITIVE);\
CASE(INCONSISTENT);\
CASE(FOCUSED);\
CASE(BACKDROP);\
CASE(DIR_LTR);\
CASE(DIR_RTL);\
CASE(LINK);\
CASE(VISITED);\
CASE(CHECKED);\
CASE(DROP_ACTIVE)
CONVERT;
return -1;
#undef CASE
}
const QLatin1String QGtk3Interface::fromGtkState(GtkStateFlags state)
{
#define CASE(x) case GTK_STATE_FLAG_ ##x: return QLatin1String(#x)
switch (state) {
CONVERT;
}
Q_UNREACHABLE();
#undef CASE
#undef CONVERT
}
void QGtk3Interface::initColorMap()
{
// Populate map with default values
#define SAVE(src, state, prop, def)\
{ColorKey({QGtkColorSource::src, GTK_STATE_FLAG_ ##state}), ColorValue({#prop ##_L1, QGtkColorDefault::def})}
gtkColorMap = ColorMap {
SAVE(Foreground, NORMAL, theme_fg_color, Foreground),
SAVE(Foreground, BACKDROP, theme_unfocused_selected_fg_color, Foreground),
SAVE(Foreground, INSENSITIVE, insensitive_fg_color, Foreground),
SAVE(Foreground, SELECTED, theme_selected_fg_color, Foreground),
SAVE(Foreground, ACTIVE, theme_unfocused_fg_color, Foreground),
SAVE(Text, NORMAL, theme_text_color, Foreground),
SAVE(Text, ACTIVE, theme_unfocused_text_color, Foreground),
SAVE(Base, NORMAL, theme_base_color, Background),
SAVE(Base, INSENSITIVE, insensitive_base_color, Background),
SAVE(Background, NORMAL, theme_bg_color, Background),
SAVE(Background, SELECTED, theme_selected_bg_color, Background),
SAVE(Background, INSENSITIVE, insensitive_bg_color, Background),
SAVE(Background, ACTIVE, theme_unfocused_bg_color, Background),
SAVE(Background, BACKDROP, theme_unfocused_selected_bg_color, Background),
SAVE(Border, NORMAL, borders, Border),
SAVE(Border, ACTIVE, unfocused_borders, Border)
};
#undef SAVE
qCDebug(lcQGtk3Interface) << "Color map populated from defaults.";
}
// Return an image rather than an icon or a pixmap:
// Image can be cached and re-scaled to different sizes if requested multiple times
QImage QGtk3Interface::standardPixmap(QPlatformTheme::StandardPixmap standardPixmap) const
{
switch (standardPixmap) {
case QPlatformTheme::DialogDiscardButton:
return qt_gtk_get_icon(GTK_STOCK_DELETE);
case QPlatformTheme::DialogOkButton:
return qt_gtk_get_icon(GTK_STOCK_OK);
case QPlatformTheme::DialogCancelButton:
return qt_gtk_get_icon(GTK_STOCK_CANCEL);
case QPlatformTheme::DialogYesButton:
return qt_gtk_get_icon(GTK_STOCK_YES);
case QPlatformTheme::DialogNoButton:
return qt_gtk_get_icon(GTK_STOCK_NO);
case QPlatformTheme::DialogOpenButton:
return qt_gtk_get_icon(GTK_STOCK_OPEN);
case QPlatformTheme::DialogCloseButton:
return qt_gtk_get_icon(GTK_STOCK_CLOSE);
case QPlatformTheme::DialogApplyButton:
return qt_gtk_get_icon(GTK_STOCK_APPLY);
case QPlatformTheme::DialogSaveButton:
return qt_gtk_get_icon(GTK_STOCK_SAVE);
case QPlatformTheme::MessageBoxWarning:
return qt_gtk_get_icon(GTK_STOCK_DIALOG_WARNING);
case QPlatformTheme::MessageBoxQuestion:
return qt_gtk_get_icon(GTK_STOCK_DIALOG_QUESTION);
case QPlatformTheme::MessageBoxInformation:
return qt_gtk_get_icon(GTK_STOCK_DIALOG_INFO);
case QPlatformTheme::MessageBoxCritical:
return qt_gtk_get_icon(GTK_STOCK_DIALOG_ERROR);
case QPlatformTheme::CustomBase:
case QPlatformTheme::TitleBarMenuButton:
case QPlatformTheme::TitleBarMinButton:
case QPlatformTheme::TitleBarMaxButton:
case QPlatformTheme::TitleBarCloseButton:
case QPlatformTheme::TitleBarNormalButton:
case QPlatformTheme::TitleBarShadeButton:
case QPlatformTheme::TitleBarUnshadeButton:
case QPlatformTheme::TitleBarContextHelpButton:
case QPlatformTheme::DockWidgetCloseButton:
case QPlatformTheme::DesktopIcon:
case QPlatformTheme::TrashIcon:
case QPlatformTheme::ComputerIcon:
case QPlatformTheme::DriveFDIcon:
case QPlatformTheme::DriveHDIcon:
case QPlatformTheme::DriveCDIcon:
case QPlatformTheme::DriveDVDIcon:
case QPlatformTheme::DriveNetIcon:
case QPlatformTheme::DirOpenIcon:
case QPlatformTheme::DirClosedIcon:
case QPlatformTheme::DirLinkIcon:
case QPlatformTheme::DirLinkOpenIcon:
case QPlatformTheme::FileIcon:
case QPlatformTheme::FileLinkIcon:
case QPlatformTheme::ToolBarHorizontalExtensionButton:
case QPlatformTheme::ToolBarVerticalExtensionButton:
case QPlatformTheme::FileDialogStart:
case QPlatformTheme::FileDialogEnd:
case QPlatformTheme::FileDialogToParent:
case QPlatformTheme::FileDialogNewFolder:
case QPlatformTheme::FileDialogDetailedView:
case QPlatformTheme::FileDialogInfoView:
case QPlatformTheme::FileDialogContentsView:
case QPlatformTheme::FileDialogListView:
case QPlatformTheme::FileDialogBack:
case QPlatformTheme::DirIcon:
case QPlatformTheme::DialogHelpButton:
case QPlatformTheme::DialogResetButton:
case QPlatformTheme::ArrowUp:
case QPlatformTheme::ArrowDown:
case QPlatformTheme::ArrowLeft:
case QPlatformTheme::ArrowRight:
case QPlatformTheme::ArrowBack:
case QPlatformTheme::ArrowForward:
case QPlatformTheme::DirHomeIcon:
case QPlatformTheme::CommandLink:
case QPlatformTheme::VistaShield:
case QPlatformTheme::BrowserReload:
case QPlatformTheme::BrowserStop:
case QPlatformTheme::MediaPlay:
case QPlatformTheme::MediaStop:
case QPlatformTheme::MediaPause:
case QPlatformTheme::MediaSkipForward:
case QPlatformTheme::MediaSkipBackward:
case QPlatformTheme::MediaSeekForward:
case QPlatformTheme::MediaSeekBackward:
case QPlatformTheme::MediaVolume:
case QPlatformTheme::MediaVolumeMuted:
case QPlatformTheme::LineEditClearButton:
case QPlatformTheme::DialogYesToAllButton:
case QPlatformTheme::DialogNoToAllButton:
case QPlatformTheme::DialogSaveAllButton:
case QPlatformTheme::DialogAbortButton:
case QPlatformTheme::DialogRetryButton:
case QPlatformTheme::DialogIgnoreButton:
case QPlatformTheme::RestoreDefaultsButton:
case QPlatformTheme::TabCloseButton:
case QPlatformTheme::NStandardPixmap:
return QImage();
}
Q_UNREACHABLE();
}
QImage QGtk3Interface::qt_gtk_get_icon(const char* iconName) const
{
GtkIconSet* iconSet = gtk_icon_factory_lookup_default (iconName);
GdkPixbuf* icon = gtk_icon_set_render_icon_pixbuf(iconSet, context(), GTK_ICON_SIZE_DIALOG);
return qt_convert_gdk_pixbuf(icon);
}
QImage QGtk3Interface::qt_convert_gdk_pixbuf(GdkPixbuf *buf) const
{
if (!buf)
return QImage();
// Ability to convert GdkPixbuf to QImage relies on the assumptions, that
// - QImage uses uchar as a data container
// - the types guint8 and uchar are identical
const guint8 *gdata = gdk_pixbuf_read_pixels(buf);
static_assert(std::is_same<decltype(gdata), const uchar *>::value,
"guint8 has diverted from uchar. Code needs fixing.");
Q_ASSUME(gdk_pixbuf_get_bits_per_sample(buf) == 8);
Q_ASSUME(gdk_pixbuf_get_n_channels(buf) == 4);
const uchar *data = static_cast<const uchar *>(gdata);
const int width = gdk_pixbuf_get_width(buf);
const int height = gdk_pixbuf_get_height(buf);
const int bpl = gdk_pixbuf_get_rowstride(buf);
QImage converted(data, width, height, bpl, QImage::Format_ARGB32);
return converted.copy(); // detatch to survive lifetime of buf
}
GtkWidget *QGtk3Interface::qt_new_gtkWidget(QGtkWidget type) const
{
#define CASE(Type)\
case QGtkWidget::Type: return Type ##_new();
#define CASEN(Type)\
case QGtkWidget::Type: return Type ##_new(nullptr);
switch (type) {
CASE(gtk_menu_bar)
CASE(gtk_menu)
CASE(gtk_button)
case QGtkWidget::gtk_button_box: return gtk_button_box_new(GtkOrientation::GTK_ORIENTATION_HORIZONTAL);
CASE(gtk_check_button)
CASEN(gtk_radio_button)
CASEN(gtk_frame)
CASE(gtk_statusbar)
CASE(gtk_entry)
case QGtkWidget::gtk_popup: return gtk_window_new(GTK_WINDOW_POPUP);
CASE(gtk_notebook)
CASE(gtk_toolbar)
CASE(gtk_tree_view)
CASE(gtk_combo_box)
CASE(gtk_combo_box_text)
CASE(gtk_progress_bar)
CASE(gtk_fixed)
CASE(gtk_separator_menu_item)
CASE(gtk_offscreen_window)
case QGtkWidget::gtk_Default: return nullptr;
}
#undef CASE
#undef CASEN
Q_UNREACHABLE();
}
GdkRGBA QGtk3Interface::genericColor(GtkStyleContext *con, GtkStateFlags state, QGtkColorDefault def) const
{
GdkRGBA color;
#define CASE(def, call)\
case QGtkColorDefault::def:\
gtk_style_context_get_ ##call(con, state, &color);\
break;
switch (def) {
CASE(Foreground, color)
CASE(Background, background_color)
CASE(Border, border_color)
}
return color;
#undef CASE
}
// Deliver a QColor from a GTK widget, a source type and a GTK widget state
// Fall back to the generic color getter of source/state if the property name does not exist
// Fall back to a hard coded generic color getter of source/state are not mapped
QColor QGtk3Interface::color(GtkWidget *widget, QGtkColorSource source, GtkStateFlags state) const
{
GdkRGBA col;
GtkStyleContext *con = context(widget);
#define CASE(src, def)\
case QGtkColorSource::src: {\
const ColorKey key = ColorKey({QGtkColorSource::src, state});\
if (gtkColorMap.contains(key)) {\
const ColorValue val = gtkColorMap.value(key);\
if (!gtk_style_context_lookup_color(con, val.propertyName.toUtf8().constData(), &col)) {\
col = genericColor(con, state, val.genericSource);\
qCDebug(lcQGtk3Interface) << "Property name" << val.propertyName << "not found.\n"\
<< "Falling back to " << val.genericSource;\
}\
} else {\
col = genericColor(con, state, QGtkColorDefault::def);\
qCDebug(lcQGtk3Interface) << "No color source found for" << QGtkColorSource::src\
<< fromGtkState(state) << "\n Falling back to"\
<< QGtkColorDefault::def;\
}\
}\
break;
switch (source) {
CASE(Foreground, Foreground)
CASE(Background, Background)
CASE(Text, Foreground)
CASE(Base, Background)
CASE(Border, Border)
}
return fromGdkColor(col);
#undef CASE
}
// Deliver a widget pointer
GtkWidget *QGtk3Interface::widget(QGtkWidget type) const
{
if (type == QGtkWidget::gtk_Default)
return nullptr;
// Return from cache
if (GtkWidget *w = cache.value(type))
return w;
// Create new item and cache it
GtkWidget *w = qt_new_gtkWidget(type);
cache.insert(type, w);
return w;
}
// Return widget syle context or default style
GtkStyleContext *QGtk3Interface::context(GtkWidget *w) const
{
if (w)
return gtk_widget_get_style_context(w);
return gtk_widget_get_style_context(widget(QGtkWidget::gtk_entry));
}
// FIXME
// Brush assets (e.g. 9-patches) can't be accessed by the GTK API.
// => brush height and width from GTK will be ignored for the time being,
// because it is unknown if they relate to a plain brush or an image brush.
QBrush QGtk3Interface::brush(QGtkWidget wtype, QGtkColorSource source, GtkStateFlags state) const
{
return QBrush(color(widget(wtype), source, state));
}
const QString QGtk3Interface::themeName() const
{
gchar *theme_name;
GtkSettings *settings = gtk_settings_get_default();
if (!settings)
return QString();
g_object_get(settings, "gtk-theme-name", &theme_name, nullptr);
return QLatin1StringView(theme_name);
}
inline constexpr QGtk3Interface::QGtkWidget QGtk3Interface::toWidgetType(QPlatformTheme::Font type)
{
switch (type) {
case QPlatformTheme::SystemFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::MenuFont: return QGtkWidget::gtk_menu;
case QPlatformTheme::MenuBarFont: return QGtkWidget::gtk_menu_bar;
case QPlatformTheme::MenuItemFont: return QGtkWidget::gtk_menu;
case QPlatformTheme::MessageBoxFont: return QGtkWidget::gtk_popup;
case QPlatformTheme::LabelFont: return QGtkWidget::gtk_popup;
case QPlatformTheme::TipLabelFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::StatusBarFont: return QGtkWidget::gtk_statusbar;
case QPlatformTheme::TitleBarFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::MdiSubWindowTitleFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::DockWidgetTitleFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::PushButtonFont: return QGtkWidget::gtk_button;
case QPlatformTheme::CheckBoxFont: return QGtkWidget::gtk_check_button;
case QPlatformTheme::RadioButtonFont: return QGtkWidget::gtk_radio_button;
case QPlatformTheme::ToolButtonFont: return QGtkWidget::gtk_button;
case QPlatformTheme::ItemViewFont: return QGtkWidget::gtk_entry;
case QPlatformTheme::ListViewFont: return QGtkWidget::gtk_tree_view;
case QPlatformTheme::HeaderViewFont: return QGtkWidget::gtk_fixed;
case QPlatformTheme::ListBoxFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::ComboMenuItemFont: return QGtkWidget::gtk_combo_box;
case QPlatformTheme::ComboLineEditFont: return QGtkWidget::gtk_combo_box_text;
case QPlatformTheme::SmallFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::MiniFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::FixedFont: return QGtkWidget::gtk_fixed;
case QPlatformTheme::GroupBoxTitleFont: return QGtkWidget::gtk_Default;
case QPlatformTheme::TabButtonFont: return QGtkWidget::gtk_button;
case QPlatformTheme::EditorFont: return QGtkWidget::gtk_entry;
case QPlatformTheme::NFonts: return QGtkWidget::gtk_Default;
}
Q_UNREACHABLE();
}
inline constexpr QFont::Style QGtk3Interface::toFontStyle(PangoStyle style)
{
switch (style) {
case PANGO_STYLE_ITALIC: return QFont::StyleItalic;
case PANGO_STYLE_OBLIQUE: return QFont::StyleOblique;
case PANGO_STYLE_NORMAL: return QFont::StyleNormal;
}
// This is reached when GTK has introduced a new font style
Q_UNREACHABLE();
}
inline constexpr int QGtk3Interface::toFontWeight(PangoWeight weight)
{
// GTK PangoWeight can be directly converted to QFont::Weight
// unless one of the enums changes.
static_assert(PANGO_WEIGHT_THIN == 100 && PANGO_WEIGHT_ULTRAHEAVY == 1000,
"Pango font weight enum changed. Fix conversion.");
static_assert(QFont::Thin == 100 && QFont::Black == 900,
"QFont::Weight enum changed. Fix conversion.");
return qBound(1, static_cast<int>(weight), 1000);
}
QFont QGtk3Interface::font(QPlatformTheme::Font type) const
{
GtkStyleContext *con = context(widget(toWidgetType(type)));
if (!con)
return QFont();
const PangoFontDescription *gtkFont = gtk_style_context_get_font(con, GTK_STATE_FLAG_NORMAL);
if (!gtkFont)
return QFont();
const QString family = QString::fromLatin1(pango_font_description_get_family(gtkFont));
if (family.isEmpty())
return QFont();
const int weight = toFontWeight(pango_font_description_get_weight(gtkFont));
// Creating a QFont() creates a futex lockup on a theme change
// QFont doesn't have a constructor with float point size
// => create a dummy point size and set it later.
QFont font(family, 1, weight);
font.setPointSizeF(static_cast<float>(pango_font_description_get_size(gtkFont)/PANGO_SCALE));
font.setStyle(toFontStyle(pango_font_description_get_style(gtkFont)));
// fix pixel pitch if fixed font is requested
// NOTE: GTK allows to specify a non fixed font as the system's fixed font.
// => the returned font may still not be a fixed font.
if (type == QPlatformTheme::FixedFont) {
font.setFixedPitch(true);
if (!QFontInfo(font).fixedPitch()) {
qCDebug(lcQGtk3Interface) << "No fixed pitch font found in font family"
<< font.family() << ". falling back to a default"
<< "fixed pitch font";
font.setFamily("monospace"_L1);
}
}
return font;
}
QIcon QGtk3Interface::fileIcon(const QFileInfo &fileInfo) const
{
GFile *file = g_file_new_for_path(fileInfo.absoluteFilePath().toLatin1().constData());
if (!file)
return QIcon();
GFileInfo *info = g_file_query_info (file, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
G_FILE_QUERY_INFO_NONE, nullptr, nullptr);
if (!info) {
g_object_unref(file);
return QIcon();
}
GIcon *icon = g_file_info_get_icon(info);
if (!icon) {
g_object_unref(file);
g_object_unref(info);
return QIcon();
}
GtkIconTheme *theme = gtk_icon_theme_get_default();
GtkIconInfo *iconInfo = gtk_icon_theme_lookup_by_gicon(theme, icon, GTK_ICON_SIZE_BUTTON,
GTK_ICON_LOOKUP_FORCE_SIZE);
if (!iconInfo) {
g_object_unref(file);
g_object_unref(info);
g_object_unref(icon);
return QIcon();
}
GdkPixbuf *buf = gtk_icon_info_load_icon(iconInfo, nullptr);
QImage image = qt_convert_gdk_pixbuf(buf);
g_object_unref(file);
g_object_unref(info);
g_object_unref(icon);
g_object_unref(buf);
return QIcon(QPixmap::fromImage(image));
}
QT_END_NAMESPACE

View File

@ -0,0 +1,171 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QGTK3INTERFACE_H
#define QGTK3INTERFACE_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include <QtCore/QString>
#include <QtCore/QLibrary>
#include <QtCore/QCache>
#include <private/qflatmap_p.h>
#include <QtCore/QObject>
#include <QtGui/QPalette>
#include <QtWidgets/QWidget>
#include <QtCore/QLoggingCategory>
#include <QtGui/QPixmap>
#include <qpa/qplatformtheme.h>
#undef signals // Collides with GTK symbols
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <glib.h>
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcQGtk3Interface);
using namespace Qt::StringLiterals;
class QGtk3Storage;
class QGtk3Interface
{
Q_GADGET
public:
QGtk3Interface(QGtk3Storage *);
~QGtk3Interface();
// Enum representing GTK widget types
enum class QGtkWidget {
gtk_menu_bar,
gtk_menu,
gtk_button,
gtk_button_box,
gtk_check_button,
gtk_radio_button,
gtk_frame,
gtk_statusbar,
gtk_entry,
gtk_popup,
gtk_notebook,
gtk_toolbar,
gtk_tree_view,
gtk_combo_box,
gtk_combo_box_text,
gtk_progress_bar,
gtk_fixed,
gtk_separator_menu_item,
gtk_Default,
gtk_offscreen_window
};
Q_ENUM(QGtkWidget)
// Enum representing color sources of a GTK theme
enum class QGtkColorSource {
Foreground,
Background,
Text,
Base,
Border
};
Q_ENUM(QGtkColorSource)
// Enum for default color getter
enum class QGtkColorDefault {
Foreground,
Background,
Border
};
Q_ENUM(QGtkColorDefault)
// Create a brush from GTK widget type, color source and color state
QBrush brush(QGtkWidget wtype, QGtkColorSource source, GtkStateFlags state) const;
// Font & icon getters
QImage standardPixmap(QPlatformTheme::StandardPixmap standardPixmap) const;
QFont font(QPlatformTheme::Font type) const;
QIcon fileIcon(const QFileInfo &fileInfo) const;
// Return current GTK theme name
const QString themeName() const;
// Convert GTK state to/from string
static int toGtkState(const QString &state);
static const QLatin1String fromGtkState(GtkStateFlags state);
private:
// Map colors to GTK property names and default to generic color getters
struct ColorKey {
QGtkColorSource colorSource = QGtkColorSource::Background;
GtkStateFlags state = GTK_STATE_FLAG_NORMAL;
// struct becomes key of a map, so operator< is needed
bool operator<(const ColorKey& other) const {
return std::tie(colorSource, state) <
std::tie(other.colorSource, other.state);
}
QDebug operator<<(QDebug dbg)
{
return dbg << "QGtk3Interface::ColorKey(colorSource=" << colorSource << ", GTK state=" << fromGtkState(state) << ")";
}
};
struct ColorValue {
QString propertyName = QString();
QGtkColorDefault genericSource = QGtkColorDefault::Background;
QDebug operator<<(QDebug dbg)
{
return dbg << "QGtk3Interface::ColorValue(propertyName=" << propertyName << ", genericSource=" << genericSource << ")";
}
};
typedef QFlatMap<ColorKey, ColorValue> ColorMap;
ColorMap gtkColorMap;
void initColorMap();
GdkRGBA genericColor(GtkStyleContext *con, GtkStateFlags state, QGtkColorDefault def) const;
// Cache for GTK widgets
mutable QFlatMap<QGtkWidget, GtkWidget *> cache;
// Converters for GTK icon and GDK pixbuf
QImage qt_gtk_get_icon(const char *iconName) const;
QImage qt_convert_gdk_pixbuf(GdkPixbuf *buf) const;
// Create new GTK widget object
GtkWidget *qt_new_gtkWidget(QGtkWidget type) const;
// Deliver GTK Widget from cache or create new
GtkWidget *widget(QGtkWidget type) const;
// Get a GTK widget's style context. Default settings style context if nullptr
GtkStyleContext *context(GtkWidget *widget = nullptr) const;
// Convert GTK color into QColor
static inline QColor fromGdkColor (const GdkRGBA &c)
{ return QColor::fromRgbF(c.red, c.green, c.blue, c.alpha); }
// get a QColor of a GTK widget (default settings style if nullptr)
QColor color (GtkWidget *widget, QGtkColorSource source, GtkStateFlags state) const;
// Mappings for GTK fonts
inline static constexpr QGtkWidget toWidgetType(QPlatformTheme::Font);
inline static constexpr QFont::Style toFontStyle(PangoStyle style);
inline static constexpr int toFontWeight(PangoWeight weight);
};
QT_END_NAMESPACE
#endif // QGTK3INTERFACE_H

View File

@ -0,0 +1,404 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qgtk3json_p.h"
#include <QtCore/QFile>
#include <QMetaEnum>
QT_BEGIN_NAMESPACE
QLatin1String QGtk3Json::fromPalette(QPlatformTheme::Palette palette)
{
return QLatin1String(QMetaEnum::fromType<QPlatformTheme::Palette>().valueToKey(static_cast<int>(palette)));
}
QLatin1String QGtk3Json::fromGtkState(GtkStateFlags state)
{
return QGtk3Interface::fromGtkState(state);
}
QLatin1String fromColor(const QColor &color)
{
return QLatin1String(QByteArray(color.name(QColor::HexRgb).toLatin1()));
}
QLatin1String QGtk3Json::fromColorRole(QPalette::ColorRole role)
{
return QLatin1String(QMetaEnum::fromType<QPalette::ColorRole>().valueToKey(static_cast<int>(role)));
}
QLatin1String QGtk3Json::fromColorGroup(QPalette::ColorGroup group)
{
return QLatin1String(QMetaEnum::fromType<QPalette::ColorGroup>().valueToKey(static_cast<int>(group)));
}
QLatin1String QGtk3Json::fromGdkSource(QGtk3Interface::QGtkColorSource source)
{
return QLatin1String(QMetaEnum::fromType<QGtk3Interface::QGtkColorSource>().valueToKey(static_cast<int>(source)));
}
QLatin1String QGtk3Json::fromWidgetType(QGtk3Interface::QGtkWidget widgetType)
{
return QLatin1String(QMetaEnum::fromType<QGtk3Interface::QGtkWidget>().valueToKey(static_cast<int>(widgetType)));
}
QLatin1String QGtk3Json::fromAppearance(Qt::Appearance app)
{
return QLatin1String(QMetaEnum::fromType<Qt::Appearance>().valueToKey(static_cast<int>(app)));
}
#define CONVERT(type, key, def)\
bool ok;\
const int intVal = QMetaEnum::fromType<type>().keyToValue(key.toLatin1().constData(), &ok);\
return ok ? static_cast<type>(intVal) : type::def
Qt::Appearance QGtk3Json::toAppearance(const QString &appearance)
{
CONVERT(Qt::Appearance, appearance, Unknown);
}
QPlatformTheme::Palette QGtk3Json::toPalette(const QString &palette)
{
CONVERT(QPlatformTheme::Palette, palette, NPalettes);
}
GtkStateFlags QGtk3Json::toGtkState(const QString &type)
{
int i = QGtk3Interface::toGtkState(type);
if (i < 0)
return GTK_STATE_FLAG_NORMAL;
return static_cast<GtkStateFlags>(i);
}
QColor toColor(const QStringView &color)
{
return QColor::fromString(color);
}
QPalette::ColorRole QGtk3Json::toColorRole(const QString &role)
{
CONVERT(QPalette::ColorRole, role, NColorRoles);
}
QPalette::ColorGroup QGtk3Json::toColorGroup(const QString &group)
{
CONVERT(QPalette::ColorGroup, group, NColorGroups);
}
QGtk3Interface::QGtkColorSource QGtk3Json::toGdkSource(const QString &source)
{
CONVERT(QGtk3Interface::QGtkColorSource, source, Background);
}
QLatin1String QGtk3Json::fromSourceType(QGtk3Storage::SourceType sourceType)
{
return QLatin1String(QMetaEnum::fromType<QGtk3Storage::SourceType>().valueToKey(static_cast<int>(sourceType)));
}
QGtk3Storage::SourceType QGtk3Json::toSourceType(const QString &sourceType)
{
CONVERT(QGtk3Storage::SourceType, sourceType, Invalid);
}
QGtk3Interface::QGtkWidget QGtk3Json::toWidgetType(const QString &widgetType)
{
CONVERT(QGtk3Interface::QGtkWidget, widgetType, gtk_offscreen_window);
}
#undef CONVERT
bool QGtk3Json::save(const QGtk3Storage::PaletteMap &map, const QString &fileName,
QJsonDocument::JsonFormat format)
{
QJsonDocument doc = save(map);
if (doc.isEmpty()) {
qWarning() << "Nothing to save to" << fileName;
return false;
}
QFile file(fileName);
if (!file.open(QIODevice::WriteOnly)) {
qWarning() << "Unable to open file" << fileName << "for writing.";
return false;
}
if (!file.write(doc.toJson(format))) {
qWarning() << "Unable to serialize Json document.";
return false;
}
file.close();
qInfo() << "Saved mapping data to" << fileName;
return true;
}
const QJsonDocument QGtk3Json::save(const QGtk3Storage::PaletteMap &map)
{
QJsonObject paletteObject;
for (auto paletteIterator = map.constBegin(); paletteIterator != map.constEnd();
++paletteIterator) {
const QGtk3Storage::BrushMap &bm = paletteIterator.value();
QFlatMap<QPalette::ColorRole, QGtk3Storage::BrushMap> brushMaps;
for (auto brushIterator = bm.constBegin(); brushIterator != bm.constEnd();
++brushIterator) {
const QPalette::ColorRole role = brushIterator.key().colorRole;
if (brushMaps.contains(role)) {
brushMaps.value(role).insert(brushIterator.key(), brushIterator.value());
} else {
QGtk3Storage::BrushMap newMap;
newMap.insert(brushIterator.key(), brushIterator.value());
brushMaps.insert(role, newMap);
}
}
QJsonObject brushArrayObject;
for (auto brushMapIterator = brushMaps.constBegin();
brushMapIterator != brushMaps.constEnd(); ++brushMapIterator) {
QJsonArray brushArray;
int brushIndex = 0;
const QGtk3Storage::BrushMap &bm = brushMapIterator.value();
for (auto brushIterator = bm.constBegin(); brushIterator != bm.constEnd();
++brushIterator) {
QJsonObject brushObject;
const QGtk3Storage::TargetBrush tb = brushIterator.key();
QGtk3Storage::Source s = brushIterator.value();
brushObject.insert(ceColorGroup, fromColorGroup(tb.colorGroup));
brushObject.insert(ceAppearance, fromAppearance(tb.appearance));
brushObject.insert(ceSourceType, fromSourceType(s.sourceType));
QJsonObject sourceObject;
switch (s.sourceType) {
case QGtk3Storage::SourceType::Gtk: {
sourceObject.insert(ceGtkWidget, fromWidgetType(s.gtk3.gtkWidgetType));
sourceObject.insert(ceGdkSource, fromGdkSource(s.gtk3.source));
sourceObject.insert(ceGtkState, fromGtkState(s.gtk3.state));
sourceObject.insert(ceWidth, s.gtk3.width);
sourceObject.insert(ceHeight, s.gtk3.height);
}
break;
case QGtk3Storage::SourceType::Fixed: {
QJsonObject fixedObject;
fixedObject.insert(ceColor, s.fix.fixedBrush.color().name());
fixedObject.insert(ceWidth, s.fix.fixedBrush.texture().width());
fixedObject.insert(ceHeight, s.fix.fixedBrush.texture().height());
sourceObject.insert(ceBrush, fixedObject);
}
break;
case QGtk3Storage::SourceType::Modified:{
sourceObject.insert(ceColorGroup, fromColorGroup(s.rec.colorGroup));
sourceObject.insert(ceColorRole, fromColorRole(s.rec.colorRole));
sourceObject.insert(ceAppearance, fromAppearance(s.rec.appearance));
sourceObject.insert(ceRed, s.rec.deltaRed);
sourceObject.insert(ceGreen, s.rec.deltaGreen);
sourceObject.insert(ceBlue, s.rec.deltaBlue);
sourceObject.insert(ceWidth, s.rec.width);
sourceObject.insert(ceHeight, s.rec.height);
sourceObject.insert(ceLighter, s.rec.lighter);
}
break;
case QGtk3Storage::SourceType::Invalid:
break;
}
brushObject.insert(ceData, sourceObject);
brushArray.insert(brushIndex, brushObject);
++brushIndex;
}
brushArrayObject.insert(fromColorRole(brushMapIterator.key()), brushArray);
}
paletteObject.insert(fromPalette(paletteIterator.key()), brushArrayObject);
}
QJsonObject top;
top.insert(cePalettes, paletteObject);
return paletteObject.keys().isEmpty() ? QJsonDocument() : QJsonDocument(top);
}
bool QGtk3Json::load(QGtk3Storage::PaletteMap &map, const QString &fileName)
{
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcQGtk3Interface) << "Unable to open file:" << fileName;
return false;
}
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err);
if (err.error != QJsonParseError::NoError) {
qWarning(lcQGtk3Interface) << "Unable to parse Json document from" << fileName
<< err.error << err.errorString();
return false;
}
if (Q_LIKELY(load(map, doc))) {
qInfo() << "GTK mapping successfully imported from" << fileName;
return true;
}
qWarning() << "File" << fileName << "could not be loaded.";
return false;
}
bool QGtk3Json::load(QGtk3Storage::PaletteMap &map, const QJsonDocument &doc)
{
#define GETSTR(obj, key)\
if (!obj.contains(key)) {\
qCDebug(lcQGtk3Interface) << key << "missing for palette" << paletteName\
<< ", Brush" << colorRoleName;\
return false;\
}\
value = obj[key].toString()
#define GETINT(obj, key, var) GETSTR(obj, key);\
if (!obj[key].isDouble()) {\
qCDebug(lcQGtk3Interface) << key << "type mismatch" << value\
<< "is not an integer!"\
<< "(Palette" << paletteName << "), Brush" << colorRoleName;\
return false;\
}\
const int var = obj[key].toInt()
map.clear();
const QJsonObject top(doc.object());
if (doc.isEmpty() || top.isEmpty() || !top.contains(cePalettes)) {
qCDebug(lcQGtk3Interface) << "Document does not contain Palettes.";
return false;
}
const QStringList &paletteList = top[cePalettes].toObject().keys();
for (const QString &paletteName : paletteList) {
bool ok;
const int intVal = QMetaEnum::fromType<QPlatformTheme::Palette>().keyToValue(paletteName
.toLatin1().constData(), &ok);
if (!ok) {
qCDebug(lcQGtk3Interface) << "Invalid Palette name:" << paletteName;
return false;
}
const QJsonObject &paletteObject = top[cePalettes][paletteName].toObject();
const QStringList &brushList = paletteObject.keys();
if (brushList.isEmpty()) {
qCDebug(lcQGtk3Interface) << "Palette" << paletteName << "does not contain brushes";
return false;
}
const QPlatformTheme::Palette paletteType = static_cast<QPlatformTheme::Palette>(intVal);
QGtk3Storage::BrushMap brushes;
const QStringList &colorRoles = paletteObject.keys();
for (const QString &colorRoleName : colorRoles) {
const int intVal = QMetaEnum::fromType<QPalette::ColorRole>().keyToValue(colorRoleName
.toLatin1().constData(), &ok);
if (!ok) {
qCDebug(lcQGtk3Interface) << "Palette" << paletteName
<< "contains invalid color role" << colorRoleName;
return false;
}
const QPalette::ColorRole colorRole = static_cast<QPalette::ColorRole>(intVal);
const QJsonArray &brushArray = paletteObject[colorRoleName].toArray();
for (int brushIndex = 0; brushIndex < brushArray.size(); ++brushIndex) {
const QJsonObject brushObject = brushArray.at(brushIndex).toObject();
if (brushObject.isEmpty()) {
qCDebug(lcQGtk3Interface) << "Brush specification missing at for palette"
<< paletteName << ", Brush" << colorRoleName;
return false;
}
QString value;
GETSTR(brushObject, ceSourceType);
const QGtk3Storage::SourceType sourceType = toSourceType(value);
GETSTR(brushObject, ceColorGroup);
const QPalette::ColorGroup colorGroup = toColorGroup(value);
GETSTR(brushObject, ceAppearance);
const Qt::Appearance appearance = toAppearance(value);
QGtk3Storage::TargetBrush tb(colorGroup, colorRole, appearance);
QGtk3Storage::Source s;
if (!brushObject.contains(ceData) || !brushObject[ceData].isObject()) {
qCDebug(lcQGtk3Interface) << "Source specification missing for palette" << paletteName
<< "Brush" << colorRoleName;
return false;
}
const QJsonObject &sourceObject = brushObject[ceData].toObject();
switch (sourceType) {
case QGtk3Storage::SourceType::Gtk: {
GETSTR(sourceObject, ceGdkSource);
const QGtk3Interface::QGtkColorSource gtkSource = toGdkSource(value);
GETSTR(sourceObject, ceGtkState);
const GtkStateFlags gtkState = toGtkState(value);
GETSTR(sourceObject, ceGtkWidget);
const QGtk3Interface::QGtkWidget widgetType = toWidgetType(value);
GETINT(sourceObject, ceHeight, height);
GETINT(sourceObject, ceWidth, width);
s = QGtk3Storage::Source(widgetType, gtkSource, gtkState, width, height);
}
break;
case QGtk3Storage::SourceType::Fixed: {
if (!sourceObject.contains(ceBrush)) {
qCDebug(lcQGtk3Interface) << "Fixed brush specification missing for palette" << paletteName
<< "Brush" << colorRoleName;
return false;
}
const QJsonObject &fixedSource = sourceObject[ceBrush].toObject();
GETINT(fixedSource, ceWidth, width);
GETINT(fixedSource, ceHeight, height);
GETSTR(fixedSource, ceColor);
const QColor color(value);
if (!color.isValid()) {
qCDebug(lcQGtk3Interface) << "Color" << value << "can't be parsed for:" << paletteName
<< "Brush" << colorRoleName;
return false;
}
const QBrush fixedBrush = (width < 0 && height < 0)
? QBrush(color, QPixmap(width, height))
: QBrush(color);
s = QGtk3Storage::Source(fixedBrush);
}
break;
case QGtk3Storage::SourceType::Modified: {
GETSTR(sourceObject, ceColorGroup);
const QPalette::ColorGroup colorGroup = toColorGroup(value);
GETSTR(sourceObject, ceColorRole);
const QPalette::ColorRole colorRole = toColorRole(value);
GETSTR(sourceObject, ceAppearance);
const Qt::Appearance appearance = toAppearance(value);
GETINT(sourceObject, ceLighter, lighter);
GETINT(sourceObject, ceRed, red);
GETINT(sourceObject, ceBlue, blue);
GETINT(sourceObject, ceGreen, green);
s = QGtk3Storage::Source(colorGroup, colorRole, appearance,
lighter, red, green, blue);
}
break;
case QGtk3Storage::SourceType::Invalid:
qCDebug(lcQGtk3Interface) << "Invalid source type for palette" << paletteName
<< "Brush." << colorRoleName;
return false;
}
brushes.insert(tb, s);
}
}
map.insert(paletteType, brushes);
}
return true;
}
QT_END_NAMESPACE

View File

@ -0,0 +1,102 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QGTK3JSON_P_H
#define QGTK3JSON_P_H
#include <QtCore/QCache>
#include <QtCore/QJsonArray>
#include <QtCore/QJsonDocument>
#include <QtCore/QJsonObject>
#include <QtCore/QMap>
#include <QtCore/QString>
#include <QtGui/QGuiApplication>
#include <QtGui/QPalette>
#include <qpa/qplatformtheme.h>
#include "qgtk3interface_p.h"
#include "qgtk3storage_p.h"
#undef signals // Collides with GTK symbols
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <glib.h>
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
QT_BEGIN_NAMESPACE
class QGtk3Json
{
Q_GADGET
private:
QGtk3Json(){};
public:
// Convert enums to strings
static QLatin1String fromPalette(QPlatformTheme::Palette palette);
static QLatin1String fromGtkState(GtkStateFlags type);
static QLatin1String fromColor(const QColor &Color);
static QLatin1String fromColorRole(QPalette::ColorRole role);
static QLatin1String fromColorGroup(QPalette::ColorGroup group);
static QLatin1String fromGdkSource(QGtk3Interface::QGtkColorSource source);
static QLatin1String fromSourceType(QGtk3Storage::SourceType sourceType);
static QLatin1String fromWidgetType(QGtk3Interface::QGtkWidget widgetType);
static QLatin1String fromAppearance(Qt::Appearance app);
// Convert strings to enums
static QPlatformTheme::Palette toPalette(const QString &palette);
static GtkStateFlags toGtkState(const QString &type);
static QColor toColor(const QString &Color);
static QPalette::ColorRole toColorRole(const QString &role);
static QPalette::ColorGroup toColorGroup(const QString &group);
static QGtk3Interface::QGtkColorSource toGdkSource(const QString &source);
static QGtk3Storage::SourceType toSourceType(const QString &sourceType);
static QGtk3Interface::QGtkWidget toWidgetType(const QString &widgetType);
static Qt::Appearance toAppearance(const QString &appearance);
// Json keys
static constexpr QLatin1StringView cePalettes = "QtGtk3Palettes"_L1;
static constexpr QLatin1StringView cePalette = "PaletteType"_L1;
static constexpr QLatin1StringView ceGtkState = "GtkStateType"_L1;
static constexpr QLatin1StringView ceGtkWidget = "GtkWidgetType"_L1;
static constexpr QLatin1StringView ceColor = "Color"_L1;
static constexpr QLatin1StringView ceColorRole = "ColorRole"_L1;
static constexpr QLatin1StringView ceColorGroup = "ColorGroup"_L1;
static constexpr QLatin1StringView ceGdkSource = "GdkSource"_L1;
static constexpr QLatin1StringView ceSourceType = "SourceType"_L1;
static constexpr QLatin1StringView ceLighter = "Lighter"_L1;
static constexpr QLatin1StringView ceRed = "DeltaRed"_L1;
static constexpr QLatin1StringView ceGreen = "DeltaGreen"_L1;
static constexpr QLatin1StringView ceBlue = "DeltaBlue"_L1;
static constexpr QLatin1StringView ceWidth = "Width"_L1;
static constexpr QLatin1StringView ceHeight = "Height"_L1;
static constexpr QLatin1StringView ceBrush = "FixedBrush"_L1;
static constexpr QLatin1StringView ceData = "SourceData"_L1;
static constexpr QLatin1StringView ceBrushes = "Brushes"_L1;
static constexpr QLatin1StringView ceAppearance = "Appearance"_L1;
// Save to a file
static bool save(const QGtk3Storage::PaletteMap &map, const QString &fileName,
QJsonDocument::JsonFormat format = QJsonDocument::Indented);
// Save to a Json document
static const QJsonDocument save(const QGtk3Storage::PaletteMap &map);
// Load from a file
static bool load(QGtk3Storage::PaletteMap &map, const QString &fileName);
// Load from a Json document
static bool load(QGtk3Storage::PaletteMap &map, const QJsonDocument &doc);
};
QT_END_NAMESPACE
#endif // QGTK3JSON_P_H

View File

@ -0,0 +1,470 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qgtk3json_p.h"
#include "qgtk3storage_p.h"
#include <qpa/qwindowsysteminterface.h>
QT_BEGIN_NAMESPACE
QGtk3Storage::QGtk3Storage()
{
m_interface.reset(new QGtk3Interface(this));
populateMap();
}
// Set a brush from a source and resolve recursions
QBrush QGtk3Storage::brush(const Source &source, const BrushMap &map) const
{
switch (source.sourceType) {
case SourceType::Gtk:
return m_interface ? QBrush(m_interface->brush(source.gtk3.gtkWidgetType,
source.gtk3.source, source.gtk3.state))
: QBrush();
case SourceType::Modified: {
// don't loop through modified sources, break if modified source not found
Source recSource = brush(TargetBrush(source.rec.colorGroup, source.rec.colorRole,
source.rec.appearance), map);
if (!recSource.isValid() || (recSource.sourceType == SourceType::Modified))
return QBrush();
// Set brush and alter color
QBrush b = brush(recSource, map);
if (source.rec.width > 0 && source.rec.height > 0)
b.setTexture(QPixmap(source.rec.width, source.rec.height));
QColor c = b.color().lighter(source.rec.lighter);
c = QColor((c.red() + source.rec.deltaRed),
(c.green() + source.rec.deltaGreen),
(c.blue() + source.rec.deltaBlue));
b.setColor(c);
return b;
}
case SourceType::Fixed:
return source.fix.fixedBrush;
case SourceType::Invalid:
return QBrush();
}
// needed because of the scope after recursive
Q_UNREACHABLE();
}
// Find source for a recursion and take dark/light/unknown into consideration
QGtk3Storage::Source QGtk3Storage::brush(const TargetBrush &b, const BrushMap &map) const
{
#define FIND(brush) if (map.contains(brush))\
return map.value(brush)
// Return exact match
FIND(b);
// unknown appearance can find anything
if (b.appearance == Qt::Appearance::Unknown) {
FIND(TargetBrush(b, Qt::Appearance::Dark));
FIND(TargetBrush(b, Qt::Appearance::Light));
}
// Color group All can always be found
if (b.colorGroup != QPalette::All)
return brush(TargetBrush(QPalette::All, b.colorRole, b.appearance), map);
// Brush not found
return Source();
#undef FIND
}
// Create a simple standard palette
QPalette QGtk3Storage::standardPalette()
{
QColor backgroundColor(0xd4, 0xd0, 0xc8);
QColor lightColor(backgroundColor.lighter());
QColor darkColor(backgroundColor.darker());
const QBrush darkBrush(darkColor);
QColor midColor(Qt::gray);
QPalette palette(Qt::black, backgroundColor, lightColor, darkColor,
midColor, Qt::black, Qt::white);
palette.setBrush(QPalette::Disabled, QPalette::WindowText, darkBrush);
palette.setBrush(QPalette::Disabled, QPalette::Text, darkBrush);
palette.setBrush(QPalette::Disabled, QPalette::ButtonText, darkBrush);
palette.setBrush(QPalette::Disabled, QPalette::Base, QBrush(backgroundColor));
return palette;
}
// Deliver a palette styled according to the current GTK Theme
const QPalette *QGtk3Storage::palette(QPlatformTheme::Palette type) const
{
if (type >= QPlatformTheme::NPalettes)
return nullptr;
if (m_paletteCache[type].has_value()) {
qCDebug(lcQGtk3Interface) << "Returning palette from cache:"
<< QGtk3Json::fromPalette(type);
return &m_paletteCache[type].value();
}
// Read system palette as a baseline first
if (!m_paletteCache[QPlatformTheme::SystemPalette].has_value() && type != QPlatformTheme::SystemPalette)
palette();
// Fall back to system palette for unknown types
if (!m_palettes.contains(type) && type != QPlatformTheme::SystemPalette) {
qCDebug(lcQGtk3Interface) << "Returning system palette for unknown type"
<< QGtk3Json::fromPalette(type);
return palette();
}
BrushMap brushes = m_palettes.value(type);
// Standard palette is base for system palette. System palette is base for all others.
QPalette p = QPalette( type == QPlatformTheme::SystemPalette ? standardPalette()
: m_paletteCache[QPlatformTheme::SystemPalette].value());
qCDebug(lcQGtk3Interface) << "Creating palette:" << QGtk3Json::fromPalette(type);
for (auto i = brushes.begin(); i != brushes.end(); ++i) {
Source source = i.value();
// Brush is set if
// - theme and source appearance match
// - or either of them is unknown
const auto appSource = i.key().appearance;
const auto appTheme = appearance();
const bool setBrush = (appSource == appTheme) ||
(appSource == Qt::Appearance::Unknown) ||
(appTheme == Qt::Appearance::Unknown);
if (setBrush) {
p.setBrush(i.key().colorGroup, i.key().colorRole, brush(source, brushes));
}
}
m_paletteCache[type].emplace(p);
if (type == QPlatformTheme::SystemPalette)
qCDebug(lcQGtk3Interface) << "System Palette defined" << themeName() << appearance() << p;
return &m_paletteCache[type].value();
}
const QFont *QGtk3Storage::font(QPlatformTheme::Font type) const
{
if (m_fontCache[type].has_value())
return &m_fontCache[type].value();
m_fontCache[type].emplace(m_interface->font(type));
return &m_fontCache[type].value();
}
QPixmap QGtk3Storage::standardPixmap(QPlatformTheme::StandardPixmap standardPixmap,
const QSizeF &size) const
{
if (m_pixmapCache.contains(standardPixmap))
return QPixmap::fromImage(m_pixmapCache.object(standardPixmap)->scaled(size.toSize()));
if (!m_interface)
return QPixmap();
QImage image = m_interface->standardPixmap(standardPixmap);
if (image.isNull())
return QPixmap();
m_pixmapCache.insert(standardPixmap, new QImage(image));
return QPixmap::fromImage(image.scaled(size.toSize()));
}
QIcon QGtk3Storage::fileIcon(const QFileInfo &fileInfo) const
{
return m_interface ? m_interface->fileIcon(fileInfo) : QIcon();
}
void QGtk3Storage::clear()
{
m_appearance = Qt::Appearance::Unknown;
m_palettes.clear();
for (auto &cache : m_paletteCache)
cache.reset();
for (auto &cache : m_fontCache)
cache.reset();
}
void QGtk3Storage::handleThemeChange()
{
clear();
populateMap();
QWindowSystemInterface::handleThemeChange();
}
void QGtk3Storage::populateMap()
{
static QString m_themeName;
// Distiguish initialization, theme change or call without theme change
const QString newThemeName = themeName();
if (m_themeName == newThemeName)
return;
clear();
// Derive appearance from theme name
m_appearance = newThemeName.contains("dark"_L1, Qt::CaseInsensitive)
? Qt::Appearance::Dark : Qt::Appearance::Light;
if (m_themeName.isEmpty()) {
qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << m_appearance;
} else {
qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << m_appearance;
}
m_themeName = newThemeName;
// create standard mapping or load from Json file?
const QString jsonInput = qEnvironmentVariable("QT_GUI_GTK_JSON");
if (!jsonInput.isEmpty()) {
if (load(jsonInput)) {
return;
} else {
qWarning() << "Falling back to standard GTK mapping.";
}
}
createMapping();
const QString jsonOutput = qEnvironmentVariable("QT_GUI_GTK_JSON_SAVE");
if (!jsonOutput.isEmpty() && !save(jsonOutput))
qWarning() << "File" << jsonOutput << "could not be saved.\n";
}
const QGtk3Storage::PaletteMap QGtk3Storage::savePalettes() const
{
const QString hard = qEnvironmentVariable("QT_GUI_GTK_JSON_HARDCODED");
if (!hard.contains("true"_L1, Qt::CaseInsensitive))
return m_palettes;
// Json output is supposed to be readable without GTK connection
// convert palette map into hard coded brushes
PaletteMap map = m_palettes;
for (auto paletteIterator = map.begin(); paletteIterator != map.end();
++paletteIterator) {
QGtk3Storage::BrushMap &bm = paletteIterator.value();
for (auto brushIterator = bm.begin(); brushIterator != bm.end();
++brushIterator) {
QGtk3Storage::Source &s = brushIterator.value();
switch (s.sourceType) {
// Read the brush and convert it into a fixed brush
case SourceType::Gtk: {
const QBrush fixedBrush = brush(s, bm);
s.fix.fixedBrush = fixedBrush;
s.sourceType = SourceType::Fixed;
}
break;
case SourceType::Fixed:
case SourceType::Modified:
case SourceType::Invalid:
break;
}
}
}
return map;
}
bool QGtk3Storage::save(const QString &filename, QJsonDocument::JsonFormat f) const
{
return QGtk3Json::save(savePalettes(), filename, f);
}
QJsonDocument QGtk3Storage::save() const
{
return QGtk3Json::save(savePalettes());
}
bool QGtk3Storage::load(const QString &filename)
{
return QGtk3Json::load(m_palettes, filename);
}
void QGtk3Storage::createMapping()
{
// Hard code standard mapping
BrushMap map;
Source source;
// Define a GTK source
#define GTK(wtype, colorSource, state)\
source = Source(QGtk3Interface::QGtkWidget::gtk_ ##wtype,\
QGtk3Interface::QGtkColorSource::colorSource, GTK_STATE_FLAG_ ##state)
// Define a modified source
#define LIGHTER(group, role, lighter)\
source = Source(QPalette::group, QPalette::role,\
Qt::Appearance::Unknown, lighter)
#define MODIFY(group, role, red, green, blue)\
source = Source(QPalette::group, QPalette::role,\
Qt::Appearance::Unknown, red, green, blue)
// Define fixed source
#define FIX(color) source = FixedSource(color);
// Add the source to a target brush
// Use default Qt::Appearance::Unknown, if no appearance was specified
#define ADD_2(group, role) map.insert(TargetBrush(QPalette::group, QPalette::role), source);
#define ADD_3(group, role, app) map.insert(TargetBrush(QPalette::group, QPalette::role,\
Qt::Appearance::app), source);
#define ADD_X(x, group, role, app, FUNC, ...) FUNC
#define ADD(...) ADD_X(,##__VA_ARGS__, ADD_3(__VA_ARGS__), ADD_2(__VA_ARGS__))
// Save target brushes to a palette type
#define SAVE(palette) m_palettes.insert(QPlatformTheme::palette, map)
// Clear brushes to start next palette
#define CLEAR map.clear()
/*
* Macro ussage:
*
* 1. Define a source
*
* GTK(QGtkWidget, QGtkColorSource, GTK_STATE_FLAG)
* Fetch the color from a GtkWidget, related to a source and a state.
*
* LIGHTER(ColorGroup, ColorROle, lighter)
* Use a color of the same QPalette related to ColorGroup and ColorRole.
* Make the color lighter (if lighter >100) or darker (if lighter < 100)
*
* MODIFY(ColorGroup, ColorRole, red, green, blue)
* Use a color of the same QPalette related to ColorGroup and ColorRole.
* Modify it by adding red, green, blue.
*
* FIX(const QBrush &)
* Use a fixed brush without querying GTK
*
* 2. Define the target
*
* Use ADD(ColorGroup, ColorRole) to use the defined source for the
* color group / role in the current palette.
*
* Use ADD(ColorGroup, ColorRole, Appearance) to use the defined source
* only for a specific appearance
*
* 3. Save mapping
* Save the defined mappings for a specific palette.
* If a mapping entry does not cover all color groups and roles of a palette,
* the system palette will be used for the remaining values.
* If the system palette does not have all combination of color groups and roles,
* the remaining ones will be populated by a hard coded fusion-style like palette.
*
* 4. Clear mapping
* Use CLEAR to clear the mapping and begin a new one.
*/
// System palette
// background color and calculate derivates
GTK(Default, Background, INSENSITIVE);
ADD(Normal, Window);
ADD(Normal, Button);
ADD(Normal, Base);
ADD(Inactive, Base);
ADD(Normal, Window); // redundant
ADD(Inactive, Window);
LIGHTER(Normal, Window, 125);
ADD(Normal, Light);
LIGHTER(Normal, Window, 70);
ADD(Normal, Shadow);
LIGHTER(Normal, Window, 80);
ADD(Normal, Dark);
GTK(button, Foreground, ACTIVE);
ADD(Normal, WindowText);
ADD(Inactive, WindowText);
LIGHTER(Normal, WindowText, 50);
ADD(Disabled, Text);
ADD(Disabled, WindowText);
//ADD(Normal, ButtonText);
ADD(Inactive, ButtonText);
GTK(button, Text, NORMAL);
ADD(Disabled, ButtonText);
// special background colors
GTK(Default, Background, SELECTED);
ADD(Disabled, Highlight);
ADD(Normal, Highlight);
GTK(entry, Foreground, SELECTED);
ADD(Normal, HighlightedText);
GTK(entry, Background, ACTIVE);
ADD(Inactive, HighlightedText);
// text color and friends
GTK(entry, Text, NORMAL);
ADD(Normal, ButtonText);
ADD(Normal, WindowText);
ADD(Disabled, WindowText);
ADD(Disabled, HighlightedText);
GTK(Default, Text, NORMAL);
ADD(Normal, Text);
ADD(Inactive, Text);
ADD(Normal, HighlightedText);
LIGHTER(Normal, Base, 93);
ADD(All, AlternateBase);
GTK(Default, Foreground, NORMAL);
ADD(All, ToolTipText);
MODIFY(Normal, Text, 100, 100, 100);
ADD(All, PlaceholderText, Light);
MODIFY(Normal, Text, -100, -100, -100);
ADD(All, PlaceholderText, Dark);
SAVE(SystemPalette);
CLEAR;
// Checkbox and Radio Button
GTK(button, Text, ACTIVE);
ADD(Normal, Base, Dark);
GTK(button, Text, NORMAL);
ADD(Normal, Base, Light);
SAVE(CheckBoxPalette);
SAVE(RadioButtonPalette);
CLEAR;
// ComboBox, GroupBox, Frame
GTK(combo_box, Text, NORMAL);
ADD(Normal, ButtonText, Dark);
ADD(Normal, Text, Dark);
GTK(combo_box, Text, ACTIVE);
ADD(Normal, ButtonText, Light);
ADD(Normal, Text, Light);
SAVE(ComboBoxPalette);
SAVE(GroupBoxPalette);
CLEAR;
// Menu bar
GTK(Default, Text, ACTIVE);
ADD(Normal, ButtonText);
SAVE(MenuPalette);
CLEAR;
// LineEdit
GTK(Default, Background, NORMAL);
ADD(All, Base);
SAVE(TextLineEditPalette);
CLEAR;
#undef GTK
#undef REC
#undef FIX
#undef ADD
#undef ADD_2
#undef ADD_3
#undef ADD_X
#undef SAVE
#undef LOAD
}
QT_END_NAMESPACE

View File

@ -0,0 +1,234 @@
// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef QGTK3STORAGE_P_H
#define QGTK3STORAGE_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists purely as an
// implementation detail. This header file may change from version to
// version without notice, or even be removed.
//
// We mean it.
//
#include "qgtk3interface_p.h"
#include <QtCore/QJsonDocument>
#include <QtCore/QCache>
#include <QtCore/QString>
#include <QtGui/QGuiApplication>
#include <QtGui/QPalette>
#include <qpa/qplatformtheme.h>
#include <private/qflatmap_p.h>
QT_BEGIN_NAMESPACE
class QGtk3Storage
{
Q_GADGET
public:
QGtk3Storage();
enum class SourceType {
Gtk,
Fixed,
Modified,
Invalid
};
Q_ENUM(SourceType)
// Standard GTK source: Populate a brush from GTK
struct Gtk3Source {
QGtk3Interface::QGtkWidget gtkWidgetType;
QGtk3Interface::QGtkColorSource source;
GtkStateFlags state;
int width = -1;
int height = -1;
QDebug operator<<(QDebug dbg)
{
return dbg << "QGtkStorage::Gtk3Source(gtkwidgetType=" << gtkWidgetType << ", source="
<< source << ", state=" << state << ", width=" << width << ", height="
<< height << ")";
}
};
// Recursive source: Populate a brush by altering another source
struct RecursiveSource {
QPalette::ColorGroup colorGroup;
QPalette::ColorRole colorRole;
Qt::Appearance appearance;
int lighter = 100;
int deltaRed = 0;
int deltaGreen = 0;
int deltaBlue = 0;
int width = -1;
int height = -1;
QDebug operator<<(QDebug dbg)
{
return dbg << "QGtkStorage::RecursiceSource(colorGroup=" << colorGroup << ", colorRole="
<< colorRole << ", appearance=" << appearance << ", lighter=" << lighter
<< ", deltaRed="<< deltaRed << "deltaBlue =" << deltaBlue << "deltaGreen="
<< deltaGreen << ", width=" << width << ", height=" << height << ")";
}
};
// Fixed source: Populate a brush with fixed values rather than reading GTK
struct FixedSource {
QBrush fixedBrush;
QDebug operator<<(QDebug dbg)
{
return dbg << "QGtkStorage::FixedSource(" << fixedBrush << ")";
}
};
// Data source for brushes
struct Source {
SourceType sourceType = SourceType::Invalid;
Gtk3Source gtk3;
RecursiveSource rec;
FixedSource fix;
// GTK constructor
Source(QGtk3Interface::QGtkWidget wtype, QGtk3Interface::QGtkColorSource csource,
GtkStateFlags cstate, int bwidth = -1, int bheight = -1) : sourceType(SourceType::Gtk)
{
gtk3.gtkWidgetType = wtype;
gtk3.source = csource;
gtk3.state = cstate;
gtk3.width = bwidth;
gtk3.height = bheight;
}
// Recursive constructor for darker/lighter colors
Source(QPalette::ColorGroup group, QPalette::ColorRole role,
Qt::Appearance app, int p_lighter = 100)
: sourceType(SourceType::Modified)
{
rec.colorGroup = group;
rec.colorRole = role;
rec.appearance = app;
rec.lighter = p_lighter;
}
// Recursive ocnstructor for color modification
Source(QPalette::ColorGroup group, QPalette::ColorRole role,
Qt::Appearance app, int p_red, int p_green, int p_blue)
: sourceType(SourceType::Modified)
{
rec.colorGroup = group;
rec.colorRole = role;
rec.appearance = app;
rec.deltaRed = p_red;
rec.deltaGreen = p_green;
rec.deltaBlue = p_blue;
}
// Recursive constructor for all: color modification and darker/lighter
Source(QPalette::ColorGroup group, QPalette::ColorRole role,
Qt::Appearance app, int p_lighter,
int p_red, int p_green, int p_blue) : sourceType(SourceType::Modified)
{
rec.colorGroup = group;
rec.colorRole = role;
rec.appearance = app;
rec.lighter = p_lighter;
rec.deltaRed = p_red;
rec.deltaGreen = p_green;
rec.deltaBlue = p_blue;
}
// Fixed Source constructor
Source(const QBrush &brush) : sourceType(SourceType::Fixed)
{
fix.fixedBrush = brush;
};
// Invalid constructor and getter
Source() : sourceType(SourceType::Invalid) {};
bool isValid() const { return sourceType != SourceType::Invalid; }
// Debug
QDebug operator<<(QDebug dbg)
{
return dbg << "QGtk3Storage::Source(sourceType=" << sourceType << ")";
}
};
// Struct with key attributes to identify a brush: color group, color role and appearance
struct TargetBrush {
QPalette::ColorGroup colorGroup;
QPalette::ColorRole colorRole;
Qt::Appearance appearance;
// Generic constructor
TargetBrush(QPalette::ColorGroup group, QPalette::ColorRole role,
Qt::Appearance app = Qt::Appearance::Unknown) :
colorGroup(group), colorRole(role), appearance(app) {};
// Copy constructor with appearance modifier for dark/light aware search
TargetBrush(const TargetBrush &other, Qt::Appearance app) :
colorGroup(other.colorGroup), colorRole(other.colorRole), appearance(app) {};
// struct becomes key of a map, so operator< is needed
bool operator<(const TargetBrush& other) const {
return std::tie(colorGroup, colorRole, appearance) <
std::tie(other.colorGroup, other.colorRole, other.appearance);
}
};
// Mapping a palette's brushes to their GTK sources
typedef QFlatMap<TargetBrush, Source> BrushMap;
// Storage of palettes and their GTK sources
typedef QFlatMap<QPlatformTheme::Palette, BrushMap> PaletteMap;
// Public getters
const QPalette *palette(QPlatformTheme::Palette = QPlatformTheme::SystemPalette) const;
QPixmap standardPixmap(QPlatformTheme::StandardPixmap standardPixmap, const QSizeF &size) const;
Qt::Appearance appearance() const { return m_appearance; };
static QPalette standardPalette();
const QString themeName() const { return m_interface ? m_interface->themeName() : QString(); };
const QFont *font(QPlatformTheme::Font type) const;
QIcon fileIcon(const QFileInfo &fileInfo) const;
// Initialization
void populateMap();
void handleThemeChange();
private:
// Storage for palettes and their brushes
PaletteMap m_palettes;
std::unique_ptr<QGtk3Interface> m_interface;
Qt::Appearance m_appearance = Qt::Appearance::Unknown;
// Caches for Pixmaps, fonts and palettes
mutable QCache<QPlatformTheme::StandardPixmap, QImage> m_pixmapCache;
mutable std::array<std::optional<QPalette>, QPlatformTheme::Palette::NPalettes> m_paletteCache;
mutable std::array<std::optional<QFont>, QPlatformTheme::NFonts> m_fontCache;
// Search brush with a given GTK3 source
QBrush brush(const Source &source, const BrushMap &map) const;
// Get GTK3 source for a target brush
Source brush (const TargetBrush &brush, const BrushMap &map) const;
// clear cache, palettes and appearance
void clear();
// Data creation, import & export
void createMapping ();
const PaletteMap savePalettes() const;
bool save(const QString &filename, const QJsonDocument::JsonFormat f = QJsonDocument::Indented) const;
QJsonDocument save() const;
bool load(const QString &filename);
};
QT_END_NAMESPACE
#endif // QGTK3STORAGE_H

View File

@ -118,6 +118,8 @@ QGtk3Theme::QGtk3Theme()
qputenv("XCURSOR_THEME", cursorTheme.toUtf8());
}
}
m_storage.reset(new QGtk3Storage);
}
static inline QVariant gtkGetLongPressTime()
@ -175,6 +177,8 @@ QString QGtk3Theme::gtkFontName() const
Qt::Appearance QGtk3Theme::appearance() const
{
if (m_storage)
return m_storage->appearance();
/*
https://docs.gtk.org/gtk3/running.html
@ -189,9 +193,9 @@ Qt::Appearance QGtk3Theme::appearance() const
to override any other settings.
*/
QString themeName = qEnvironmentVariable("GTK_THEME");
const QRegularExpression darkRegex(QStringLiteral("[:-]dark"), QRegularExpression::CaseInsensitiveOption);
if (!themeName.isEmpty())
return darkRegex.match(themeName).hasMatch() ? Qt::Appearance::Dark : Qt::Appearance::Light;
return themeName.contains("dark"_L1, Qt::CaseInsensitive)
? Qt::Appearance::Dark : Qt::Appearance::Light;
/*
https://docs.gtk.org/gtk3/property.Settings.gtk-application-prefer-dark-theme.html
@ -209,7 +213,8 @@ Qt::Appearance QGtk3Theme::appearance() const
*/
themeName = gtkSetting("gtk-theme-name");
if (!themeName.isEmpty())
return darkRegex.match(themeName).hasMatch() ? Qt::Appearance::Dark : Qt::Appearance::Light;
return themeName.contains("dark"_L1, Qt::CaseInsensitive)
? Qt::Appearance::Dark : Qt::Appearance::Light;
return Qt::Appearance::Unknown;
}
@ -267,4 +272,25 @@ bool QGtk3Theme::useNativeFileDialog()
return gtk_check_version(3, 15, 5) == nullptr;
}
const QPalette *QGtk3Theme::palette(Palette type) const
{
return m_storage ? m_storage->palette(type) : QPlatformTheme::palette(type);
}
QPixmap QGtk3Theme::standardPixmap(StandardPixmap sp, const QSizeF &size) const
{
return m_storage ? m_storage->standardPixmap(sp, size) : QPlatformTheme::standardPixmap(sp, size);
}
const QFont *QGtk3Theme::font(Font type) const
{
return m_storage ? m_storage->font(type) : QGnomeTheme::font(type);
}
QIcon QGtk3Theme::fileIcon(const QFileInfo &fileInfo,
QPlatformTheme::IconOptions iconOptions) const
{
return m_storage ? m_storage->fileIcon(fileInfo) : QGnomeTheme::fileIcon(fileInfo, iconOptions);
}
QT_END_NAMESPACE

View File

@ -6,6 +6,7 @@
#include <private/qtguiglobal_p.h>
#include <private/qgenericunixthemes_p.h>
#include "qgtk3storage_p.h"
QT_BEGIN_NAMESPACE
@ -25,9 +26,16 @@ public:
QPlatformMenu* createPlatformMenu() const override;
QPlatformMenuItem* createPlatformMenuItem() const override;
const QPalette *palette(Palette type = SystemPalette) const override;
const QFont *font(Font type = SystemFont) const override;
QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override;
QIcon fileIcon(const QFileInfo &fileInfo,
QPlatformTheme::IconOptions iconOptions = { }) const override;
static const char *name;
private:
static bool useNativeFileDialog();
std::unique_ptr<QGtk3Storage> m_storage;
};
QT_END_NAMESPACE