From d0b4e8a601bc2f81ddb23c124acc99defa26c02f Mon Sep 17 00:00:00 2001
From: Jan Grulich <jgrulich@redhat.com>
Date: Fri, 22 Mar 2024 12:12:51 +0100
Subject: [PATCH] QGtk3Theme: Add support for xdg-desktop-portal to get color scheme

Use xdg-desktop-portal to get color scheme used by GNOME. Recent GNOME
versions use this as primary way to switch between light and dark theme.
Make this a preferred way to get color scheme and fallback to previous
methods in case xdg-desktop-portal cannot be used. Also update app theme
on runtime when color scheme changes, not only when theme is changed.

[ChangeLog][Platform Specific Changes][Linux] Add support for
xdg-desktop-portal to get color scheme in QGtk3Theme.

Fixes: QTBUG-116197
Change-Id: Ib3ffad405bc795ed6f4de4af411efc45721662b9
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Axel Spoerl <axel.spoerl@qt.io>
Reviewed-by: Santhosh Kumar <santhosh.kumar.selvaraj@qt.io>
---

diff --git a/src/plugins/platformthemes/gtk3/CMakeLists.txt b/src/plugins/platformthemes/gtk3/CMakeLists.txt
index becfccc..6d3c7bf 100644
--- a/src/plugins/platformthemes/gtk3/CMakeLists.txt
+++ b/src/plugins/platformthemes/gtk3/CMakeLists.txt
@@ -35,6 +35,13 @@
         Qt::GuiPrivate
 )
 
+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/qgtk3portalinterface.cpp b/src/plugins/platformthemes/gtk3/qgtk3portalinterface.cpp
new file mode 100644
index 0000000..1ffdda7
--- /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 0000000..25a5f58
--- /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
index 90c0282..2877b28 100644
--- a/src/plugins/platformthemes/gtk3/qgtk3storage.cpp
+++ b/src/plugins/platformthemes/gtk3/qgtk3storage.cpp
@@ -21,6 +21,9 @@
 QGtk3Storage::QGtk3Storage()
 {
     m_interface.reset(new QGtk3Interface(this));
+#if QT_CONFIG(dbus)
+    m_portalInterface.reset(new QGtk3PortalInterface(this));
+#endif
     populateMap();
 }
 
@@ -273,7 +276,6 @@
  */
 void QGtk3Storage::handleThemeChange()
 {
-    clear();
     populateMap();
     QWindowSystemInterface::handleThemeChange();
 }
@@ -331,21 +333,32 @@
     static QString m_themeName;
 
     // Distiguish initialization, theme change or call without theme change
+    Qt::ColorScheme newColorScheme = Qt::ColorScheme::Unknown;
     const QString newThemeName = themeName();
-    if (m_themeName == newThemeName)
+
+#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();
 
-    // Derive color scheme from theme name
-    m_colorScheme = newThemeName.contains("dark"_L1, Qt::CaseInsensitive)
-                   ? Qt::ColorScheme::Dark : m_interface->colorSchemeByColors();
-
     if (m_themeName.isEmpty()) {
-        qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << m_colorScheme;
+        qCDebug(lcQGtk3Interface) << "GTK theme initialized:" << newThemeName << newColorScheme;
     } else {
-        qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << m_colorScheme;
+        qCDebug(lcQGtk3Interface) << "GTK theme changed to:" << newThemeName << newColorScheme;
     }
+    m_colorScheme = newColorScheme;
     m_themeName = newThemeName;
 
     // create standard mapping or load from Json file?
diff --git a/src/plugins/platformthemes/gtk3/qgtk3storage_p.h b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h
index 37c5bf5..4519226 100644
--- a/src/plugins/platformthemes/gtk3/qgtk3storage_p.h
+++ b/src/plugins/platformthemes/gtk3/qgtk3storage_p.h
@@ -16,6 +16,9 @@
 //
 
 #include "qgtk3interface_p.h"
+#if QT_CONFIG(dbus)
+#include "qgtk3portalinterface_p.h"
+#endif
 
 #include <QtCore/QJsonDocument>
 #include <QtCore/QCache>
@@ -205,7 +208,9 @@
     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;
 
