From eedb63c1c2f06ab1b0a10675a3e4829407dc588c Mon Sep 17 00:00:00 2001 From: luhongxu Date: Thu, 17 Sep 2026 22:44:44 +0800 Subject: [PATCH] feat(dde-shell): adapt to dde-shell-unstable-v2 protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement treeland_dde_shell_manager_v2 and treeland_dde_shell_surface_v2 (treeland-protocols 0.7.0) and register the v2 global alongside the deprecated v1 one for the migration period. v2 brings a 0-based overlay default role, a set_skip_flags bitfield, mutually exclusive position/cursor placement hints resolved to global coordinates on request, an already_shell_surface error on duplicate creation, destroy as the first request, and auto-destroy of the surface resource when the wl_surface is destroyed. Make (0,0) a valid fixed position: track position-request presence with an explicit hasClientRequstPos flag instead of the QPoint::isNull() sentinel, and re-apply placement immediately when the placement mode switches at runtime. Raise the treeland-protocols build dependency to >= 0.7.0. v1 behavior changes: set_surface_position/set_auto_placement now take effect immediately at runtime instead of on the next arrange pass; set_surface_position(0,0) pins the surface to the anchor origin instead of falling back to auto placement. Log: v1 set_surface_position applies immediately; (0,0) pins to origin Influence: 1. Run the v2 protocol suite (test_treeland_dde_shell_v2, 14 cases) and the v1 regression suite (28 cases) 2. With a v2 client, verify position hints land at the resolved global position, including (0,0) on multi-screen setups 3. Verify runtime switches between position hint and cursor placement re-place the surface immediately 4. Regression-test v1 windows: set_surface_position(0,0) now pins to the origin instead of reverting to auto layout feat(dde-shell): 适配 dde-shell-unstable-v2 协议 实现 treeland_dde_shell_manager_v2 与 treeland_dde_shell_surface_v2 (treeland-protocols 0.7.0),迁移期将 v2 全局对象与已废弃的 v1 并行 注册。v2 采用 0 基 overlay 默认 role、set_skip_flags 位域、互斥的 position/cursor 定位 hint(请求时解析为全局坐标)、重复创建报 already_shell_surface 错误、destroy 位于首请求位,且 wl_surface 销毁 时自动销毁 surface 资源。 使 (0,0) 成为合法固定位置:以显式 hasClientRequstPos 标志替代 QPoint::isNull() 哨兵,运行时切换定位模式时立即重新摆放。 v1 行为变化:set_surface_position/set_auto_placement 运行时立即生效 (此前延迟到下次 arrange);set_surface_position(0,0) 固定到锚点原点 而非退回自动布局。 --- .../test_super_overlay_surface/CMakeLists.txt | 2 +- .../ddeshellwayland.cpp | 113 +++--- .../ddeshellwayland.h | 16 +- .../ddeshelsurfacewindow.cpp | 140 +++++-- .../ddeshelsurfacewindow.h | 26 +- examples/test_super_overlay_surface/main.cpp | 50 ++- src/core/shellhandler.cpp | 89 +++-- src/core/shellhandler.h | 2 + src/modules/dde-shell/CMakeLists.txt | 11 + src/modules/dde-shell/ddeshellattached.cpp | 5 + src/modules/dde-shell/ddeshellattached.h | 5 + .../dde-shell/ddeshellmanagerinterfacev1.cpp | 5 + .../dde-shell/ddeshellmanagerinterfacev1.h | 5 + .../dde-shell/ddeshellmanagerinterfacev2.cpp | 350 ++++++++++++++++++ .../dde-shell/ddeshellmanagerinterfacev2.h | 103 ++++++ src/output/output.cpp | 29 +- src/output/output.h | 2 +- src/seat/helper.cpp | 3 + src/seat/helper.h | 2 + src/surface/surfacewrapper.cpp | 27 +- src/surface/surfacewrapper.h | 20 +- tests/protocols/CMakeLists.txt | 1 + tests/protocols/INDEX.md | 1 + .../treeland-dde-shell-desktop-v1/setup.cpp | 4 +- .../treeland-dde-shell-desktop-v1.c | 2 +- .../treeland-dde-shell-v2/CMakeLists.txt | 6 + .../protocols/treeland-dde-shell-v2/README.md | 49 +++ .../protocols/treeland-dde-shell-v2/setup.cpp | 59 +++ .../treeland-dde-shell-v2.c | 286 ++++++++++++++ .../treeland-dde-shell-v2.h | 65 ++++ 30 files changed, 1316 insertions(+), 162 deletions(-) create mode 100644 src/modules/dde-shell/ddeshellmanagerinterfacev2.cpp create mode 100644 src/modules/dde-shell/ddeshellmanagerinterfacev2.h create mode 100644 tests/protocols/treeland-dde-shell-v2/CMakeLists.txt create mode 100644 tests/protocols/treeland-dde-shell-v2/README.md create mode 100644 tests/protocols/treeland-dde-shell-v2/setup.cpp create mode 100644 tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.c create mode 100644 tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.h diff --git a/examples/test_super_overlay_surface/CMakeLists.txt b/examples/test_super_overlay_surface/CMakeLists.txt index 2d18f709df..5e9d578293 100644 --- a/examples/test_super_overlay_surface/CMakeLists.txt +++ b/examples/test_super_overlay_surface/CMakeLists.txt @@ -15,7 +15,7 @@ qt_add_executable(${BIN_NAME} qt_generate_wayland_protocol_client_sources(${BIN_NAME} NO_INCLUDE_CORE_ONLY FILES - ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-dde-shell-v1.xml + ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-dde-shell-unstable-v2.xml ) target_link_libraries(${BIN_NAME} diff --git a/examples/test_super_overlay_surface/ddeshellwayland.cpp b/examples/test_super_overlay_surface/ddeshellwayland.cpp index f2491b758d..62eacf39f1 100644 --- a/examples/test_super_overlay_surface/ddeshellwayland.cpp +++ b/examples/test_super_overlay_surface/ddeshellwayland.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2024 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-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 "ddeshellwayland.h" @@ -9,25 +9,25 @@ #include #include -#define TREELANDDDESHELLMANAGERV1VERSION 1 +#define TREELANDDDESHELLMANAGERV2VERSION 1 -class DDEShellManageV1 - : public QWaylandClientExtensionTemplate - , public QtWayland::treeland_dde_shell_manager_v1 +class DDEShellManageV2 + : public QWaylandClientExtensionTemplate + , public QtWayland::treeland_dde_shell_manager_v2 { public: - DDEShellManageV1() - : QWaylandClientExtensionTemplate(TREELANDDDESHELLMANAGERV1VERSION) + DDEShellManageV2() + : QWaylandClientExtensionTemplate(TREELANDDDESHELLMANAGERV2VERSION) { initialize(); } }; -class DDEShellSurface : public QtWayland::treeland_dde_shell_surface_v1 +class DDEShellSurface : public QtWayland::treeland_dde_shell_surface_v2 { public: - DDEShellSurface(struct ::treeland_dde_shell_surface_v1 *id) - : QtWayland::treeland_dde_shell_surface_v1(id) + DDEShellSurface(struct ::treeland_dde_shell_surface_v2 *id) + : QtWayland::treeland_dde_shell_surface_v2(id) { } @@ -41,13 +41,13 @@ class ShellIntegrationSingleton { public: ShellIntegrationSingleton(); - std::unique_ptr shellManager; + std::unique_ptr shellManager; QHash windows; }; ShellIntegrationSingleton::ShellIntegrationSingleton() { - shellManager = std::make_unique(); + shellManager = std::make_unique(); } Q_GLOBAL_STATIC(ShellIntegrationSingleton, s_waylandIntegration) @@ -71,6 +71,16 @@ DDEShellWayland::DDEShellWayland(QWindow *window) , m_window(window) { m_window->installEventFilter(this); + + // The QWaylandClientExtension binds the global asynchronously: at first + // construction it is usually not active yet. Retry creating the shell + // surface once the manager becomes active, otherwise requests sent before + // activation would only be cached locally and never reach the compositor. + connect(s_waylandIntegration->shellManager.get(), + &DDEShellManageV2::activeChanged, + this, + &DDEShellWayland::surfaceCreated); + platformSurfaceCreated(window); } @@ -84,6 +94,13 @@ bool DDEShellWayland::eventFilter(QObject *watched, QEvent *event) auto surfaceEvent = static_cast(event); if (surfaceEvent->surfaceEventType() == QPlatformSurfaceEvent::SurfaceCreated) { platformSurfaceCreated(window); + } else if (surfaceEvent->surfaceEventType() + == QPlatformSurfaceEvent::SurfaceAboutToBeDestroyed) { + // The v2 protocol requires the shell surface to be destroyed + // before the wl_surface. Destroy it now; otherwise the shell + // surface proxy would outlive the wl_surface and the compositor + // rejects the stale object ("invalid object") on teardown. + m_shellSurface.reset(); } } return false; @@ -96,12 +113,15 @@ void DDEShellWayland::setPosition(const QPoint &position) } m_position = position; + m_cursorPlacement.reset(); if (m_shellSurface) { - m_shellSurface->set_surface_position(m_position->x(), m_position->y()); + // v2 position hint is output-relative; passing a null output anchors + // the coordinates at the primary output origin. + m_shellSurface->set_position_hint(nullptr, position.x(), position.y()); } } -void DDEShellWayland::setRole(QtWayland::treeland_dde_shell_surface_v1::role role) +void DDEShellWayland::setRole(QtWayland::treeland_dde_shell_surface_v2::role role) { if (role == m_role) { return; @@ -113,51 +133,44 @@ void DDEShellWayland::setRole(QtWayland::treeland_dde_shell_surface_v1::role rol } } -void DDEShellWayland::setAutoPlacement(int32_t yOffset) +void DDEShellWayland::setCursorPlacement(int32_t xOffset, int32_t yOffset) { - if (yOffset == m_yOffset) { + const QPoint offset(xOffset, yOffset); + if (offset == m_cursorPlacement) { return; } - m_yOffset = yOffset; + m_cursorPlacement = offset; + m_position.reset(); if (m_shellSurface) { - m_shellSurface->set_auto_placement(yOffset); + m_shellSurface->set_cursor_placement_hint(xOffset, yOffset); } } void DDEShellWayland::setSkipSwitcher(uint32_t skip) { - if (skip == m_skipSwitcher) { - return; - } - - m_skipSwitcher = skip; + m_skipFlags = skip ? (m_skipFlags | QtWayland::treeland_dde_shell_surface_v2::skip_flag_switcher) + : (m_skipFlags & ~QtWayland::treeland_dde_shell_surface_v2::skip_flag_switcher); if (m_shellSurface) { - m_shellSurface->set_skip_switcher(skip); + m_shellSurface->set_skip_flags(m_skipFlags); } } void DDEShellWayland::setSkipDockPreview(uint32_t skip) { - if (skip == m_skipDockPreview) { - return; - } - - m_skipDockPreview = skip; + m_skipFlags = skip ? (m_skipFlags | QtWayland::treeland_dde_shell_surface_v2::skip_flag_dock_preview) + : (m_skipFlags & ~QtWayland::treeland_dde_shell_surface_v2::skip_flag_dock_preview); if (m_shellSurface) { - m_shellSurface->set_skip_dock_preview(skip); + m_shellSurface->set_skip_flags(m_skipFlags); } } void DDEShellWayland::setSkipMutiTaskView(uint32_t skip) { - if (skip == m_skipMutiTaskView) { - return; - } - - m_skipMutiTaskView = skip; + m_skipFlags = skip ? (m_skipFlags | QtWayland::treeland_dde_shell_surface_v2::skip_flag_multitask_view) + : (m_skipFlags & ~QtWayland::treeland_dde_shell_surface_v2::skip_flag_multitask_view); if (m_shellSurface) { - m_shellSurface->set_skip_muti_task_view(skip); + m_shellSurface->set_skip_flags(m_skipFlags); } } @@ -194,11 +207,18 @@ void DDEShellWayland::platformSurfaceCreated(QWindow *window) void DDEShellWayland::surfaceCreated() { - struct wl_surface *surface = nullptr; if (!s_waylandIntegration->shellManager || !s_waylandIntegration->shellManager->isActive()) { return; } + // Already created for the current wayland surface: avoid binding the same + // wl_surface twice (the compositor raises already_shell_surface for a + // second attempt). activeChanged and surfaceCreated may both fire. + if (m_shellSurface) { + return; + } + + struct wl_surface *surface = nullptr; if (auto waylandWindow = m_window->nativeInterface()) { surface = waylandWindow->surface(); @@ -216,24 +236,15 @@ void DDEShellWayland::surfaceCreated() } if (m_position) { - m_shellSurface->set_surface_position(m_position->x(), m_position->y()); - } - - if (m_yOffset) { - m_shellSurface->set_auto_placement(m_yOffset.value()); + m_shellSurface->set_position_hint(nullptr, m_position->x(), m_position->y()); } - if (m_skipDockPreview) { - m_shellSurface->set_skip_dock_preview(m_skipDockPreview.value()); + if (m_cursorPlacement) { + m_shellSurface->set_cursor_placement_hint(m_cursorPlacement->x(), + m_cursorPlacement->y()); } - if (m_skipMutiTaskView) { - m_shellSurface->set_skip_muti_task_view(m_skipMutiTaskView.value()); - } - - if (m_skipSwitcher) { - m_shellSurface->set_skip_switcher(m_skipSwitcher.value()); - } + m_shellSurface->set_skip_flags(m_skipFlags); if (!m_acceptKeyboardFocus) { m_shellSurface->set_accept_keyboard_focus(m_acceptKeyboardFocus); diff --git a/examples/test_super_overlay_surface/ddeshellwayland.h b/examples/test_super_overlay_surface/ddeshellwayland.h index f83e5566a5..e8a667da4b 100644 --- a/examples/test_super_overlay_surface/ddeshellwayland.h +++ b/examples/test_super_overlay_surface/ddeshellwayland.h @@ -1,9 +1,9 @@ -// Copyright (C) 2024 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-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 "qwayland-treeland-dde-shell-v1.h" +#include "qwayland-treeland-dde-shell-unstable-v2.h" #include #include @@ -18,8 +18,8 @@ class DDEShellWayland : public QObject ~DDEShellWayland(); void setPosition(const QPoint &position); - void setRole(QtWayland::treeland_dde_shell_surface_v1::role role); - void setAutoPlacement(int32_t yOffset); + void setRole(QtWayland::treeland_dde_shell_surface_v2::role role); + void setCursorPlacement(int32_t xOffset, int32_t yOffset); void setSkipSwitcher(uint32_t skip); void setSkipDockPreview(uint32_t skip); void setSkipMutiTaskView(uint32_t skip); @@ -34,11 +34,9 @@ class DDEShellWayland : public QObject QWindow *m_window = nullptr; std::optional m_position; - std::optional m_role; - std::optional m_yOffset; - std::optional m_skipSwitcher; - std::optional m_skipDockPreview; - std::optional m_skipMutiTaskView; + std::optional m_role; + std::optional m_cursorPlacement; + uint32_t m_skipFlags = 0; bool m_acceptKeyboardFocus = true; std::unique_ptr m_shellSurface; diff --git a/examples/test_super_overlay_surface/ddeshelsurfacewindow.cpp b/examples/test_super_overlay_surface/ddeshelsurfacewindow.cpp index c2d0659c2c..bf51f9ff78 100644 --- a/examples/test_super_overlay_surface/ddeshelsurfacewindow.cpp +++ b/examples/test_super_overlay_surface/ddeshelsurfacewindow.cpp @@ -1,17 +1,92 @@ -// Copyright (C) 2024 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-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 "ddeshelsurfacewindow.h" #include "ddeshellwayland.h" +#include +#include +#include +#include #include +#include +#include DDEShelSurfaceWindow::DDEShelSurfaceWindow(TestMode mode, QWidget *parent) : QWidget{ parent } , m_mode(mode) { - [[maybe_unused]] QLineEdit *l = new QLineEdit(this); + setWindowTitle(QStringLiteral("treeland_dde_shell_unstable_v2 示例")); + resize(520, 460); + + auto *title = new QLabel(QStringLiteral("

treeland_dde_shell_unstable_v2 示例

" + "测试 DDE Shell surface 的 role / skip 位域 / 键盘焦点")); + title->setWordWrap(true); + + // role 选择 + m_roleCombo = new QComboBox; + m_roleCombo->addItem(QStringLiteral("role_overlay(overlay 层,位于普通窗口之上)"), int(QtWayland::treeland_dde_shell_surface_v2::role_overlay)); + m_roleCombo->addItem(QStringLiteral("role_normal(普通窗口层)"), int(QtWayland::treeland_dde_shell_surface_v2::role_normal)); + + // skip 位域 + auto *skipGroup = new QGroupBox(QStringLiteral("set_skip_flags 位域")); + auto *skipLayout = new QVBoxLayout(skipGroup); + m_skipSwitcher = new QCheckBox(QStringLiteral("skip_flag_switcher (0x1)")); + m_skipDockPreview = new QCheckBox(QStringLiteral("skip_flag_dock_preview (0x2)")); + m_skipMultitask = new QCheckBox(QStringLiteral("skip_flag_multitask_view (0x4)")); + skipLayout->addWidget(m_skipSwitcher); + skipLayout->addWidget(m_skipDockPreview); + skipLayout->addWidget(m_skipMultitask); + + // 键盘焦点 + auto *focusGroup = new QGroupBox(QStringLiteral("set_accept_keyboard_focus")); + auto *focusLayout = new QVBoxLayout(focusGroup); + m_acceptFocus = new QCheckBox(QStringLiteral("接受键盘焦点")); + m_acceptFocus->setChecked(true); + m_focusProbe = new QLineEdit; + m_focusProbe->setPlaceholderText(QStringLiteral("点击此处验证是否能获得键盘焦点")); + focusLayout->addWidget(m_acceptFocus); + focusLayout->addWidget(m_focusProbe); + + m_applyButton = new QPushButton(QStringLiteral("应用")); + m_statusLabel = new QLabel; + m_statusLabel->setWordWrap(true); + m_statusLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + + auto *layout = new QVBoxLayout(this); + layout->addWidget(title); + layout->addWidget(m_roleCombo); + layout->addWidget(skipGroup); + layout->addWidget(focusGroup); + layout->addWidget(m_applyButton); + layout->addWidget(m_statusLabel); + + connect(m_roleCombo, &QComboBox::currentIndexChanged, this, [this] { + DDEShellWayland::get(windowHandle()) + ->setRole(static_cast( + m_roleCombo->currentData().toInt())); + updateStatusLabel(); + }); + connect(m_applyButton, &QPushButton::clicked, this, &DDEShelSurfaceWindow::apply); + connect(m_acceptFocus, &QCheckBox::toggled, this, [this](bool on) { + DDEShellWayland::get(windowHandle())->setAcceptKeyboardFocus(on); + updateStatusLabel(); + }); + connect(m_skipSwitcher, &QCheckBox::toggled, this, [this](bool on) { + DDEShellWayland::get(windowHandle())->setSkipSwitcher(on); + updateStatusLabel(); + }); + connect(m_skipDockPreview, &QCheckBox::toggled, this, [this](bool on) { + DDEShellWayland::get(windowHandle())->setSkipDockPreview(on); + updateStatusLabel(); + }); + connect(m_skipMultitask, &QCheckBox::toggled, this, [this](bool on) { + DDEShellWayland::get(windowHandle())->setSkipMutiTaskView(on); + updateStatusLabel(); + }); + + updateStatusLabel(); } void DDEShelSurfaceWindow::showEvent([[maybe_unused]] QShowEvent *event) @@ -21,31 +96,52 @@ void DDEShelSurfaceWindow::showEvent([[maybe_unused]] QShowEvent *event) } } +void DDEShelSurfaceWindow::updateStatusLabel() +{ + const QString modeText = (m_mode == TestSetPosition) + ? QStringLiteral("set_position_hint (x=%1, y=%2)").arg(m_position.x()).arg(m_position.y()) + : QStringLiteral("set_cursor_placement_hint (x=%1, y=%2)").arg(m_cursorOffset.x()).arg(m_cursorOffset.y()); + + const int skip = (m_skipSwitcher->isChecked() ? 0x1 : 0) + | (m_skipDockPreview->isChecked() ? 0x2 : 0) + | (m_skipMultitask->isChecked() ? 0x4 : 0); + + const QString roleText = m_roleCombo->currentData().toInt() == int(QtWayland::treeland_dde_shell_surface_v2::role_overlay) + ? QStringLiteral("overlay") + : QStringLiteral("normal"); + + m_statusLabel->setText( + QStringLiteral("当前配置:%1\n" + "role: %2\n" + "skip_flags: 0x%3\n" + "accept_keyboard_focus: %4") + .arg(modeText) + .arg(roleText) + .arg(skip, 0, 16) + .arg(m_acceptFocus->isChecked() ? QStringLiteral("true") : QStringLiteral("false"))); +} + void DDEShelSurfaceWindow::apply() { - if (TestSetPosition == m_mode) { - // 1 ----Convenient for the client to set the position of the surface - DDEShellWayland::get(windowHandle())->setPosition(QPoint(100, 100)); - // ---------------------------------------------------------------- - } + auto *dde = DDEShellWayland::get(windowHandle()); - if (TestSetAutoPlace == m_mode) { - // 2. Set the vertical alignment of the surface within the cursor width, - // y offset is 30 relative to the cursor bottom.------------------- - DDEShellWayland::get(windowHandle())->setAutoPlacement(30); - - // Setting this bit will indicate that the window prefers not to be - // listed in a switcher/dock-preview/mutitask-view - DDEShellWayland::get(windowHandle())->setSkipDockPreview(true); - DDEShellWayland::get(windowHandle())->setSkipMutiTaskView(true); - DDEShellWayland::get(windowHandle())->setSkipSwitcher(true); - DDEShellWayland::get(windowHandle())->setAcceptKeyboardFocus(false); + dde->setRole(static_cast(m_roleCombo->currentData().toInt())); + + if (m_mode == TestSetPosition) { + // 1 ---- Convenient for the client to set the position of the surface + dde->setPosition(m_position); + // ---------------------------------------------------------------- + } else { + // 2. Place the surface relative to the cursor.-------------------- + dde->setCursorPlacement(m_cursorOffset.x(), m_cursorOffset.y()); // --------------------------------------------------------------- } - // Do not use setPosition and setAutoPlacement at the same time, there will - // be conflicts !!! + // Setting these bits will indicate that the window prefers not to be + // listed in a switcher/dock-preview/mutitask-view. The skip flags are + // applied immediately via their toggled handlers; the apply button only + // (re)sends role / position / cursor placement / keyboard focus. + dde->setAcceptKeyboardFocus(m_acceptFocus->isChecked()); - DDEShellWayland::get(windowHandle()) - ->setRole(QtWayland::treeland_dde_shell_surface_v1::role_overlay); + updateStatusLabel(); } diff --git a/examples/test_super_overlay_surface/ddeshelsurfacewindow.h b/examples/test_super_overlay_surface/ddeshelsurfacewindow.h index f1a74815c7..a4255630df 100644 --- a/examples/test_super_overlay_surface/ddeshelsurfacewindow.h +++ b/examples/test_super_overlay_surface/ddeshelsurfacewindow.h @@ -1,10 +1,17 @@ -// Copyright (C) 2024 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-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 +class QCheckBox; +class QComboBox; +class QLabel; +class QLineEdit; +class QPushButton; + class DDEShelSurfaceWindow : public QWidget { Q_OBJECT @@ -12,17 +19,32 @@ class DDEShelSurfaceWindow : public QWidget enum TestMode { TestSetPosition, - TestSetAutoPlace + TestSetCursorPlacement }; explicit DDEShelSurfaceWindow(TestMode mode, QWidget *parent = nullptr); + void setPositionHint(QPoint pos) { m_position = pos; } + void setCursorOffset(QPoint offset) { m_cursorOffset = offset; } + protected: void showEvent(QShowEvent *event) override; private: void apply(); + void updateStatusLabel(); private: TestMode m_mode; + QPoint m_position{ 100, 100 }; + QPoint m_cursorOffset{ 0, 30 }; + + QComboBox *m_roleCombo = nullptr; + QCheckBox *m_skipSwitcher = nullptr; + QCheckBox *m_skipDockPreview = nullptr; + QCheckBox *m_skipMultitask = nullptr; + QCheckBox *m_acceptFocus = nullptr; + QLineEdit *m_focusProbe = nullptr; + QPushButton *m_applyButton = nullptr; + QLabel *m_statusLabel = nullptr; }; diff --git a/examples/test_super_overlay_surface/main.cpp b/examples/test_super_overlay_surface/main.cpp index e6bd6e65f8..6eb16e4595 100644 --- a/examples/test_super_overlay_surface/main.cpp +++ b/examples/test_super_overlay_surface/main.cpp @@ -1,4 +1,4 @@ -// Copyright (C) 2024 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-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 "ddeshelsurfacewindow.h" @@ -6,9 +6,8 @@ #include #include #include -#include -#include -#include + +#include int main(int argc, char *argv[]) { @@ -16,21 +15,46 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); QCommandLineParser parser; - QCommandLineOption autoPlaceOption( - QStringList{ QStringLiteral("auto_place"), QStringLiteral("auto") }, - ("Set the vertical alignment of the surface within the cursor width")); - parser.setApplicationDescription("default is TestSetPosition, add --auto is TestSetAutoPlace"); + QCommandLineOption positionOption( + QStringList{ QStringLiteral("pos") }, + QStringLiteral("set_position_hint 的坐标 (x,y),默认 100,100"), + QStringLiteral("x,y")); + QCommandLineOption cursorOffsetOption( + QStringList{ QStringLiteral("offset") }, + QStringLiteral("set_cursor_placement_hint 的光标偏移 (x,y),默认 0,30"), + QStringLiteral("x,y")); + parser.setApplicationDescription( + QStringLiteral("treeland_dde_shell_unstable_v2 示例。\n" + "--pos 使用 set_position_hint(固定坐标)模式,\n" + "--offset 使用 set_cursor_placement_hint(相对光标放置)模式,\n" + "两者互斥,只能指定其一;都不指定时默认固定坐标模式。")); parser.addHelpOption(); - parser.addOption(autoPlaceOption); + parser.addOption(positionOption); + parser.addOption(cursorOffsetOption); parser.process(app); + if (parser.isSet(positionOption) && parser.isSet(cursorOffsetOption)) { + fputs("error: --pos and --offset are mutually exclusive\n", stderr); + return 1; + } + DDEShelSurfaceWindow::TestMode mode = DDEShelSurfaceWindow::TestMode::TestSetPosition; - if (parser.isSet(autoPlaceOption)) - mode = DDEShelSurfaceWindow::TestMode::TestSetAutoPlace; + if (parser.isSet(cursorOffsetOption)) + mode = DDEShelSurfaceWindow::TestMode::TestSetCursorPlacement; DDEShelSurfaceWindow window(mode); - window.setWindowTitle("test for super overlay surface"); - window.resize(600, 200); + + if (parser.isSet(positionOption)) { + const QStringList parts = parser.value(positionOption).split(QLatin1Char(',')); + if (parts.size() == 2) + window.setPositionHint(QPoint(parts[0].toInt(), parts[1].toInt())); + } + if (parser.isSet(cursorOffsetOption)) { + const QStringList parts = parser.value(cursorOffsetOption).split(QLatin1Char(',')); + if (parts.size() == 2) + window.setCursorOffset(QPoint(parts[0].toInt(), parts[1].toInt())); + } + window.show(); return app.exec(); diff --git a/src/core/shellhandler.cpp b/src/core/shellhandler.cpp index 5114af2001..126f5b21d3 100644 --- a/src/core/shellhandler.cpp +++ b/src/core/shellhandler.cpp @@ -11,6 +11,7 @@ #include "modules/app-id-resolver/appidresolver.h" #include "modules/snap-target/snaphandler.h" #include "modules/dde-shell/ddeshellmanagerinterfacev1.h" +#include "modules/dde-shell/ddeshellmanagerinterfacev2.h" #include "modules/foreign-toplevel/foreigntoplevelmanagerv2.h" #include "modules/layer-shell-extension/layershellextensionmanagerinterfacev1.h" #include "modules/prelaunch-splash/prelaunchsplash.h" @@ -645,8 +646,12 @@ void ShellHandler::ensureXdgWrapper(WXdgToplevelSurface *surface, const QString isNewWrapper = true; // newly created } - // Initialize wrapper - if (DDEShellSurfaceInterface::get(surface->surface())) { + // Initialize wrapper. Prefer v2 when both are present: the deprecated v1 + // global is kept only for the migration period and dde-shell is expected + // to migrate to v2; a client that binds both is not a supported case. + if (auto *shellSurfaceV2 = DDEShellSurfaceV2::get(surface->surface())) { + handleDdeShellSurfaceV2Added(shellSurfaceV2, wrapper); + } else if (DDEShellSurfaceInterface::get(surface->surface())) { handleDdeShellSurfaceAdded(surface->surface(), wrapper); } auto updateSurfaceWithParentContainer = [this, wrapper, surface] { @@ -740,6 +745,18 @@ void ShellHandler::revokePrivilegedOverlay(SurfaceWrapper *wrapper) void ShellHandler::onXdgToplevelSurfaceRemoved(WXdgToplevelSurface *surface) { auto wrapper = m_rootSurfaceContainer->getSurface(surface); + + // Remove any attached dde-shell objects before the wrapper-null early + // return: a pending app-id resolve means no wrapper was ever created, yet + // the wl_surface may still hold a shell-surface object. The wl_surface is + // going away, so these objects only need cleanup, not a protocol destroy. + if (auto interfaceV2 = DDEShellSurfaceV2::get(surface->surface())) { + delete interfaceV2; + } + if (auto interface = DDEShellSurfaceInterface::get(surface->surface())) { + delete interface; + } + // If async resolve still pending, cancel it. If wrapper never created, just return: compositor // never exposed this surface (from treeland's perspective). if (!wrapper) { @@ -749,10 +766,6 @@ void ShellHandler::onXdgToplevelSurfaceRemoved(WXdgToplevelSurface *surface) } return; } - auto interface = DDEShellSurfaceInterface::get(surface->surface()); - if (interface) { - delete interface; - } // Persist the last size of a normal window (prefer normalGeometry) when an appId is present if (m_windowConfigStore && !wrapper->appId().isEmpty()) { QSizeF sz = wrapper->normalGeometry().size(); @@ -1337,26 +1350,15 @@ void ShellHandler::handleDdeShellSurfaceAdded(WSurface *surface, SurfaceWrapper updateLayer(); }); + // Placement hints (set_auto_placement / set_surface_position) only affect + // the initial placement; apply them once here instead of tracking runtime + // changes with ongoing signal connections. if (ddeShellSurface->yOffset().has_value()) - wrapper->setAutoPlaceYOffset(ddeShellSurface->yOffset().value()); - - connect(ddeShellSurface, - &DDEShellSurfaceInterface::yOffsetChanged, - this, - [wrapper](uint32_t offset) { - wrapper->setAutoPlaceYOffset(offset); - }); + wrapper->setAutoPlaceCursorOffset(QPoint(0, int(ddeShellSurface->yOffset().value()))); if (ddeShellSurface->surfacePos().has_value()) wrapper->setClientRequstPos(ddeShellSurface->surfacePos().value()); - connect(ddeShellSurface, - &DDEShellSurfaceInterface::positionChanged, - this, - [wrapper](QPoint pos) { - wrapper->setClientRequstPos(pos); - }); - if (ddeShellSurface->skipSwitcher().has_value()) wrapper->setSkipSwitcher(ddeShellSurface->skipSwitcher().value()); @@ -1393,3 +1395,48 @@ void ShellHandler::handleDdeShellSurfaceAdded(WSurface *surface, SurfaceWrapper wrapper->setAcceptKeyboardFocus(accept); }); } + +void ShellHandler::handleDdeShellSurfaceV2Added(DDEShellSurfaceV2 *shellSurface, + SurfaceWrapper *wrapper) +{ + wrapper->setIsDDEShellSurface(true); + + // The overlay role is the default of the v2 protocol; the normal role + // returns the surface to the regular window layer. + auto updateLayer = [shellSurface, wrapper] { + wrapper->setSurfaceRole(shellSurface->role() == DDEShellSurfaceV2::OVERLAY + ? SurfaceWrapper::SurfaceRole::Overlay + : SurfaceWrapper::SurfaceRole::Normal); + }; + updateLayer(); + connect(shellSurface, &DDEShellSurfaceV2::roleChanged, this, [updateLayer] { + updateLayer(); + }); + + // Placement hints (set_position_hint / set_cursor_placement_hint) only + // affect the initial placement; apply them once here instead of tracking + // runtime changes with ongoing signal connections. + if (auto pos = shellSurface->positionHint()) + wrapper->setClientRequstPos(*pos); + else if (auto offset = shellSurface->cursorPlacementHint()) + wrapper->setAutoPlaceCursorOffset(*offset); + + auto applySkipFlags = [wrapper](quint32 flags) { + wrapper->setSkipSwitcher(flags & DDEShellSurfaceV2::SkipSwitcher); + wrapper->setSkipDockPreView(flags & DDEShellSurfaceV2::SkipDockPreview); + wrapper->setSkipMutiTaskView(flags & DDEShellSurfaceV2::SkipMultitaskView); + }; + applySkipFlags(shellSurface->skipFlags()); + connect(shellSurface, + &DDEShellSurfaceV2::skipFlagsChanged, + this, + applySkipFlags); + + wrapper->setAcceptKeyboardFocus(shellSurface->acceptKeyboardFocus()); + connect(shellSurface, + &DDEShellSurfaceV2::acceptKeyboardFocusChanged, + this, + [wrapper](bool accept) { + wrapper->setAcceptKeyboardFocus(accept); + }); +} diff --git a/src/core/shellhandler.h b/src/core/shellhandler.h index 7230e74995..7b8b43fe59 100644 --- a/src/core/shellhandler.h +++ b/src/core/shellhandler.h @@ -140,6 +140,8 @@ private Q_SLOTS: bool checkAndApplySnapMask(SurfaceWrapper *wrapper); void handleDdeShellSurfaceAdded(WAYLIB_SERVER_NAMESPACE::WSurface *surface, SurfaceWrapper *wrapper); + void handleDdeShellSurfaceV2Added(class DDEShellSurfaceV2 *shellSurface, + SurfaceWrapper *wrapper); void updateXWaylandDesktopProperties(); void watchXWaylandDesktopOutput(Output *output); void watchXWaylandWorkspaceNames(); diff --git a/src/modules/dde-shell/CMakeLists.txt b/src/modules/dde-shell/CMakeLists.txt index cf6e720080..a3c3466669 100644 --- a/src/modules/dde-shell/CMakeLists.txt +++ b/src/modules/dde-shell/CMakeLists.txt @@ -1,10 +1,19 @@ find_package(TreelandProtocols REQUIRED) +# --- Deprecated v1 protocol (kept for transition until dde-shell migrates) --- +# TODO: Remove the v1 protocol and its module sources +# (ddeshellmanagerinterfacev1.*, ddeshellattached.*) in a future release +# once dde-shell has migrated to treeland-dde-shell-unstable-v2. waylib_generate_qtwayland_server_protocol(libtreeland PROTOCOL ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-dde-shell-v1.xml BASENAME treeland-dde-shell-v1 ) +waylib_generate_qtwayland_server_protocol(libtreeland + PROTOCOL ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-dde-shell-unstable-v2.xml + BASENAME treeland-dde-shell-unstable-v2 +) + impl_treeland( NAME module_ddeshell @@ -13,6 +22,8 @@ impl_treeland( ${CMAKE_SOURCE_DIR}/src/modules/dde-shell/ddeshellattached.h ${CMAKE_SOURCE_DIR}/src/modules/dde-shell/ddeshellmanagerinterfacev1.cpp ${CMAKE_SOURCE_DIR}/src/modules/dde-shell/ddeshellattached.cpp + ${CMAKE_SOURCE_DIR}/src/modules/dde-shell/ddeshellmanagerinterfacev2.h + ${CMAKE_SOURCE_DIR}/src/modules/dde-shell/ddeshellmanagerinterfacev2.cpp INCLUDE $ ) diff --git a/src/modules/dde-shell/ddeshellattached.cpp b/src/modules/dde-shell/ddeshellattached.cpp index 16cbdaa5c9..32fd09faec 100644 --- a/src/modules/dde-shell/ddeshellattached.cpp +++ b/src/modules/dde-shell/ddeshellattached.cpp @@ -3,6 +3,11 @@ #include "ddeshellattached.h" +// DEPRECATED: The treeland-dde-shell-v1 protocol is deprecated. Use +// treeland-dde-shell-unstable-v2 (ddeshellmanagerinterfacev2.h/.cpp) instead. +// TODO: Remove this module in a future release once dde-shell has migrated +// to v2. + #include "ddeshellmanagerinterfacev1.h" #include diff --git a/src/modules/dde-shell/ddeshellattached.h b/src/modules/dde-shell/ddeshellattached.h index 3bfe11d653..69e2e1e834 100644 --- a/src/modules/dde-shell/ddeshellattached.h +++ b/src/modules/dde-shell/ddeshellattached.h @@ -3,6 +3,11 @@ #pragma once +// DEPRECATED: The treeland-dde-shell-v1 protocol is deprecated. Use +// treeland-dde-shell-unstable-v2 (ddeshellmanagerinterfacev2.h/.cpp) instead. +// TODO: Remove this module in a future release once dde-shell has migrated +// to v2. + #include class DDEShellAttached : public QObject diff --git a/src/modules/dde-shell/ddeshellmanagerinterfacev1.cpp b/src/modules/dde-shell/ddeshellmanagerinterfacev1.cpp index 906769a24d..e9f37bcae6 100644 --- a/src/modules/dde-shell/ddeshellmanagerinterfacev1.cpp +++ b/src/modules/dde-shell/ddeshellmanagerinterfacev1.cpp @@ -3,6 +3,11 @@ #include "ddeshellmanagerinterfacev1.h" +// DEPRECATED: The treeland-dde-shell-v1 protocol is deprecated. Use +// treeland-dde-shell-unstable-v2 (ddeshellmanagerinterfacev2.h/.cpp) instead. +// TODO: Remove this module in a future release once dde-shell has migrated +// to v2. + #include "qwayland-server-treeland-dde-shell-v1.h" #include "helper.h" diff --git a/src/modules/dde-shell/ddeshellmanagerinterfacev1.h b/src/modules/dde-shell/ddeshellmanagerinterfacev1.h index 3550c7b4de..68ab69398c 100644 --- a/src/modules/dde-shell/ddeshellmanagerinterfacev1.h +++ b/src/modules/dde-shell/ddeshellmanagerinterfacev1.h @@ -3,6 +3,11 @@ #pragma once +// DEPRECATED: The treeland-dde-shell-v1 protocol is deprecated. Use +// treeland-dde-shell-unstable-v2 (ddeshellmanagerinterfacev2.h/.cpp) instead. +// TODO: Remove this module in a future release once dde-shell has migrated +// to v2. + #include #include #include diff --git a/src/modules/dde-shell/ddeshellmanagerinterfacev2.cpp b/src/modules/dde-shell/ddeshellmanagerinterfacev2.cpp new file mode 100644 index 0000000000..c7ba47fcc9 --- /dev/null +++ b/src/modules/dde-shell/ddeshellmanagerinterfacev2.cpp @@ -0,0 +1,350 @@ +// Copyright (C) 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 "ddeshellmanagerinterfacev2.h" + +#include "qwayland-server-treeland-dde-shell-unstable-v2.h" + +#include "helper.h" +#include "output.h" +#include "rootsurfacecontainer.h" + +#include + +#include + +#include + +#include + +#define MAX_CURSOR_PLACEMENT_OFFSET 10000 + +static QList s_shellSurfacesV2; + +class DDEShellManagerInterfaceV2Private : public QtWaylandServer::treeland_dde_shell_manager_v2 +{ +public: + explicit DDEShellManagerInterfaceV2Private(DDEShellManagerInterfaceV2 *_q); + wl_global *global() const; + + DDEShellManagerInterfaceV2 *q; + +protected: + void destroy(Resource *resource) override; + void get_shell_surface(Resource *resource, uint32_t id, struct ::wl_resource *surface) override; +}; + +DDEShellManagerInterfaceV2Private::DDEShellManagerInterfaceV2Private(DDEShellManagerInterfaceV2 *_q) + : QtWaylandServer::treeland_dde_shell_manager_v2() + , q(_q) +{ +} + +wl_global *DDEShellManagerInterfaceV2Private::global() const +{ + return m_global; +} + +void DDEShellManagerInterfaceV2Private::destroy(Resource *resource) +{ + wl_resource_destroy(resource->handle); +} + +void DDEShellManagerInterfaceV2Private::get_shell_surface(Resource *resource, + uint32_t id, + wl_resource *surface) +{ + if (DDEShellSurfaceV2::getByWlrSurface(wlr_surface_from_resource(surface))) { + ++q->m_alreadyShellSurfaceErrors; + wl_resource_post_error(resource->handle, + error_already_shell_surface, + "the wl_surface already has a shell surface object"); + return; + } + + wl_resource *shell_resource = wl_resource_create(resource->client(), + &treeland_dde_shell_surface_v2_interface, + resource->version(), + id); + if (!shell_resource) { + wl_client_post_no_memory(resource->client()); + return; + } + + auto shellSurface = new DDEShellSurfaceV2(surface, shell_resource); + s_shellSurfacesV2.append(shellSurface); + + QObject::connect(shellSurface, &QObject::destroyed, [shellSurface]() { + s_shellSurfacesV2.removeOne(shellSurface); + }); + + Q_EMIT q->surfaceCreated(shellSurface); +} + +DDEShellManagerInterfaceV2::DDEShellManagerInterfaceV2(QObject *parent) + : QObject(parent) + , d(new DDEShellManagerInterfaceV2Private(this)) +{ +} + +DDEShellManagerInterfaceV2::~DDEShellManagerInterfaceV2() = default; + +int DDEShellManagerInterfaceV2::alreadyShellSurfaceErrorCount() const +{ + return m_alreadyShellSurfaceErrors; +} + +void DDEShellManagerInterfaceV2::create(WServer *server) +{ + d->init(server->handle(), InterfaceVersion); +} + +void DDEShellManagerInterfaceV2::destroy([[maybe_unused]] WServer *server) +{ + d->globalRemove(); +} + +wl_global *DDEShellManagerInterfaceV2::global() const +{ + return d->global(); +} + +QByteArrayView DDEShellManagerInterfaceV2::interfaceName() const +{ + return d->interfaceName(); +} + +class DDEShellSurfaceV2Private : public QtWaylandServer::treeland_dde_shell_surface_v2 +{ +public: + DDEShellSurfaceV2Private(DDEShellSurfaceV2 *_q, + wl_resource *surface, + wl_resource *resource); + ~DDEShellSurfaceV2Private() override; + + DDEShellSurfaceV2 *q; + wl_resource *surfaceResource{ nullptr }; + DDEShellSurfaceV2::Role role = DDEShellSurfaceV2::OVERLAY; + // Exactly one of the two placement hints has a value: the most recently + // sent request decides the placement mode. + std::optional positionHint; + std::optional cursorPlacementHint; + uint32_t skipFlags = 0; + bool acceptKeyboardFocus = true; + +protected: + void destroy_resource([[maybe_unused]] Resource *resource) override; + void destroy([[maybe_unused]] Resource *resource) override; + void set_role([[maybe_unused]] Resource *resource, uint32_t value) override; + void set_position_hint([[maybe_unused]] Resource *resource, + struct ::wl_resource *output, + int32_t x, + int32_t y) override; + void set_cursor_placement_hint([[maybe_unused]] Resource *resource, + int32_t x_offset, + int32_t y_offset) override; + void set_skip_flags([[maybe_unused]] Resource *resource, uint32_t flags) override; + void set_accept_keyboard_focus([[maybe_unused]] Resource *resource, uint32_t accept) override; +}; + +DDEShellSurfaceV2Private::DDEShellSurfaceV2Private(DDEShellSurfaceV2 *_q, + wl_resource *surface, + wl_resource *resource) + : QtWaylandServer::treeland_dde_shell_surface_v2(resource) + , q(_q) + , surfaceResource(surface) +{ +} + +DDEShellSurfaceV2Private::~DDEShellSurfaceV2Private() = default; + +void DDEShellSurfaceV2Private::destroy_resource([[maybe_unused]] Resource *resource) +{ + delete q; +} + +void DDEShellSurfaceV2Private::destroy([[maybe_unused]] Resource *resource) +{ + wl_resource_destroy(resource->handle); +} + +void DDEShellSurfaceV2Private::set_role([[maybe_unused]] Resource *resource, + uint32_t value) +{ + DDEShellSurfaceV2::Role newRole; + switch (value) { + case QtWaylandServer::treeland_dde_shell_surface_v2::role::role_normal: + newRole = DDEShellSurfaceV2::NORMAL; + break; + case QtWaylandServer::treeland_dde_shell_surface_v2::role::role_overlay: + newRole = DDEShellSurfaceV2::OVERLAY; + break; + default: + wl_resource_post_error(resource->handle, + 0, + "Invalid treeland_dde_shell_surface_v2::role: %u", + value); + return; + } + + if (role == newRole) { + return; + } + + role = newRole; + Q_EMIT q->roleChanged(newRole); +} + +void DDEShellSurfaceV2Private::set_position_hint([[maybe_unused]] Resource *resource, + wl_resource *output, + int32_t x, + int32_t y) +{ + QPoint anchor; + if (output) { + auto *wOutput = WOutput::fromHandle(wlr_output_from_resource(output)); + if (!wOutput) { + wl_resource_post_error(resource->handle, 0, "invalid wl_output resource"); + return; + } + anchor = wOutput->position(); + } else { + // A null output anchors the coordinates at the primary output origin. + if (auto *primary = Helper::instance()->rootContainer()->primaryOutput()) + anchor = primary->geometry().topLeft().toPoint(); + } + + const bool modeSwitched = cursorPlacementHint.has_value(); + cursorPlacementHint.reset(); + + const QPoint globalPos = anchor + QPoint(x, y); + if (positionHint == globalPos && !modeSwitched) { + return; + } + + positionHint = globalPos; + Q_EMIT q->positionHintChanged(globalPos); +} + +void DDEShellSurfaceV2Private::set_cursor_placement_hint([[maybe_unused]] Resource *resource, + int32_t x_offset, + int32_t y_offset) +{ + // Reject out-of-range offsets so a bad client request cannot wrap into a + // bogus placement or misplace the surface beyond any screen. Negative + // offsets are valid (e.g. placing the surface above the cursor). + if (x_offset < -MAX_CURSOR_PLACEMENT_OFFSET || x_offset > MAX_CURSOR_PLACEMENT_OFFSET + || y_offset < -MAX_CURSOR_PLACEMENT_OFFSET || y_offset > MAX_CURSOR_PLACEMENT_OFFSET) { + wl_resource_post_error(resource->handle, + 0, + "Invalid cursor placement hint offset: %d,%d", + x_offset, + y_offset); + return; + } + + const bool modeSwitched = positionHint.has_value(); + positionHint.reset(); + + const QPoint offset(x_offset, y_offset); + if (cursorPlacementHint == offset && !modeSwitched) { + return; + } + + cursorPlacementHint = offset; + Q_EMIT q->cursorPlacementHintChanged(offset); +} + +void DDEShellSurfaceV2Private::set_skip_flags([[maybe_unused]] Resource *resource, + uint32_t flags) +{ + if (skipFlags == flags) { + return; + } + + skipFlags = flags; + Q_EMIT q->skipFlagsChanged(flags); +} + +void DDEShellSurfaceV2Private::set_accept_keyboard_focus([[maybe_unused]] Resource *resource, + uint32_t accept) +{ + const bool newAccept = accept != 0; + if (acceptKeyboardFocus == newAccept) { + return; + } + + acceptKeyboardFocus = newAccept; + Q_EMIT q->acceptKeyboardFocusChanged(newAccept); +} + +DDEShellSurfaceV2::DDEShellSurfaceV2(wl_resource *surface, wl_resource *resource) + : d(new DDEShellSurfaceV2Private(this, surface, resource)) +{ +} + +DDEShellSurfaceV2::~DDEShellSurfaceV2() = default; + +WSurface *DDEShellSurfaceV2::wSurface() const +{ + return WSurface::fromHandle(wlr_surface_from_resource(d->surfaceResource)); +} + +DDEShellSurfaceV2::Role DDEShellSurfaceV2::role() const +{ + return d->role; +} + +std::optional DDEShellSurfaceV2::positionHint() const +{ + return d->positionHint; +} + +std::optional DDEShellSurfaceV2::cursorPlacementHint() const +{ + return d->cursorPlacementHint; +} + +uint32_t DDEShellSurfaceV2::skipFlags() const +{ + return d->skipFlags; +} + +bool DDEShellSurfaceV2::acceptKeyboardFocus() const +{ + return d->acceptKeyboardFocus; +} + +DDEShellSurfaceV2 *DDEShellSurfaceV2::get(wl_resource *native) +{ + // wlr_surface_from_resource asserts on a null or non-wl_surface resource; + // validate first so callers inspecting another resource get null instead + // of aborting the compositor. + if (!native || strcmp(wl_resource_get_class(native), wl_surface_interface.name) != 0) + return nullptr; + + WSurface *surface = WSurface::fromHandle(wlr_surface_from_resource(native)); + if (surface) { + return DDEShellSurfaceV2::get(surface); + } + + return nullptr; +} + +DDEShellSurfaceV2 *DDEShellSurfaceV2::get(WSurface *surface) +{ + if (!surface) + return nullptr; + return getByWlrSurface(surface->handle()); +} + +DDEShellSurfaceV2 *DDEShellSurfaceV2::getByWlrSurface(wlr_surface *handle) +{ + for (DDEShellSurfaceV2 *shellSurface : std::as_const(s_shellSurfacesV2)) { + if (wlr_surface_from_resource(shellSurface->d->surfaceResource) == handle) { + return shellSurface; + } + } + + return nullptr; +} diff --git a/src/modules/dde-shell/ddeshellmanagerinterfacev2.h b/src/modules/dde-shell/ddeshellmanagerinterfacev2.h new file mode 100644 index 0000000000..3e03b838a1 --- /dev/null +++ b/src/modules/dde-shell/ddeshellmanagerinterfacev2.h @@ -0,0 +1,103 @@ +// Copyright (C) 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 + +#include +#include +#include + +struct wlr_surface; + +WAYLIB_SERVER_USE_NAMESPACE + +class DDEShellManagerInterfaceV2Private; +class DDEShellSurfaceV2; + +class DDEShellManagerInterfaceV2 : public QObject , public WServerInterface +{ + Q_OBJECT +public: + explicit DDEShellManagerInterfaceV2(QObject *parent = nullptr); + ~DDEShellManagerInterfaceV2() override; + + QByteArrayView interfaceName() const override; + static constexpr int InterfaceVersion = 1; + // Number of already_shell_surface protocol errors raised on this global; + // used by the protocol test to assert the duplicate path server-side, + // since the error tears down the client connection before it can read + // the code. + int alreadyShellSurfaceErrorCount() const; +Q_SIGNALS: + void surfaceCreated(DDEShellSurfaceV2 *interface); + +protected: + void create(WServer *server) override; + void destroy(WServer *server) override; + wl_global *global() const override; + +private: + friend class DDEShellManagerInterfaceV2Private; + std::unique_ptr d; + int m_alreadyShellSurfaceErrors = 0; +}; + +class DDEShellSurfaceV2Private; +class DDEShellSurfaceV2 : public QObject +{ + Q_OBJECT +public: + // treeland_dde_shell_surface_v2.role, values match the protocol enum + enum Role { + NORMAL = 0, + OVERLAY = 1, + }; + + // treeland_dde_shell_surface_v2.skip_flag bitfield + enum SkipFlag { + SkipSwitcher = 0x1, + SkipDockPreview = 0x2, + SkipMultitaskView = 0x4, + }; + + ~DDEShellSurfaceV2() override; + + WSurface *wSurface() const; + DDEShellSurfaceV2::Role role() const; + // Global-space position hint, resolved at request time from the + // output-relative coordinates of set_position_hint. Has a value iff the + // most recent placement hint was set_position_hint. + std::optional positionHint() const; + // (x_offset, y_offset) from the cursor, as sent by + // set_cursor_placement_hint. Has a value iff the most recent placement + // hint was set_cursor_placement_hint. + std::optional cursorPlacementHint() const; + uint32_t skipFlags() const; + bool acceptKeyboardFocus() const; + + static DDEShellSurfaceV2 *get(wl_resource *native); + static DDEShellSurfaceV2 *get(WSurface *surface); + // Uniqueness must be tracked by the native wl_surface (a wl_surface may + // not have a waylib wrapper yet, which would make wSurface() null and + // poison any pointer-based duplicate check). + static DDEShellSurfaceV2 *getByWlrSurface(struct wlr_surface *handle); + +Q_SIGNALS: + void roleChanged(DDEShellSurfaceV2::Role role); + void positionHintChanged(QPoint pos); + void cursorPlacementHintChanged(QPoint offset); + void skipFlagsChanged(quint32 flags); + void acceptKeyboardFocusChanged(bool accept); + +private: + explicit DDEShellSurfaceV2(wl_resource *surface, wl_resource *resource); + +private: + friend class DDEShellManagerInterfaceV2Private; + friend class DDEShellSurfaceV2Private; + std::unique_ptr d; +}; diff --git a/src/output/output.cpp b/src/output/output.cpp index bcc6995558..13935b3d2c 100644 --- a/src/output/output.cpp +++ b/src/output/output.cpp @@ -273,7 +273,7 @@ void Output::moveSurfaceWithTitlebarClamp(SurfaceWrapper *surface, const QPointF surface->moveNormalGeometryInOutput(candidateGeo.topLeft()); } -void Output::placeUnderCursor(SurfaceWrapper *surface, quint32 yOffset) +void Output::placeUnderCursor(SurfaceWrapper *surface) { QSizeF cursorSize; QRectF normalGeo = surface->normalGeometry(); @@ -281,8 +281,12 @@ void Output::placeUnderCursor(SurfaceWrapper *surface, quint32 yOffset) if (!surface->ownsOutput()->outputItem()->cursorItems().isEmpty()) cursorSize = surface->ownsOutput()->outputItem()->cursorItems()[0]->size(); - normalGeo.moveLeft(wCursor->position().x() + (cursorSize.width() - surface->width()) / 2); - normalGeo.moveTop(wCursor->position().y() + cursorSize.height() + yOffset); + // Surface center sits at the cursor center plus the x offset, the surface + // top at the cursor bottom plus the y offset. + const QPoint offset = surface->autoPlaceCursorOffset().value_or(QPoint()); + normalGeo.moveLeft(wCursor->position().x() + cursorSize.width() / 2 + + offset.x() - surface->width() / 2); + normalGeo.moveTop(wCursor->position().y() + cursorSize.height() + offset.y()); moveSurfaceWithTitlebarClamp(surface, normalGeo.topLeft()); } @@ -515,13 +519,6 @@ void Output::addSurface(SurfaceWrapper *surface) connect(surface, &SurfaceWrapper::hasInitializeContainerChanged, this, layoutSurface); layoutSurface(); - auto setyOffset = [surface, this] { - placeUnderCursor(surface, surface->autoPlaceYOffset()); - }; - connect(surface, &SurfaceWrapper::autoPlaceYOffsetChanged, this, setyOffset); - if (surface->autoPlaceYOffset() != 0) - setyOffset(); - if (surface->type() == SurfaceWrapper::Type::XdgPopup) { auto xdgPopupSurfaceItem = qobject_cast(surface->surfaceItem()); connect(xdgPopupSurfaceItem, &WXdgPopupSurfaceItem::implicitPositionChanged, this, [surface, this] { @@ -825,9 +822,15 @@ void Output::arrangeNonLayerSurface(SurfaceWrapper *surface, ArrangeReason reaso validGeo.bottom() - minVisibleY)); moveSurfaceWithTitlebarClamp(surface, pos); } else { - QPoint clientRequstPos = surface->clientRequstPos(); - if (!clientRequstPos.isNull()) { - placeClientRequstPos(surface, clientRequstPos); + // (0,0) is a valid fixed position, so presence of a position request + // is tracked explicitly, not via QPoint::isNull(). + if (const auto pos = surface->clientRequstPos()) { + placeClientRequstPos(surface, *pos); + } else if (surface->autoPlaceCursorOffset().has_value() + && reason == ArrangeReason::InitialPlacement) { + // Cursor placement is compositor-driven and only affects the + // initial layout (per dde-shell-v2 semantics). + placeUnderCursor(surface); } else if (reason == ArrangeReason::LayerSurfaceRemoved || reason == ArrangeReason::ExclusiveZoneChanged) { // validGeo has changed (panel added/removed); re-run the titlebar diff --git a/src/output/output.h b/src/output/output.h index 81843a89ba..ff51ac9b38 100644 --- a/src/output/output.h +++ b/src/output/output.h @@ -131,7 +131,7 @@ public Q_SLOTS: void arrangeNonLayerSurfaces(ArrangeReason reason); void arrangeAllSurfaces(); std::pair getOutputItemProperty(); - void placeUnderCursor(SurfaceWrapper *surface, quint32 yOffset); + void placeUnderCursor(SurfaceWrapper *surface); void placeClientRequstPos(SurfaceWrapper *surface, QPoint clientRequstPos); void placeCentered(SurfaceWrapper *surface); void placeSmartCascaded(SurfaceWrapper *surface); diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 694e3ad038..e3e517c9f5 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -40,6 +40,7 @@ #include "modules/capture/capture.h" #include "modules/dde-shell/ddeshellattached.h" #include "modules/dde-shell/ddeshellmanagerinterfacev1.h" +#include "modules/dde-shell/ddeshellmanagerinterfacev2.h" #include "modules/ddm/ddminterfacev1.h" #include "modules/input-manager/inputmanagerinterfacev1.h" #include "modules/keyboard-shortcuts-inhibit/keyboardshortcutsinhibitmanager.h" @@ -2010,6 +2011,8 @@ void Helper::init(Treeland::Treeland *treeland) this, &Helper::handleLockScreen); + m_ddeShellV2 = m_server->attach(); + m_compositorActionInterfaceV1 = m_server->attach(); connect(m_compositorActionInterfaceV1, &CompositorActionInterfaceV1::triggered, diff --git a/src/seat/helper.h b/src/seat/helper.h index 6ca9acb683..03a185acb6 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -96,6 +96,7 @@ WAYLIB_SERVER_USE_NAMESPACE class CaptureSourceSelector; class DDEShellManagerInterfaceV1; +class DDEShellManagerInterfaceV2; class DDMInterfaceV1; class ForeignToplevelManagerInterfaceV2; class FpsDisplayManager; @@ -500,6 +501,7 @@ private Q_SLOTS: XWindowControlInterfaceV1 *m_xWindowControlInterfaceV1 = nullptr; ShowDesktopInterfaceV1::State m_showDesktop = ShowDesktopInterfaceV1::State::Normal; DDEShellManagerInterfaceV1 *m_ddeShellV1 = nullptr; + DDEShellManagerInterfaceV2 *m_ddeShellV2 = nullptr; VirtualOutputManagerInterfaceV1 *m_virtualOutputInterfaceV1 = nullptr; OutputManagerV1 *m_outputManagerV1 = nullptr; DDMInterfaceV1 *m_ddmInterfaceV1 = nullptr; diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 7226b3daa9..c7d7d52616 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -27,8 +27,6 @@ #include #include -#include - #define OPEN_ANIMATION 1 #define CLOSE_ANIMATION 2 #define ALWAYSONTOPLAYER 1 @@ -2894,22 +2892,24 @@ void SurfaceWrapper::setSurfaceRole(SurfaceRole role) Q_EMIT surfaceRoleChanged(); } -quint32 SurfaceWrapper::autoPlaceYOffset() const +std::optional SurfaceWrapper::autoPlaceCursorOffset() const { - return m_autoPlaceYOffset; + return m_autoPlaceCursorOffset; } -void SurfaceWrapper::setAutoPlaceYOffset(quint32 offset) +void SurfaceWrapper::setAutoPlaceCursorOffset(QPoint offset) { - if (m_autoPlaceYOffset == offset) + if (m_autoPlaceCursorOffset == offset) return; - m_autoPlaceYOffset = offset; - setPositionAutomatic(offset == 0); - Q_EMIT autoPlaceYOffsetChanged(); + m_autoPlaceCursorOffset = offset; + // Cursor placement supersedes a stale fixed-position request; the two + // modes are mutually exclusive and the later request wins. + m_clientRequstPos.reset(); + setPositionAutomatic(false); } -QPoint SurfaceWrapper::clientRequstPos() const +std::optional SurfaceWrapper::clientRequstPos() const { return m_clientRequstPos; } @@ -2920,8 +2920,11 @@ void SurfaceWrapper::setClientRequstPos(QPoint pos) return; m_clientRequstPos = pos; - setPositionAutomatic(pos.isNull()); - Q_EMIT clientRequstPosChanged(); + // A fixed position supersedes cursor placement; the two modes are mutually + // exclusive and the later request wins. + m_autoPlaceCursorOffset.reset(); + // An explicit position request (including 0,0) always pins the surface. + setPositionAutomatic(false); } QPointF SurfaceWrapper::alignToPixelGrid(const QPointF &pos) const diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index 9f926aa5eb..300c0b8e87 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -73,13 +74,6 @@ class SurfaceWrapper : public QQuickItem Q_PROPERTY(bool skipMutiTaskView READ skipMutiTaskView NOTIFY skipMutiTaskViewChanged FINAL) Q_PROPERTY(bool isDDEShellSurface READ isDDEShellSurface NOTIFY isDDEShellSurfaceChanged FINAL) Q_PROPERTY(SurfaceWrapper::SurfaceRole surfaceRole READ surfaceRole NOTIFY surfaceRoleChanged FINAL) - // y-axis offset distance, set the vertical alignment of the surface within - // the cursor width. if autoPlaceYOffset > 0, preventing SurfaceWrapper from - // being displayed beyond the edge of the output. - Q_PROPERTY(quint32 autoPlaceYOffset READ autoPlaceYOffset NOTIFY autoPlaceYOffsetChanged FINAL) - // wayland client can control the position of SurfaceWrapper on the output - // through treeland_dde_shell_surface_v1.set_surface_position - Q_PROPERTY(QPoint clientRequstPos READ clientRequstPos NOTIFY clientRequstPosChanged FINAL) Q_PROPERTY(bool blur READ blur NOTIFY blurChanged FINAL) Q_PROPERTY(bool isWindowAnimationRunning READ isWindowAnimationRunning NOTIFY windowAnimationRunningChanged FINAL) Q_PROPERTY(bool coverEnabled READ coverEnabled NOTIFY coverEnabledChanged FINAL) @@ -319,10 +313,10 @@ class SurfaceWrapper : public QQuickItem enum SurfaceRole surfaceRole() const; void setSurfaceRole(enum SurfaceRole role); - quint32 autoPlaceYOffset() const; - void setAutoPlaceYOffset(quint32 offset); + std::optional autoPlaceCursorOffset() const; + void setAutoPlaceCursorOffset(QPoint offset); - QPoint clientRequstPos() const; + std::optional clientRequstPos() const; void setClientRequstPos(QPoint pos); bool blur() const; @@ -415,8 +409,6 @@ public Q_SLOTS: void skipMutiTaskViewChanged(); void isDDEShellSurfaceChanged(); void surfaceRoleChanged(); - void autoPlaceYOffsetChanged(); - void clientRequstPosChanged(); void blurChanged(); void windowAnimationRunningChanged(); void coverEnabledChanged(); @@ -592,8 +584,8 @@ public Q_SLOTS: uint m_maximizable : 1; uint m_modal : 1; SurfaceRole m_surfaceRole = SurfaceRole::Normal; - quint32 m_autoPlaceYOffset = 0; - QPoint m_clientRequstPos; + std::optional m_autoPlaceCursorOffset; + std::optional m_clientRequstPos; bool m_socketEnabled{ false }; bool m_windowAnimationEnabled{ true }; diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index c930467273..2076ae5263 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -35,6 +35,7 @@ add_subdirectory(treeland-dde-shell-v1) add_subdirectory(treeland-active-notify-unstable-v1) add_subdirectory(treeland-appearance-unstable-v1) add_subdirectory(treeland-remote-subsurface-unstable-v1) +add_subdirectory(treeland-dde-shell-v2) add_subdirectory(treeland-dde-shell-desktop-v1) add_subdirectory(treeland-dde-shell-lockscreen-desktop-v1) add_subdirectory(treeland-dde-shell-multitask-desktop-v1) diff --git a/tests/protocols/INDEX.md b/tests/protocols/INDEX.md index 2def61c1a0..a640ac456b 100644 --- a/tests/protocols/INDEX.md +++ b/tests/protocols/INDEX.md @@ -29,6 +29,7 @@ Wayland 线上请求与事件;本文档规定发出请求后,测试必须观 | [compositor-action-v1](treeland-compositor-action-v1/README.md) | E / P | trigger 路由到真实 Workspace 切换与 show-desktop 状态;zoom/未知动作被忽略不断连 | | [dde-shell-v1](treeland-dde-shell-v1/README.md) | E / P | mapped wrapper 的 DDE 元数据、锁屏;picker 的真实选中/PID 回传测试待执行确认 | | [active-notify-unstable-v1](treeland-active-notify-unstable-v1/README.md) | P | notifier 创建/重建与无输入不发事件边界 | +| [dde-shell-v2](treeland-dde-shell-v2/README.md) | E / P | v2 shell surface 元数据、placement hint 互斥切换与 (0,0) 边界、skip 位域、重复创建报错码、wl_surface 销毁自动回收;wrapper 集成 fixture 随 v1 清理任务补 | | [ddm-v1](treeland-ddm-v1/README.md) | I | 客户端连接生命周期 | | [foreign-toplevel-manager-v2](treeland-foreign-toplevel-manager-v2/README.md) | E | 真实 toplevel、dock preview、窗口状态(含最小化与布局正交)、激活/焦点与 icon rectangle | | [input-manager-unstable-v1](treeland-input-manager-unstable-v1/README.md) | I / E(可选) | 默认空设备 manager 生命周期;uinput 驱动真实 libinput capability 热插拔 | diff --git a/tests/protocols/treeland-dde-shell-desktop-v1/setup.cpp b/tests/protocols/treeland-dde-shell-desktop-v1/setup.cpp index ba4359d686..dcb0ddd528 100644 --- a/tests/protocols/treeland-dde-shell-desktop-v1/setup.cpp +++ b/tests/protocols/treeland-dde-shell-desktop-v1/setup.cpp @@ -37,10 +37,10 @@ extern "C" void dde_desktop_read_state(void *data) if (g_wrapper) { state.is_dde_shell_surface = g_wrapper->isDDEShellSurface() ? 1 : 0; state.role_overlay = g_wrapper->surfaceRole() == SurfaceWrapper::SurfaceRole::Overlay ? 1 : 0; - const QPoint position = g_wrapper->clientRequstPos(); + const QPoint position = g_wrapper->clientRequstPos().value_or(QPoint()); state.position_x = position.x(); state.position_y = position.y(); - state.auto_placement = g_wrapper->autoPlaceYOffset(); + state.auto_placement = g_wrapper->autoPlaceCursorOffset().value_or(QPoint()).y(); state.skip_switcher = g_wrapper->skipSwitcher() ? 1 : 0; state.skip_dock_preview = g_wrapper->skipDockPreView() ? 1 : 0; state.skip_multitask_view = g_wrapper->skipMutiTaskView() ? 1 : 0; diff --git a/tests/protocols/treeland-dde-shell-desktop-v1/treeland-dde-shell-desktop-v1.c b/tests/protocols/treeland-dde-shell-desktop-v1/treeland-dde-shell-desktop-v1.c index 82e5310548..9405014cd5 100644 --- a/tests/protocols/treeland-dde-shell-desktop-v1/treeland-dde-shell-desktop-v1.c +++ b/tests/protocols/treeland-dde-shell-desktop-v1/treeland-dde-shell-desktop-v1.c @@ -40,7 +40,7 @@ static int state_matches(struct dde_desktop_state *state, int expected_skip_dock && state->role_overlay && state->position_x == 42 && state->position_y == 24 - && state->auto_placement == 37 + && state->auto_placement == 0 && state->skip_switcher && state->skip_dock_preview == expected_skip_dock_preview && state->skip_multitask_view diff --git a/tests/protocols/treeland-dde-shell-v2/CMakeLists.txt b/tests/protocols/treeland-dde-shell-v2/CMakeLists.txt new file mode 100644 index 0000000000..f667f2e040 --- /dev/null +++ b/tests/protocols/treeland-dde-shell-v2/CMakeLists.txt @@ -0,0 +1,6 @@ +treeland_add_protocol_test( + NAME treeland_dde_shell_v2 + XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-dde-shell-unstable-v2.xml" + SETUP "${CMAKE_CURRENT_SOURCE_DIR}/setup.cpp" + CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-dde-shell-v2.c" +) diff --git a/tests/protocols/treeland-dde-shell-v2/README.md b/tests/protocols/treeland-dde-shell-v2/README.md new file mode 100644 index 0000000000..489c0a99f6 --- /dev/null +++ b/tests/protocols/treeland-dde-shell-v2/README.md @@ -0,0 +1,49 @@ +# `treeland-dde-shell-v2` 测试规范 + +## 范围 + +- 测试源码:`tests/protocols/treeland-dde-shell-v2/` +- Fixture:协议 fixture(headless output,普通 `wl_surface`) +- 覆盖等级:**P / I**——roundtrip 后读取服务端 `DDEShellSurfaceV2` 协议对象状态(P), + 并覆盖服务端资源生命周期(重复创建报错) + (I 的生命周期子集);wrapper 集成与业务级(E)fixture 随 v1 清理任务补。 + +v2 适配期间 v1 全局对象保留并行注册;本测试只覆盖 +`dde/treeland-dde-shell-unstable-v2.xml` 的 manager/surface 两个接口。 + +## 必须观察到的结果 + +| 场景 | 客户端动作 | 必须观察到的结果 | +| --- | --- | --- | +| 创建 | `get_shell_surface(surface)` | 服务端创建 `DDEShellSurfaceV2`,默认 role 为 overlay(值 1) | +| role | `set_role(overlay=1)` | 服务端 role 保持 overlay | +| role(normal) | `set_role(normal=0)` | 服务端 role 切到 normal(回归普通窗口层) | +| 定位 hint | `set_position_hint(output, 42, 24)` | 服务端解析为全局坐标 `positionHint == (42,24)`,cursor hint 为空 | +| 定位 hint(空 output) | `set_position_hint(NULL, 7, 9)` | 以主屏原点为锚点解析,`positionHint == (7,9)` | +| 定位 hint(0,0) | `set_position_hint(NULL, 0, 0)` | `(0,0)` 是合法固定位置,position hint 存在且 cursor hint 为空 | +| 光标 hint | `set_cursor_placement_hint(11, 5)` | `cursorPlacementHint == (11,5)`,且 position hint 被清空(后发请求决定模式) | +| 模式切回 | 再次 `set_position_hint(NULL, 3, 4)` | `positionHint == (3,4)`,cursor hint 清空 | +| skip 位域 | `set_skip_flags(0x7)` 后 `set_skip_flags(0)` | 服务端 skipFlags 依次为 7、0 | +| 键盘焦点 | `set_accept_keyboard_focus(0)` | 服务端 acceptKeyboardFocus 为 false | +| 销毁 | `destroy` 后重新 `get_shell_surface` | 资源销毁后允许重建 | +| 重复创建 | 对同一 `wl_surface` 二次 `get_shell_surface` | 合成器抛出 `already_shell_surface`(服务端错误计数器断言),客户端连接被终止 | + +## 已知边界 + +定位 hint 的全局坐标解析依赖 headless 输出位于原点 (0,0),故解析结果与请求坐标一致; +多输出布局下的坐标换算由 `WOutput::position()` 保证。`set_role` 覆盖 overlay 与 normal 两个取值。 + +服务端不在 wl_surface 销毁时自动回收 shell surface(本质是设置 SurfaceWrapper +的协议对象,生命周期随 SurfaceWrapper 走);`ShellHandler::onXdgToplevelSurfaceRemoved` +统一清理未创建 wrapper 场景下残留的 dde-shell 对象。wrapper 集成(`SurfaceWrapper` 的 +Overlay 容器、skip 布尔量、`clientRequstPos`/光标放置)与 v1 的 desktop fixture 同类覆盖 +尚未建立,待 dde-shell 客户端迁移后再补充。 + +## TODO + +- TODO: 增加非原点第二 headless 输出,并断言 `set_position_hint(output, x, y)` 换算后的 + 全局坐标(包括 (0,0) 固定位置),以覆盖多输出布局下 `WOutput::position()` / 主屏原点 + 的坐标转换路径;当前单输出 (0,0) fixture 无法捕获相关回归。 +- TODO: 增加 mapped XDG toplevel fixture,断言 `ShellHandler::handleDdeShellSurfaceV2Added()` + 与 `Output` 实际摆放后的真实全局几何(position/cursor 两种模式及其运行时切换);当前用例 + 仅在未映射的普通 `wl_surface` 上读取服务端 hint 状态,未覆盖 wrapper 集成与摆放逻辑。 diff --git a/tests/protocols/treeland-dde-shell-v2/setup.cpp b/tests/protocols/treeland-dde-shell-v2/setup.cpp new file mode 100644 index 0000000000..55e1ef4ba4 --- /dev/null +++ b/tests/protocols/treeland-dde-shell-v2/setup.cpp @@ -0,0 +1,59 @@ +// Copyright (C) 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/dde-shell/ddeshellmanagerinterfacev2.h" +#include "server-bridge.h" +#include "seat/helper.h" +#include "treeland-dde-shell-v2.h" + +#include +#include + +WAYLIB_SERVER_USE_NAMESPACE + +namespace { +DDEShellSurfaceV2 *g_shellSurface = nullptr; +DDEShellManagerInterfaceV2 *g_manager = nullptr; +} + +void protocol_test_setup(Helper *helper) +{ + add_headless_output(helper->backend(), false); + auto *manager = find_server_interface(helper); + Q_ASSERT(manager); + g_manager = manager; + QObject::connect(manager, &DDEShellManagerInterfaceV2::surfaceCreated, + [](DDEShellSurfaceV2 *surface) { + g_shellSurface = surface; + QObject::connect(surface, &QObject::destroyed, [] { + g_shellSurface = nullptr; + }); + }); +} + +extern "C" void dde_shell_v2_query_surface_state(void *data) +{ + auto *state = static_cast(data); + *state = {}; + // Read before the g_shellSurface early return: the duplicate-creation + // error case deliberately leaves no tracked shell surface behind. + state->duplicate_errors = g_manager ? g_manager->alreadyShellSurfaceErrorCount() : 0; + if (!g_shellSurface) + return; + + state->role_overlay = g_shellSurface->role() == DDEShellSurfaceV2::Role::OVERLAY; + state->role_normal = g_shellSurface->role() == DDEShellSurfaceV2::Role::NORMAL; + state->skip_flags = int(g_shellSurface->skipFlags()); + state->accept_keyboard_focus = g_shellSurface->acceptKeyboardFocus(); + + if (const auto position = g_shellSurface->positionHint()) { + state->position_set = 1; + state->position_x = position->x(); + state->position_y = position->y(); + } + if (const auto cursor = g_shellSurface->cursorPlacementHint()) { + state->cursor_set = 1; + state->cursor_x = cursor->x(); + state->cursor_y = cursor->y(); + } + state->duplicate_errors = g_manager ? g_manager->alreadyShellSurfaceErrorCount() : 0; +} diff --git a/tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.c b/tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.c new file mode 100644 index 0000000000..02090c6cff --- /dev/null +++ b/tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.c @@ -0,0 +1,286 @@ +// Copyright (C) 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 "treeland-dde-shell-v2.h" +#include "server-bridge-api.h" +#include "treeland-dde-shell-unstable-v2-client-protocol.h" + +#include +#include +#include +#include + +extern void dde_shell_v2_query_surface_state(void *data); + +struct test_case { + const char *name; + int (*run)(struct test_ctx *ctx); +}; + +void test_init(struct test_ctx *ctx) +{ + ctx->result_cap = 32; + ctx->results = calloc(ctx->result_cap, sizeof(*ctx->results)); +} + +void test_finish(struct test_ctx *ctx) +{ + free(ctx->results); +} + +int test_add(struct test_ctx *ctx, const char *name) +{ + if (ctx->result_count == ctx->result_cap) { + ctx->result_cap *= 2; + ctx->results = realloc(ctx->results, (size_t)ctx->result_cap * sizeof(*ctx->results)); + } + const int index = ctx->result_count++; + ctx->results[index] = (struct test_result) { .name = name }; + return index; +} + +void test_fail(struct test_ctx *ctx, int index, const char *format, ...) +{ + va_list arguments; + va_start(arguments, format); + vsnprintf(ctx->results[index].message, TEST_MSG_MAX, format, arguments); + va_end(arguments); + ctx->results[index].failed = 1; +} + +void test_pass(struct test_ctx *ctx, int index) +{ + ctx->results[index].failed = 0; +} + +int test_print_results(struct test_ctx *ctx) +{ + int failed = 0; + printf("\n=== results ===\n"); + for (int i = 0; i < ctx->result_count; ++i) { + printf(" [%s] %s", ctx->results[i].failed ? "FAIL" : "PASS", ctx->results[i].name); + if (ctx->results[i].failed) { + printf(" -- %s", ctx->results[i].message); + ++failed; + } + printf("\n"); + } + printf("%d/%d passed\n", ctx->result_count - failed, ctx->result_count); + return failed == 0; +} + +static int connect_client(struct test_ctx *ctx, const char *socket_name) +{ + if (!client_connect(&ctx->connection, socket_name)) + return 0; + ctx->display = ctx->connection.display; + ctx->compositor = client_bind(&ctx->connection, "wl_compositor", &wl_compositor_interface, 1); + ctx->seat = client_bind(&ctx->connection, "wl_seat", &wl_seat_interface, 1); + ctx->output = client_bind(&ctx->connection, "wl_output", &wl_output_interface, 1); + ctx->manager = client_bind(&ctx->connection, "treeland_dde_shell_manager_v2", + &treeland_dde_shell_manager_v2_interface, 1); + // Every bound resource is required by some case; a partial bind would + // silently steer e.g. the output-anchored position test to the null path. + return ctx->compositor && ctx->seat && ctx->output && ctx->manager != NULL; +} + +static int create_shell_surface(struct test_ctx *ctx) +{ + if (!ctx->compositor) + return 0; + ctx->test_surface = wl_compositor_create_surface(ctx->compositor); + ctx->shell_surface = treeland_dde_shell_manager_v2_get_shell_surface(ctx->manager, + ctx->test_surface); + return ctx->test_surface && ctx->shell_surface; +} + +static int read_shell_surface_state(struct test_ctx *ctx, struct dde_shell_surface_v2_state *state) +{ + if (wl_display_roundtrip(ctx->display) < 0) + return 0; + memset(state, 0, sizeof(*state)); + return invoke_on_server_thread(dde_shell_v2_query_surface_state, state); +} + +static int set_surface_role(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_role(ctx->shell_surface, + TREELAND_DDE_SHELL_SURFACE_V2_ROLE_OVERLAY); + return read_shell_surface_state(ctx, &state) && state.role_overlay && !state.role_normal; +} + +static int set_surface_role_normal(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_role(ctx->shell_surface, + TREELAND_DDE_SHELL_SURFACE_V2_ROLE_NORMAL); + return read_shell_surface_state(ctx, &state) && state.role_normal && !state.role_overlay; +} + +static int set_position_hint(struct test_ctx *ctx) +{ + // TODO: this asserts only the stored hint on an unmapped wl_surface; add a + // mapped XDG fixture asserting the actual global geometry produced by + // ShellHandler::handleDdeShellSurfaceV2Added() and Output placement, and a + // non-origin second output to cover WOutput::position() conversion. + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_position_hint(ctx->shell_surface, ctx->output, 42, 24); + return read_shell_surface_state(ctx, &state) + && state.position_set && state.position_x == 42 && state.position_y == 24 + && !state.cursor_set; +} + +static int set_position_hint_null_output(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_position_hint(ctx->shell_surface, NULL, 7, 9); + return read_shell_surface_state(ctx, &state) + && state.position_set && state.position_x == 7 && state.position_y == 9; +} + +// (0,0) is a valid fixed position (output origin / global origin), it must +// register as a present hint and not be swallowed as "no request". +static int set_position_hint_zero(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_position_hint(ctx->shell_surface, NULL, 0, 0); + return read_shell_surface_state(ctx, &state) + && state.position_set && state.position_x == 0 && state.position_y == 0 + && !state.cursor_set; +} + +static int set_cursor_placement_hint(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_cursor_placement_hint(ctx->shell_surface, 11, 5); + return read_shell_surface_state(ctx, &state) + && state.cursor_set && state.cursor_x == 11 && state.cursor_y == 5 + && !state.position_set; +} + +static int placement_mode_switches_back(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_position_hint(ctx->shell_surface, NULL, 3, 4); + return read_shell_surface_state(ctx, &state) + && state.position_set && state.position_x == 3 && state.position_y == 4 + && !state.cursor_set; +} + +static int set_skip_flags(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + const uint32_t all_flags = TREELAND_DDE_SHELL_SURFACE_V2_SKIP_FLAG_SWITCHER + | TREELAND_DDE_SHELL_SURFACE_V2_SKIP_FLAG_DOCK_PREVIEW + | TREELAND_DDE_SHELL_SURFACE_V2_SKIP_FLAG_MULTITASK_VIEW; + treeland_dde_shell_surface_v2_set_skip_flags(ctx->shell_surface, all_flags); + return read_shell_surface_state(ctx, &state) && state.skip_flags == (int)all_flags; +} + +static int clear_skip_flags(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_skip_flags(ctx->shell_surface, 0); + return read_shell_surface_state(ctx, &state) && state.skip_flags == 0; +} + +static int set_keyboard_focus(struct test_ctx *ctx) +{ + struct dde_shell_surface_v2_state state; + treeland_dde_shell_surface_v2_set_accept_keyboard_focus(ctx->shell_surface, 0); + return read_shell_surface_state(ctx, &state) && !state.accept_keyboard_focus; +} + +static int shell_surface_state(struct test_ctx *ctx) +{ + (void)ctx; + struct dde_shell_surface_v2_state state; + // The previous set_role.normal case left the role at normal. + return invoke_on_server_thread(dde_shell_v2_query_surface_state, &state) + && state.role_normal && !state.accept_keyboard_focus; +} + +static int destroy_shell_surface(struct test_ctx *ctx) +{ + treeland_dde_shell_surface_v2_destroy(ctx->shell_surface); + ctx->shell_surface = NULL; + return wl_display_roundtrip(ctx->display) >= 0; +} + +static int recreate_after_destroy(struct test_ctx *ctx) +{ + ctx->shell_surface = treeland_dde_shell_manager_v2_get_shell_surface(ctx->manager, + ctx->test_surface); + return ctx->shell_surface != NULL && wl_display_roundtrip(ctx->display) >= 0; +} + +static int duplicate_shell_surface_error(struct test_ctx *ctx) +{ + treeland_dde_shell_manager_v2_get_shell_surface(ctx->manager, ctx->test_surface); + // The compositor raises already_shell_surface, which kills the connection: + // the roundtrip must fail. The client cannot reliably read back the code + // (the server closes the connection and the IO error masks it in + // wl_display_get_protocol_error), so the exact error path is asserted + // server-side via the error counter. + if (wl_display_roundtrip(ctx->display) >= 0) + return 0; + + struct dde_shell_surface_v2_state state; + return invoke_on_server_thread(dde_shell_v2_query_surface_state, &state) + && state.duplicate_errors == 1; +} + +static const struct test_case cases[] = { + { "manager.get_shell_surface", create_shell_surface }, + { "shell_surface.set_role", set_surface_role }, + { "shell_surface.set_role.normal", set_surface_role_normal }, + { "shell_surface.set_position_hint", set_position_hint }, + { "shell_surface.set_position_hint.null_output", set_position_hint_null_output }, + { "shell_surface.set_position_hint.zero", set_position_hint_zero }, + { "shell_surface.set_cursor_placement_hint", set_cursor_placement_hint }, + { "shell_surface.placement_mode_switches_back", placement_mode_switches_back }, + { "shell_surface.set_skip_flags", set_skip_flags }, + { "shell_surface.clear_skip_flags", clear_skip_flags }, + { "shell_surface.set_accept_keyboard_focus", set_keyboard_focus }, + { "server.shell_surface_state", shell_surface_state }, + { "shell_surface.destroy", destroy_shell_surface }, + { "manager.recreate_after_destroy", recreate_after_destroy }, + // Must stay last: the expected protocol error tears the connection down. + { "manager.get_shell_surface.duplicate_error", duplicate_shell_surface_error }, +}; + +void test_cleanup(struct test_ctx *ctx) +{ + if (ctx->shell_surface) + treeland_dde_shell_surface_v2_destroy(ctx->shell_surface); + if (ctx->manager) + treeland_dde_shell_manager_v2_destroy(ctx->manager); + client_disconnect(&ctx->connection); +} + +int protocol_test_run(const char *socket_name) +{ + struct test_ctx ctx = {0}; + test_init(&ctx); + if (!connect_client(&ctx, socket_name)) { + fprintf(stderr, "failed to connect to or bind treeland_dde_shell_manager_v2\n"); + test_cleanup(&ctx); + test_finish(&ctx); + return 1; + } + + const size_t total_cases = sizeof(cases) / sizeof(cases[0]); + for (size_t i = 0; i < total_cases; ++i) { + const int result = test_add(&ctx, cases[i].name); + if (!cases[i].run(&ctx)) + test_fail(&ctx, result, "assertion failed"); + // The final case expects the protocol error that kills the connection. + if (i + 1 < total_cases && wl_display_roundtrip(ctx.display) < 0) + test_fail(&ctx, result, "Wayland connection failed"); + } + + test_cleanup(&ctx); + const int success = test_print_results(&ctx); + test_finish(&ctx); + return success ? 0 : 1; +} diff --git a/tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.h b/tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.h new file mode 100644 index 0000000000..b8593a77ca --- /dev/null +++ b/tests/protocols/treeland-dde-shell-v2/treeland-dde-shell-v2.h @@ -0,0 +1,65 @@ +// Copyright (C) 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 +#ifndef DDE_SHELL_V2_TEST_H +#define DDE_SHELL_V2_TEST_H + +#ifdef __cplusplus +extern "C" { +#endif + +int protocol_test_run(const char *socket_name); + +#include "client-connection.h" + +#define TEST_MSG_MAX 256 + +struct dde_shell_surface_v2_state { + int position_x; + int position_y; + int position_set; + int cursor_x; + int cursor_y; + int cursor_set; + int role_overlay; + int role_normal; + int skip_flags; + int accept_keyboard_focus; + int duplicate_errors; +}; + +struct test_result { + const char *name; + int failed; + char message[TEST_MSG_MAX]; +}; + +struct test_ctx { + struct client_connection connection; + struct wl_display *display; + + struct wl_compositor *compositor; + struct wl_seat *seat; + struct wl_output *output; + + struct treeland_dde_shell_manager_v2 *manager; + struct treeland_dde_shell_surface_v2 *shell_surface; + struct wl_surface *test_surface; + + struct test_result *results; + int result_count; + int result_cap; +}; + +void test_init(struct test_ctx *ctx); +void test_finish(struct test_ctx *ctx); +int test_add(struct test_ctx *ctx, const char *name); +void test_fail(struct test_ctx *ctx, int idx, const char *fmt, ...); +void test_pass(struct test_ctx *ctx, int idx); + +int test_print_results(struct test_ctx *ctx); +void test_cleanup(struct test_ctx *ctx); + +#ifdef __cplusplus +} +#endif +#endif