diff options
Diffstat (limited to 'src/plugins/platformthemes')
20 files changed, 2815 insertions, 195 deletions
diff --git a/src/plugins/platformthemes/CMakeLists.txt b/src/plugins/platformthemes/CMakeLists.txt index a3c1f4fa9b..e5abcd1a11 100644 --- a/src/plugins/platformthemes/CMakeLists.txt +++ b/src/plugins/platformthemes/CMakeLists.txt @@ -1,4 +1,6 @@ -# Generated from platformthemes.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + if(QT_FEATURE_dbus AND QT_FEATURE_mimetype AND QT_FEATURE_regularexpression AND UNIX AND NOT APPLE) add_subdirectory(xdgdesktopportal) diff --git a/src/plugins/platformthemes/gtk3/CMakeLists.txt b/src/plugins/platformthemes/gtk3/CMakeLists.txt index 62e752bd92..6d3c7bf3a2 100644 --- a/src/plugins/platformthemes/gtk3/CMakeLists.txt +++ b/src/plugins/platformthemes/gtk3/CMakeLists.txt @@ -1,7 +1,11 @@ -# Generated from gtk3.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause -qt_find_package(GTK3) # special case -qt_find_package(X11) # special case +qt_find_package(GTK3) + +if(QT_FEATURE_xlib) + qt_find_package(X11) +endif() ##################################################################### ## QGtk3ThemePlugin Plugin: @@ -16,16 +20,29 @@ 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 + NO_PCH_SOURCES + qgtk3dialoghelpers.cpp # undef QT_NO_FOREACH DEFINES GDK_VERSION_MIN_REQUIRED=GDK_VERSION_3_6 - LIBRARIES # special case + LIBRARIES PkgConfig::GTK3 Qt::Core Qt::CorePrivate Qt::Gui Qt::GuiPrivate - X11::X11 # special case ) -#### Keys ignored in scope 1:.:.:gtk3.pro:<TRUE>: -# PLUGIN_EXTENDS = "-" +qt_internal_extend_target(QGtk3ThemePlugin CONDITION QT_FEATURE_dbus + SOURCES + qgtk3portalinterface.cpp + LIBRARIES + Qt::DBus +) + +qt_internal_extend_target(QGtk3ThemePlugin CONDITION QT_FEATURE_xlib + LIBRARIES + X11::X11 +) diff --git a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp index dac0f47d22..08419ec7dc 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.cpp @@ -1,6 +1,8 @@ // Copyright (C) 2016 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 +#undef QT_NO_FOREACH // this file contains unported legacy Q_FOREACH uses + #include "qgtk3dialoghelpers.h" #include "qgtk3theme.h" @@ -12,17 +14,26 @@ #include <qfileinfo.h> #include <private/qguiapplication_p.h> +#include <private/qgenericunixservices_p.h> +#include <qpa/qplatformintegration.h> #include <qpa/qplatformfontdatabase.h> #undef signals #include <gtk/gtk.h> #include <gdk/gdk.h> -#include <gdk/gdkx.h> #include <pango/pango.h> +#if QT_CONFIG(xlib) && defined(GDK_WINDOWING_X11) +#include <gdk/gdkx.h> +#endif + +#ifdef GDK_WINDOWING_WAYLAND +#include <gdk/gdkwayland.h> +#endif + // The size of the preview we display for selected image files. We set height // larger than width because generally there is more free space vertically -// than horiztonally (setting the preview image will alway expand the width of +// than horizontally (setting the preview image will always expand the width of // the dialog, but usually not the height). The image's aspect ratio will always // be preserved. #define PREVIEW_WIDTH 256 @@ -32,12 +43,10 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -class QGtk3Dialog : public QWindow +class QGtk3Dialog { - Q_OBJECT - public: - QGtk3Dialog(GtkWidget *gtkWidget); + QGtk3Dialog(GtkWidget *gtkWidget, QPlatformDialogHelper *helper); ~QGtk3Dialog(); GtkDialog *gtkDialog() const; @@ -46,23 +55,20 @@ public: bool show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent); void hide(); -Q_SIGNALS: - void accept(); - void reject(); - protected: - static void onResponse(QGtk3Dialog *dialog, int response); - -private slots: - void onParentWindowDestroyed(); + static void onResponse(QPlatformDialogHelper *helper, int response); private: GtkWidget *gtkWidget; + QPlatformDialogHelper *helper; + Qt::WindowModality modality; }; -QGtk3Dialog::QGtk3Dialog(GtkWidget *gtkWidget) : gtkWidget(gtkWidget) +QGtk3Dialog::QGtk3Dialog(GtkWidget *gtkWidget, QPlatformDialogHelper *helper) + : gtkWidget(gtkWidget) + , helper(helper) { - g_signal_connect_swapped(G_OBJECT(gtkWidget), "response", G_CALLBACK(onResponse), this); + g_signal_connect_swapped(G_OBJECT(gtkWidget), "response", G_CALLBACK(onResponse), helper); g_signal_connect(G_OBJECT(gtkWidget), "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); } @@ -79,43 +85,52 @@ GtkDialog *QGtk3Dialog::gtkDialog() const void QGtk3Dialog::exec() { - if (modality() == Qt::ApplicationModal) { + if (modality == Qt::ApplicationModal) { // block input to the whole app, including other GTK dialogs gtk_dialog_run(gtkDialog()); } else { // block input to the window, allow input to other GTK dialogs QEventLoop loop; - connect(this, SIGNAL(accept()), &loop, SLOT(quit())); - connect(this, SIGNAL(reject()), &loop, SLOT(quit())); + loop.connect(helper, SIGNAL(accept()), SLOT(quit())); + loop.connect(helper, SIGNAL(reject()), SLOT(quit())); loop.exec(); } } bool QGtk3Dialog::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWindow *parent) { - if (parent) { - connect(parent, &QWindow::destroyed, this, &QGtk3Dialog::onParentWindowDestroyed, - Qt::UniqueConnection); - } - setParent(parent); - setFlags(flags); - setModality(modality); + Q_UNUSED(flags); + this->modality = modality; gtk_widget_realize(gtkWidget); // creates X window GdkWindow *gdkWindow = gtk_widget_get_window(gtkWidget); if (parent) { - if (GDK_IS_X11_WINDOW(gdkWindow)) { + if (false) { +#if defined(GDK_WINDOWING_WAYLAND) && GTK_CHECK_VERSION(3, 22, 0) + } else if (GDK_IS_WAYLAND_WINDOW(gdkWindow)) { + const auto unixServices = dynamic_cast<QGenericUnixServices *>( + QGuiApplicationPrivate::platformIntegration()->services()); + if (unixServices) { + const auto handle = unixServices->portalWindowIdentifier(parent); + if (handle.startsWith("wayland:"_L1)) { + auto handleBa = handle.sliced(8).toUtf8(); + gdk_wayland_window_set_transient_for_exported(gdkWindow, handleBa.data()); + } + } +#endif +#if QT_CONFIG(xlib) && defined(GDK_WINDOWING_X11) + } else if (GDK_IS_X11_WINDOW(gdkWindow)) { GdkDisplay *gdkDisplay = gdk_window_get_display(gdkWindow); XSetTransientForHint(gdk_x11_display_get_xdisplay(gdkDisplay), gdk_x11_window_get_xid(gdkWindow), parent->winId()); +#endif } } if (modality != Qt::NonModal) { gdk_window_set_modal_hint(gdkWindow, true); - QGuiApplicationPrivate::showModalWindow(this); } gtk_widget_show(gtkWidget); @@ -125,30 +140,20 @@ bool QGtk3Dialog::show(Qt::WindowFlags flags, Qt::WindowModality modality, QWind void QGtk3Dialog::hide() { - QGuiApplicationPrivate::hideModalWindow(this); gtk_widget_hide(gtkWidget); } -void QGtk3Dialog::onResponse(QGtk3Dialog *dialog, int response) +void QGtk3Dialog::onResponse(QPlatformDialogHelper *helper, int response) { if (response == GTK_RESPONSE_OK) - emit dialog->accept(); + emit helper->accept(); else - emit dialog->reject(); -} - -void QGtk3Dialog::onParentWindowDestroyed() -{ - // The QGtk3*DialogHelper classes own this object. Make sure the parent doesn't delete it. - setParent(nullptr); + emit helper->reject(); } QGtk3ColorDialogHelper::QGtk3ColorDialogHelper() { - d.reset(new QGtk3Dialog(gtk_color_chooser_dialog_new("", nullptr))); - connect(d.data(), SIGNAL(accept()), this, SLOT(onAccepted())); - connect(d.data(), SIGNAL(reject()), this, SIGNAL(reject())); - + d.reset(new QGtk3Dialog(gtk_color_chooser_dialog_new("", nullptr), this)); g_signal_connect_swapped(d->gtkDialog(), "notify::rgba", G_CALLBACK(onColorChanged), this); } @@ -193,11 +198,6 @@ QColor QGtk3ColorDialogHelper::currentColor() const return QColor::fromRgbF(gdkColor.red, gdkColor.green, gdkColor.blue, gdkColor.alpha); } -void QGtk3ColorDialogHelper::onAccepted() -{ - emit accept(); -} - void QGtk3ColorDialogHelper::onColorChanged(QGtk3ColorDialogHelper *dialog) { emit dialog->currentColorChanged(dialog->currentColor()); @@ -217,10 +217,7 @@ QGtk3FileDialogHelper::QGtk3FileDialogHelper() GTK_FILE_CHOOSER_ACTION_OPEN, qUtf8Printable(QGtk3Theme::defaultStandardButtonText(QPlatformDialogHelper::Cancel)), GTK_RESPONSE_CANCEL, qUtf8Printable(QGtk3Theme::defaultStandardButtonText(QPlatformDialogHelper::Ok)), GTK_RESPONSE_OK, - NULL))); - - connect(d.data(), SIGNAL(accept()), this, SLOT(onAccepted())); - connect(d.data(), SIGNAL(reject()), this, SIGNAL(reject())); + NULL), this)); g_signal_connect(GTK_FILE_CHOOSER(d->gtkDialog()), "selection-changed", G_CALLBACK(onSelectionChanged), this); g_signal_connect_swapped(GTK_FILE_CHOOSER(d->gtkDialog()), "current-folder-changed", G_CALLBACK(onCurrentFolderChanged), this); @@ -343,11 +340,6 @@ QString QGtk3FileDialogHelper::selectedNameFilter() const return _filterNames.value(gtkFilter); } -void QGtk3FileDialogHelper::onAccepted() -{ - emit accept(); -} - void QGtk3FileDialogHelper::onSelectionChanged(GtkDialog *gtkDialog, QGtk3FileDialogHelper *helper) { QString selection; @@ -503,10 +495,7 @@ void QGtk3FileDialogHelper::setNameFilters(const QStringList &filters) QGtk3FontDialogHelper::QGtk3FontDialogHelper() { - d.reset(new QGtk3Dialog(gtk_font_chooser_dialog_new("", nullptr))); - connect(d.data(), SIGNAL(accept()), this, SLOT(onAccepted())); - connect(d.data(), SIGNAL(reject()), this, SIGNAL(reject())); - + d.reset(new QGtk3Dialog(gtk_font_chooser_dialog_new("", nullptr), this)); g_signal_connect_swapped(d->gtkDialog(), "notify::font", G_CALLBACK(onFontChanged), this); } @@ -610,11 +599,6 @@ QFont QGtk3FontDialogHelper::currentFont() const return font; } -void QGtk3FontDialogHelper::onAccepted() -{ - emit accept(); -} - void QGtk3FontDialogHelper::onFontChanged(QGtk3FontDialogHelper *dialog) { emit dialog->currentFontChanged(dialog->currentFont()); @@ -631,5 +615,3 @@ void QGtk3FontDialogHelper::applyOptions() QT_END_NAMESPACE #include "moc_qgtk3dialoghelpers.cpp" - -#include "qgtk3dialoghelpers.moc" diff --git a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h index e5c4c72539..89f48d8b01 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h +++ b/src/plugins/platformthemes/gtk3/qgtk3dialoghelpers.h @@ -35,9 +35,6 @@ public: void setCurrentColor(const QColor &color) override; QColor currentColor() const override; -private Q_SLOTS: - void onAccepted(); - private: static void onColorChanged(QGtk3ColorDialogHelper *helper); void applyOptions(); @@ -66,9 +63,6 @@ public: void selectNameFilter(const QString &filter) override; QString selectedNameFilter() const override; -private Q_SLOTS: - void onAccepted(); - private: static void onSelectionChanged(GtkDialog *dialog, QGtk3FileDialogHelper *helper); static void onCurrentFolderChanged(QGtk3FileDialogHelper *helper); @@ -102,9 +96,6 @@ public: void setCurrentFont(const QFont &font) override; QFont currentFont() const override; -private Q_SLOTS: - void onAccepted(); - private: static void onFontChanged(QGtk3FontDialogHelper *helper); void applyOptions(); diff --git a/src/plugins/platformthemes/gtk3/qgtk3interface.cpp b/src/plugins/platformthemes/gtk3/qgtk3interface.cpp new file mode 100644 index 0000000000..a35e211fbf --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3interface.cpp @@ -0,0 +1,702 @@ +// 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/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); +} + +/*! + \internal + \brief Converts a string into the GtkStateFlags enum. + + Converts a string formatted GTK color \param state into an enum value. + Returns an integer corresponding to GtkStateFlags. + Returns -1 if \param state does not correspond to a valid enum key. + */ +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 +} + +/*! + \internal + \brief Returns \param state converted into a string. + */ +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 +} + +/*! + \internal + \brief Populates the internal map used to find a GTK color's source and fallback generic color. + */ +void QGtk3Interface::initColorMap() +{ + #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."; +} + +/*! + \internal + \brief Returns a QImage corresponding to \param standardPixmap. + + A QImage (not a QPixmap) is returned so it can be cached and re-scaled in case the pixmap is + requested multiple times with different resolutions. + + \note Rather than defaulting to a QImage(), all QPlatformTheme::StandardPixmap enum values have + been mentioned explicitly. + That way they can be covered more easily in case additional icons are provided by GTK. + */ +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(); +} + +/*! + \internal + \brief Returns a QImage for a given GTK \param iconName. + */ +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); +} + +/*! + \internal + \brief Returns a QImage converted from the GDK pixel buffer \param buf. + + The ability to convert GdkPixbuf to QImage relies on the following assumptions: + \list + \li QImage uses uchar as a data container (unasserted) + \li the types guint8 and uchar are identical (statically asserted) + \li GDK pixel buffer uses 8 bits per sample (assumed at runtime) + \li GDK pixel buffer has 4 channels (assumed at runtime) + \endlist + */ +QImage QGtk3Interface::qt_convert_gdk_pixbuf(GdkPixbuf *buf) const +{ + if (!buf) + return QImage(); + + 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_ASSERT(gdk_pixbuf_get_bits_per_sample(buf) == 8); + Q_ASSERT(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_RGBA8888); + + // convert to more optimal format and detach to survive lifetime of buf + return converted.convertToFormat(QImage::Format_ARGB32_Premultiplied); +} + +/*! + \internal + \brief Instantiate a new GTK widget. + + Returns a pointer to a new GTK widget of \param type, allocated on the heap. + Returns nullptr of gtk_Default has is passed. + */ +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(); +} + +/*! + \internal + \brief Read a GTK widget's color from a generic color getter. + + This method returns a generic color of \param con, a given GTK style context. + The requested color is defined by \param def and the GTK color-state \param state. + The return type is GDK color in RGBA format. + */ +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 +} + +/*! + \internal + \brief Read a GTK widget's color from a property. + + Returns a color of GTK-widget \param widget, defined by \param source and \param state. + The return type is GDK color in RGBA format. + + \note If no corresponding property can be found for \param source, the method falls back to a + suitable generic color. + */ +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 +} + +/*! + \internal + \brief Get pointer to a GTK widget by \param type. + + Returns the pointer to a GTK widget, specified by \param type. + GTK widgets are cached, so that only one instance of each type is created. + \note + The method returns nullptr for the enum value gtk_Default. + */ +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; +} + +/*! + \internal + \brief Access a GTK widget's style context. + + Returns the pointer to the style context of GTK widget \param w. + + \note If \param w is nullptr, the GTK default style context (entry style) is returned. + */ +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)); +} + +/*! + \internal + \brief Create a QBrush from a GTK widget. + + Returns a QBrush corresponding to GTK widget type \param wtype, \param source and \param state. + + Brush height and width is ignored in GTK3, because brush assets (e.g. 9-patches) + can't be accessed by the GTK3 API. It's therefore unknown, if the brush relates only to colors, + or to a pixmap based style. + + */ +QBrush QGtk3Interface::brush(QGtkWidget wtype, QGtkColorSource source, GtkStateFlags state) const +{ + // FIXME: When a color's pixmap can be accessed via the GTK API, + // read it and set it in the brush. + return QBrush(color(widget(wtype), source, state)); +} + +/*! + \internal + \brief Returns the name of the current GTK theme. + */ +QString QGtk3Interface::themeName() const +{ + QString name; + + if (GtkSettings *settings = gtk_settings_get_default()) { + gchar *theme_name; + g_object_get(settings, "gtk-theme-name", &theme_name, nullptr); + name = QLatin1StringView(theme_name); + g_free(theme_name); + } + + return name; +} + +/*! + \internal + \brief Determine color scheme by colors. + + Returns the color scheme of the current GTK theme, heuristically determined by the + lightness difference between default background and foreground colors. + + \note Returns Unknown in the unlikely case that both colors have the same lightness. + */ +Qt::ColorScheme QGtk3Interface::colorSchemeByColors() const +{ + const QColor background = color(widget(QGtkWidget::gtk_Default), + QGtkColorSource::Background, + GTK_STATE_FLAG_ACTIVE); + const QColor foreground = color(widget(QGtkWidget::gtk_Default), + QGtkColorSource::Foreground, + GTK_STATE_FLAG_ACTIVE); + + if (foreground.lightness() > background.lightness()) + return Qt::ColorScheme::Dark; + if (foreground.lightness() < background.lightness()) + return Qt::ColorScheme::Light; + return Qt::ColorScheme::Unknown; +} + +/*! + \internal + \brief Map font type to GTK widget type. + + Returns the GTK widget type corresponding to the given QPlatformTheme::Font \param type. + */ +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_combo_box; + 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_Default; + 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(); +} + +/*! + \internal + \brief Convert pango \param style to QFont::Style. + */ +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(); +} + +/*! + \internal + \brief Convert pango font \param weight to an int, representing font weight in Qt. + + Compatibility of PangoWeight is statically asserted. + The minimum (1) and maximum (1000) weight in Qt is respeced. + */ +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); +} + +/*! + \internal + \brief Return a GTK styled font. + + Returns the QFont corresponding to \param type by reading the corresponding + GTK widget type's font. + + \note GTK allows to specify a non fixed font as the system's fixed font. + If a fixed font is requested, the method fixes the pitch and falls back to monospace, + unless a suitable fixed pitch font is found. + */ +QFont QGtk3Interface::font(QPlatformTheme::Font type) const +{ + GtkStyleContext *con = context(widget(toWidgetType(type))); + if (!con) + return QFont(); + + // explicitly add provider for fixed font + GtkCssProvider *cssProvider = nullptr; + if (type == QPlatformTheme::FixedFont) { + cssProvider = gtk_css_provider_new(); + gtk_style_context_add_class (con, GTK_STYLE_CLASS_MONOSPACE); + const char *fontSpec = "* {font-family: monospace;}"; + gtk_css_provider_load_from_data(cssProvider, fontSpec, -1, NULL); + gtk_style_context_add_provider(con, GTK_STYLE_PROVIDER(cssProvider), + GTK_STYLE_PROVIDER_PRIORITY_USER); + } + + // remove monospace provider from style context and unref it + QScopeGuard guard([&](){ + if (cssProvider) { + gtk_style_context_remove_provider(con, GTK_STYLE_PROVIDER(cssProvider)); + g_object_unref(cssProvider); + } + }); + + 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))); + + 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; +} + +/*! + \internal + \brief Returns a GTK styled file icon for \param fileInfo. + */ +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_ICON, + 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, 16, + GTK_ICON_LOOKUP_FORCE_SIZE); + if (!iconInfo) { + g_object_unref(file); + g_object_unref(info); + 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(buf); + return QIcon(QPixmap::fromImage(image)); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/gtk3/qgtk3interface_p.h b/src/plugins/platformthemes/gtk3/qgtk3interface_p.h new file mode 100644 index 0000000000..c43932a4fa --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3interface_p.h @@ -0,0 +1,211 @@ +// 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/QCache> +#include <private/qflatmap_p.h> +#include <QtCore/QObject> +#include <QtGui/QIcon> +#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; + +/*! + \internal + \brief The QGtk3Interface class centralizes communication with the GTK3 library. + + By encapsulating all GTK version specific syntax and conversions, it makes Qt's GTK theme + independent from GTK versions. + + \note + Including GTK3 headers requires #undef signals, which disables Qt signal/slot handling. + */ + +class QGtk3Interface +{ + Q_GADGET +public: + QGtk3Interface(QGtk3Storage *); + ~QGtk3Interface(); + + /*! + * \internal + \enum QGtk3Interface::QGtkWidget + \brief Represents GTK widget types used to obtain color information. + + \note The enum value gtk_Default refers to the GTK default style, rather than to a specific widget. + */ + 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) + + /*! + \internal + \enum QGtk3Interface::QGtkColorSource + \brief The QGtkColorSource enum represents the source of a color within a GTK widgets style context. + + If the current GTK theme provides such a color for a given widget, the color can be read + from the style context by passing the enum's key as a property name to the GTK method + gtk_style_context_lookup_color. The method will return false, if no color has been found. + */ + enum class QGtkColorSource { + Foreground, + Background, + Text, + Base, + Border + }; + Q_ENUM(QGtkColorSource) + + /*! + \internal + \enum QGtk3Interface::QGtkColorDefault + \brief The QGtkColorDefault enum represents generic GTK colors. + + The GTK3 methods gtk_style_context_get_color, gtk_style_context_get_background_color, and + gtk_style_context_get_foreground_color always return the respective colors with a widget's + style context. Unless set as a property by the current GTK theme, GTK's default colors will + be returned. + These generic default colors, represented by the GtkColorDefault enum, are used as a + back, if a specific color property is requested but not defined in the current GTK theme. + */ + 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 + QString themeName() const; + + // Derive color scheme from default colors + Qt::ColorScheme colorSchemeByColors() 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 diff --git a/src/plugins/platformthemes/gtk3/qgtk3json.cpp b/src/plugins/platformthemes/gtk3/qgtk3json.cpp new file mode 100644 index 0000000000..eb81e563be --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3json.cpp @@ -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::fromColorScheme(Qt::ColorScheme app) +{ + return QLatin1String(QMetaEnum::fromType<Qt::ColorScheme>().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::ColorScheme QGtk3Json::toColorScheme(const QString &colorScheme) +{ + CONVERT(Qt::ColorScheme, colorScheme, 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(ceColorScheme, fromColorScheme(tb.colorScheme)); + 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(ceColorScheme, fromColorScheme(s.rec.colorScheme)); + 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)) {\ + qCInfo(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()) {\ + qCInfo(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)) { + qCInfo(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) { + qCInfo(lcQGtk3Interface) << "Invalid Palette name:" << paletteName; + return false; + } + const QJsonObject &paletteObject = top[cePalettes][paletteName].toObject(); + const QStringList &brushList = paletteObject.keys(); + if (brushList.isEmpty()) { + qCInfo(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) { + qCInfo(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()) { + qCInfo(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, ceColorScheme); + const Qt::ColorScheme colorScheme = toColorScheme(value); + QGtk3Storage::TargetBrush tb(colorGroup, colorRole, colorScheme); + QGtk3Storage::Source s; + + if (!brushObject.contains(ceData) || !brushObject[ceData].isObject()) { + qCInfo(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)) { + qCInfo(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()) { + qCInfo(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, ceColorScheme); + const Qt::ColorScheme colorScheme = toColorScheme(value); + GETINT(sourceObject, ceLighter, lighter); + GETINT(sourceObject, ceRed, red); + GETINT(sourceObject, ceBlue, blue); + GETINT(sourceObject, ceGreen, green); + s = QGtk3Storage::Source(colorGroup, colorRole, colorScheme, + lighter, red, green, blue); + } + break; + + case QGtk3Storage::SourceType::Invalid: + qInfo(lcQGtk3Interface) << "Invalid source type for palette" << paletteName + << "Brush." << colorRoleName; + return false; + } + brushes.insert(tb, s); + } + } + map.insert(paletteType, brushes); + } + return true; +} + +QT_END_NAMESPACE + diff --git a/src/plugins/platformthemes/gtk3/qgtk3json_p.h b/src/plugins/platformthemes/gtk3/qgtk3json_p.h new file mode 100644 index 0000000000..daf280612c --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3json_p.h @@ -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 fromColorScheme(Qt::ColorScheme colorScheme); + + // 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::ColorScheme toColorScheme(const QString &colorScheme); + + // 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 ceColorScheme = "ColorScheme"_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 diff --git a/src/plugins/platformthemes/gtk3/qgtk3menu.cpp b/src/plugins/platformthemes/gtk3/qgtk3menu.cpp index 5ca50920a2..c4ea0e5e33 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3menu.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3menu.cpp @@ -49,6 +49,7 @@ QGtk3MenuItem::QGtk3MenuItem() m_checkable(false), m_checked(false), m_enabled(true), + m_exclusive(false), m_underline(false), m_invalid(true), m_menu(nullptr), @@ -122,13 +123,13 @@ static QString convertMnemonics(QString text, bool *found) { *found = false; - qsizetype i = text.length() - 1; + qsizetype i = text.size() - 1; while (i >= 0) { const QChar c = text.at(i); if (c == u'&') { if (i == 0 || text.at(i - 1) != u'&') { // convert Qt to GTK mnemonic - if (i < text.length() - 1 && !text.at(i + 1).isSpace()) { + if (i < text.size() - 1 && !text.at(i + 1).isSpace()) { text.replace(i, 1, u'_'); *found = true; } @@ -328,7 +329,7 @@ void QGtk3Menu::insertMenuItem(QPlatformMenuItem *item, QPlatformMenuItem *befor GtkWidget *handle = gitem->create(); int index = m_items.indexOf(static_cast<QGtk3MenuItem *>(before)); if (index < 0) - index = m_items.count(); + index = m_items.size(); m_items.insert(index, gitem); gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), handle, index); } diff --git a/src/plugins/platformthemes/gtk3/qgtk3portalinterface.cpp b/src/plugins/platformthemes/gtk3/qgtk3portalinterface.cpp new file mode 100644 index 0000000000..1ffdda74fa --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3portalinterface.cpp @@ -0,0 +1,123 @@ +// Copyright (C) 2024 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 "qgtk3portalinterface_p.h" +#include "qgtk3storage_p.h" + +#include <QtDBus/QDBusArgument> +#include <QtDBus/QDBusConnection> +#include <QtDBus/QDBusMessage> +#include <QtDBus/QDBusPendingCall> +#include <QtDBus/QDBusPendingCallWatcher> +#include <QtDBus/QDBusPendingReply> +#include <QtDBus/QDBusVariant> +#include <QtDBus/QtDBus> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcQGtk3PortalInterface, "qt.qpa.gtk"); + +using namespace Qt::StringLiterals; + +static constexpr QLatin1StringView appearanceInterface("org.freedesktop.appearance"); +static constexpr QLatin1StringView colorSchemeKey("color-scheme"); + +const QDBusArgument &operator>>(const QDBusArgument &argument, QMap<QString, QVariantMap> &map) +{ + argument.beginMap(); + map.clear(); + + while (!argument.atEnd()) { + QString key; + QVariantMap value; + argument.beginMapEntry(); + argument >> key >> value; + argument.endMapEntry(); + map.insert(key, value); + } + + argument.endMap(); + return argument; +} + +QGtk3PortalInterface::QGtk3PortalInterface(QGtk3Storage *s) + : m_storage(s) { + qRegisterMetaType<QDBusVariant>(); + qDBusRegisterMetaType<QMap<QString, QVariantMap>>(); + + queryColorScheme(); + + if (!s) { + qCDebug(lcQGtk3PortalInterface) << "QGtk3PortalInterface instantiated without QGtk3Storage." + << "No reaction to runtime theme changes."; + } +} + +Qt::ColorScheme QGtk3PortalInterface::colorScheme() const +{ + return m_colorScheme; +} + +void QGtk3PortalInterface::queryColorScheme() { + QDBusConnection connection = QDBusConnection::sessionBus(); + QDBusMessage message = QDBusMessage::createMethodCall( + "org.freedesktop.portal.Desktop"_L1, + "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Settings"_L1, "ReadAll"_L1); + message << QStringList{ appearanceInterface }; + + QDBusPendingCall pendingCall = connection.asyncCall(message); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall, this); + QObject::connect( + watcher, &QDBusPendingCallWatcher::finished, this, + [this](QDBusPendingCallWatcher *watcher) { + QDBusPendingReply<QMap<QString, QVariantMap>> reply = *watcher; + if (reply.isValid()) { + QMap<QString, QVariantMap> settings = reply.value(); + if (!settings.isEmpty()) { + settingChanged(appearanceInterface, colorSchemeKey, + QDBusVariant(settings.value(appearanceInterface).value(colorSchemeKey))); + } + } else { + qCDebug(lcQGtk3PortalInterface) << "Failed to query org.freedesktop.portal.Settings: " + << reply.error().message(); + } + watcher->deleteLater(); + }); + + QDBusConnection::sessionBus().connect( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Settings"_L1, "SettingChanged"_L1, this, + SLOT(settingChanged(QString, QString, QDBusVariant))); +} + +void QGtk3PortalInterface::settingChanged(const QString &group, const QString &key, + const QDBusVariant &value) +{ + if (group == appearanceInterface && key == colorSchemeKey) { + const uint colorScheme = value.variant().toUInt(); + // From org.freedesktop.portal.Settings.xml + // "1" - Prefer dark appearance + Qt::ColorScheme newColorScheme = colorScheme == 1 ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light; + if (m_colorScheme != newColorScheme) { + m_colorScheme = newColorScheme; + if (m_storage) + m_storage->handleThemeChange(); + } + } +} + +QT_END_NAMESPACE + +#include "moc_qgtk3portalinterface_p.cpp" diff --git a/src/plugins/platformthemes/gtk3/qgtk3portalinterface_p.h b/src/plugins/platformthemes/gtk3/qgtk3portalinterface_p.h new file mode 100644 index 0000000000..25a5f58ab1 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3portalinterface_p.h @@ -0,0 +1,49 @@ +// Copyright (C) 2024 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 QGTK3PORTALINTERFACE_H +#define QGTK3PORTALINTERFACE_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/QObject> +#include <QtCore/QLoggingCategory> + +QT_BEGIN_NAMESPACE + +class QDBusVariant; +class QGtk3Storage; + +Q_DECLARE_LOGGING_CATEGORY(lcQGtk3PortalInterface); + +class QGtk3PortalInterface : public QObject +{ + Q_OBJECT +public: + QGtk3PortalInterface(QGtk3Storage *s); + ~QGtk3PortalInterface() = default; + + Qt::ColorScheme colorScheme() const; + +private Q_SLOTS: + void settingChanged(const QString &group, const QString &key, + const QDBusVariant &value); +private: + void queryColorScheme(); + + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::Unknown; + QGtk3Storage *m_storage = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QGTK3PORTALINTERFACE_H diff --git a/src/plugins/platformthemes/gtk3/qgtk3storage.cpp b/src/plugins/platformthemes/gtk3/qgtk3storage.cpp new file mode 100644 index 0000000000..2877b28590 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3storage.cpp @@ -0,0 +1,686 @@ +// 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)); +#if QT_CONFIG(dbus) + m_portalInterface.reset(new QGtk3PortalInterface(this)); +#endif + populateMap(); +} + +/*! + \internal + \enum QGtk3Storage::SourceType + \brief This enum represents the type of a color source. + + \value Gtk Color is read from a GTK widget + \value Fixed A fixed brush is specified + \value Modified The color is a modification of another color (fixed or read from GTK) + \omitvalue Invalid + */ + +/*! + \internal + \brief Find a brush from a source. + + Returns a QBrush from a given \param source and a \param map of available brushes + to search from. + + A null QBrush is returned, if no brush corresponding to the source has been found. + */ +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.colorScheme), 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(); +} + +/*! + \internal + \brief Recurse to find a source brush for modification. + + Returns the source specified by the target brush \param b in the \param map of brushes. + Takes dark/light/unknown into consideration. + Returns an empty brush if no suitable one can be found. + */ +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 color scheme can find anything + if (b.colorScheme == Qt::ColorScheme::Unknown) { + FIND(TargetBrush(b, Qt::ColorScheme::Dark)); + FIND(TargetBrush(b, Qt::ColorScheme::Light)); + } + + // Color group All can always be found + if (b.colorGroup != QPalette::All) + return brush(TargetBrush(QPalette::All, b.colorRole, b.colorScheme), map); + + // Brush not found + return Source(); +#undef FIND +} + +/*! + \internal + \brief Returns a simple, hard coded base palette. + + Create a hard coded palette with default colors as a fallback for any color that can't be + obtained from GTK. + + \note This palette will be used as a default baseline for the system palette, which then + will be used as a default baseline for any other palette type. + */ +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; +} + +/*! + \internal + \brief Return a GTK styled QPalette. + + Returns the pointer to a (cached) QPalette for \param type, with its brushes + populated 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 color scheme match + // - or either of them is unknown + const auto appSource = i.key().colorScheme; + const auto appTheme = colorScheme(); + const bool setBrush = (appSource == appTheme) || + (appSource == Qt::ColorScheme::Unknown) || + (appTheme == Qt::ColorScheme::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() << colorScheme() << p; + + return &m_paletteCache[type].value(); +} + +/*! + \internal + \brief Return a GTK styled font. + + Returns a QFont of \param type, styled according to the current GTK theme. +*/ +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(); +} + +/*! + \internal + \brief Return a GTK styled standard pixmap if available. + + Returns a pixmap specified by \param standardPixmap and \param size. + Returns an empty pixmap if GTK doesn't support the requested one. + */ +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())); +} + +/*! + \internal + \brief Returns a GTK styled file icon corresponding to \param fileInfo. + */ +QIcon QGtk3Storage::fileIcon(const QFileInfo &fileInfo) const +{ + return m_interface ? m_interface->fileIcon(fileInfo) : QIcon(); +} + +/*! + \internal + \brief Clears all caches. + */ +void QGtk3Storage::clear() +{ + m_colorScheme = Qt::ColorScheme::Unknown; + m_palettes.clear(); + for (auto &cache : m_paletteCache) + cache.reset(); + + for (auto &cache : m_fontCache) + cache.reset(); +} + +/*! + \internal + \brief Handles a theme change at runtime. + + Clear all caches, re-populate with current GTK theme and notify the window system interface. + This method is a callback for the theme change signal sent from GTK. + */ +void QGtk3Storage::handleThemeChange() +{ + populateMap(); + QWindowSystemInterface::handleThemeChange(); +} + +/*! + \internal + \brief Populates a map with information about how to locate colors in GTK. + + This method creates a data structure to locate color information for each brush of a QPalette + within GTK. The structure can hold mapping information for each QPlatformTheme::Palette + enum value. If no specific mapping is stored for an enum value, the system palette is returned + instead of a specific one. If no mapping is stored for the system palette, it will fall back to + QGtk3Storage::standardPalette. + + The method will populate the data structure with a standard mapping, covering the following + palette types: + \list + \li QPlatformTheme::SystemPalette + \li QPlatformTheme::CheckBoxPalette + \li QPlatformTheme::RadioButtonPalette + \li QPlatformTheme::ComboBoxPalette + \li QPlatformTheme::GroupBoxPalette + \li QPlatformTheme::MenuPalette + \li QPlatformTheme::TextLineEditPalette + \endlist + + The method will check the environment variable {{QT_GUI_GTK_JSON_SAVE}}. If it points to a + valid path with write access, it will write the standard mapping into a Json file. + That Json file can be modified and/or extended. + The Json syntax is + - "QGtk3Palettes" (top level value) + - QPlatformTheme::Palette + - QPalette::ColorRole + - Qt::ColorScheme + - Qt::ColorGroup + - Source data + - Source Type + - [source data] + + If the environment variable {{QT_GUI_GTK_JSON_HARDCODED}} contains the keyword \c true, + all sources are converted to fixed sources. In that case, they contain the hard coded HexRGBA + values read from GTK. + + The method will also check the environment variable {{QT_GUI_GTK_JSON}}. If it points to a valid + Json file with read access, it will be parsed instead of creating a standard mapping. + Parsing errors will be printed out with qCInfo if the logging category {{qt.qpa.gtk}} is activated. + In case of a parsing error, the method will fall back to creating a standard mapping. + + \note + If a Json file contains only fixed brushes (e.g. exported with {{QT_GUI_GTK_JSON_HARDCODED=true}}), + no colors will be imported from GTK. + */ +void QGtk3Storage::populateMap() +{ + static QString m_themeName; + + // Distiguish initialization, theme change or call without theme change + Qt::ColorScheme newColorScheme = Qt::ColorScheme::Unknown; + const QString newThemeName = themeName(); + +#if QT_CONFIG(dbus) + // Prefer color scheme we get from xdg-desktop-portal as this is what GNOME + // relies on these days + newColorScheme = m_portalInterface->colorScheme(); +#endif + + if (newColorScheme == Qt::ColorScheme::Unknown) { + // Derive color scheme from theme name + newColorScheme = newThemeName.contains("dark"_L1, Qt::CaseInsensitive) + ? Qt::ColorScheme::Dark : m_interface->colorSchemeByColors(); + } + + if (m_themeName == newThemeName && m_colorScheme == newColorScheme) + return; + + clear(); + + if (m_themeName.isEmpty()) { + qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << newColorScheme; + } else { + qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << newColorScheme; + } + m_colorScheme = newColorScheme; + 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"; +} + +/*! + \internal + \brief Return a palette map for saving. + + This method returns the existing palette map, if the environment variable + {{QT_GUI_GTK_JSON_HARDCODED}} is not set or does not contain the keyword \c true. + If it contains the keyword \c true, it returns a palette map with all brush + sources converted to fixed sources. + */ +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; +} + +/*! + \internal + \brief Saves current palette mapping to a \param filename with Json format \param f. + + Saves the current palette mapping into a QJson file, + taking {{QT_GUI_GTK_JSON_HARDCODED}} into consideration. + Returns \c true if saving was successful and \c false otherwise. + */ +bool QGtk3Storage::save(const QString &filename, QJsonDocument::JsonFormat f) const +{ + return QGtk3Json::save(savePalettes(), filename, f); +} + +/*! + \internal + \brief Returns a QJsonDocument with current palette mapping. + + Saves the current palette mapping into a QJsonDocument, + taking {{QT_GUI_GTK_JSON_HARDCODED}} into consideration. + Returns \c true if saving was successful and \c false otherwise. + */ +QJsonDocument QGtk3Storage::save() const +{ + return QGtk3Json::save(savePalettes()); +} + +/*! + \internal + \brief Loads palette mapping from Json file \param filename. + + Returns \c true if the file was successfully parsed and \c false otherwise. + */ +bool QGtk3Storage::load(const QString &filename) +{ + return QGtk3Json::load(m_palettes, filename); +} + +/*! + \internal + \brief Creates a standard palette mapping. + + The method creates a hard coded standard mapping, used if no external Json file + containing a valid mapping has been specified in the environment variable {{QT_GUI_GTK_JSON}}. + */ +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::ColorScheme::Unknown, lighter) +#define MODIFY(group, role, red, green, blue)\ + source = Source(QPalette::group, QPalette::role,\ + Qt::ColorScheme::Unknown, red, green, blue) + + // Define fixed source +#define FIX(color) source = FixedSource(color); + + // Add the source to a target brush + // Use default Qt::ColorScheme::Unknown, if no color scheme 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::ColorScheme::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 usage: + + 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, ColorScheme) to use the defined source + only for a specific color scheme + + 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(All, Window); + ADD(All, Button); + ADD(All, Base); + LIGHTER(Normal, Window, 125); + ADD(Normal, Light); + ADD(Inactive, Light); + LIGHTER(Normal, Window, 70); + ADD(Normal, Shadow); + LIGHTER(Normal, Window, 80); + ADD(Normal, Dark); + ADD(Inactive, Dark) + + GTK(button, Foreground, ACTIVE); + ADD(Inactive, WindowText); + LIGHTER(Normal, WindowText, 50); + ADD(Disabled, Text); + ADD(Disabled, WindowText); + ADD(Disabled, ButtonText); + + GTK(button, Text, NORMAL); + ADD(Inactive, ButtonText); + + // special background colors + GTK(Default, Background, SELECTED); + ADD(Disabled, Highlight); + ADD(Normal, Highlight); + ADD(Inactive, Highlight); + + GTK(entry, Foreground, SELECTED); + ADD(Normal, HighlightedText); + ADD(Inactive, HighlightedText); + + // text color and friends + GTK(entry, Text, NORMAL); + ADD(Normal, ButtonText); + ADD(Normal, 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); + MODIFY(Normal, Text, 100, 100, 100); + ADD(All, PlaceholderText, Light); + MODIFY(Normal, Text, -100, -100, -100); + ADD(All, PlaceholderText, Dark); + + // Light, midlight, dark, mid, shadow colors + LIGHTER(Normal, Button, 125); + ADD(All, Light) + LIGHTER(Normal, Button, 113); + ADD(All, Midlight) + LIGHTER(Normal, Button, 113); + ADD(All, Mid) + LIGHTER(Normal, Button, 87); + ADD(All, Dark) + LIGHTER(Normal, Button, 5); + ADD(All, Shadow) + + SAVE(SystemPalette); + CLEAR; + } + + // Label and TabBar Palette + { + GTK(entry, Text, NORMAL); + ADD(Normal, WindowText); + ADD(Inactive, WindowText); + + SAVE(LabelPalette); + SAVE(TabBarPalette); + CLEAR; + } + + // Checkbox and RadioButton Palette + { + GTK(button, Text, ACTIVE); + ADD(Normal, Base, Dark); + ADD(Inactive, WindowText, Dark); + + GTK(Default, Foreground, NORMAL); + ADD(All, Text); + + GTK(Default, Background, NORMAL); + ADD(All, Base); + + GTK(button, Text, NORMAL); + ADD(Normal, Base, Light); + ADD(Inactive, WindowText, Light); + + SAVE(CheckBoxPalette); + SAVE(RadioButtonPalette); + CLEAR; + } + + // ComboBox, GroupBox & Frame Palette + { + GTK(combo_box, Text, NORMAL); + ADD(Normal, ButtonText, Dark); + ADD(Normal, Text, Dark); + ADD(Inactive, WindowText, Dark); + + GTK(combo_box, Text, ACTIVE); + ADD(Normal, ButtonText, Light); + ADD(Normal, Text, Light); + ADD(Inactive, WindowText, Light); + + SAVE(ComboBoxPalette); + SAVE(GroupBoxPalette); + CLEAR; + } + + // MenuBar Palette + { + GTK(Default, Text, ACTIVE); + ADD(Normal, ButtonText); + SAVE(MenuPalette); + CLEAR; + } + + // LineEdit Palette + { + 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 diff --git a/src/plugins/platformthemes/gtk3/qgtk3storage_p.h b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h new file mode 100644 index 0000000000..45192263a9 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h @@ -0,0 +1,240 @@ +// 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" +#if QT_CONFIG(dbus) +#include "qgtk3portalinterface_p.h" +#endif + +#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 documented in cpp file. Please keep it in line with updates made here. + 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::ColorScheme colorScheme; + 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 << ", colorScheme=" << colorScheme << ", 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::ColorScheme scheme, int p_lighter = 100) + : sourceType(SourceType::Modified) + { + rec.colorGroup = group; + rec.colorRole = role; + rec.colorScheme = scheme; + rec.lighter = p_lighter; + } + + // Recursive ocnstructor for color modification + Source(QPalette::ColorGroup group, QPalette::ColorRole role, + Qt::ColorScheme scheme, int p_red, int p_green, int p_blue) + : sourceType(SourceType::Modified) + { + rec.colorGroup = group; + rec.colorRole = role; + rec.colorScheme = scheme; + 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::ColorScheme scheme, int p_lighter, + int p_red, int p_green, int p_blue) : sourceType(SourceType::Modified) + { + rec.colorGroup = group; + rec.colorRole = role; + rec.colorScheme = scheme; + 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 color scheme + struct TargetBrush { + QPalette::ColorGroup colorGroup; + QPalette::ColorRole colorRole; + Qt::ColorScheme colorScheme; + + // Generic constructor + TargetBrush(QPalette::ColorGroup group, QPalette::ColorRole role, + Qt::ColorScheme scheme = Qt::ColorScheme::Unknown) : + colorGroup(group), colorRole(role), colorScheme(scheme) {}; + + // Copy constructor with color scheme modifier for dark/light aware search + TargetBrush(const TargetBrush &other, Qt::ColorScheme scheme) : + colorGroup(other.colorGroup), colorRole(other.colorRole), colorScheme(scheme) {}; + + // struct becomes key of a map, so operator< is needed + bool operator<(const TargetBrush& other) const { + return std::tie(colorGroup, colorRole, colorScheme) < + std::tie(other.colorGroup, other.colorRole, other.colorScheme); + } + }; + + // 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::ColorScheme colorScheme() const { return m_colorScheme; }; + 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; +#if QT_CONFIG(dbus) + std::unique_ptr<QGtk3PortalInterface> m_portalInterface; +#endif + + Qt::ColorScheme m_colorScheme = Qt::ColorScheme::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 color scheme + 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 diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp index 22a079732a..9d23ba7e48 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp @@ -5,15 +5,20 @@ #include "qgtk3dialoghelpers.h" #include "qgtk3menu.h" #include <QVariant> -#include <QtCore/qregularexpression.h> +#include <QGuiApplication> +#include <qpa/qwindowsysteminterface.h> #undef signals #include <gtk/gtk.h> +#if QT_CONFIG(xcb_xlib) #include <X11/Xlib.h> +#endif QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + const char *QGtk3Theme::name = "gtk3"; template <typename T> @@ -49,13 +54,25 @@ void gtkMessageHandler(const gchar *log_domain, QGtk3Theme::QGtk3Theme() { + // Ensure gtk uses the same windowing system, but let it + // fallback in case GDK_BACKEND environment variable + // filters the preferred one out + if (QGuiApplication::platformName().startsWith("wayland"_L1)) + gdk_set_allowed_backends("wayland,x11"); + else if (QGuiApplication::platformName() == "xcb"_L1) + gdk_set_allowed_backends("x11,wayland"); + +#if QT_CONFIG(xcb_xlib) // gtk_init will reset the Xlib error handler, and that causes // Qt applications to quit on X errors. Therefore, we need to manually restore it. int (*oldErrorHandler)(Display *, XErrorEvent *) = XSetErrorHandler(nullptr); +#endif gtk_init(nullptr, nullptr); +#if QT_CONFIG(xcb_xlib) XSetErrorHandler(oldErrorHandler); +#endif /* Initialize some types here so that Gtk+ does not crash when reading * the treemodel for GtkFontChooser. @@ -65,6 +82,29 @@ QGtk3Theme::QGtk3Theme() /* Use our custom log handler. */ g_log_set_handler("Gtk", G_LOG_LEVEL_MESSAGE, gtkMessageHandler, nullptr); + +#define SETTING_CONNECT(setting) g_signal_connect(settings, "notify::" setting, G_CALLBACK(notifyThemeChanged), nullptr) + auto notifyThemeChanged = [] { + QWindowSystemInterface::handleThemeChange(); + }; + + GtkSettings *settings = gtk_settings_get_default(); + SETTING_CONNECT("gtk-cursor-blink-time"); + SETTING_CONNECT("gtk-double-click-distance"); + SETTING_CONNECT("gtk-double-click-time"); + SETTING_CONNECT("gtk-long-press-time"); + SETTING_CONNECT("gtk-entry-password-hint-timeout"); + SETTING_CONNECT("gtk-dnd-drag-threshold"); + SETTING_CONNECT("gtk-icon-theme-name"); + SETTING_CONNECT("gtk-fallback-icon-theme"); + SETTING_CONNECT("gtk-font-name"); + SETTING_CONNECT("gtk-application-prefer-dark-theme"); + SETTING_CONNECT("gtk-theme-name"); + SETTING_CONNECT("gtk-cursor-theme-name"); + SETTING_CONNECT("gtk-cursor-theme-size"); +#undef SETTING_CONNECT + + m_storage.reset(new QGtk3Storage); } static inline QVariant gtkGetLongPressTime() @@ -99,8 +139,14 @@ QVariant QGtk3Theme::themeHint(QPlatformTheme::ThemeHint hint) const return QVariant(gtkSetting("gtk-icon-theme-name")); case QPlatformTheme::SystemIconFallbackThemeName: return QVariant(gtkSetting("gtk-fallback-icon-theme")); - case QPlatformTheme::PreselectFirstFileInDirectory: - return true; + case QPlatformTheme::MouseCursorTheme: + return QVariant(gtkSetting("gtk-cursor-theme-name")); + case QPlatformTheme::MouseCursorSize: { + int s = gtkSetting<gint>("gtk-cursor-theme-size"); + if (s > 0) + return QVariant(QSize(s, s)); + return QGnomeTheme::themeHint(hint); + } default: return QGnomeTheme::themeHint(hint); } @@ -114,45 +160,10 @@ QString QGtk3Theme::gtkFontName() const return QGnomeTheme::gtkFontName(); } -QPlatformTheme::Appearance QGtk3Theme::appearance() const +Qt::ColorScheme QGtk3Theme::colorScheme() const { - /* - https://docs.gtk.org/gtk3/running.html - - It's possible to set a theme variant after the theme name when using GTK_THEME: - - GTK_THEME=Adwaita:dark - - Some themes also have "-dark" as part of their name. - - We test this environment variable first because the documentation says - it's mainly used for easy debugging, so it should be possible to use it - 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() ? Appearance::Dark : Appearance::Light; - - /* - https://docs.gtk.org/gtk3/property.Settings.gtk-application-prefer-dark-theme.html - - This setting controls which theme is used when the theme specified by - gtk-theme-name provides both light and dark variants. We can save a - regex check by testing this property first. - */ - const auto preferDark = gtkSetting<bool>("gtk-application-prefer-dark-theme"); - if (preferDark) - return Appearance::Dark; - - /* - https://docs.gtk.org/gtk3/property.Settings.gtk-theme-name.html - */ - themeName = gtkSetting("gtk-theme-name"); - if (!themeName.isEmpty()) - return darkRegex.match(themeName).hasMatch() ? Appearance::Dark : Appearance::Light; - - return Appearance::Unknown; + Q_ASSERT(m_storage); + return m_storage->colorScheme(); } bool QGtk3Theme::usePlatformNativeDialog(DialogType type) const @@ -208,4 +219,30 @@ bool QGtk3Theme::useNativeFileDialog() return gtk_check_version(3, 15, 5) == nullptr; } +const QPalette *QGtk3Theme::palette(Palette type) const +{ + Q_ASSERT(m_storage); + return m_storage->palette(type); +} + +QPixmap QGtk3Theme::standardPixmap(StandardPixmap sp, const QSizeF &size) const +{ + Q_ASSERT(m_storage); + return m_storage->standardPixmap(sp, size); +} + +const QFont *QGtk3Theme::font(Font type) const +{ + Q_ASSERT(m_storage); + return m_storage->font(type); +} + +QIcon QGtk3Theme::fileIcon(const QFileInfo &fileInfo, + QPlatformTheme::IconOptions iconOptions) const +{ + Q_UNUSED(iconOptions); + Q_ASSERT(m_storage); + return m_storage->fileIcon(fileInfo); +} + QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.h b/src/plugins/platformthemes/gtk3/qgtk3theme.h index 0f274234d5..2828cc56e6 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.h +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.h @@ -4,7 +4,9 @@ #ifndef QGTK3THEME_H #define QGTK3THEME_H +#include <private/qtguiglobal_p.h> #include <private/qgenericunixthemes_p.h> +#include "qgtk3storage_p.h" QT_BEGIN_NAMESPACE @@ -16,7 +18,7 @@ public: virtual QVariant themeHint(ThemeHint hint) const override; virtual QString gtkFontName() const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; @@ -24,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 diff --git a/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt b/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt index 82fb94e31d..6228e83ec7 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt +++ b/src/plugins/platformthemes/xdgdesktopportal/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from xdgdesktopportal.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QXdgDesktopPortalThemePlugin Plugin: @@ -19,6 +20,3 @@ qt_internal_add_plugin(QXdgDesktopPortalThemePlugin Qt::Gui Qt::GuiPrivate ) - -#### Keys ignored in scope 1:.:.:xdgdesktopportal.pro:<TRUE>: -# PLUGIN_EXTENDS = "-" diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp index 55eb0b5aff..1c162be8fc 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog.cpp @@ -3,6 +3,10 @@ #include "qxdgdesktopportalfiledialog_p.h" +#include <private/qgenericunixservices_p.h> +#include <private/qguiapplication_p.h> +#include <qpa/qplatformintegration.h> + #include <QDBusConnection> #include <QDBusMessage> #include <QDBusPendingCall> @@ -69,15 +73,12 @@ const QDBusArgument &operator >>(const QDBusArgument &arg, QXdgDesktopPortalFile class QXdgDesktopPortalFileDialogPrivate { public: - QXdgDesktopPortalFileDialogPrivate(QPlatformFileDialogHelper *nativeFileDialog) + QXdgDesktopPortalFileDialogPrivate(QPlatformFileDialogHelper *nativeFileDialog, uint fileChooserPortalVersion) : nativeFileDialog(nativeFileDialog) + , fileChooserPortalVersion(fileChooserPortalVersion) { } - WId winId = 0; - bool directoryMode = false; - bool modal = false; - bool multipleFiles = false; - bool saveFile = false; + QEventLoop loop; QString acceptLabel; QString directory; QString title; @@ -88,19 +89,27 @@ public: QString selectedMimeTypeFilter; QString selectedNameFilter; QStringList selectedFiles; - QPlatformFileDialogHelper *nativeFileDialog = nullptr; + std::unique_ptr<QPlatformFileDialogHelper> nativeFileDialog; + uint fileChooserPortalVersion = 0; + bool failedToOpen = false; + bool directoryMode = false; + bool multipleFiles = false; + bool saveFile = false; }; -QXdgDesktopPortalFileDialog::QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper *nativeFileDialog) +QXdgDesktopPortalFileDialog::QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper *nativeFileDialog, uint fileChooserPortalVersion) : QPlatformFileDialogHelper() - , d_ptr(new QXdgDesktopPortalFileDialogPrivate(nativeFileDialog)) + , d_ptr(new QXdgDesktopPortalFileDialogPrivate(nativeFileDialog, fileChooserPortalVersion)) { Q_D(QXdgDesktopPortalFileDialog); if (d->nativeFileDialog) { - connect(d->nativeFileDialog, SIGNAL(accept()), this, SIGNAL(accept())); - connect(d->nativeFileDialog, SIGNAL(reject()), this, SIGNAL(reject())); + connect(d->nativeFileDialog.get(), SIGNAL(accept()), this, SIGNAL(accept())); + connect(d->nativeFileDialog.get(), SIGNAL(reject()), this, SIGNAL(reject())); } + + d->loop.connect(this, SIGNAL(accept()), SLOT(quit())); + d->loop.connect(this, SIGNAL(reject()), SLOT(quit())); } QXdgDesktopPortalFileDialog::~QXdgDesktopPortalFileDialog() @@ -144,7 +153,7 @@ void QXdgDesktopPortalFileDialog::initializeDialog() setDirectory(options()->initialDirectory()); } -void QXdgDesktopPortalFileDialog::openPortal() +void QXdgDesktopPortalFileDialog::openPortal(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent) { Q_D(QXdgDesktopPortalFileDialog); @@ -152,26 +161,26 @@ void QXdgDesktopPortalFileDialog::openPortal() "/org/freedesktop/portal/desktop"_L1, "org.freedesktop.portal.FileChooser"_L1, d->saveFile ? "SaveFile"_L1 : "OpenFile"_L1); - QString parentWindowId = "x11:"_L1 + QString::number(d->winId, 16); - QVariantMap options; if (!d->acceptLabel.isEmpty()) options.insert("accept_label"_L1, d->acceptLabel); - options.insert("modal"_L1, d->modal); + options.insert("modal"_L1, windowModality != Qt::NonModal); options.insert("multiple"_L1, d->multipleFiles); options.insert("directory"_L1, d->directoryMode); - if (d->saveFile) { - if (!d->directory.isEmpty()) - options.insert("current_folder"_L1, QFile::encodeName(d->directory).append('\0')); - - if (!d->selectedFiles.isEmpty()) { - // current_file for the file to be pre-selected, current_name for the file name to be pre-filled - // current_file accepts absolute path while current_name accepts just file name - options.insert("current_file"_L1, QFile::encodeName(d->selectedFiles.first()).append('\0')); - options.insert("current_name"_L1, QFileInfo(d->selectedFiles.first()).fileName()); - } + if (!d->directory.isEmpty()) + options.insert("current_folder"_L1, QFile::encodeName(d->directory).append('\0')); + + if (d->saveFile && !d->selectedFiles.isEmpty()) { + // current_file for the file to be pre-selected, current_name for the file name to be + // pre-filled current_file accepts absolute path and requires the file to exist while + // current_name accepts just file name + QFileInfo selectedFileInfo(d->selectedFiles.constFirst()); + if (selectedFileInfo.exists()) + options.insert("current_file"_L1, + QFile::encodeName(d->selectedFiles.constFirst()).append('\0')); + options.insert("current_name"_L1, selectedFileInfo.fileName()); } // Insert filters @@ -204,6 +213,9 @@ void QXdgDesktopPortalFileDialog::openPortal() filter.name = mimeType.comment(); filter.filterConditions = filterConditions; + if (filter.name.isEmpty()) + filter.name = mimeTypefilter; + filterList << filter; if (!d->selectedMimeTypeFilter.isEmpty() && d->selectedMimeTypeFilter == mimeTypefilter) @@ -257,14 +269,29 @@ void QXdgDesktopPortalFileDialog::openPortal() // TODO choices a(ssa(ss)s) // List of serialized combo boxes to add to the file chooser. - message << parentWindowId << d->title << options; + auto unixServices = dynamic_cast<QGenericUnixServices *>( + QGuiApplicationPrivate::platformIntegration()->services()); + if (parent && unixServices) + message << unixServices->portalWindowIdentifier(parent); + else + message << QString(); + + message << d->title << options; QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall); - connect(watcher, &QDBusPendingCallWatcher::finished, this, [this] (QDBusPendingCallWatcher *watcher) { + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, d, windowFlags, windowModality, parent] (QDBusPendingCallWatcher *watcher) { QDBusPendingReply<QDBusObjectPath> reply = *watcher; - if (reply.isError()) { - Q_EMIT reject(); + // Any error means the dialog is not shown and we need to fallback + d->failedToOpen = reply.isError(); + if (d->failedToOpen) { + if (d->nativeFileDialog) { + d->nativeFileDialog->show(windowFlags, windowModality, parent); + if (d->loop.isRunning()) + d->nativeFileDialog->exec(); + } else { + Q_EMIT reject(); + } } else { QDBusConnection::sessionBus().connect(nullptr, reply.value().path(), @@ -298,7 +325,7 @@ QUrl QXdgDesktopPortalFileDialog::directory() const { Q_D(const QXdgDesktopPortalFileDialog); - if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly)) + if (d->nativeFileDialog && useNativeFileDialog()) return d->nativeFileDialog->directory(); return d->directory; @@ -320,7 +347,7 @@ QList<QUrl> QXdgDesktopPortalFileDialog::selectedFiles() const { Q_D(const QXdgDesktopPortalFileDialog); - if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly)) + if (d->nativeFileDialog && useNativeFileDialog()) return d->nativeFileDialog->selectedFiles(); QList<QUrl> files; @@ -375,16 +402,13 @@ void QXdgDesktopPortalFileDialog::exec() { Q_D(QXdgDesktopPortalFileDialog); - if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly)) { + if (d->nativeFileDialog && useNativeFileDialog()) { d->nativeFileDialog->exec(); return; } // HACK we have to avoid returning until we emit that the dialog was accepted or rejected - QEventLoop loop; - loop.connect(this, SIGNAL(accept()), SLOT(quit())); - loop.connect(this, SIGNAL(reject()), SLOT(quit())); - loop.exec(); + d->loop.exec(); } void QXdgDesktopPortalFileDialog::hide() @@ -401,13 +425,10 @@ bool QXdgDesktopPortalFileDialog::show(Qt::WindowFlags windowFlags, Qt::WindowMo initializeDialog(); - d->modal = windowModality != Qt::NonModal; - d->winId = parent ? parent->winId() : 0; - - if (d->nativeFileDialog && (options()->fileMode() == QFileDialogOptions::Directory || options()->fileMode() == QFileDialogOptions::DirectoryOnly)) + if (d->nativeFileDialog && useNativeFileDialog(OpenFallback)) return d->nativeFileDialog->show(windowFlags, windowModality, parent); - openPortal(); + openPortal(windowFlags, windowModality, parent); return true; } @@ -437,6 +458,23 @@ void QXdgDesktopPortalFileDialog::gotResponse(uint response, const QVariantMap & } } +bool QXdgDesktopPortalFileDialog::useNativeFileDialog(QXdgDesktopPortalFileDialog::FallbackType fallbackType) const +{ + Q_D(const QXdgDesktopPortalFileDialog); + + if (d->failedToOpen && fallbackType != OpenFallback) + return true; + + if (d->fileChooserPortalVersion < 3) { + if (options()->fileMode() == QFileDialogOptions::Directory) + return true; + else if (options()->fileMode() == QFileDialogOptions::DirectoryOnly) + return true; + } + + return false; +} + QT_END_NAMESPACE #include "moc_qxdgdesktopportalfiledialog_p.cpp" diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog_p.h b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog_p.h index 4ae84ba726..f309307cd6 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog_p.h +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportalfiledialog_p.h @@ -15,6 +15,11 @@ class QXdgDesktopPortalFileDialog : public QPlatformFileDialogHelper Q_OBJECT Q_DECLARE_PRIVATE(QXdgDesktopPortalFileDialog) public: + enum FallbackType { + GenericFallback, + OpenFallback + }; + enum ConditionType : uint { GlobalPattern = 0, MimeType = 1 @@ -33,7 +38,7 @@ public: }; typedef QList<Filter> FilterList; - QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper *nativeFileDialog = nullptr); + QXdgDesktopPortalFileDialog(QPlatformFileDialogHelper *nativeFileDialog = nullptr, uint fileChooserPortalVersion = 0); ~QXdgDesktopPortalFileDialog(); bool defaultNameFilterDisables() const override; @@ -56,7 +61,8 @@ private Q_SLOTS: private: void initializeDialog(); - void openPortal(); + void openPortal(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent); + bool useNativeFileDialog(FallbackType fallbackType = GenericFallback) const; QScopedPointer<QXdgDesktopPortalFileDialogPrivate> d_ptr; }; diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp index 60d5474ed2..355d3e6cc9 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.cpp @@ -20,8 +20,9 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -class QXdgDesktopPortalThemePrivate : public QPlatformThemePrivate -{ +class QXdgDesktopPortalThemePrivate : public QObject + { + Q_OBJECT public: enum XdgColorschemePref { None, @@ -30,7 +31,7 @@ public: }; QXdgDesktopPortalThemePrivate() - : QPlatformThemePrivate() + : QObject() { } ~QXdgDesktopPortalThemePrivate() @@ -40,7 +41,7 @@ public: /*! \internal - Converts the given Freedesktop color scheme setting \a colorschemePref to a QPlatformTheme::Appearance value. + Converts the given Freedesktop color scheme setting \a colorschemePref to a Qt::ColorScheme value. Specification: https://github.com/flatpak/xdg-desktop-portal/blob/d7a304a00697d7d608821253cd013f3b97ac0fb6/data/org.freedesktop.impl.portal.Settings.xml#L33-L45 Unfortunately the enum numerical values are not defined identically, so we have to convert them. @@ -53,18 +54,29 @@ public: 1: Prefer dark appearance | 2: Dark 2: Prefer light appearance | 1: Light */ - static QPlatformTheme::Appearance appearanceFromXdgPref(const XdgColorschemePref colorschemePref) + static Qt::ColorScheme colorSchemeFromXdgPref(const XdgColorschemePref colorschemePref) { switch (colorschemePref) { - case PreferDark: return QPlatformTheme::Appearance::Dark; - case PreferLight: return QPlatformTheme::Appearance::Light; - default: return QPlatformTheme::Appearance::Unknown; + case PreferDark: return Qt::ColorScheme::Dark; + case PreferLight: return Qt::ColorScheme::Light; + default: return Qt::ColorScheme::Unknown; } } +public Q_SLOTS: + void settingChanged(const QString &group, const QString &key, + const QDBusVariant &value) + { + if (group == "org.freedesktop.appearance"_L1 && key == "color-scheme"_L1) { + colorScheme = colorSchemeFromXdgPref(static_cast<XdgColorschemePref>(value.variant().toUInt())); + QWindowSystemInterface::handleThemeChange(); + } + } + +public: QPlatformTheme *baseTheme = nullptr; uint fileChooserPortalVersion = 0; - QPlatformTheme::Appearance appearance = QPlatformTheme::Appearance::Unknown; + Qt::ColorScheme colorScheme = Qt::ColorScheme::Unknown; }; QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() @@ -75,7 +87,7 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() QStringList themeNames; themeNames += QGuiApplicationPrivate::platform_integration->themeNames(); // 1) Look for a theme plugin. - for (const QString &themeName : qAsConst(themeNames)) { + for (const QString &themeName : std::as_const(themeNames)) { d->baseTheme = QPlatformThemeFactory::create(themeName, nullptr); if (d->baseTheme) break; @@ -84,7 +96,7 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() // 2) If no theme plugin was found ask the platform integration to // create a theme if (!d->baseTheme) { - for (const QString &themeName : qAsConst(themeNames)) { + for (const QString &themeName : std::as_const(themeNames)) { d->baseTheme = QGuiApplicationPrivate::platform_integration->createPlatformTheme(themeName); if (d->baseTheme) break; @@ -104,7 +116,7 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() message << "org.freedesktop.portal.FileChooser"_L1 << "version"_L1; QDBusPendingCall pendingCall = QDBusConnection::sessionBus().asyncCall(message); QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(pendingCall); - QObject::connect(watcher, &QDBusPendingCallWatcher::finished, [d] (QDBusPendingCallWatcher *watcher) { + QObject::connect(watcher, &QDBusPendingCallWatcher::finished, watcher, [d] (QDBusPendingCallWatcher *watcher) { QDBusPendingReply<QVariant> reply = *watcher; if (reply.isValid()) { d->fileChooserPortalVersion = reply.value().toUInt(); @@ -124,8 +136,13 @@ QXdgDesktopPortalTheme::QXdgDesktopPortalTheme() if (reply.isValid()) { const QDBusVariant dbusVariant = qvariant_cast<QDBusVariant>(reply.value()); const QXdgDesktopPortalThemePrivate::XdgColorschemePref xdgPref = static_cast<QXdgDesktopPortalThemePrivate::XdgColorschemePref>(dbusVariant.variant().toUInt()); - d->appearance = QXdgDesktopPortalThemePrivate::appearanceFromXdgPref(xdgPref); + d->colorScheme = QXdgDesktopPortalThemePrivate::colorSchemeFromXdgPref(xdgPref); } + + QDBusConnection::sessionBus().connect( + "org.freedesktop.portal.Desktop"_L1, "/org/freedesktop/portal/desktop"_L1, + "org.freedesktop.portal.Settings"_L1, "SettingChanged"_L1, d_ptr.get(), + SLOT(settingChanged(QString, QString, QDBusVariant))); } QPlatformMenuItem* QXdgDesktopPortalTheme::createPlatformMenuItem() const @@ -166,11 +183,12 @@ QPlatformDialogHelper* QXdgDesktopPortalTheme::createPlatformDialogHelper(Dialog { Q_D(const QXdgDesktopPortalTheme); - if (type == FileDialog) { + if (type == FileDialog && d->fileChooserPortalVersion) { // Older versions of FileChooser portal don't support opening directories, therefore we fallback // to native file dialog opened inside the sandbox to open a directory. - if (d->fileChooserPortalVersion < 3 && d->baseTheme->usePlatformNativeDialog(type)) - return new QXdgDesktopPortalFileDialog(static_cast<QPlatformFileDialogHelper*>(d->baseTheme->createPlatformDialogHelper(type))); + if (d->baseTheme->usePlatformNativeDialog(type)) + return new QXdgDesktopPortalFileDialog(static_cast<QPlatformFileDialogHelper*>(d->baseTheme->createPlatformDialogHelper(type)), + d->fileChooserPortalVersion); return new QXdgDesktopPortalFileDialog; } @@ -204,10 +222,12 @@ QVariant QXdgDesktopPortalTheme::themeHint(ThemeHint hint) const return d->baseTheme->themeHint(hint); } -QPlatformTheme::Appearance QXdgDesktopPortalTheme::appearance() const +Qt::ColorScheme QXdgDesktopPortalTheme::colorScheme() const { Q_D(const QXdgDesktopPortalTheme); - return d->appearance; + if (d->colorScheme == Qt::ColorScheme::Unknown) + return d->baseTheme->colorScheme(); + return d->colorScheme; } QPixmap QXdgDesktopPortalTheme::standardPixmap(StandardPixmap sp, const QSizeF &size) const @@ -244,3 +264,5 @@ QString QXdgDesktopPortalTheme::standardButtonText(int button) const } QT_END_NAMESPACE + +#include "qxdgdesktopportaltheme.moc" diff --git a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h index 4390ab73d8..1ac04c45e6 100644 --- a/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h +++ b/src/plugins/platformthemes/xdgdesktopportal/qxdgdesktopportaltheme.h @@ -34,7 +34,7 @@ public: QVariant themeHint(ThemeHint hint) const override; - Appearance appearance() const override; + Qt::ColorScheme colorScheme() const override; QPixmap standardPixmap(StandardPixmap sp, const QSizeF &size) const override; QIcon fileIcon(const QFileInfo &fileInfo, |