diff --git a/src/common/treelandlogging.cpp b/src/common/treelandlogging.cpp index fe198194b..6fc52597a 100644 --- a/src/common/treelandlogging.cpp +++ b/src/common/treelandlogging.cpp @@ -84,3 +84,6 @@ Q_LOGGING_CATEGORY(lcTlXwayland, "treeland.xwayland") // DDM integration Q_LOGGING_CATEGORY(lcTlDdm, "treeland.ddm") + +// XdgDialog module +Q_LOGGING_CATEGORY(lcTlXdgDialog, "treeland.protocol.xdg-dialog") diff --git a/src/common/treelandlogging.h b/src/common/treelandlogging.h index 103d83a0f..7bff69ea4 100644 --- a/src/common/treelandlogging.h +++ b/src/common/treelandlogging.h @@ -88,4 +88,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcTlXwayland) // DDM integration Q_DECLARE_LOGGING_CATEGORY(lcTlDdm) +// XdgDialog module +Q_DECLARE_LOGGING_CATEGORY(lcTlXdgDialog) + #endif // TREELAND_LOGGING_H diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 74329c2f7..e9066a968 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -46,3 +46,4 @@ add_subdirectory(wallpaper) add_subdirectory(activation) add_subdirectory(input-manager) add_subdirectory(keyboard-state-notify) +add_subdirectory(xdg-dialog) diff --git a/src/modules/xdg-dialog/CMakeLists.txt b/src/modules/xdg-dialog/CMakeLists.txt new file mode 100644 index 000000000..a491a0c50 --- /dev/null +++ b/src/modules/xdg-dialog/CMakeLists.txt @@ -0,0 +1,17 @@ +pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir) + +local_qtwayland_server_protocol_treeland(libtreeland + PROTOCOL ${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-dialog/xdg-dialog-v1.xml + BASENAME xdg-dialog-v1 +) + +impl_treeland( + NAME + module_xdg_dialog + SOURCE + ${CMAKE_SOURCE_DIR}/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.h + ${CMAKE_SOURCE_DIR}/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.cpp + ${CMAKE_BINARY_DIR}/src/modules/xdg-dialog/wayland-xdg-dialog-v1-server-protocol.c + INCLUDE + $ +) diff --git a/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.cpp b/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.cpp new file mode 100644 index 000000000..7b573a736 --- /dev/null +++ b/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.cpp @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2026 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 "xdgdialogmanagerinterfacev1.h" +#include "common/treelandlogging.h" +#include "qwayland-server-xdg-dialog-v1.h" + +#include +#include + +#include +#include + +#include +#include + +extern "C" { +#include +#include +} + +WAYLIB_SERVER_USE_NAMESPACE + +class XdgDialogManagerInterfaceV1Private; + +class XdgDialogV1Resource : public QtWaylandServer::xdg_dialog_v1 +{ +public: + XdgDialogV1Resource(XdgDialogManagerInterfaceV1Private *manager, + WSurface *surface, + struct ::wl_resource *toplevelResource, + struct ::wl_resource *resource) + : QtWaylandServer::xdg_dialog_v1(resource) + , m_manager(manager) + , m_surface(surface) + , m_toplevelResource(toplevelResource) + , m_modal(false) + { + } + + WSurface *surface() const { return m_surface; } + bool modal() const { return m_modal; } + struct ::wl_resource *toplevelResource() const { return m_toplevelResource; } + +protected: + void destroy(Resource *resource) override + { + wl_resource_destroy(resource->handle); + } + + void destroy_resource(Resource *) override; + void set_modal(Resource *) override; + void unset_modal(Resource *) override; + +private: + XdgDialogManagerInterfaceV1Private *m_manager; + QPointer m_surface; + struct ::wl_resource *m_toplevelResource; + uint m_modal : 1; +}; + +class XdgDialogManagerInterfaceV1Private : public QtWaylandServer::xdg_wm_dialog_v1 +{ +public: + explicit XdgDialogManagerInterfaceV1Private(XdgDialogManagerInterfaceV1 *q) + : QtWaylandServer::xdg_wm_dialog_v1() + , q(q) + { + } + + wl_global *globalHandle() const { return m_global; } + + void emitModalChanged(WSurface *surface, bool modal) + { + Q_EMIT q->surfaceModalChanged(surface, modal); + } + + void registerToplevel(struct ::wl_resource *toplevelResource) + { + m_usedToplevels.insert(toplevelResource); + } + + void unregisterToplevel(struct ::wl_resource *toplevelResource) + { + m_usedToplevels.remove(toplevelResource); + } + +protected: + void destroy_global() override + { + qCDebug(lcTlXdgDialog) << "xdg_wm_dialog_v1 global destroyed"; + } + + void destroy(Resource *resource) override + { + wl_resource_destroy(resource->handle); + } + + void get_xdg_dialog(Resource *resource, uint32_t id, struct ::wl_resource *toplevel) override + { + auto *wlrToplevel = wlr_xdg_toplevel_from_resource(toplevel); + if (!wlrToplevel) { + qCWarning(lcTlXdgDialog) << "get_xdg_dialog: invalid xdg_toplevel resource"; + return; + } + + if (m_usedToplevels.contains(toplevel)) { + qCWarning(lcTlXdgDialog) << "get_xdg_dialog: xdg_toplevel already has a xdg_dialog_v1"; + wl_resource_post_error(resource->handle, + error_already_used, + "xdg_toplevel already has a xdg_dialog_v1"); + return; + } + + auto *wlrXdgSurface = wlrToplevel->base; + auto *wlrSurface = wlrXdgSurface->surface; + auto *wsurface = WSurface::fromHandle(wlrSurface); + if (!wsurface) { + qCWarning(lcTlXdgDialog) << "get_xdg_dialog: no WSurface for xdg_toplevel"; + return; + } + + auto *dialogResource = wl_resource_create(resource->client(), + &xdg_dialog_v1_interface, + wl_resource_get_version(resource->handle), + id); + if (!dialogResource) { + wl_resource_post_no_memory(resource->handle); + return; + } + + registerToplevel(toplevel); + new XdgDialogV1Resource(this, wsurface, toplevel, dialogResource); + } + +private: + XdgDialogManagerInterfaceV1 *q; + QSet m_usedToplevels; +}; + +void XdgDialogV1Resource::destroy_resource(Resource *) +{ + m_manager->unregisterToplevel(m_toplevelResource); + delete this; +} + +void XdgDialogV1Resource::set_modal(Resource *) +{ + if (m_modal) + return; + m_modal = true; + if (m_surface) + m_manager->emitModalChanged(m_surface, true); +} + +void XdgDialogV1Resource::unset_modal(Resource *) +{ + if (!m_modal) + return; + m_modal = false; + if (m_surface) + m_manager->emitModalChanged(m_surface, false); +} + +XdgDialogManagerInterfaceV1::XdgDialogManagerInterfaceV1(QObject *parent) + : QObject(parent) + , WServerInterface() + , d(new XdgDialogManagerInterfaceV1Private(this)) +{ +} + +XdgDialogManagerInterfaceV1::~XdgDialogManagerInterfaceV1() = default; + +QByteArrayView XdgDialogManagerInterfaceV1::interfaceName() const +{ + return d->interfaceName(); +} + +void XdgDialogManagerInterfaceV1::create(WServer *server) +{ + d->init(server->handle()->handle(), InterfaceVersion); +} + +void XdgDialogManagerInterfaceV1::destroy([[maybe_unused]] WServer *server) +{ + d->globalRemove(); +} + +wl_global *XdgDialogManagerInterfaceV1::global() const +{ + return d->globalHandle(); +} diff --git a/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.h b/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.h new file mode 100644 index 000000000..7eda00c2d --- /dev/null +++ b/src/modules/xdg-dialog/xdgdialogmanagerinterfacev1.h @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 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 + +WAYLIB_SERVER_BEGIN_NAMESPACE +class WSurface; +WAYLIB_SERVER_END_NAMESPACE + +class XdgDialogManagerInterfaceV1Private; + +class XdgDialogManagerInterfaceV1 : public QObject, public WAYLIB_SERVER_NAMESPACE::WServerInterface +{ + Q_OBJECT +public: + explicit XdgDialogManagerInterfaceV1(QObject *parent = nullptr); + ~XdgDialogManagerInterfaceV1() override; + + QByteArrayView interfaceName() const override; + + static constexpr int InterfaceVersion = 1; + +Q_SIGNALS: + void surfaceModalChanged(WAYLIB_SERVER_NAMESPACE::WSurface *surface, bool modal); + +protected: + void create(WAYLIB_SERVER_NAMESPACE::WServer *server) override; + void destroy(WAYLIB_SERVER_NAMESPACE::WServer *server) override; + wl_global *global() const override; + +private: + std::unique_ptr d; +}; diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index cf3b0b839..a32e453cf 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -1621,6 +1621,19 @@ void Helper::init(Treeland::Treeland *treeland) } }); + m_xdgDialogManagerInterfaceV1 = m_server->attach(); + connect(m_xdgDialogManagerInterfaceV1, + &XdgDialogManagerInterfaceV1::surfaceModalChanged, + this, + [this](WSurface *wsurface, bool modal) { + auto *xdgToplevel = WXdgToplevelSurface::fromSurface(wsurface); + if (!xdgToplevel) { + qCDebug(lcTlXdgDialog) << "surfaceModalChanged for non-toplevel surface, ignoring"; + return; + } + xdgToplevel->setModal(modal); + }); + m_screensaverInterfaceV1 = m_server->attach(); m_outputPowerManager = qw_output_power_manager_v1::create(*m_server->handle()); diff --git a/src/seat/helper.h b/src/seat/helper.h index d2eb4b071..9d77bdf01 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -10,6 +10,7 @@ #include "modules/wallpaper/wallpapermanagerinterfacev1.h" #include "modules/wallpaper/wallpapernotifierinterfacev1.h" #include "modules/window-management/windowmanagementinterfacev1.h" +#include "modules/xdg-dialog/xdgdialogmanagerinterfacev1.h" #include "utils/fpsdisplaymanager.h" #include @@ -394,6 +395,7 @@ private Q_SLOTS: qw_output_power_manager_v1 *m_outputPowerManager = nullptr; qw_ext_foreign_toplevel_image_capture_source_manager_v1 *m_foreignToplevelImageCaptureManager = nullptr; ActivationManagerInterfaceV1 *m_activationManagerV1 = nullptr; + XdgDialogManagerInterfaceV1 *m_xdgDialogManagerInterfaceV1 = nullptr; ShellHandler *m_shellHandler = nullptr; WXdgDecorationManager *m_xdgDecorationManager = nullptr; WXdgToplevelTagManagerV1 *m_xdgToplevelTagManagerV1 = nullptr; diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 71b0dce04..e52f1570b 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -65,6 +66,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_attention(false) , m_resizable(false) , m_maximizable(false) + , m_modal(false) , m_appId(appId) { QQmlEngine::setContextForObject(this, qmlEngine->rootContext()); @@ -100,6 +102,7 @@ SurfaceWrapper::SurfaceWrapper(SurfaceWrapper *original, QQuickItem *parent) , m_attention(false) , m_resizable(false) , m_maximizable(false) + , m_modal(false) , m_appId(original->m_appId) { QQmlEngine::setContextForObject(this, m_engine->rootContext()); @@ -168,6 +171,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_attention(false) , m_resizable(false) , m_maximizable(false) + , m_modal(false) , m_appId(appId) { QQmlEngine::setContextForObject(this, qmlEngine->rootContext()); @@ -353,6 +357,16 @@ void SurfaceWrapper::setup() } updateSizeCapabilities(); + if (m_type == Type::XdgToplevel) { + auto *xdgToplevel = qobject_cast(m_shellSurface); + if (xdgToplevel) { + setModal(xdgToplevel->modal()); + connect(xdgToplevel, &WXdgToplevelSurface::modalChanged, + this, [this, xdgToplevel] { setModal(xdgToplevel->modal()); }); + } + } + // XWayland surfaces do not support xdg-dialog-v1 modal state yet + if (!m_prelaunchSplash) { setImplicitSize(m_surfaceItem->implicitWidth(), m_surfaceItem->implicitHeight()); connect(m_surfaceItem, &WSurfaceItem::implicitWidthChanged, this, [this] { @@ -2013,6 +2027,19 @@ bool SurfaceWrapper::isMaximizable() const return m_maximizable; } +bool SurfaceWrapper::modal() const +{ + return m_modal; +} + +void SurfaceWrapper::setModal(bool modal) +{ + if (m_modal == modal) + return; + m_modal = modal; + Q_EMIT modalChanged(); +} + bool SurfaceWrapper::blur() const { return m_blur; diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index facf14c16..1c27aba6c 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -83,6 +83,7 @@ class SurfaceWrapper : public QQuickItem Q_PROPERTY(bool isActivated READ isActivated NOTIFY isActivatedChanged FINAL) Q_PROPERTY(bool isResizable READ isResizable NOTIFY resizableChanged FINAL) Q_PROPERTY(bool isMaximizable READ isMaximizable NOTIFY maximizableChanged FINAL) + Q_PROPERTY(bool modal READ modal NOTIFY modalChanged FINAL) public: enum class Type @@ -243,6 +244,7 @@ class SurfaceWrapper : public QQuickItem // without pushing policy into waylib or QML. bool isResizable() const; bool isMaximizable() const; + bool modal() const; bool hasActiveCapability() const; bool hasCapability(WToplevelSurface::Capability cap) const; @@ -351,6 +353,7 @@ public Q_SLOTS: void isActivatedChanged(); void resizableChanged(); void maximizableChanged(); + void modalChanged(); void attentionChanged(); void surfaceItemCreated(); // Emitted once after surfaceItem is constructed void prelaunchSplashChanged(); @@ -398,6 +401,7 @@ public Q_SLOTS: Q_SLOT void onHideAnimationFinished(); void updateExplicitAlwaysOnTop(); void updateSizeCapabilities(); + void setModal(bool modal); void startMinimizeAnimation(const QRectF &iconGeometry, uint direction); Q_SLOT void onMinimizeAnimationFinished(); void startShowDesktopAnimation(bool show); @@ -475,6 +479,7 @@ public Q_SLOTS: uint m_attention : 1; uint m_resizable : 1; uint m_maximizable : 1; + uint m_modal : 1; SurfaceRole m_surfaceRole = SurfaceRole::Normal; quint32 m_autoPlaceYOffset = 0; QPoint m_clientRequstPos; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5f127b0db..6428a2c7a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,3 +8,4 @@ add_subdirectory(test_protocol_virtual-output) add_subdirectory(test_protocol_wallpaper-color) add_subdirectory(test_protocol_window-management) add_subdirectory(test_protocol_prelaunch-splash) +add_subdirectory(test_protocol_xdg-dialog) diff --git a/tests/test_protocol_xdg-dialog/CMakeLists.txt b/tests/test_protocol_xdg-dialog/CMakeLists.txt new file mode 100644 index 000000000..b94d866af --- /dev/null +++ b/tests/test_protocol_xdg-dialog/CMakeLists.txt @@ -0,0 +1,21 @@ +find_package(Qt6 REQUIRED COMPONENTS Test) + +add_executable(test_protocol_xdg-dialog + main.cpp +) + +target_link_libraries(test_protocol_xdg-dialog + PRIVATE + libtreeland + Qt::Test +) + +add_test(NAME test_protocol_xdg-dialog COMMAND test_protocol_xdg-dialog) + +set_property(TEST test_protocol_xdg-dialog PROPERTY + ENVIRONMENT "QT_QPA_PLATFORM=offscreen" +) + +set_property(TEST test_protocol_xdg-dialog PROPERTY + TIMEOUT 10 +) diff --git a/tests/test_protocol_xdg-dialog/main.cpp b/tests/test_protocol_xdg-dialog/main.cpp new file mode 100644 index 000000000..fa349d195 --- /dev/null +++ b/tests/test_protocol_xdg-dialog/main.cpp @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2026 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 "modules/xdg-dialog/xdgdialogmanagerinterfacev1.h" + +#include + +#include +#include + +WAYLIB_SERVER_USE_NAMESPACE + +class XdgDialogV1Test : public QObject +{ + Q_OBJECT + + WServer *m_server = nullptr; + XdgDialogManagerInterfaceV1 *m_protocol = nullptr; + +public: + XdgDialogV1Test(QObject *parent = nullptr) + : QObject(parent) + { + } + +private Q_SLOTS: + + void initTestCase() + { + m_server = new WServer(); + } + + void testCreateProtocol() + { + m_protocol = m_server->attach(); + QVERIFY(m_protocol != nullptr); + } + + void testVerifyProtocol() + { + QVERIFY(m_protocol != nullptr); + } + + void testSignals() + { + QVERIFY(m_protocol != nullptr); + + QSignalSpy modalSpy(m_protocol, &XdgDialogManagerInterfaceV1::surfaceModalChanged); + QVERIFY(modalSpy.isValid()); + } + + void cleanupTestCase() + { + m_server->deleteLater(); + m_server = nullptr; + m_protocol = nullptr; + } +}; + +QTEST_MAIN(XdgDialogV1Test) +#include "main.moc" diff --git a/waylib/src/server/protocols/wxdgtoplevelsurface.cpp b/waylib/src/server/protocols/wxdgtoplevelsurface.cpp index e14647dbe..96c768fb5 100644 --- a/waylib/src/server/protocols/wxdgtoplevelsurface.cpp +++ b/waylib/src/server/protocols/wxdgtoplevelsurface.cpp @@ -48,6 +48,7 @@ class Q_DECL_HIDDEN WXdgToplevelSurfacePrivate : public WToplevelSurfacePrivate uint maximized:1; uint minimized:1; uint fullscreen:1; + uint modal:1; QSize minimumSize; QSize maximumSize = QSize(INT_MAX, INT_MAX); @@ -62,6 +63,7 @@ WXdgToplevelSurfacePrivate::WXdgToplevelSurfacePrivate(WXdgToplevelSurface *qq, , maximized(false) , minimized(false) , fullscreen(false) + , modal(false) { initHandle(hh); } @@ -344,6 +346,12 @@ QString WXdgToplevelSurface::description() const return d->description; } +bool WXdgToplevelSurface::modal() const +{ + W_DC(WXdgToplevelSurface); + return d->modal; +} + bool WXdgToplevelSurface::isInitialized() const { W_DC(WXdgToplevelSurface); @@ -384,6 +392,15 @@ void WXdgToplevelSurface::setDescription(const QString &description) Q_EMIT descriptionChanged(); } +void WXdgToplevelSurface::setModal(bool modal) +{ + W_D(WXdgToplevelSurface); + if (d->modal == modal) + return; + d->modal = modal; + Q_EMIT modalChanged(); +} + void WXdgToplevelSurface::setResizeing(bool resizeing) { handle()->set_resizing(resizeing); diff --git a/waylib/src/server/protocols/wxdgtoplevelsurface.h b/waylib/src/server/protocols/wxdgtoplevelsurface.h index 4a8480d09..a2a7c0694 100644 --- a/waylib/src/server/protocols/wxdgtoplevelsurface.h +++ b/waylib/src/server/protocols/wxdgtoplevelsurface.h @@ -22,6 +22,7 @@ class WAYLIB_SERVER_EXPORT WXdgToplevelSurface : public WXdgSurface Q_PROPERTY(WXdgSurface* parentXdgSurface READ parentXdgSurface NOTIFY parentXdgSurfaceChanged FINAL) Q_PROPERTY(QString tag READ tag NOTIFY tagChanged FINAL) Q_PROPERTY(QString description READ description NOTIFY descriptionChanged FINAL) + Q_PROPERTY(bool modal READ modal NOTIFY modalChanged FINAL) QML_NAMED_ELEMENT(WaylandXdgToplevelSurface) QML_UNCREATABLE("Only create in C++") @@ -57,8 +58,10 @@ class WAYLIB_SERVER_EXPORT WXdgToplevelSurface : public WXdgSurface QString tag() const; QString description() const; + bool modal() const; void setTag(const QString &tag); void setDescription(const QString &description); + void setModal(bool modal); bool isInitialized() const override; @@ -79,6 +82,7 @@ public Q_SLOTS: void tagChanged(); void descriptionChanged(); + void modalChanged(); }; WAYLIB_SERVER_END_NAMESPACE