From c30d7eba5a062ed78dc0a9b53484934d08b956fa Mon Sep 17 00:00:00 2001 From: YaoBing Xiao Date: Mon, 3 Nov 2025 16:49:39 +0800 Subject: [PATCH] feat: sync Xresouces and xsettings scale with treeland scale changes - allows X11 applications running under XWayland to automatically follow the new max scale without needing to restart. - improves user experience and visual consistency between native Wayland and X11 clients when display scaling is adjusted. Log: --- src/CMakeLists.txt | 8 + src/common/treelandlogging.cpp | 3 + src/common/treelandlogging.h | 3 + src/core/shellhandler.cpp | 12 - src/seat/helper.cpp | 35 ++ src/seat/helper.h | 4 + src/xsettings/abstractsettings.cpp | 14 + src/xsettings/abstractsettings.h | 35 ++ src/xsettings/settingmanager.cpp | 109 ++++++ src/xsettings/settingmanager.h | 46 +++ src/xsettings/xresource.cpp | 171 +++++++++ src/xsettings/xresource.h | 111 ++++++ src/xsettings/xsettings.cpp | 566 +++++++++++++++++++++++++++++ src/xsettings/xsettings.h | 215 +++++++++++ 14 files changed, 1320 insertions(+), 12 deletions(-) create mode 100644 src/xsettings/abstractsettings.cpp create mode 100644 src/xsettings/abstractsettings.h create mode 100644 src/xsettings/settingmanager.cpp create mode 100644 src/xsettings/settingmanager.h create mode 100644 src/xsettings/xresource.cpp create mode 100644 src/xsettings/xresource.h create mode 100644 src/xsettings/xsettings.cpp create mode 100644 src/xsettings/xsettings.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d4e316a36..16bcf3f76 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -173,6 +173,14 @@ qt_add_qml_module(libtreeland workspace/workspaceanimationcontroller.h workspace/workspacemodel.cpp workspace/workspacemodel.h + xsettings/abstractsettings.h + xsettings/abstractsettings.cpp + xsettings/xsettings.h + xsettings/xsettings.cpp + xsettings/xresource.h + xsettings/xresource.cpp + xsettings/settingmanager.h + xsettings/settingmanager.cpp QML_FILES core/qml/PrimaryOutput.qml diff --git a/src/common/treelandlogging.cpp b/src/common/treelandlogging.cpp index 9ed62c6c2..01ad1aebf 100644 --- a/src/common/treelandlogging.cpp +++ b/src/common/treelandlogging.cpp @@ -60,3 +60,6 @@ Q_LOGGING_CATEGORY(treelandGreeter, "treeland.greeter") // FPS display Q_LOGGING_CATEGORY(treelandFpsDisplay, "treeland.fpsdisplay") + +// xsettings +Q_LOGGING_CATEGORY(treelandXsettings, "treeland.xsettings") diff --git a/src/common/treelandlogging.h b/src/common/treelandlogging.h index 0ea42eb56..59d07270b 100644 --- a/src/common/treelandlogging.h +++ b/src/common/treelandlogging.h @@ -65,4 +65,7 @@ Q_DECLARE_LOGGING_CATEGORY(treelandGreeter) // FPS display Q_DECLARE_LOGGING_CATEGORY(treelandFpsDisplay) +// xsettings +Q_DECLARE_LOGGING_CATEGORY(treelandXsettings) + #endif // TREELAND_LOGGING_H diff --git a/src/core/shellhandler.cpp b/src/core/shellhandler.cpp index a4a37feb3..22f49a354 100644 --- a/src/core/shellhandler.cpp +++ b/src/core/shellhandler.cpp @@ -105,18 +105,6 @@ WXWayland *ShellHandler::createXWayland(WServer *server, xwayland->setAtomSupported(atomPid, true); auto atomNoTitlebar = xwayland->atom("_DEEPIN_NO_TITLEBAR"); xwayland->setAtomSupported(atomNoTitlebar, true); - // TODO: set other xsettings and sync - setResourceManagerAtom( - xwayland, - QString("Xft.dpi:\t%1") - .arg(96 * m_rootSurfaceContainer->window()->effectiveDevicePixelRatio()) - .toUtf8()); - connect(Helper::instance()->window(), - &WOutputRenderWindow::effectiveDevicePixelRatioChanged, - xwayland, - [xwayland, this](qreal dpr) { - setResourceManagerAtom(xwayland, QString("Xft.dpi:\t%1").arg(96 * dpr).toUtf8()); - }); }); return xwayland; } diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index fa8754983..93c7caba4 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -41,6 +41,7 @@ #include "core/treeland.h" #include "greeter/greeterproxy.h" #include "modules/screensaver/screensaverinterfacev1.h" +#include "xsettings/settingmanager.h" #include #include @@ -100,6 +101,7 @@ #include #include #include +#include #include #include @@ -176,6 +178,16 @@ Session::~Session() { qCDebug(treelandCore) << "Deleting session for uid:" << uid << socket; Q_EMIT aboutToBeDestroyed(); + + if (settingManagerThread) { + settingManagerThread->quit(); + settingManagerThread->wait(QDeadlineTimer(25000)); + } + + if (settingManager) { + delete settingManager; + settingManager = nullptr; + } if (xwayland) Helper::instance()->shellHandler()->removeXWayland(xwayland); if (socket) @@ -2299,6 +2311,29 @@ std::shared_ptr Helper::ensureSession(int id, uid_t uid) if (!session->noTitlebarAtom) { qCWarning(treelandInput) << "Failed to intern atom:" << _DEEPIN_NO_TITLEBAR; } + session->settingManager = new SettingManager(session->xwayland->xcbConnection(), + session->xwayland); + session->settingManagerThread = new QThread(session->xwayland); + + session->settingManager->moveToThread(session->settingManagerThread); + connect(session->settingManagerThread, &QThread::started, this, [this, session]{ + const qreal scale = m_rootSurfaceContainer->window()->effectiveDevicePixelRatio(); + QMetaObject::invokeMethod(session->settingManager, [session, scale]() { + session->settingManager->setGlobalScale(scale); + session->settingManager->apply(); + }, Qt::QueuedConnection); + + QObject::connect(Helper::instance()->window(), + &WOutputRenderWindow::effectiveDevicePixelRatioChanged, + session->settingManager, + [session](qreal dpr) { + session->settingManager->setGlobalScale(dpr); + session->settingManager->apply(); + }, Qt::QueuedConnection); + }); + + connect(session->settingManagerThread, &QThread::finished, session->settingManagerThread, &QThread::deleteLater); + session->settingManagerThread->start(); } }); return xwayland; diff --git a/src/seat/helper.h b/src/seat/helper.h index 1770d7a30..fa90898af 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -101,6 +101,8 @@ class DDMInterfaceV1; class TreelandConfig; class FpsDisplayManager; class ScreensaverInterfaceV1; +class SettingManager; + struct wlr_idle_inhibitor_v1; struct wlr_output_power_v1_set_mode_event; struct wlr_ext_foreign_toplevel_image_capture_source_manager_v1_request; @@ -121,6 +123,8 @@ struct Session : QObject { WSocket *socket = nullptr; WXWayland *xwayland = nullptr; quint32 noTitlebarAtom = XCB_ATOM_NONE; + SettingManager *settingManager = nullptr; + QThread *settingManagerThread = nullptr; ~Session(); diff --git a/src/xsettings/abstractsettings.cpp b/src/xsettings/abstractsettings.cpp new file mode 100644 index 000000000..fc2c18a80 --- /dev/null +++ b/src/xsettings/abstractsettings.cpp @@ -0,0 +1,14 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "abstractsettings.h" + +AbstractSettings::AbstractSettings(xcb_connection_t *connection, QObject *parent) + : QObject(parent) + , m_connection(connection) +{ +} + +AbstractSettings::~AbstractSettings() +{ +} diff --git a/src/xsettings/abstractsettings.h b/src/xsettings/abstractsettings.h new file mode 100644 index 000000000..494614373 --- /dev/null +++ b/src/xsettings/abstractsettings.h @@ -0,0 +1,35 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include + +#include +#include + +class AbstractSettings : public QObject +{ + Q_OBJECT +public: + explicit AbstractSettings(xcb_connection_t *connection, QObject *parent = nullptr); + ~AbstractSettings() override; + + virtual bool initialized() const = 0; + virtual bool isEmpty() const = 0; + + virtual bool contains(const QByteArray &property) const = 0; + virtual QVariant getPropertyValue(const QByteArray &property) const = 0; + virtual void setPropertyValue(const QByteArray &property, const QVariant &value) = 0; + virtual QByteArrayList propertyList() const = 0; + virtual void apply() = 0; + +Q_SIGNALS: + void propertyChanged(const QByteArray &property, const QVariant &value); + void propertyAdded(const QByteArray &property, const QVariant &value); + void propertyRemoved(const QByteArray &property, const QVariant &value); + +protected: + xcb_connection_t *m_connection = nullptr; + xcb_atom_t m_atom = XCB_ATOM_NONE; +}; diff --git a/src/xsettings/settingmanager.cpp b/src/xsettings/settingmanager.cpp new file mode 100644 index 000000000..91040fa9d --- /dev/null +++ b/src/xsettings/settingmanager.cpp @@ -0,0 +1,109 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "settingmanager.h" +#include "common/treelandlogging.h" + +const static qreal BASE_DPI = 96; +const static qreal XSETTINGS_BASE_DPI_FIXED = BASE_DPI * 1024; + +SettingManager::SettingManager(xcb_connection_t *connection, QObject *parent) + : QObject(parent) + , m_resource(new XResource(connection, this)) + , m_settings(new XSettings(connection, this)) +{ +} + +SettingManager::~SettingManager() +{ +} + +void SettingManager::setGTKTheme(const QString &themeName) +{ + m_resource->setPropertyValue(XResource::toByteArray(XResource::Gtk_ThemeName), themeName); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Gtk_ThemeName), themeName); +} + +QString SettingManager::GTKTheme() const +{ + return m_settings->getPropertyValue(XSettings::toByteArray(XSettings::Gtk_ThemeName)).toString(); +} + +void SettingManager::setFont(const QString &name) +{ + m_resource->setPropertyValue(XResource::toByteArray(XResource::Gtk_FontName), name); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Gtk_FontName), name); +} + +QString SettingManager::font() const +{ + return m_settings->getPropertyValue(XSettings::toByteArray(XSettings::Gtk_FontName)).toString(); +} + +void SettingManager::setIconTheme(const QString &theme) +{ + m_resource->setPropertyValue(XResource::toByteArray(XResource::Gtk_IconThemeName), theme); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Gtk_IconThemeName), theme); +} + +QString SettingManager::iconTheme() const +{ + return m_settings->getPropertyValue(XSettings::toByteArray(XSettings::Gtk_IconThemeName)).toString(); +} + +void SettingManager::setSoundTheme(const QString &theme) +{ + m_resource->setPropertyValue(XResource::toByteArray(XResource::Net_SoundThemeName), theme); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Net_SoundThemeName), theme); +} + +QString SettingManager::soundTheme() const +{ + return m_settings->getPropertyValue(XSettings::toByteArray(XSettings::Net_SoundThemeName)).toString(); +} + +void SettingManager::setCursorTheme(const QString &theme) +{ + m_resource->setPropertyValue(XResource::toByteArray(XResource::Gtk_CursorThemeName), theme); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Gtk_CursorThemeName), theme); +} + +QString SettingManager::cursorTheme() const +{ + return m_settings->getPropertyValue(XSettings::toByteArray(XSettings::Gtk_CursorThemeName)).toString(); +} + +void SettingManager::setCursorSize(qreal value) +{ + m_resource->setPropertyValue(XResource::toByteArray(XResource::Xcursor_Size), value); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Xcursor_Size), value); +} + +qreal SettingManager::cursorSize() const +{ + return m_settings->getPropertyValue(XSettings::toByteArray(XSettings::Xcursor_Size)).toReal(); +} + +void SettingManager::setDoubleClickInterval(int interval) +{ + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Net_DoubleClickTime), interval); +} + +void SettingManager::setGlobalScale(qreal scale) +{ + m_resource->setPropertyValue(XResource::toByteArray(XResource::Xft_DPI), scale * BASE_DPI); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Gdk_WindowScalingFactor), qFloor(scale)); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Gdk_UnscaledDPI), scale * XSETTINGS_BASE_DPI_FIXED); + m_settings->setPropertyValue(XSettings::toByteArray(XSettings::Xft_DPI), scale * XSETTINGS_BASE_DPI_FIXED); +} + +qreal SettingManager::globalScale() const +{ + return m_settings->getPropertyValue(XSettings::toByteArray(XSettings::Gdk_UnscaledDPI)).toReal() / XSETTINGS_BASE_DPI_FIXED; +} + +void SettingManager::apply() +{ + m_resource->apply(); + m_settings->apply(); +} diff --git a/src/xsettings/settingmanager.h b/src/xsettings/settingmanager.h new file mode 100644 index 000000000..6e74248b4 --- /dev/null +++ b/src/xsettings/settingmanager.h @@ -0,0 +1,46 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include "xresource.h" +#include "xsettings.h" + +#include + +class SettingManager : public QObject +{ + Q_OBJECT +public: + explicit SettingManager(xcb_connection_t *connection, QObject *parent = nullptr); + ~SettingManager() override; + + void setGTKTheme(const QString &themeName); + QString GTKTheme() const; + + void setFont(const QString &name); + QString font() const; + + void setIconTheme(const QString &theme); + QString iconTheme() const; + + void setSoundTheme(const QString &theme); + QString soundTheme() const; + + void setCursorTheme(const QString &theme); + QString cursorTheme() const; + + void setCursorSize(qreal value); + qreal cursorSize() const; + + void setDoubleClickInterval(int interval); + + void setGlobalScale(qreal scale); + qreal globalScale() const; + + void apply(); + +private: + XResource *m_resource = nullptr; + XSettings *m_settings = nullptr; +}; diff --git a/src/xsettings/xresource.cpp b/src/xsettings/xresource.cpp new file mode 100644 index 000000000..5b8a39169 --- /dev/null +++ b/src/xsettings/xresource.cpp @@ -0,0 +1,171 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "xresource.h" +#include "common/treelandlogging.h" + +#define XRESOURCE_ATOM_NAME "RESOURCE_MANAGER" + +static QPair splitXResourceLine(const QByteArray &line) +{ + int pos = -1; + bool escaped = false; + + for (int i = 0; i < line.size(); ++i) { + if (line[i] == '\\') { + escaped = !escaped; + } else if (line[i] == ':' && !escaped) { + pos = i; + break; + } else { + escaped = false; + } + } + + if (pos == -1) + return {line.trimmed(), QByteArray()}; + + QByteArray key = line.left(pos).trimmed(); + QByteArray value = line.mid(pos + 1).trimmed(); + value.replace("\\:", ":"); + + return {key, value}; +} + +XResource::XResource(xcb_connection_t *connection, QObject *parent) + : AbstractSettings(connection, parent) +{ + const xcb_setup_t *setup = xcb_get_setup(m_connection); + xcb_screen_iterator_t iter = xcb_setup_roots_iterator(setup); + m_root = iter.data->root; + xcb_intern_atom_cookie_t cookie = xcb_intern_atom(m_connection, 0, strlen(XRESOURCE_ATOM_NAME), XRESOURCE_ATOM_NAME); + xcb_intern_atom_reply_t *reply = xcb_intern_atom_reply(m_connection, cookie, nullptr); + if (reply) { + m_atom = reply->atom; + } + + free(reply); +} + +XResource::~XResource() +{ +} + +QByteArray XResource::toByteArray(XResourceKey key) +{ + switch (key) { + case Xft_DPI: return QByteArrayLiteral("Xft.dpi"); + case Xft_Antialias: return QByteArrayLiteral("Xft.antialias"); + case Xft_Hinting: return QByteArrayLiteral("Xft.hinting"); + case Xft_HintStyle: return QByteArrayLiteral("Xft.hintstyle"); + case Xft_RGBA: return QByteArrayLiteral("Xft.rgba"); + case Xft_LCDFilter: return QByteArrayLiteral("Xft.lcdfilter"); + case Xcursor_Theme: return QByteArrayLiteral("Xcursor.theme"); + case Xcursor_Size: return QByteArrayLiteral("Xcursor.size"); + case Xcursor_ThemeCore: return QByteArrayLiteral("Xcursor.theme_core"); + case Gtk_FontName: return QByteArrayLiteral("Gtk/FontName"); + case Gtk_ThemeName: return QByteArrayLiteral("Gtk/ThemeName"); + case Gtk_IconThemeName: return QByteArrayLiteral("Gtk/IconThemeName"); + case Gtk_CursorThemeName: return QByteArrayLiteral("Gtk/CursorThemeName"); + case Gtk_CursorThemeSize: return QByteArrayLiteral("Gtk/CursorThemeSize"); + case Gdk_WindowScalingFactor: return QByteArrayLiteral("Gdk/WindowScalingFactor"); + case Gdk_UnscaledDPI: return QByteArrayLiteral("Gdk/UnscaledDPI"); + case Net_ThemeName: return QByteArrayLiteral("Net/ThemeName"); + case Net_IconThemeName: return QByteArrayLiteral("Net/IconThemeName"); + case Net_SoundThemeName: return QByteArrayLiteral("Net/SoundThemeName"); + default: return QByteArrayLiteral(""); + } +} + +bool XResource::initialized() const +{ + return true; +} + +bool XResource::isEmpty() const +{ + return m_resources.isEmpty(); +} + +bool XResource::contains(const QByteArray &property) const +{ + return m_resources.contains(property); +} + +QVariant XResource::getPropertyValue(const QByteArray &property) const +{ + auto it = m_resources.constFind(property); + return it != m_resources.constEnd() ? it.value() : QVariant(); +} + +void XResource::setPropertyValue(const QByteArray &property, const QVariant &value) +{ + QVariant &xvalue = m_resources[property]; + if (xvalue == value) + return; + + if (!value.isValid()) + return; + + m_resources[property] = value; +} + +QByteArrayList XResource::propertyList() const +{ + QByteArrayList merged; + for (auto v : m_resources.keys()) + merged.append(v); + + return merged; +} + +void XResource::apply() +{ + QByteArray text; + for (auto it = m_resources.constBegin(); it != m_resources.constEnd(); ++it) { + text.append(it.key()); + text.append(": "); + text.append(it.value().toString().toUtf8()); + text.append('\n'); + } + + xcb_change_property(m_connection, + XCB_PROP_MODE_REPLACE, + m_root, + m_atom, + XCB_ATOM_STRING, + 8, + text.size(), + text.constData()); + xcb_flush(m_connection); +} + +void XResource::reload() +{ + m_resources.clear(); + + xcb_get_property_cookie_t cookie = + xcb_get_property(m_connection, 0, m_root, m_atom, + XCB_ATOM_STRING, 0, UINT32_MAX); + xcb_get_property_reply_t *reply = + xcb_get_property_reply(m_connection, cookie, nullptr); + if (!reply) { + qCCritical(treelandXsettings) << "xcb_intern_atom_reply returned nullptr, atom:" << XRESOURCE_ATOM_NAME; + return; + } + + int len = xcb_get_property_value_length(reply); + const char *data = (const char *)xcb_get_property_value(reply); + QByteArray text(data, len); + free(reply); + + const QList lines = text.split('\n'); + for (const QByteArray &line : std::as_const(lines)) { + if (line.trimmed().isEmpty()) + continue; + + auto [key, value] = splitXResourceLine(line); + if (!key.isEmpty()) + m_resources[key] = QString::fromUtf8(value); + } +} diff --git a/src/xsettings/xresource.h b/src/xsettings/xresource.h new file mode 100644 index 000000000..4197d85f5 --- /dev/null +++ b/src/xsettings/xresource.h @@ -0,0 +1,111 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include "abstractsettings.h" + +/*## Common `Xresources` (X11) Settings + +| Category | Property | Example Value | Description | +| ------------------------ | --------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------- | +| **Xft (Font Rendering)** | `Xft.dpi` | `96` / `144` / `192` | DPI setting used by font rendering. Often overridden by `xsettingsd` or `xrdb`. | +| | `Xft.antialias` | `1` or `0` | Enables (1) or disables (0) font anti-aliasing. | +| | `Xft.hinting` | `1` or `0` | Enables (1) or disables (0) font hinting. | +| | `Xft.hintstyle` | `hintnone` / `hintslight` / `hintmedium` / `hintfull` | Controls the degree of font hinting. | +| | `Xft.rgba` | `none` / `rgb` / `bgr` / `vrgb` / `vbgr` | Subpixel rendering order; depends on monitor pixel layout. | +| | `Xft.lcdfilter` | `lcdnone` / `lcddefault` / `lcdlight` / `lcdlegacy` | LCD subpixel filtering mode. | + +| **Xcursor (Mouse Cursor)** | `Xcursor.theme` | `breeze_cursors` / `Adwaita` / `DMZ-White` | Name of the cursor theme. | +| | `Xcursor.size` | `24` / `32` / `48` | Cursor size in pixels. | +| | `Xcursor.theme_core` | `true` / `false` | Whether to apply the theme to the core (legacy) cursor shapes. | + +| **XTerm / URxvt (Terminal)** | `XTerm*faceName` / `URxvt.font` | `"Noto Sans Mono:size=10"` | Sets the font for terminal text. | +| | `XTerm*faceSize` / `URxvt.fontSize` | `10` | Font size. | +| | `XTerm*geometry` | `80x24` | Default terminal geometry. | +| | `XTerm*scrollBar` | `true` / `false` | Show or hide scroll bar. | +| | `URxvt*scrollBar_right` | `true` / `false` | Scrollbar on the right side. | +| | `URxvt*perl-ext-common` | `default,matcher` | List of enabled Perl extensions (URxvt only). | + +| **GTK Theme (through xrdb bridge)** | `Gtk/FontName` | `"Noto Sans 10"` | Default GTK interface font. | +| | `Gtk/ThemeName` | `"Adwaita-dark"` | GTK theme name. | +| | `Gtk/IconThemeName` | `"breeze"` | GTK icon theme. | +| | `Gtk/CursorThemeName` | `"breeze_cursors"` | GTK cursor theme. | +| | `Gtk/CursorThemeSize` | `24` / `48` | Cursor size. | + +| **General UI (X Toolkit apps)** | `*foreground` | `#ffffff` | Default text color. | +| | `*background` | `#000000` | Default background color. | +| | `*cursorColor` | `#ffcc00` | Text cursor color. | +| | `*highlightColor` | `#0078d7` | Highlight selection color. | +| | `*borderColor` | `#444444` | Border color for some widgets. | +| | `*font` | `fixed` / `9x15` / `xft:Noto Sans-10` | Fallback font setting for older X11 apps. | + +| **Misc (Desktop & Toolkit Integration)** | `Net/ThemeName` | `"Breeze"` | GTK / DE theme name for toolkits integrating via XSettings. | +| | `Net/IconThemeName` | `"Papirus"` | Global icon theme. | +| | `Net/SoundThemeName` | `"freedesktop"` | Name of sound theme for event sounds. | +| | `Gdk/WindowScalingFactor` | `1` / `2` | Window scaling factor (used by GTK). | +| | `Gdk/UnscaledDPI` | `98304` | Unscaled base DPI (×1024 fixed point). | + +## Notes on Value Calculation + +| Concept | Explanation | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | +| `Xft.dpi` | Usually calculated as:
`DPI = 25.4 × (screen_pixel_height / physical_height_mm)`
Example: 2160p on 15.6" (~141 dpi) | +| `Gdk/UnscaledDPI` | GTK stores this as `DPI × 1024`. Example: `96 × 1024 = 98304`. | +| `Xft.hintstyle` & `rgba` | Correspond to fontconfig settings — usually set via `~/.config/fontconfig/fonts.conf` or inherited from DE. | +| `Xcursor.size` | Often scaled according to `Gdk/WindowScalingFactor`. |*/ + +class XResource : public AbstractSettings +{ + Q_OBJECT +public: + enum XResourceKey { + Unknown = 0, + + Xft_DPI, + Xft_Antialias, + Xft_Hinting, + Xft_HintStyle, + Xft_RGBA, + Xft_LCDFilter, + + Xcursor_Theme, + Xcursor_Size, + Xcursor_ThemeCore, + + Gtk_FontName, + Gtk_ThemeName, + Gtk_IconThemeName, + Gtk_CursorThemeName, + Gtk_CursorThemeSize, + + Gdk_WindowScalingFactor, + Gdk_UnscaledDPI, + + Net_ThemeName, + Net_IconThemeName, + Net_SoundThemeName + }; + Q_ENUM(XResourceKey) + + explicit XResource(xcb_connection_t *connection, QObject *parent = nullptr); + ~XResource() override; + + static QByteArray toByteArray(XResourceKey key); + + bool initialized() const override; + bool isEmpty() const override; + + bool contains(const QByteArray &property) const override; + QVariant getPropertyValue(const QByteArray &property) const override; + void setPropertyValue(const QByteArray &property, const QVariant &value) override; + QByteArrayList propertyList() const override; + void apply() override; + +private: + void reload(); + +private: + xcb_window_t m_root = XCB_WINDOW_NONE; + QMap m_resources; +}; diff --git a/src/xsettings/xsettings.cpp b/src/xsettings/xsettings.cpp new file mode 100644 index 000000000..16f79f15c --- /dev/null +++ b/src/xsettings/xsettings.cpp @@ -0,0 +1,566 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "xsettings.h" +#include "common/treelandlogging.h" + +#include +#include +#include + +#define kMaxPropertySize 4096 +#define XSETTINGS_ATOM_NAME "_XSETTINGS_SETTINGS" +#define MANAGER_ATOM_NAME "MANAGER" +#define XSETTINGS_NOTIFY_ATOM_NAME "_XSETTINGS_SETTINGS_NOTIFY" +#define XSETTINGS_SIGNAL_ATOM_NAME "_XSETTINGS_SETTINGS_SIGNAL" + +static int round_to_nearest_multiple_of_4(int value) +{ + int remainder = value % 4; + if (!remainder) + return value; + return value + 4 - remainder; +} + +XSettings::XSettings(xcb_connection_t *connection, QObject *parent) + : AbstractSettings(connection, parent) +{ + initX11(-1, true); +} + +XSettings::~XSettings() +{ +} + +QByteArray XSettings::toByteArray(XSettingsKey key) +{ + switch (key) { + case Xft_DPI: return QByteArrayLiteral("Xft/DPI"); + case Xft_Antialias: return QByteArrayLiteral("Xft/Antialias"); + case Xft_Hinting: return QByteArrayLiteral("Xft/Hinting"); + case Xft_HintStyle: return QByteArrayLiteral("Xft/HintStyle"); + case Xft_RGBA: return QByteArrayLiteral("Xft/RGBA"); + case Xft_LCDFilter: return QByteArrayLiteral("Xft/Lcdfilter"); + + case Xcursor_Theme: return QByteArrayLiteral("Xcursor/Theme"); + case Xcursor_Size: return QByteArrayLiteral("Xcursor/Size"); + case Xcursor_ThemeCore: return QByteArrayLiteral("Xcursor/ThemeCore"); + + case Gdk_WindowScalingFactor: return QByteArrayLiteral("Gdk/WindowScalingFactor"); + case Gdk_UnscaledDPI: return QByteArrayLiteral("Gdk/UnscaledDPI"); + + case Gtk_FontName: return QByteArrayLiteral("Gtk/FontName"); + case Gtk_ThemeName: return QByteArrayLiteral("Gtk/ThemeName"); + case Gtk_IconThemeName: return QByteArrayLiteral("Gtk/IconThemeName"); + case Gtk_CursorThemeName: return QByteArrayLiteral("Gtk/CursorThemeName"); + case Gtk_CursorThemeSize: return QByteArrayLiteral("Gtk/CursorThemeSize"); + case Gtk_RecentFilesEnabled: return QByteArrayLiteral("Gtk/RecentFilesEnabled"); + case Gtk_ShowStatusShapes: return QByteArrayLiteral("Gtk/ShowStatusShapes"); + case Gtk_ShowInputMethodMenu: return QByteArrayLiteral("Gtk/ShowInputMethodMenu"); + case Gtk_TimeoutInitial: return QByteArrayLiteral("Gtk/TimeoutInitial"); + case Gtk_TimeoutRepeat: return QByteArrayLiteral("Gtk/TimeoutRepeat"); + case Gtk_DecorationLayout: return QByteArrayLiteral("Gtk/DecorationLayout"); + case Gtk_IMModule: return QByteArrayLiteral("Gtk/IMModule"); + case Gtk_ShellShowsDesktop: return QByteArrayLiteral("Gtk/ShellShowsDesktop"); + case Gtk_MenuImages: return QByteArrayLiteral("Gtk/MenuImages"); + case Gtk_EnablePrimaryPaste: return QByteArrayLiteral("Gtk/EnablePrimaryPaste"); + case Gtk_KeynavUseCaret: return QByteArrayLiteral("Gtk/KeynavUseCaret"); + case Gtk_ShellShowsAppMenu: return QByteArrayLiteral("Gtk/ShellShowsAppMenu"); + case Gtk_CanChangeAccels: return QByteArrayLiteral("Gtk/CanChangeAccels"); + case Gtk_DialogsUseHeader: return QByteArrayLiteral("Gtk/DialogsUseHeader"); + case Gtk_ToolbarStyle: return QByteArrayLiteral("Gtk/ToolbarStyle"); + case Gtk_KeyThemeName: return QByteArrayLiteral("Gtk/KeyThemeName"); + case Gtk_IMPreeditStyle: return QByteArrayLiteral("Gtk/IMPreeditStyle"); + case Gtk_EnableAnimations: return QByteArrayLiteral("Gtk/EnableAnimations"); + case Gtk_ToolbarIconSize: return QByteArrayLiteral("Gtk/ToolbarIconSize"); + case Gtk_IMStatusStyle: return QByteArrayLiteral("Gtk/IMStatusStyle"); + case Gtk_RecentFilesMaxAge: return QByteArrayLiteral("Gtk/RecentFilesMaxAge"); + case Gtk_Modules: return QByteArrayLiteral("Gtk/Modules"); + case Gtk_AutoMnemonics: return QByteArrayLiteral("Gtk/AutoMnemonics"); + case Gtk_ColorScheme: return QByteArrayLiteral("Gtk/ColorScheme"); + case Gtk_MenuBarAccel: return QByteArrayLiteral("Gtk/MenuBarAccel"); + case Gtk_ColorPalette: return QByteArrayLiteral("Gtk/ColorPalette"); + case Gtk_OverlayScrolling: return QByteArrayLiteral("Gtk/OverlayScrolling"); + case Gtk_SessionBusId: return QByteArrayLiteral("Gtk/SessionBusId"); + case Gtk_ShowUnicodeMenu: return QByteArrayLiteral("Gtk/ShowUnicodeMenu"); + case Gtk_CursorBlinkTimeout: return QByteArrayLiteral("Gtk/CursorBlinkTimeout"); + case Gtk_ButtonImages: return QByteArrayLiteral("Gtk/ButtonImages"); + case Gtk_TitlebarRightClick: return QByteArrayLiteral("Gtk/TitlebarRightClick"); + case Gtk_TitlebarDoubleClick: return QByteArrayLiteral("Gtk/TitlebarDoubleClick"); + case Gtk_TitlebarMiddleClick: return QByteArrayLiteral("Gtk/TitlebarMiddleClick"); + case Gtk_MonospaceFontName: return QByteArrayLiteral("Gtk/MonospaceFontName"); + case Gtk_ApplicationPreferDarkTheme: return QByteArrayLiteral("Gtk/ApplicationPreferDarkTheme"); + case Gtk_PrimaryButtonWarpsSlider: return QByteArrayLiteral("Gtk/PrimaryButtonWarpsSlider"); + + case Net_DndDragThreshold: return QByteArrayLiteral("Net/DndDragThreshold"); + case Net_CursorBlinkTime: return QByteArrayLiteral("Net/CursorBlinkTime"); + case Net_ThemeName: return QByteArrayLiteral("Net/ThemeName"); + case Net_DoubleClickTime: return QByteArrayLiteral("Net/DoubleClickTime"); + case Net_CursorBlink: return QByteArrayLiteral("Net/CursorBlink"); + case Net_FallbackIconTheme: return QByteArrayLiteral("Net/FallbackIconTheme"); + case Net_EnableEventSounds: return QByteArrayLiteral("Net/EnableEventSounds"); + case Net_IconThemeName: return QByteArrayLiteral("Net/IconThemeName"); + case Net_SoundThemeName: return QByteArrayLiteral("Net/SoundThemeName"); + case Net_EnableInputFeedbackSounds: return QByteArrayLiteral("Net/EnableInputFeedbackSounds"); + case Net_PreferDarkTheme: return QByteArrayLiteral("Net/PreferDarkTheme"); + + default: return QByteArrayLiteral(""); + } +} + +bool XSettings::initialized() const +{ + return !m_windows.empty(); +} + +bool XSettings::isEmpty() const +{ + return m_settings.empty(); +} + +bool XSettings::contains(const QByteArray &property) const +{ + return m_settings.contains(property); +} + +QVariant XSettings::getPropertyValue(const QByteArray &property) const +{ + auto it = m_settings.constFind(property); + if (it == m_settings.constEnd()) + return QVariant(); + + return it->value; +} + +void XSettings::setPropertyValue(const QByteArray &property, const QVariant &value) +{ + XSettingsPropertyValue &xvalue = m_settings[property]; + if (xvalue.value == value) + return; + + if (!value.isValid()) + return; + + xvalue.updateValue(value, xvalue.last_change_serial + 1); + ++m_serial; +} + +QByteArray XSettings::depopulateSettings() +{ + QByteArray xSettings; + uint number_of_settings = m_settings.size(); + xSettings.reserve(12 + number_of_settings * 12); + char byteOrder = QSysInfo::ByteOrder == QSysInfo::LittleEndian ? XCB_IMAGE_ORDER_LSB_FIRST : XCB_IMAGE_ORDER_MSB_FIRST; + + xSettings.append(byteOrder); //byte-order + xSettings.append(3, '\0'); //unused + xSettings.append((char*)&m_serial, sizeof(m_serial)); //SERIAL + xSettings.append((char*)&number_of_settings, sizeof(number_of_settings)); //N_SETTINGS + uint number_of_settings_index = xSettings.size() - sizeof(number_of_settings); + for (auto i = m_settings.constBegin(); i != m_settings.constEnd(); ++i) { + const XSettingsPropertyValue &value = i.value(); + if (!value.value.isValid()) { + --number_of_settings; + continue; + } + + char type = XSettingsTypeString; + const QByteArray &key = i.key(); + quint16 key_size = key.size(); + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + switch (value.value.typeId()) { +#else + switch (value.value.type()) { +#endif + case QMetaType::QColor: + type = XSettingsTypeColor; + break; + case QMetaType::Int: + case QMetaType::Bool: + type = XSettingsTypeInteger; + break; + default: + break; + } + + xSettings.append(type); //type + xSettings.append('\0'); //unused + xSettings.append((char*)&key_size, 2); //name-len + xSettings.append(key.constData()); //name + xSettings.append(3 - (key_size + 3) % 4, '\0'); // 4-byte alignment + xSettings.append((char*)&value.last_change_serial, 4); //last-change-serial + + QByteArray value_data; + + if (type == XSettingsTypeInteger) { + qint32 int_value = value.value.toInt(); + value_data.append((char*)&int_value, 4); + } else if (type == XSettingsTypeColor) { + const QColor &color = qvariant_cast(value.value); + quint16 red = color.red(); + quint16 green = color.green(); + quint16 blue = color.blue(); + quint16 alpha = color.alpha(); + + value_data.append((char*)&red, 2); + value_data.append((char*)&green, 2); + value_data.append((char*)&blue, 2); + value_data.append((char*)&alpha, 2); + } else { + const QByteArray &string_data = value.value.toByteArray(); + quint32 data_size = string_data.size(); + value_data.append((char*)&data_size, 4); + value_data.append(string_data); + value_data.append(3 - (string_data.size() + 3) % 4, '\0'); // 4-byte alignment + } + + xSettings.append(value_data); + } + + if (number_of_settings == 0) { + return QByteArray(); + } + memcpy(xSettings.data() + number_of_settings_index, &number_of_settings, sizeof(number_of_settings)); + + return xSettings; +} + +void XSettings::populateSettings(const QByteArray &xSettings) +{ + if (xSettings.length() < 12) + return; + char byteOrder = xSettings.at(0); + if (byteOrder != XCB_IMAGE_ORDER_LSB_FIRST && byteOrder != XCB_IMAGE_ORDER_MSB_FIRST) { + return; + } + +#define ADJUST_BO(b, t, x) \ + ((b == XCB_IMAGE_ORDER_LSB_FIRST) ? \ + qFromLittleEndian(x) : \ + qFromBigEndian(x)) +#define VALIDATE_LENGTH(x) \ + if ((size_t)xSettings.length() < (offset + local_offset + 12 + x)) { \ + return; \ + } + + m_serial = ADJUST_BO(byteOrder, qint32, xSettings.mid(4,4).constData()); + uint number_of_settings = ADJUST_BO(byteOrder, quint32, xSettings.mid(8,4).constData()); + const char *data = xSettings.constData() + 12; + size_t offset = 0; + QSet keys; + keys.reserve(number_of_settings); + + for (uint i = 0; i < number_of_settings; i++) { + int local_offset = 0; + VALIDATE_LENGTH(2); + XSettingsType type = static_cast(*reinterpret_cast(data + offset)); + local_offset += 2; + + VALIDATE_LENGTH(2); + quint16 name_len = ADJUST_BO(byteOrder, quint16, data + offset + local_offset); + local_offset += 2; + + VALIDATE_LENGTH(name_len); + QByteArray name(data + offset + local_offset, name_len); + local_offset += round_to_nearest_multiple_of_4(name_len); + + VALIDATE_LENGTH(4); + int last_change_serial = ADJUST_BO(byteOrder, qint32, data + offset + local_offset); + Q_UNUSED(last_change_serial); + local_offset += 4; + + QVariant value; + if (type == XSettingsTypeString) { + VALIDATE_LENGTH(4); + int value_length = ADJUST_BO(byteOrder, qint32, data + offset + local_offset); + local_offset+=4; + VALIDATE_LENGTH(value_length); + QByteArray value_string(data + offset + local_offset, value_length); + value.setValue(value_string); + local_offset += round_to_nearest_multiple_of_4(value_length); + } else if (type == XSettingsTypeInteger) { + VALIDATE_LENGTH(4); + int value_length = ADJUST_BO(byteOrder, qint32, data + offset + local_offset); + local_offset += 4; + value.setValue(value_length); + } else if (type == XSettingsTypeColor) { + VALIDATE_LENGTH(2*4); + quint16 red = ADJUST_BO(byteOrder, quint16, data + offset + local_offset); + local_offset += 2; + quint16 green = ADJUST_BO(byteOrder, quint16, data + offset + local_offset); + local_offset += 2; + quint16 blue = ADJUST_BO(byteOrder, quint16, data + offset + local_offset); + local_offset += 2; + quint16 alpha= ADJUST_BO(byteOrder, quint16, data + offset + local_offset); + local_offset += 2; + QColor color_value(red,green,blue,alpha); + value.setValue(color_value); + } + offset += local_offset; + + m_settings[name].updateValue(value,last_change_serial); + keys << name; + } + + for (const QByteArray &key : m_settings.keys()) { + if (!keys.contains(key)) { + m_settings[key].updateValue(QVariant(), INT_MAX); + m_settings.remove(key); + } + } +} + +void XSettings::setSettings(const QByteArray &data) +{ + xcb_grab_server(m_connection); + + foreach (xcb_window_t win, m_windows) { + xcb_change_property(m_connection, + XCB_PROP_MODE_REPLACE, + win, + m_atom, + m_atom, + 8, data.size(), data.constData()); + + if (win) { + xcb_client_message_event_t notify_event; + memset(¬ify_event, 0, sizeof(notify_event)); + + notify_event.response_type = XCB_CLIENT_MESSAGE; + notify_event.format = 32; + notify_event.sequence = 0; + notify_event.window = win; + notify_event.type = m_notifyAtom; + notify_event.data.data32[0] = win; + notify_event.data.data32[1] = m_atom; + + xcb_send_event(m_connection, false, win, XCB_EVENT_MASK_PROPERTY_CHANGE, (const char *)¬ify_event); + } + } + xcb_ungrab_server(m_connection); +} + +QByteArrayList XSettings::propertyList() const +{ + QByteArrayList merged; + for (auto v : m_settings.keys()) + merged.append(v); + + return merged; +} + +void XSettings::apply() +{ + setSettings(depopulateSettings()); +} + +bool XSettings::initX11(int screen, bool replace) { + xcb_intern_atom_cookie_t atomCookie = + xcb_intern_atom(m_connection, 0, strlen(XSETTINGS_ATOM_NAME), XSETTINGS_ATOM_NAME); + xcb_intern_atom_reply_t *atomReply = xcb_intern_atom_reply(m_connection, atomCookie, nullptr); + + if (!atomReply) { + qCCritical(treelandXsettings) << "xcb_intern_atom_reply return nullptr for" << XSETTINGS_ATOM_NAME; + return false; + } + + m_atom = atomReply->atom; + free(atomReply); + Q_ASSERT(m_atom != XCB_NONE); + + xcb_intern_atom_cookie_t notifyAtomCookie = + xcb_intern_atom(m_connection, 0, strlen(XSETTINGS_NOTIFY_ATOM_NAME), XSETTINGS_NOTIFY_ATOM_NAME); + xcb_intern_atom_reply_t *notifyAtomReply = xcb_intern_atom_reply(m_connection, notifyAtomCookie, nullptr); + + if (!notifyAtomReply) { + qCCritical(treelandXsettings) << "xcb_intern_atom_reply return nullptr for" << XSETTINGS_NOTIFY_ATOM_NAME; + return false; + } + + m_notifyAtom = notifyAtomReply->atom; + free(notifyAtomReply); + Q_ASSERT(m_notifyAtom != XCB_NONE); + + xcb_intern_atom_cookie_t signalAtomCookie = + xcb_intern_atom(m_connection, 0, strlen(XSETTINGS_SIGNAL_ATOM_NAME), XSETTINGS_SIGNAL_ATOM_NAME); + xcb_intern_atom_reply_t *signalAtomReply = xcb_intern_atom_reply(m_connection, signalAtomCookie, nullptr); + + if (!signalAtomReply) { + qCCritical(treelandXsettings) << "xcb_intern_atom_reply return nullptr for" << XSETTINGS_SIGNAL_ATOM_NAME; + return false; + } + + m_signalAtom = signalAtomReply->atom; + free(signalAtomReply); + Q_ASSERT(m_signalAtom != XCB_NONE); + + char data[kMaxPropertySize] = {0}; + size_t bytesWritten = 128; + + const xcb_setup_t *setup = xcb_get_setup(m_connection); + int screen_count = xcb_setup_roots_length(setup); + + int min_screen = 0; + int max_screen = screen_count - 1; + if (screen >= 0) + min_screen = max_screen = screen; + + for (int s = min_screen; s <= max_screen; ++s) { + xcb_window_t win; + xcb_timestamp_t timestamp; + if (!createWindow(s, &win, ×tamp)) { + qCCritical(treelandXsettings) << "xsettingsd Unable to create window on screen" << s; + return false; + } + + xcb_change_property(m_connection, + XCB_PROP_MODE_REPLACE, + win, + m_atom, + m_atom, + 8, + bytesWritten, + data); + if (!manageScreen(s, win, timestamp, replace)) + return false; + + m_windows.push_back(win); + qCDebug(treelandXsettings) << "XSettings: registered window" << win << "for screen" << s; + } + + xcb_flush(m_connection); + return true; +} + +bool XSettings::createWindow(int screen, xcb_window_t *out_win, xcb_timestamp_t *out_time) { + const xcb_setup_t *setup = xcb_get_setup(m_connection); + xcb_screen_iterator_t it = xcb_setup_roots_iterator(setup); + for (int i = 0; i < screen; ++i) + xcb_screen_next(&it); + + xcb_screen_t *scr = it.data; + if (!scr) { + qCCritical(treelandXsettings) << "xcb_setup_roots_iterator failed"; + return false; + } + + xcb_window_t win = xcb_generate_id(m_connection); + uint32_t values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE }; + + xcb_create_window(m_connection, + XCB_COPY_FROM_PARENT, + win, + scr->root, + -1, -1, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + scr->root_visual, + XCB_CW_EVENT_MASK, + values); + + const char *name = "xsettings-manager"; + xcb_change_property(m_connection, + XCB_PROP_MODE_REPLACE, + win, + XCB_ATOM_WM_NAME, + XCB_ATOM_STRING, + 8, + strlen(name), + name); + + *out_win = win; + *out_time = XCB_CURRENT_TIME; + + qCDebug(treelandXsettings) << "Created XSETTINGS window" << win << "named" << name; + + return true; +} + +bool XSettings::manageScreen(int screen, xcb_window_t win, xcb_timestamp_t timestamp, bool replace) { + char sel_name[64]; + snprintf(sel_name, sizeof(sel_name), "_XSETTINGS_S%d", screen); + + xcb_intern_atom_cookie_t sel_cookie = + xcb_intern_atom(m_connection, 0, strlen(sel_name), sel_name); + xcb_intern_atom_reply_t *sel_reply = xcb_intern_atom_reply(m_connection, sel_cookie, nullptr); + if (!sel_reply) { + qCCritical(treelandXsettings) + << "xcb_intern_atom_reply return nullptr for" + << QString("_XSETTINGS_S%1").arg(screen); + + return false; + } + + xcb_atom_t selection_atom = sel_reply->atom; + free(sel_reply); + + xcb_grab_server(m_connection); + xcb_get_selection_owner_cookie_t owner_cookie = xcb_get_selection_owner(m_connection, selection_atom); + xcb_get_selection_owner_reply_t *owner_reply = xcb_get_selection_owner_reply(m_connection, owner_cookie, nullptr); + xcb_window_t owner = XCB_NONE; + if (owner_reply) { + owner = owner_reply->owner; + free(owner_reply); + } + + if (owner != XCB_NONE && !replace) { + xcb_ungrab_server(m_connection); + qCCritical(treelandXsettings) + << "xsettings: Another XSETTINGS manager exists for screen" + << screen + << "owner window" << owner; + return false; + } + + xcb_set_selection_owner(m_connection, win, selection_atom, timestamp); + + xcb_get_selection_owner_reply_t *reply = + xcb_get_selection_owner_reply(m_connection, + xcb_get_selection_owner(m_connection, selection_atom), + nullptr); + bool ok = reply && reply->owner == win; + free(reply); + xcb_ungrab_server(m_connection); + + if (!ok) { + qCCritical(treelandXsettings) << "xsettingsd: Failed to acquire ownership of" << sel_name; + return false; + } + + const xcb_setup_t *setup = xcb_get_setup(m_connection); + xcb_screen_iterator_t it = xcb_setup_roots_iterator(setup); + for (int i = 0; i < screen; ++i) + xcb_screen_next(&it); + xcb_window_t root = it.data->root; + xcb_intern_atom_cookie_t man_cookie = + xcb_intern_atom(m_connection, 0, strlen(MANAGER_ATOM_NAME), MANAGER_ATOM_NAME); + xcb_intern_atom_reply_t *man_reply = + xcb_intern_atom_reply(m_connection, man_cookie, nullptr); + if (!man_reply) { + qCCritical(treelandXsettings) << "Failed to intern MANAGER atom"; + return false; + } + + xcb_atom_t manager_atom = man_reply->atom; + free(man_reply); + + xcb_client_message_event_t ev = {}; + ev.response_type = XCB_CLIENT_MESSAGE; + ev.window = root; + ev.type = manager_atom; + ev.format = 32; + ev.data.data32[0] = timestamp; + ev.data.data32[1] = selection_atom; + ev.data.data32[2] = win; + ev.data.data32[3] = 0; + ev.data.data32[4] = 0; + + xcb_send_event(m_connection, + false, + root, + XCB_EVENT_MASK_STRUCTURE_NOTIFY, + reinterpret_cast(&ev)); + xcb_flush(m_connection); + + return true; +} diff --git a/src/xsettings/xsettings.h b/src/xsettings/xsettings.h new file mode 100644 index 000000000..354f9bb92 --- /dev/null +++ b/src/xsettings/xsettings.h @@ -0,0 +1,215 @@ +// Copyright (C) 2025 UnionTech Software Technology Co., Ltd. +// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#pragma once + +#include "abstractsettings.h" + +class XSettingsPropertyValue +{ +public: + XSettingsPropertyValue() + {} + + bool updateValue(const QVariant &value, int last_change_serial) + { + if (last_change_serial <= this->last_change_serial) + return false; + this->value = value; + this->last_change_serial = last_change_serial; + + return true; + } + + QVariant value; + int last_change_serial = -1; +}; + +/*## **Gtk (Graphical Toolkit) Settings** +| **Property** | **Example Value** | **Meaning / Purpose (for comments)** | +| ------------------------- | --------------------------------------- | ------------------------------------------------------------------------ | +| `Gdk/UnscaledDPI` | `98304` | Physical display DPI ×1024, before scaling. Used internally by GDK to compute display density. | +| `Gdk/WindowScalingFactor` | `2` | Global scaling factor for HiDPI screens (integer). Affects window buffer scaling in GDK. | +| `Gtk/RecentFilesEnabled` | `1` | Enables “recent files” feature in GTK applications. | +| `Gtk/ShowStatusShapes` | `0` | Whether to display shapes or indicators in status icons. | +| `Gtk/ShowInputMethodMenu` | `0` | Controls visibility of the input method (IME) menu in text widgets. | +| `Gtk/TimeoutInitial` | `200` | Keyboard repeat initial delay (milliseconds). | +| `Gtk/TimeoutRepeat` | `20` | Keyboard repeat rate (milliseconds between repeats). | +| `Gtk/DecorationLayout` | `":minimize,maximize,close"` | Titlebar button order (client-side decorations). | +| `Gtk/IMModule` | `"ibus"` | Input method module (e.g., ibus, fcitx, xim). | +| `Gtk/ShellShowsDesktop` | `0` | Whether the desktop shell displays the desktop. | +| `Gtk/MenuImages` | `0` | Show or hide icons in application menus. | +| `Gtk/EnablePrimaryPaste` | `1` | Enables middle-click paste using PRIMARY selection buffer. | +| `Gtk/KeynavUseCaret` | `0` | Enables caret navigation in widgets via keyboard. | +| `Gtk/ShellShowsAppMenu` | `0` | Whether the global app menu is displayed by shell. | +| `Gtk/CanChangeAccels` | `0` | Allows users to change keyboard shortcuts at runtime. | +| `Gtk/FontName` | `"Noto Sans, 10"` | Default UI font for GTK widgets. | +| `Gtk/CursorThemeSize` | `48` | Size of cursor icons (pixels). | +| `Gtk/DialogsUseHeader` | `1` | Use modern client-side header bars in dialogs. | +| `Gtk/ToolbarStyle` | `"both-horiz"` | Toolbar layout: icons, text, or both. | +| `Gtk/KeyThemeName` | `"Default"` | Keybinding theme (e.g., “Emacs”). | +| `Gtk/IMPreeditStyle` | `"callback"` | Preedit (composition) display style for IME input. | +| `Gtk/EnableAnimations` | `1` | Enables widget animations. | +| `Gtk/CursorThemeName` | `"breeze_cursors"` | Cursor icon theme. | +| `Gtk/ToolbarIconSize` | `"large"` | Default toolbar icon size. | +| `Gtk/IMStatusStyle` | `"callback"` | IME status display style. | +| `Gtk/RecentFilesMaxAge` | `-1` | Max age (in days) for items in “recent files”; `-1` disables expiration. | +| `Gtk/Modules` | `"canberra-gtk-module:gail:atk-bridge"` | List of GTK modules to load (sound, accessibility, etc.). | +| `Gtk/AutoMnemonics` | `1` | Enables automatic mnemonics (Alt shortcuts). | +| `Gtk/ColorScheme` | `""` | Deprecated; used for color overrides. | +| `Gtk/MenuBarAccel` | `"F10"` | Keyboard shortcut to focus the menu bar. | +| `Gtk/ColorPalette` | `"black:white:gray50:..."` | Palette used in color selection dialogs. | +| `Gtk/OverlayScrolling` | `1` | Enables overlay scrollbars. | +| `Gtk/SessionBusId` | `"1412549d..."` | D-Bus session bus ID (auto-generated). | +| `Gtk/ShowUnicodeMenu` | `0` | Enables Unicode character input menu in text widgets. | +| `Gtk/CursorBlinkTimeout` | `10` | Timeout before text cursor stops blinking (seconds). | +| `Gtk/ButtonImages` | `0` | Show icons on buttons. | +| `Gtk/TitlebarRightClick` | `"menu"` | Defines action when right-clicking titlebar (e.g., show menu). | +| `Gtk/TitlebarDoubleClick` | `"toggle-maximize"` | Action for double-click on titlebar. | +| `Gtk/TitlebarMiddleClick` | `"none"` | Action for middle-click on titlebar. | +| `Gtk/ThemeName` | | Explicit GTK widget theme (distinct from `Net/ThemeName`). | +| `Gtk/MonospaceFontName` | | Font for monospaced text (terminals, editors). | +| `Gtk/ApplicationPreferDarkTheme` | `0` | App-specific dark theme preference. 0 Light theme,1 Dark theme | +| `Gtk/PrimaryButtonWarpsSlider` | `0` | Determines if primary click warps scrollbar thumb. | + +## **Net (Freedesktop Desktop Interoperability) Settings** + +| **Property** | **Example Value** | **Meaning / Purpose (for comments)** | +| ------------------------------- | ----------------- | -------------------------------------------------------------- | +| `Net/DndDragThreshold` | `8` | Pixel distance threshold before drag-and-drop begins. | +| `Net/CursorBlinkTime` | `1000` | Blink interval of text cursor (milliseconds). | +| `Net/ThemeName` | `"deepin"` | Current GTK/desktop theme name. | +| `Net/DoubleClickTime` | `400` | Maximum time between clicks for double-click recognition (ms). | +| `Net/CursorBlink` | `1` | Whether the text cursor blinks. | +| `Net/FallbackIconTheme` | `"gnome"` | Fallback icon theme when main one is missing icons. | +| `Net/EnableEventSounds` | `1` | Enables UI event sounds (button click, notifications, etc.). | +| `Net/IconThemeName` | `"breeze"` | Icon theme used for GTK applications. | +| `Net/SoundThemeName` | `"__custom"` | Sound theme name for system sounds. | +| `Net/EnableInputFeedbackSounds` | `0` | Enables keypress feedback sounds. | +| `Net/PreferDarkTheme` | | Indicates preference for dark theme variants. | + +## **Xft (X FreeType) Font Settings** + +| **Property** | **Example Value** | **Meaning / Purpose (for comments)** | +| --------------- | ----------------- | ------------------------------------------------------------------- | +| `Xft/Antialias` | `1` | Enables anti-aliased font rendering. | +| `Xft/Hinting` | `1` | Enables font hinting. | +| `Xft/HintStyle` | `"hintslight"` | Hinting level (`hintnone`, `hintslight`, `hintmedium`, `hintfull`). | +| `Xft/DPI` | `196608` | Effective DPI (×1024). Controls font scaling. | +| `Xft/RGBA` | `"none"` | Subpixel rendering type (`rgb`, `bgr`, `vrgb`, `vbgr`, `none`). | +| `Xft/Lcdfilter` | | LCD filtering method (`lcddefault`, `lcdlight`, etc.) for subpixel rendering. |*/ + +class XSettings : public AbstractSettings +{ + Q_OBJECT +public: + enum XSettingsKey { + Unknown = 0, + + Xft_DPI, + Xft_Antialias, + Xft_Hinting, + Xft_HintStyle, + Xft_RGBA, + Xft_LCDFilter, + + Xcursor_Theme, + Xcursor_Size, + Xcursor_ThemeCore, + + Gdk_WindowScalingFactor, + Gdk_UnscaledDPI, + + Gtk_FontName, + Gtk_ThemeName, + Gtk_IconThemeName, + Gtk_CursorThemeName, + Gtk_CursorThemeSize, + Gtk_RecentFilesEnabled, + Gtk_ShowStatusShapes, + Gtk_ShowInputMethodMenu, + Gtk_TimeoutInitial, + Gtk_TimeoutRepeat, + Gtk_DecorationLayout, + Gtk_IMModule, + Gtk_ShellShowsDesktop, + Gtk_MenuImages, + Gtk_EnablePrimaryPaste, + Gtk_KeynavUseCaret, + Gtk_ShellShowsAppMenu, + Gtk_CanChangeAccels, + Gtk_DialogsUseHeader, + Gtk_ToolbarStyle, + Gtk_KeyThemeName, + Gtk_IMPreeditStyle, + Gtk_EnableAnimations, + Gtk_ToolbarIconSize, + Gtk_IMStatusStyle, + Gtk_RecentFilesMaxAge, + Gtk_Modules, + Gtk_AutoMnemonics, + Gtk_ColorScheme, + Gtk_MenuBarAccel, + Gtk_ColorPalette, + Gtk_OverlayScrolling, + Gtk_SessionBusId, + Gtk_ShowUnicodeMenu, + Gtk_CursorBlinkTimeout, + Gtk_ButtonImages, + Gtk_TitlebarRightClick, + Gtk_TitlebarDoubleClick, + Gtk_TitlebarMiddleClick, + Gtk_MonospaceFontName, + Gtk_ApplicationPreferDarkTheme, + Gtk_PrimaryButtonWarpsSlider, + + Net_DndDragThreshold, + Net_CursorBlinkTime, + Net_ThemeName, + Net_DoubleClickTime, + Net_CursorBlink, + Net_FallbackIconTheme, + Net_EnableEventSounds, + Net_IconThemeName, + Net_SoundThemeName, + Net_EnableInputFeedbackSounds, + Net_PreferDarkTheme + }; + Q_ENUM(XSettingsKey) + + enum XSettingsType { + XSettingsTypeInteger = 0, + XSettingsTypeString, + XSettingsTypeColor + }; + Q_ENUM(XSettingsType) + + explicit XSettings(xcb_connection_t *connection, QObject *parent = nullptr); + ~XSettings() override; + + static QByteArray toByteArray(XSettingsKey key); + + bool initialized() const override; + bool isEmpty() const override; + + bool contains(const QByteArray &property) const override; + QVariant getPropertyValue(const QByteArray &property) const override; + void setPropertyValue(const QByteArray &property, const QVariant &value) override; + QByteArrayList propertyList() const override; + void apply() override; + +private: + bool initX11(int screen, bool replace); + bool createWindow(int screen, xcb_window_t *out_win, xcb_timestamp_t *out_time); + bool manageScreen(int screen, xcb_window_t win, xcb_timestamp_t timestamp, bool replace); + QByteArray depopulateSettings(); + void populateSettings(const QByteArray &xSettings); + void setSettings(const QByteArray &data); + +private: + std::vector m_windows; + QMap m_settings; + xcb_atom_t m_notifyAtom = XCB_NONE; + xcb_atom_t m_signalAtom = XCB_NONE; + int m_serial = -1; +};