From e0c3cea6619dda18dee25feae633c9d30e6aa1f3 Mon Sep 17 00:00:00 2001 From: guolin Date: Wed, 16 Sep 2026 15:08:18 +0800 Subject: [PATCH 1/3] remove test --- tests/protocols/CMakeLists.txt | 2 - tests/protocols/INDEX.md | 2 - .../CMakeLists.txt | 12 - .../treeland-capture-desktop-v1/setup.cpp | 97 ------ .../treeland-capture-desktop-v1.c | 292 ----------------- .../treeland-capture-desktop-v1.h | 31 -- .../CMakeLists.txt | 11 - .../treeland-capture-unstable-v1/README.md | 87 ------ .../treeland-capture-unstable-v1/setup.cpp | 14 - .../treeland-capture-unstable-v1.c | 293 ------------------ .../treeland-capture-unstable-v1.h | 55 ---- 11 files changed, 896 deletions(-) delete mode 100644 tests/protocols/treeland-capture-desktop-v1/CMakeLists.txt delete mode 100644 tests/protocols/treeland-capture-desktop-v1/setup.cpp delete mode 100644 tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.c delete mode 100644 tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.h delete mode 100644 tests/protocols/treeland-capture-unstable-v1/CMakeLists.txt delete mode 100644 tests/protocols/treeland-capture-unstable-v1/README.md delete mode 100644 tests/protocols/treeland-capture-unstable-v1/setup.cpp delete mode 100644 tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.c delete mode 100644 tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.h diff --git a/tests/protocols/CMakeLists.txt b/tests/protocols/CMakeLists.txt index 2bf91fc0b3..9563757f04 100644 --- a/tests/protocols/CMakeLists.txt +++ b/tests/protocols/CMakeLists.txt @@ -29,8 +29,6 @@ add_subdirectory(wlr-output-power-management-unstable-v1) add_subdirectory(wlr-screencopy-unstable-v1) add_subdirectory(treeland-app-id-resolver-v2) add_subdirectory(treeland-app-id-resolver-desktop-v2) -add_subdirectory(treeland-capture-unstable-v1) -add_subdirectory(treeland-capture-desktop-v1) add_subdirectory(treeland-dde-shell-v1) add_subdirectory(treeland-dde-shell-desktop-v1) add_subdirectory(treeland-dde-shell-lockscreen-desktop-v1) diff --git a/tests/protocols/INDEX.md b/tests/protocols/INDEX.md index 67e49d9d27..3ede5d129d 100644 --- a/tests/protocols/INDEX.md +++ b/tests/protocols/INDEX.md @@ -25,7 +25,6 @@ Wayland 线上请求与事件;本文档规定发出请求后,测试必须观 | --- | --- | --- | | [测试框架架构](framework/README.md) | 基础设施 | runner、fixture、C client、生产状态桥接与 E/V 断言边界 | | [app-id-resolver-v1](treeland-app-id-resolver-v1/README.md) | I / E | resolver pidfd 应答;返回 app-id 转换真实 splash wrapper 为 xdg window | -| [capture-unstable-v1](treeland-capture-unstable-v1/README.md) | E / V / P | 真实窗口选择、frame copy 与目标 buffer 像素;无 source 错误 | | [dde-shell-v1](treeland-dde-shell-v1/README.md) | E / P | mapped wrapper 的 DDE 元数据、锁屏;picker 的真实选中/PID 回传测试待执行确认 | | [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 | @@ -77,7 +76,6 @@ request stub 算作 request 覆盖;生成的 client-protocol 文件本身不 | 协议 | request(已调用 / XML) | 已断言的 event 或生产结果 | 主要未覆盖 request / event / 业务结果 | | --- | --- | --- | --- | | app-id-resolver-v1 | 4 / 4 | `identify_request` 的 id、真实 pidfd;`respond` 转换同一 splash wrapper 并写入 app-id | resolver 断开 fallback、sandbox 后续策略、XWayland | -| capture-unstable-v1 | 8 / 11 | `source_ready/failed`、`buffer/buffer_done/ready/failed`;64×64 红色像素读回 | session 的 `start/frame_done`,及 `frame/object/ready/cancel` 持久流;OUTPUT/REGION、cursor、mask | | dde-shell-v1 | 26 / 27 | checker/active/picker 事件;真实 wrapper DDE 元数据、lockscreen、picker PID | `set_xwindow_position_relative`;multitask 只证明 `toggle` 请求/信号,未证明真实 UI 状态;`shutdown/switch_user` 外部会话流程 | | ddm-v1 | 0 / 7 | 无未请求 VT event;生产连接生命周期 | 所有会话/渲染控制 request 与 `switch_to_vt/acquire_vt` 的实际系统流程 | | foreign-toplevel-manager-v2 | 16 / 16 | `toplevel/identifier/closed`;真实最小化、最大化、全屏、焦点与 icon rectangle;最小化与布局状态正交(含取消布局后不卡在最小化) | `pid/title/app_id/output_enter/output_leave/state/done/parent` payload;指定 `wl_output` 的 fullscreen hint、preview 像素;`Tiling` 组合 | diff --git a/tests/protocols/treeland-capture-desktop-v1/CMakeLists.txt b/tests/protocols/treeland-capture-desktop-v1/CMakeLists.txt deleted file mode 100644 index 0a25e2cb04..0000000000 --- a/tests/protocols/treeland-capture-desktop-v1/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ - -treeland_add_protocol_test( - NAME treeland_capture_desktop_v1 - XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-capture-unstable-v1.xml" - SETUP "${CMAKE_CURRENT_SOURCE_DIR}/setup.cpp" - CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-capture-desktop-v1.c" -) - -# CaptureManagerV1 and the production CaptureSourceSelector are supplied by -# the Treeland.Capture module, which libtreeland keeps private. -target_link_libraries(test_treeland_capture_desktop_v1 PRIVATE capture) -target_compile_definitions(test_treeland_capture_desktop_v1 PRIVATE WLR_USE_UNSTABLE) diff --git a/tests/protocols/treeland-capture-desktop-v1/setup.cpp b/tests/protocols/treeland-capture-desktop-v1/setup.cpp deleted file mode 100644 index 7bf8b4e5f9..0000000000 --- a/tests/protocols/treeland-capture-desktop-v1/setup.cpp +++ /dev/null @@ -1,97 +0,0 @@ -// 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 "core/rootsurfacecontainer.h" -#include "core/shellhandler.h" -#include "modules/capture/capture.h" -#include "server-bridge.h" -#include "seat/helper.h" -#include "surface/surfacewrapper.h" -#include "treeland-capture-desktop-v1.h" -#include "workspace/workspace.h" - -#include -#include -#include - -namespace { -SurfaceWrapper *g_wrapper = nullptr; -capture_desktop_selection_state g_state {}; - -void copyState(void *data) -{ - *static_cast(data) = g_state; -} -} - -void protocol_test_setup(Helper *helper) -{ - add_headless_output(helper->backend(), false); - QObject::connect(helper->shellHandler(), - &ShellHandler::surfaceWrapperAdded, - helper, - [helper](SurfaceWrapper *wrapper) { - if (wrapper->type() != SurfaceWrapper::Type::XdgToplevel) - return; - g_wrapper = wrapper; - g_state.wrapper_ready = 1; - g_state.output_ready = !helper->rootSurfaceContainer()->outputs().isEmpty() ? 1 : 0; - }); -} - -extern "C" void capture_desktop_select_mapped_surface(void *data) -{ - auto *helper = Helper::instance(); - g_state.output_ready = !helper->rootSurfaceContainer()->outputs().isEmpty() ? 1 : 0; - if (!g_wrapper || !helper->workspace()->surfaces().contains(g_wrapper) - || !g_wrapper->surfaceItem()) { - copyState(data); - return; - } - - auto *content = g_wrapper->surfaceItem()->findItemContent(); - if (!content) { - copyState(data); - return; - } - g_state.surface_content_ready = 1; - const auto paintOrder = WOutputRenderWindow::paintOrderItemList( - helper->window()->contentItem(), [](QQuickItem *) { return true; }); - g_state.content_in_paint_order = paintOrder.contains(content) ? 1 : 0; - g_state.content_visible = content->isVisible() ? 1 : 0; - g_state.content_width = qRound(content->width()); - g_state.content_height = qRound(content->height()); - - // Helper creates this production QML selector in response to the client's - // select_source request. The test never calls CaptureContextV1::setSource(). - auto *selector = helper->rootSurfaceContainer()->findChild(); - if (!selector) { - copyState(data); - return; - } - g_state.selector_ready = 1; - - // Select the verified mapped production surface through the selector's - // production selection entry point. - helper->window()->render(); - selector->selectSurface(content); - g_state.hovered_mapped_content = 1; - - auto *context = selector->captureManager()->contextInSelection(); - auto *source = context ? context->source() : nullptr; - g_state.source_selected = source ? 1 : 0; - g_state.source_is_surface = source && source->sourceType() == CaptureSource::Surface ? 1 : 0; - if (source) { - const QSize sourceSize = source->sourceSize(); - g_state.source_width = sourceSize.width(); - g_state.source_height = sourceSize.height(); - } - copyState(data); -} - -extern "C" void capture_desktop_render_selected_source(void *data) -{ - auto *helper = Helper::instance(); - helper->window()->render(); - g_state.render_requested = 1; - copyState(data); -} diff --git a/tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.c b/tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.c deleted file mode 100644 index d6596b0316..0000000000 --- a/tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.c +++ /dev/null @@ -1,292 +0,0 @@ -// 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 _POSIX_C_SOURCE -#define _POSIX_C_SOURCE 200809L -#endif - -#include "xdg-toplevel-client.h" -#include "server-bridge-api.h" -#include "treeland-capture-desktop-v1.h" -#include "treeland-capture-unstable-v1-client-protocol.h" - -#include -#include -#include -#include -#include - -extern void capture_desktop_select_mapped_surface(void *data); -extern void capture_desktop_render_selected_source(void *data); - -struct capture_client { - struct client_connection connection; - struct xdg_toplevel_client toplevel; - struct treeland_capture_manager_v1 *manager; - struct treeland_capture_context_v1 *context; - struct treeland_capture_frame_v1 *frame; - struct wl_buffer *target_buffer; - void *target_data; - size_t target_size; - uint32_t target_format; - uint32_t target_width; - uint32_t target_height; - uint32_t target_stride; - int source_ready; - int source_failed; - uint32_t source_type; - uint32_t source_width; - uint32_t source_height; - int buffer_received; - int buffer_done; - int frame_ready; - int frame_failed; -}; - -static void context_source_ready(void *data, - struct treeland_capture_context_v1 *context, - int32_t x, - int32_t y, - uint32_t width, - uint32_t height, - uint32_t source_type) -{ - (void)context; - (void)x; - (void)y; - struct capture_client *client = data; - client->source_ready = 1; - client->source_width = width; - client->source_height = height; - client->source_type = source_type; -} - -static void context_source_failed(void *data, - struct treeland_capture_context_v1 *context, - uint32_t reason) -{ - (void)context; - (void)reason; - ((struct capture_client *)data)->source_failed = 1; -} - -static const struct treeland_capture_context_v1_listener context_listener = { - .source_ready = context_source_ready, - .source_failed = context_source_failed, -}; - -static int create_target_buffer(struct capture_client *client) -{ - const size_t size = (size_t)client->target_stride * client->target_height; - char name[64]; - snprintf(name, sizeof(name), "/treeland_capture_target_%d", (int)getpid()); - const int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); - if (fd < 0) - return 0; - shm_unlink(name); - if (ftruncate(fd, (off_t)size) < 0) { - close(fd); - return 0; - } - client->target_data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); - if (client->target_data == MAP_FAILED) { - client->target_data = NULL; - close(fd); - return 0; - } - memset(client->target_data, 0, size); - struct wl_shm_pool *pool = wl_shm_create_pool(client->toplevel.shm, fd, (int)size); - close(fd); - if (!pool) - return 0; - client->target_buffer = wl_shm_pool_create_buffer(pool, - 0, - (int)client->target_width, - (int)client->target_height, - (int)client->target_stride, - client->target_format); - wl_shm_pool_destroy(pool); - if (!client->target_buffer) - return 0; - client->target_size = size; - return 1; -} - -static void frame_buffer(void *data, - struct treeland_capture_frame_v1 *frame, - uint32_t format, - uint32_t width, - uint32_t height, - uint32_t stride) -{ - struct capture_client *client = data; - if (client->buffer_received) - return; - client->target_format = format; - client->target_width = width; - client->target_height = height; - client->target_stride = stride; - client->buffer_received = create_target_buffer(client); - if (client->buffer_received) { - treeland_capture_frame_v1_copy(frame, client->target_buffer); - wl_display_flush(client->connection.display); - } -} - -static void frame_buffer_done(void *data, struct treeland_capture_frame_v1 *frame) -{ - (void)frame; - ((struct capture_client *)data)->buffer_done = 1; -} - -static void frame_flags(void *data, struct treeland_capture_frame_v1 *frame, uint32_t flags) -{ - (void)data; - (void)frame; - (void)flags; -} - -static void frame_ready(void *data, struct treeland_capture_frame_v1 *frame) -{ - (void)frame; - ((struct capture_client *)data)->frame_ready = 1; -} - -static void frame_failed(void *data, struct treeland_capture_frame_v1 *frame) -{ - (void)frame; - ((struct capture_client *)data)->frame_failed = 1; -} - -static const struct treeland_capture_frame_v1_listener frame_listener = { - .buffer = frame_buffer, - .buffer_done = frame_buffer_done, - .flags = frame_flags, - .ready = frame_ready, - .failed = frame_failed, -}; - -static int target_is_opaque_red(const struct capture_client *client) -{ - if (!client->target_data || client->target_width != 64 || client->target_height != 64 - || client->target_stride != 64 * 4) - return 0; - const uint32_t pixel = *(const uint32_t *)client->target_data; - switch (client->target_format) { - case WL_SHM_FORMAT_ARGB8888: - return ((pixel >> 24) & 0xff) == 255 && ((pixel >> 16) & 0xff) == 255 - && ((pixel >> 8) & 0xff) == 0 && (pixel & 0xff) == 0; - case WL_SHM_FORMAT_ABGR8888: - return ((pixel >> 24) & 0xff) == 255 && (pixel & 0xff) == 255 - && ((pixel >> 8) & 0xff) == 0 && ((pixel >> 16) & 0xff) == 0; - default: - return 0; - } -} - -static void cleanup(struct capture_client *client) -{ - if (client->frame) - treeland_capture_frame_v1_destroy(client->frame); - if (client->context) - treeland_capture_context_v1_destroy(client->context); - if (client->manager) - treeland_capture_manager_v1_destroy(client->manager); - if (client->target_buffer) - wl_buffer_destroy(client->target_buffer); - if (client->target_data) - munmap(client->target_data, client->target_size); - xdg_toplevel_client_destroy(&client->toplevel); - client_disconnect(&client->connection); -} - -int protocol_test_run(const char *socket_name) -{ - struct capture_client client = { 0 }; - struct capture_desktop_selection_state selection = { 0 }; - struct capture_desktop_selection_state render = { 0 }; - int result = 1; - - if (!client_connect(&client.connection, socket_name) - || !xdg_toplevel_client_create_with_solid_buffer( - &client.connection, &client.toplevel, 64, 64, 0xffff0000u)) - goto done; - client.manager = client_bind(&client.connection, - "treeland_capture_manager_v1", - &treeland_capture_manager_v1_interface, - 1); - if (!client.manager) - goto done; - client.context = treeland_capture_manager_v1_get_context(client.manager); - if (!client.context) - goto done; - treeland_capture_context_v1_add_listener(client.context, &context_listener, &client); - - treeland_capture_context_v1_select_source(client.context, - TREELAND_CAPTURE_CONTEXT_V1_SOURCE_TYPE_WINDOW, - 0, - 0, - NULL); - if (wl_display_roundtrip(client.connection.display) < 0 - || !invoke_on_server_thread(capture_desktop_select_mapped_surface, &selection) - || wl_display_roundtrip(client.connection.display) < 0 - || !selection.output_ready || !selection.wrapper_ready || !selection.surface_content_ready - || !selection.content_in_paint_order || !selection.content_visible - || selection.content_width != 64 || selection.content_height != 64 - || !selection.selector_ready || !selection.hovered_mapped_content - || !selection.source_selected || !selection.source_is_surface - || selection.source_width != 64 || selection.source_height != 64 - || !client.source_ready || client.source_failed - || client.source_type != TREELAND_CAPTURE_CONTEXT_V1_SOURCE_TYPE_WINDOW - || client.source_width != 64 || client.source_height != 64) - goto done; - - client.frame = treeland_capture_context_v1_capture(client.context); - if (!client.frame) - goto done; - treeland_capture_frame_v1_add_listener(client.frame, &frame_listener, &client); - - // The roundtrip is a protocol ordering barrier: it proves onCapture() and - // CaptureSourceSelector::doneSelection() have run before we request the - // real output render that produces the source image. - if (wl_display_roundtrip(client.connection.display) < 0 - || !invoke_on_server_thread(capture_desktop_render_selected_source, &render) - || !render.render_requested) - goto done; - - while (!client.frame_ready && !client.frame_failed) { - if (wl_display_dispatch(client.connection.display) < 0) - goto done; - } - if (!client.frame_failed && client.buffer_received && client.buffer_done && target_is_opaque_red(&client)) - result = 0; - -done: - if (result != 0) { - fprintf(stderr, - "capture desktop failed: content=(paint=%d visible=%d %dx%d) selector=(%d,%d,%d) source=(surface=%d %dx%d) " - "event=(ready=%d failed=%d type=%u %ux%u) buffer=(%d done=%d fmt=0x%x %ux%u stride=%u)\n", - selection.content_in_paint_order, - selection.content_visible, - selection.content_width, - selection.content_height, - selection.selector_ready, - selection.hovered_mapped_content, - selection.source_selected, - selection.source_is_surface, - selection.source_width, - selection.source_height, - client.source_ready, - client.source_failed, - client.source_type, - client.source_width, - client.source_height, - client.buffer_received, - client.buffer_done, - client.target_format, - client.target_width, - client.target_height, - client.target_stride); - } - cleanup(&client); - return result; -} diff --git a/tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.h b/tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.h deleted file mode 100644 index 2210cda504..0000000000 --- a/tests/protocols/treeland-capture-desktop-v1/treeland-capture-desktop-v1.h +++ /dev/null @@ -1,31 +0,0 @@ -// 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 - -#ifdef __cplusplus -extern "C" { -#endif - -struct capture_desktop_selection_state { - int output_ready; - int wrapper_ready; - int surface_content_ready; - int content_in_paint_order; - int content_visible; - int content_width; - int content_height; - int selector_ready; - int hovered_mapped_content; - int source_selected; - int source_is_surface; - int source_width; - int source_height; - int render_requested; -}; - -int protocol_test_run(const char *socket_name); - -#ifdef __cplusplus -} -#endif diff --git a/tests/protocols/treeland-capture-unstable-v1/CMakeLists.txt b/tests/protocols/treeland-capture-unstable-v1/CMakeLists.txt deleted file mode 100644 index 00575686ea..0000000000 --- a/tests/protocols/treeland-capture-unstable-v1/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -treeland_add_protocol_test( - NAME treeland_capture_unstable_v1 - XML "${TREELAND_PROTOCOLS_DATA_DIR}/treeland-capture-unstable-v1.xml" - SETUP "${CMAKE_CURRENT_SOURCE_DIR}/setup.cpp" - CLIENT "${CMAKE_CURRENT_SOURCE_DIR}/treeland-capture-unstable-v1.c" -) - -# CaptureManagerV1 lives in the Treeland.Capture QML module library, which -# libtreeland links privately (impl_treeland), so link it explicitly here. -target_link_libraries(test_treeland_capture_unstable_v1 PRIVATE capture) -target_compile_definitions(test_treeland_capture_unstable_v1 PRIVATE WLR_USE_UNSTABLE) diff --git a/tests/protocols/treeland-capture-unstable-v1/README.md b/tests/protocols/treeland-capture-unstable-v1/README.md deleted file mode 100644 index d06df8ab22..0000000000 --- a/tests/protocols/treeland-capture-unstable-v1/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# `treeland-capture-unstable-v1` 测试规范 - -## 范围 - -本协议有两层测试,不能把基础错误路径误认为实际捕获成功。 - -| 测试源码 | Fixture | 覆盖等级 | 用途 | -| --- | --- | --- | --- | -| `treeland-capture-unstable-v1/` | 仅 `CaptureManagerV1`,没有 selector/source | P | 选择权、资源和无 source 协议错误 | -| `treeland-capture-desktop-v1/` | 完整生产 `Helper`、headless output、`ShellHandler`、QML selector 与 mapped xdg 窗口 | E / V | 真实选择窗口、复制 frame,并读取客户端目标 buffer 像素 | - -## 必须观察到的结果 - -| 场景 | 客户端动作 | 必须观察到的结果 | -| --- | --- | --- | -| context 所有权 | 两个客户端创建 context | selector 的 busy/所有权规则生效 | -| 无 source | source ready 前调用 `capture` 或 `create_session` | 连接收到 XML 规定的协议错误 | - -## 基础协议路径(P) - -第一个客户端 context 发送 `select_source(OUTPUT, 0, 0, NULL)` 后成为正在选择 source -的 context,测试断言它不会收到 `source_failed`。第二个 context 随即发送 -`select_source(WINDOW, 0, 0, NULL)`,生产 `CaptureSourceSelector` 必须发送 -`source_failed(SELECTOR_BUSY)`。销毁第一个 context 后,第三个 context 发送 -`select_source(REGION, 0, 0, NULL)`,不应再收到 busy 失败,证明销毁确实释放生产 -selector 的选择状态。 - -另外两个全新客户端各自发送 `get_context` 后立即调用 `capture()` 或 -`create_session()`。由于没有 ready source,生产 `CaptureContextV1` 必须在 display -上 post `WL_DISPLAY_ERROR_IMPLEMENTATION`;测试用 -`wl_display_get_protocol_error()` 读取该真实协议错误。 - -## 实际捕获路径(E / V) - -`treeland-capture-desktop-v1` 使用同一个客户端同时建立被捕获窗口和 capture -context,完整步骤如下。 - -1. 客户端创建 xdg-toplevel,等待并 ack 初次 `xdg_surface.configure`,然后提交 - `64×64`、`wl_shm/ARGB8888`、所有像素为 `0xffff0000`(不透明红)的 buffer。 -2. 生产 `ShellHandler` 收到 mapped xdg surface,创建 `SurfaceWrapper`,加入 - `Workspace`,并由生产 QML `SurfaceContent` 创建内部的 - `WSurfaceItemContent`。 -3. 客户端创建 `treeland_capture_context_v1`,发送 - `select_source(WINDOW, freeze=0, with_cursor=0, mask=NULL)`。这使生产 - `CaptureManagerV1` 建立 `contextInSelection`,而 `Helper` 的既有连接创建 - QML `CaptureSourceSelector`。 -4. fixture 先断言该 `WSurfaceItemContent` 位于生产 output 的 paint-order、可见且为 - `64×64`。headless QPA 不稳定地投递 Qt Quick hover/pointer,因此测试用一个仅供 - protocol test 使用的 access bridge 调用 selector 自己的 - `setSelectedSource()`;它创建真实 `CaptureSourceSurface`,而不是向 - `CaptureContextV1::setSource()` 塞入伪造 source。 -5. selector 的生产 `setSelectedSource()` 调用生产 - `CaptureContextV1::setSource()`。客户端必须收到 - `source_ready(..., width=64, height=64, source_type=WINDOW)`;fixture 同时断言 - source 的实际类型为 `CaptureSource::Surface`、尺寸为 `64×64`。 -6. 客户端发送 `capture()`。协议 roundtrip 确认生产 `onCapture()` 与 selector - `doneSelection()` 已执行;fixture 调用真实 `WOutputRenderWindow::render()`。 - `renderEnd` 驱动 `CaptureSource::createImage()`,由 `WTextureCapturer` 读取所选 - `WSurfaceItemContent` 的生产纹理。 -7. 客户端收到 `treeland_capture_frame_v1.buffer(format, 64, 64, 256)` 后,以该 - format 创建自己的 `wl_shm` 目标 buffer,发送 `frame.copy(buffer)`。生产 - `CaptureSource::copyBuffer()` 把 image 写入此 buffer,并发送 `buffer_done` 与 - `ready`。 -8. 客户端仅等待真实 `ready` 或 `failed` 事件。通过条件为:没有 `failed`、收到 - `buffer_done` 与 `ready`、目标为 `64×64`/stride `256`,且第一个像素按服务器 - 宣告的 `ARGB8888` 或 `ABGR8888` 格式解码后为 RGBA `(255,0,0,255)`。 - -已证明的完整数据流为: - -`wl_shm 红色窗口 buffer → xdg map → ShellHandler/SurfaceWrapper/QML SurfaceContent -→ CaptureSourceSelector 真实 source 提交 → CaptureSourceSurface → source_ready → -WTextureCapturer → frame.buffer → 客户端 wl_shm frame.copy → frame.ready → 红色像素`。 - -该 E/V 路径已通过本地执行。测试没有根据时间判断 selector、纹理或 frame 是否就绪: -状态推进分别由 Wayland roundtrip、服务端同步回调,以及 -`source_ready`/`buffer`/`ready` 事件驱动。 - -## 已知边界 / 下一项结果 - -当前只验证 **window/surface 的 one-shot copy**。尚未覆盖: - -- Qt Quick headless QPA 下真实鼠标 hover/release 到 `ItemSelector` 的命中分发; -- `OUTPUT`、`REGION` source 的选区和坐标裁剪; -- `freeze=1`、`with_cursor=1` 与 mask surface 的实际合成语义; -- persistent `create_session()` 的 dmabuf `frame/object/ready/frame_done` 流; -- output 的最终合成 backing buffer 像素(当前 window source 已是生产 scene - texture,目标 buffer 像素已验证)。 diff --git a/tests/protocols/treeland-capture-unstable-v1/setup.cpp b/tests/protocols/treeland-capture-unstable-v1/setup.cpp deleted file mode 100644 index 6a42b9bda2..0000000000 --- a/tests/protocols/treeland-capture-unstable-v1/setup.cpp +++ /dev/null @@ -1,14 +0,0 @@ -// 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/capture/capture.h" -#include "server-bridge.h" - -#include -#include - -WAYLIB_SERVER_USE_NAMESPACE - -void protocol_test_setup(Helper *helper) -{ - Q_ASSERT(find_server_interface(helper)); -} diff --git a/tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.c b/tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.c deleted file mode 100644 index 29b00c940f..0000000000 --- a/tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.c +++ /dev/null @@ -1,293 +0,0 @@ -// 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-capture-unstable-v1.h" -#include "treeland-capture-unstable-v1-client-protocol.h" - -#include -#include -#include -#include - -struct test_case { - const char *name; - int (*run)(struct test_ctx *ctx); -}; - -void test_init(struct test_ctx *ctx) -{ - memset(ctx, 0, sizeof(*ctx)); - ctx->result_cap = 32; - ctx->results = calloc(ctx->result_cap, sizeof(*ctx->results)); -} - -void test_destroy(struct test_ctx *ctx) -{ - free(ctx->results); - memset(ctx, 0, sizeof(*ctx)); -} - -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 void context_source_ready(void *data, struct treeland_capture_context_v1 *context, - int32_t region_x, int32_t region_y, - uint32_t region_width, uint32_t region_height, - uint32_t source_type) -{ - (void)data; - (void)context; - (void)region_x; - (void)region_y; - (void)region_width; - (void)region_height; - (void)source_type; -} - -static void context_a_source_failed(void *data, struct treeland_capture_context_v1 *context, - uint32_t reason) -{ - (void)context; - (void)reason; - ((struct test_ctx *)data)->a_source_failed_received = 1; -} - -static void context_b_source_failed(void *data, struct treeland_capture_context_v1 *context, - uint32_t reason) -{ - (void)context; - struct test_ctx *ctx = data; - ctx->b_source_failed_received = 1; - ctx->b_source_failed_reason = reason; -} - -static void context_c_source_failed(void *data, struct treeland_capture_context_v1 *context, - uint32_t reason) -{ - (void)context; - (void)reason; - ((struct test_ctx *)data)->c_source_failed_received = 1; -} - -static const struct treeland_capture_context_v1_listener context_listener_a = { - .source_ready = context_source_ready, - .source_failed = context_a_source_failed, -}; - -static const struct treeland_capture_context_v1_listener context_listener_b = { - .source_ready = context_source_ready, - .source_failed = context_b_source_failed, -}; - -static const struct treeland_capture_context_v1_listener context_listener_c = { - .source_ready = context_source_ready, - .source_failed = context_c_source_failed, -}; - -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->manager = client_bind(&ctx->connection, "treeland_capture_manager_v1", - &treeland_capture_manager_v1_interface, 1); - return ctx->manager != NULL; -} - -static int get_context(struct test_ctx *ctx) -{ - ctx->context_a = treeland_capture_manager_v1_get_context(ctx->manager); - if (ctx->context_a) - treeland_capture_context_v1_add_listener(ctx->context_a, &context_listener_a, ctx); - return ctx->context_a != NULL; -} - -static int select_source(struct test_ctx *ctx) -{ - - treeland_capture_context_v1_select_source(ctx->context_a, - TREELAND_CAPTURE_CONTEXT_V1_SOURCE_TYPE_OUTPUT, - 0, 0, NULL); - return 1; -} - -static int select_source_busy(struct test_ctx *ctx) -{ - - ctx->context_b = treeland_capture_manager_v1_get_context(ctx->manager); - if (!ctx->context_b) - return 0; - treeland_capture_context_v1_add_listener(ctx->context_b, &context_listener_b, ctx); - treeland_capture_context_v1_select_source(ctx->context_b, - TREELAND_CAPTURE_CONTEXT_V1_SOURCE_TYPE_WINDOW, - 0, 0, NULL); - return 1; -} - -static int source_failed_busy_received(struct test_ctx *ctx) -{ - return ctx->b_source_failed_received - && ctx->b_source_failed_reason == TREELAND_CAPTURE_CONTEXT_V1_SOURCE_FAILURE_SELECTOR_BUSY; -} - -static int no_source_failed_on_first_select(struct test_ctx *ctx) -{ - return ctx->a_source_failed_received == 0; -} - -static int destroy_in_selection(struct test_ctx *ctx) -{ - - treeland_capture_context_v1_destroy(ctx->context_a); - ctx->context_a = NULL; - return 1; -} - -static int select_source_after_clear(struct test_ctx *ctx) -{ - ctx->context_c = treeland_capture_manager_v1_get_context(ctx->manager); - if (!ctx->context_c) - return 0; - treeland_capture_context_v1_add_listener(ctx->context_c, &context_listener_c, ctx); - treeland_capture_context_v1_select_source(ctx->context_c, - TREELAND_CAPTURE_CONTEXT_V1_SOURCE_TYPE_REGION, - 0, 0, NULL); - return 1; -} - -static int no_source_failed_after_clear(struct test_ctx *ctx) -{ - return ctx->c_source_failed_received == 0; -} - -static struct treeland_capture_manager_v1 *connect_fresh_manager( - const char *socket_name, struct client_connection *connection) -{ - if (!client_connect(connection, socket_name)) - return NULL; - return client_bind(connection, "treeland_capture_manager_v1", - &treeland_capture_manager_v1_interface, 1); -} - -static int capture_without_source_errors(struct test_ctx *ctx) -{ - - struct client_connection connection; - struct treeland_capture_manager_v1 *manager = - connect_fresh_manager(ctx->socket_name, &connection); - if (!manager) - return 0; - struct treeland_capture_context_v1 *context = treeland_capture_manager_v1_get_context(manager); - treeland_capture_context_v1_capture(context); - const struct wl_interface *iface; - uint32_t code; - const int errored = wl_display_roundtrip(connection.display) < 0 - && wl_display_get_protocol_error(connection.display, &iface, &code) - == WL_DISPLAY_ERROR_IMPLEMENTATION; - client_disconnect(&connection); - return errored; -} - -static int create_session_without_source_errors(struct test_ctx *ctx) -{ - - struct client_connection connection; - struct treeland_capture_manager_v1 *manager = - connect_fresh_manager(ctx->socket_name, &connection); - if (!manager) - return 0; - struct treeland_capture_context_v1 *context = treeland_capture_manager_v1_get_context(manager); - treeland_capture_context_v1_create_session(context); - const struct wl_interface *iface; - uint32_t code; - const int errored = wl_display_roundtrip(connection.display) < 0 - && wl_display_get_protocol_error(connection.display, &iface, &code) - == WL_DISPLAY_ERROR_IMPLEMENTATION; - client_disconnect(&connection); - return errored; -} - -static const struct test_case cases[] = { - { "manager.get_context", get_context }, - { "context.select_source", select_source }, - { "context.select_source_busy", select_source_busy }, - { "context.event.source_failed_busy", source_failed_busy_received }, - { "context.event.no_source_failed_on_first_select", no_source_failed_on_first_select }, - { "context.destroy_in_selection", destroy_in_selection }, - { "context.select_source_after_clear", select_source_after_clear }, - { "context.event.no_source_failed_after_clear", no_source_failed_after_clear }, - { "context.capture_without_source_errors", capture_without_source_errors }, - { "context.create_session_without_source_errors", create_session_without_source_errors }, -}; - -void test_cleanup(struct test_ctx *ctx) -{ - if (ctx->context_c) treeland_capture_context_v1_destroy(ctx->context_c); - if (ctx->context_b) treeland_capture_context_v1_destroy(ctx->context_b); - if (ctx->context_a) treeland_capture_context_v1_destroy(ctx->context_a); - if (ctx->manager) treeland_capture_manager_v1_destroy(ctx->manager); - client_disconnect(&ctx->connection); -} - -int protocol_test_run(const char *socket_name) -{ - struct test_ctx ctx; - test_init(&ctx); - ctx.socket_name = socket_name; - if (!connect_client(&ctx, socket_name)) { - fprintf(stderr, "failed to connect to or bind treeland_capture_manager_v1\n"); - test_cleanup(&ctx); - test_destroy(&ctx); - return 1; - } - - for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { - const int result = test_add(&ctx, cases[i].name); - if (!cases[i].run(&ctx)) - test_fail(&ctx, result, "assertion failed"); - if (wl_display_roundtrip(ctx.display) < 0) - test_fail(&ctx, result, "Wayland connection failed"); - } - - test_cleanup(&ctx); - const int success = test_print_results(&ctx); - test_destroy(&ctx); - return success ? 0 : 1; -} diff --git a/tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.h b/tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.h deleted file mode 100644 index 718080d3db..0000000000 --- a/tests/protocols/treeland-capture-unstable-v1/treeland-capture-unstable-v1.h +++ /dev/null @@ -1,55 +0,0 @@ -// 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 TREELAND_CAPTURE_TEST_H -#define TREELAND_CAPTURE_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 test_result { - const char *name; - int failed; - char message[TEST_MSG_MAX]; -}; - -struct test_ctx { - struct client_connection connection; - struct wl_display *display; - const char *socket_name; - - struct treeland_capture_manager_v1 *manager; - - struct treeland_capture_context_v1 *context_a; - struct treeland_capture_context_v1 *context_b; - struct treeland_capture_context_v1 *context_c; - - int a_source_failed_received; - int b_source_failed_received; - uint32_t b_source_failed_reason; - int c_source_failed_received; - - struct test_result *results; - int result_count; - int result_cap; -}; - -void test_init(struct test_ctx *ctx); -void test_destroy(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 From 3f203c8ba8c69807dbff5d1aee9fa78ead860ad5 Mon Sep 17 00:00:00 2001 From: guolin Date: Fri, 14 Aug 2026 15:12:29 +0800 Subject: [PATCH 2/3] add capture snap --- CMakeLists.txt | 2 +- examples/CMakeLists.txt | 1 - examples/test_capture/CMakeLists.txt | 49 - examples/test_capture/Main.qml | 80 - examples/test_capture/capture.cpp | 288 ---- examples/test_capture/capture.h | 273 ---- examples/test_capture/images/watermark.png | Bin 2681 -> 0 bytes examples/test_capture/main.cpp | 109 -- examples/test_capture/player.cpp | 365 ----- examples/test_capture/player.h | 38 - examples/test_capture/subwindow.cpp | 16 - examples/test_capture/subwindow.h | 21 - src/CMakeLists.txt | 1 - src/core/qml/CaptureSelectorLayer.qml | 141 -- src/core/qmlengine.cpp | 11 - src/core/qmlengine.h | 3 - src/core/rootsurfacecontainer.h | 2 +- src/core/shellhandler.cpp | 74 +- src/core/shellhandler.h | 12 + src/modules/CMakeLists.txt | 2 +- src/modules/capture/capture.cpp | 1282 ----------------- src/modules/capture/capture.h | 474 ------ src/modules/capture/impl/capturev1impl.cpp | 362 ----- src/modules/capture/impl/capturev1impl.h | 124 -- .../{capture => snap-target}/CMakeLists.txt | 33 +- src/modules/snap-target/impl/snapv1impl.cpp | 87 ++ src/modules/snap-target/impl/snapv1impl.h | 38 + src/modules/snap-target/snapdetector.cpp | 69 + src/modules/snap-target/snapdetector.h | 37 + src/modules/snap-target/snaphandler.cpp | 200 +++ src/modules/snap-target/snaphandler.h | 72 + src/seat/helper.cpp | 42 +- src/seat/helper.h | 4 - src/surface/seatsurfacemanager.cpp | 2 + src/surface/surfacewrapper.cpp | 45 +- src/surface/surfacewrapper.h | 5 + 36 files changed, 658 insertions(+), 3706 deletions(-) delete mode 100644 examples/test_capture/CMakeLists.txt delete mode 100644 examples/test_capture/Main.qml delete mode 100644 examples/test_capture/capture.cpp delete mode 100644 examples/test_capture/capture.h delete mode 100644 examples/test_capture/images/watermark.png delete mode 100644 examples/test_capture/main.cpp delete mode 100644 examples/test_capture/player.cpp delete mode 100644 examples/test_capture/player.h delete mode 100644 examples/test_capture/subwindow.cpp delete mode 100644 examples/test_capture/subwindow.h delete mode 100644 src/core/qml/CaptureSelectorLayer.qml delete mode 100644 src/modules/capture/capture.cpp delete mode 100644 src/modules/capture/capture.h delete mode 100644 src/modules/capture/impl/capturev1impl.cpp delete mode 100644 src/modules/capture/impl/capturev1impl.h rename src/modules/{capture => snap-target}/CMakeLists.txt (60%) create mode 100644 src/modules/snap-target/impl/snapv1impl.cpp create mode 100644 src/modules/snap-target/impl/snapv1impl.h create mode 100644 src/modules/snap-target/snapdetector.cpp create mode 100644 src/modules/snap-target/snapdetector.h create mode 100644 src/modules/snap-target/snaphandler.cpp create mode 100644 src/modules/snap-target/snaphandler.h diff --git a/CMakeLists.txt b/CMakeLists.txt index db0cf8912b..c36cbf9d93 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -114,7 +114,7 @@ else() add_library(Wlroots::wlroots ALIAS PkgConfig::WLR_INSTALLED) endif() endif() - # The capture module generates protocol code with ws_generate(); load + # The snap-target module generates protocol code with ws_generate(); load # the vendored helpers so external builds do not depend on the scanner # helpers being exported by the installed Waylib package. include(${CMAKE_CURRENT_SOURCE_DIR}/waylib/cmake/WaylandScannerHelpers.cmake) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index df9803fc92..9ab704e622 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -10,7 +10,6 @@ add_subdirectory(test_pinch_handler) add_subdirectory(test_hide_window) add_subdirectory(test_data_control_manager) add_subdirectory(test_window_overlapped) -add_subdirectory(test_capture) add_subdirectory(test_window_appearance) add_subdirectory(test_super_overlay_surface) add_subdirectory(test_monitor_active_event) diff --git a/examples/test_capture/CMakeLists.txt b/examples/test_capture/CMakeLists.txt deleted file mode 100644 index 2a05972279..0000000000 --- a/examples/test_capture/CMakeLists.txt +++ /dev/null @@ -1,49 +0,0 @@ -find_package(PkgConfig REQUIRED) -find_package(Qt6 REQUIRED COMPONENTS Core Gui WaylandClient Quick) -if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10) - find_package(Qt6 REQUIRED COMPONENTS WaylandClientPrivate QuickPrivate) -endif() -find_package(TreelandProtocols REQUIRED) -pkg_check_modules(EGL REQUIRED IMPORTED_TARGET egl gl) -qt_add_executable(test-capture - main.cpp -) -qt_add_qml_module(test-capture - URI capture - VERSION 2.0 - SOURCES - capture.h - capture.cpp - subwindow.h - subwindow.cpp - player.h - player.cpp - QML_FILES - Main.qml -) - -qt_add_resources(test-capture "test_capture_assets" - PREFIX "/" - BASE ${CMAKE_CURRENT_LIST_DIR}/images - FILES - ${CMAKE_CURRENT_LIST_DIR}/images/watermark.png -) - -qt6_generate_wayland_protocol_client_sources(test-capture - FILES - ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-capture-unstable-v1.xml -) - -target_link_libraries(test-capture - PRIVATE - Qt6::Core - Qt6::Gui - Qt6::WaylandClient - Qt6::WaylandClientPrivate - Qt6::Quick - Qt6::QuickPrivate - Qt6::QuickControls2 - PkgConfig::EGL -) - -install(TARGETS test-capture RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/examples/test_capture/Main.qml b/examples/test_capture/Main.qml deleted file mode 100644 index 53b8354bdb..0000000000 --- a/examples/test_capture/Main.qml +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2024 Dingyuan Zhang . -// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -import QtQuick -import QtQuick.Controls -import capture - -Window { - id: canvas - width: 600 - height: 400 - visible: !TreelandCaptureManager.recordStarted - color: "transparent" - flags: regionReady ? 0 : Qt.WindowTransparentForInput - property rect captureRegion: TreelandCaptureManager.context?.captureRegion ?? Qt.rect(0, 0, 0, 0) - property bool regionReady: captureRegion.width !== 0 && captureRegion.height !== 0 - - Image { - id: watermark - visible: false - source: "qrc:/watermark.png" - sourceSize: Qt.size(100, 100) - fillMode: Image.Tile - x: captureRegion.x - y: captureRegion.y - width: captureRegion.width - height: captureRegion.height - } - - SubWindow { - id: toolBar - parent: canvas - visible: regionReady - x: captureRegion.x - y: Math.min(captureRegion.bottom, canvas.height - 2 * height) - width: row.width - height: row.height - color: "transparent" - Row { - id: row - Button { - width: 100 - height: 30 - text: watermark.visible ? "Hide watermark" : "Show watermark" - onClicked: { - watermark.visible = !watermark.visible - } - } - Button { - width: 100 - height: 30 - text: TreelandCaptureManager.record ? "Screenshot" : "Record" - onClicked: { - TreelandCaptureManager.record = !TreelandCaptureManager.record - } - } - Button { - width: 100 - height: 30 - text: "Finish" - onClicked: { - TreelandCaptureManager.finishSelect() - } - } - } - } - Window { - id: playerWindow - visible: TreelandCaptureManager.recordStarted - width: player.width - height: player.height - color: "black" - Player { - id: player - width: 100 - height: 100 - captureContext: TreelandCaptureManager.context - } - } -} diff --git a/examples/test_capture/capture.cpp b/examples/test_capture/capture.cpp deleted file mode 100644 index b66e3fb7d0..0000000000 --- a/examples/test_capture/capture.cpp +++ /dev/null @@ -1,288 +0,0 @@ -// SPDX-FileCopyrightText: 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 "capture.h" - -#include -#include -#include - -#include -#include - -inline QtWaylandClient::QWaylandIntegration *waylandIntegration() -{ - return dynamic_cast( - QGuiApplicationPrivate::platformIntegration()); -} - -inline QPointer waylandDisplay() -{ - return waylandIntegration()->display(); -} - -TreelandCaptureManager *TreelandCaptureManager::instance() -{ - static TreelandCaptureManager manager; - return &manager; -} - -TreelandCaptureManager *TreelandCaptureManager::create(QQmlEngine *, QJSEngine *) -{ - auto manager = instance(); - QQmlEngine::setObjectOwnership(manager, QQmlEngine::CppOwnership); - return manager; -} - -TreelandCaptureManager::TreelandCaptureManager() - : QWaylandClientExtensionTemplate(1) - , QtWayland::treeland_capture_manager_v1() -{ - connect(this, &TreelandCaptureManager::activeChanged, this, [this] { - if (!isActive()) { - delete m_context; - } - }); -} - -TreelandCaptureContext *TreelandCaptureManager::ensureContext() -{ - if (m_context) - return m_context; - auto context = get_context(); - m_context = new TreelandCaptureContext(context, this); - connect(m_context, &TreelandCaptureContext::destroyed, this, [this] { - m_context = nullptr; - Q_EMIT contextChanged(); - }); - Q_EMIT contextChanged(); - return m_context; -} - -void TreelandCaptureContext::treeland_capture_context_v1_source_ready(int32_t region_x, - int32_t region_y, - uint32_t region_width, - uint32_t region_height, - uint32_t source_type) -{ - m_captureRegion = QRect(region_x, region_y, region_width, region_height); - m_sourceType = static_cast(source_type); - Q_EMIT captureRegionChanged(); - Q_EMIT sourceReady(QRect(region_x, region_y, region_width, region_height), source_type); -} - -void TreelandCaptureContext::treeland_capture_context_v1_source_failed(uint32_t reason) -{ - Q_EMIT sourceFailed(reason); -} - -TreelandCaptureFrame *TreelandCaptureContext::ensureFrame() -{ - if (m_frame) - return m_frame; - auto object = QtWayland::treeland_capture_context_v1::capture(); - m_frame = new TreelandCaptureFrame(object, this); - connect(m_frame, &TreelandCaptureFrame::destroyed, this, [this] { - m_frame = nullptr; - Q_EMIT frameChanged(); - }); - Q_EMIT frameChanged(); - return m_frame; -} - -TreelandCaptureContext::TreelandCaptureContext(::treeland_capture_context_v1 *object, - QObject *parent) - : QObject(parent) - , QtWayland::treeland_capture_context_v1(object) -{ -} - -TreelandCaptureContext::~TreelandCaptureContext() -{ - delete m_frame; - delete m_session; - destroy(); -} - -void TreelandCaptureContext::selectSource(uint32_t sourceHint, - bool freeze, - bool withCursor, - ::wl_surface *mask) -{ - select_source(sourceHint, freeze, withCursor, mask); -} - -TreelandCaptureSession *TreelandCaptureContext::ensureSession() -{ - if (m_session) - return m_session; - auto object = create_session(); - m_session = new TreelandCaptureSession(object, this); - connect(m_session, &TreelandCaptureSession::destroyed, this, [this] { - m_session = nullptr; - Q_EMIT sessionChanged(); - }); - Q_EMIT sessionChanged(); - return m_session; -} - -TreelandCaptureFrame::TreelandCaptureFrame(::treeland_capture_frame_v1 *object, QObject *parent) - : QObject(parent) - , QtWayland::treeland_capture_frame_v1(object) - , m_shmBuffer(nullptr) - , m_pendingShmBuffer(nullptr) -{ -} - -TreelandCaptureFrame::~TreelandCaptureFrame() -{ - delete m_shmBuffer; - delete m_pendingShmBuffer; - destroy(); -} - -void TreelandCaptureFrame::treeland_capture_frame_v1_buffer(uint32_t format, - uint32_t width, - uint32_t height, - uint32_t stride) -{ - if (stride != width * 4) { - qDebug() << "Receive a buffer format which is not compatible with " - "QWaylandShmBuffer." - << "format:" << format << "width:" << width << "height:" << height - << "stride:" << stride; - return; - } - if (m_pendingShmBuffer) - return; // We only need one supported format - m_pendingShmBuffer = new QtWaylandClient::QWaylandShmBuffer( - waylandDisplay(), - QSize(width, height), - QtWaylandClient::QWaylandShm::formatFrom(static_cast<::wl_shm_format>(format))); - copy(m_pendingShmBuffer->buffer()); -} - -void TreelandCaptureFrame::treeland_capture_frame_v1_flags(uint32_t flags) -{ - m_flags = flags; -} - -void TreelandCaptureFrame::treeland_capture_frame_v1_ready() -{ - if (m_shmBuffer) - delete m_shmBuffer; - m_shmBuffer = m_pendingShmBuffer; - m_pendingShmBuffer = nullptr; - Q_EMIT ready(*m_shmBuffer->image()); -} - -void TreelandCaptureFrame::treeland_capture_frame_v1_failed() -{ - Q_EMIT failed(); -} - -TreelandCaptureSession::TreelandCaptureSession(::treeland_capture_session_v1 *object, - QObject *parent) - : QObject(parent) - , QtWayland::treeland_capture_session_v1(object) -{ -} - -TreelandCaptureSession::~TreelandCaptureSession() { } - -void TreelandCaptureSession::start() -{ - QtWayland::treeland_capture_session_v1::start(); - m_started = true; - Q_EMIT startedChanged(); -} - -void TreelandCaptureSession::doneFrame() -{ - // Note: Must close all fds here in the client. - for (const auto &object : std::as_const(objects())) { - close(object.fd); - } - m_objects.clear(); - frame_done(m_tvSecHi, m_tvSecLo, m_tvUsec); - m_frameValid = false; - Q_EMIT invalid(); -} - -void TreelandCaptureSession::treeland_capture_session_v1_frame(int32_t offset_x, - int32_t offset_y, - uint32_t width, - uint32_t height, - uint32_t buffer_flags, - uint32_t flags, - uint32_t format, - uint32_t mod_high, - uint32_t mod_low, - uint32_t num_objects) -{ - m_objects.clear(); - m_objects.reserve(num_objects); - m_offset = { offset_x, offset_y }; - m_bufferWidth = width; - m_bufferHeight = height; - m_bufferFlags = buffer_flags; - m_bufferFormat = format; - m_flags = static_cast(flags); - m_modifierUnion.modLow = mod_low; - m_modifierUnion.modHigh = mod_high; -} - -void TreelandCaptureSession::treeland_capture_session_v1_object(uint32_t index, - int32_t fd, - uint32_t size, - uint32_t offset, - uint32_t stride, - uint32_t plane_index) -{ - m_objects.push_back({ .index = index, - .fd = fd, - .size = size, - .offset = offset, - .stride = stride, - .planeIndex = plane_index }); -} - -void TreelandCaptureSession::treeland_capture_session_v1_ready(uint32_t tv_sec_hi, - uint32_t tv_sec_lo, - uint32_t tv_nsec) -{ - m_tvSecHi = tv_sec_hi; - m_tvSecLo = tv_sec_lo; - m_tvUsec = tv_nsec; - m_frameValid = true; - Q_EMIT ready(); -} - -void TreelandCaptureSession::treeland_capture_session_v1_cancel(uint32_t reason) -{ - if (reason == QtWayland::treeland_capture_session_v1::cancel_reason_permanent) { - for (const auto &object : std::as_const(objects())) { - close(object.fd); - } - m_objects.clear(); - } else { - doneFrame(); - } -} - -void TreelandCaptureManager::setRecord(bool newRecord) -{ - if (m_record == newRecord) - return; - m_record = newRecord; - Q_EMIT recordChanged(); -} - -bool TreelandCaptureManager::recordStarted() const -{ - if (m_context && m_context->session()) { - return m_context->session()->started(); - } else { - return false; - } -} diff --git a/examples/test_capture/capture.h b/examples/test_capture/capture.h deleted file mode 100644 index ad0c0f9084..0000000000 --- a/examples/test_capture/capture.h +++ /dev/null @@ -1,273 +0,0 @@ -// SPDX-FileCopyrightText: 2024 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-capture-unstable-v1.h" - -#include -#include - -#include -#include - -class TreelandCaptureFrame - : public QObject - , public QtWayland::treeland_capture_frame_v1 -{ - Q_OBJECT - QML_ANONYMOUS -public: - TreelandCaptureFrame(struct ::treeland_capture_frame_v1 *object, QObject *parent = nullptr); - ~TreelandCaptureFrame() override; - - inline uint flags() const - { - return m_flags; - } - -Q_SIGNALS: - void ready(QImage image); - void failed(); - -protected: - void treeland_capture_frame_v1_buffer(uint32_t format, - uint32_t width, - uint32_t height, - uint32_t stride) override; - void treeland_capture_frame_v1_flags(uint32_t flags) override; - void treeland_capture_frame_v1_ready() override; - void treeland_capture_frame_v1_failed() override; - -private: - QtWaylandClient::QWaylandShmBuffer *m_shmBuffer{ nullptr }; - QtWaylandClient::QWaylandShmBuffer *m_pendingShmBuffer{ nullptr }; - uint m_flags; -}; - -struct FrameObject -{ - uint32_t index; - int32_t fd; - uint32_t size; - uint32_t offset; - uint32_t stride; - uint32_t planeIndex; -}; - -union ModifierUnion -{ - struct - { - uint32_t modLow; - uint32_t modHigh; - }; - - uint64_t modifier; -}; - -class TreelandCaptureSession - : public QObject - , public QtWayland::treeland_capture_session_v1 -{ - Q_OBJECT - QML_ANONYMOUS - Q_PROPERTY(bool started READ started NOTIFY startedChanged FINAL) - -public: - explicit TreelandCaptureSession(::treeland_capture_session_v1 *object, - QObject *parent = nullptr); - ~TreelandCaptureSession() override; - - inline uint bufferWidth() const - { - return m_bufferWidth; - } - - inline uint bufferHeight() const - { - return m_bufferHeight; - } - - inline uint bufferFormat() const - { - return m_bufferFormat; - } - - inline uint bufferFlags() const - { - return m_bufferFlags; - } - - inline const QList &objects() const - { - return m_objects; - } - - inline ModifierUnion modifierUnion() const - { - return m_modifierUnion; - } - - inline bool started() const - { - return m_started; - } - - void start(); - void doneFrame(); - - inline bool frameValid() const - { - return m_frameValid; - } - -Q_SIGNALS: - void invalid(); - void ready(); - void startedChanged(); - -protected: - void treeland_capture_session_v1_frame(int32_t offset_x, - int32_t offset_y, - uint32_t width, - uint32_t height, - uint32_t buffer_flags, - uint32_t flags, - uint32_t format, - uint32_t mod_high, - uint32_t mod_low, - uint32_t num_objects) override; - void treeland_capture_session_v1_object(uint32_t index, - int32_t fd, - uint32_t size, - uint32_t offset, - uint32_t stride, - uint32_t plane_index) override; - void treeland_capture_session_v1_ready(uint32_t tv_sec_hi, - uint32_t tv_sec_lo, - uint32_t tv_nsec) override; - void treeland_capture_session_v1_cancel(uint32_t reason) override; - -private: - QPoint m_offset; - uint m_bufferWidth; - uint m_bufferHeight; - uint m_bufferFlags; - uint m_bufferFormat; - ModifierUnion m_modifierUnion; - QList m_objects; - QtWayland::treeland_capture_session_v1::flags m_flags; - bool m_started{ false }; - uint32_t m_tvSecHi; - uint32_t m_tvSecLo; - uint32_t m_tvUsec; - bool m_frameValid{ false }; -}; - -class TreelandCaptureContext - : public QObject - , public QtWayland::treeland_capture_context_v1 -{ - Q_OBJECT - QML_ANONYMOUS - Q_PROPERTY(TreelandCaptureFrame* frame READ frame NOTIFY frameChanged FINAL) - Q_PROPERTY(TreelandCaptureSession* session READ session NOTIFY sessionChanged FINAL) - Q_PROPERTY(QRectF captureRegion READ captureRegion NOTIFY captureRegionChanged FINAL) - QML_UNCREATABLE("Managed by C++.") - -public: - using QtWayland::treeland_capture_context_v1::source_type; - explicit TreelandCaptureContext(struct ::treeland_capture_context_v1 *object, - QObject *parent = nullptr); - ~TreelandCaptureContext() override; - - inline QRectF captureRegion() const - { - return m_captureRegion; - } - - inline QtWayland::treeland_capture_context_v1::source_type sourceType() const - { - return m_sourceType; - } - - inline TreelandCaptureFrame *frame() const - { - return m_frame; - } - - inline TreelandCaptureSession *session() const - { - return m_session; - } - - void selectSource(uint32_t sourceHint, bool freeze, bool withCursor, ::wl_surface *mask); - TreelandCaptureFrame *ensureFrame(); - TreelandCaptureSession *ensureSession(); - -Q_SIGNALS: - void sourceReady(QRect region, uint32_t sourceType); - void sourceFailed(uint32_t reason); - void frameChanged(); - void sessionChanged(); - void captureRegionChanged(); - -protected: - void treeland_capture_context_v1_source_ready(int32_t region_x, - int32_t region_y, - uint32_t region_width, - uint32_t region_height, - uint32_t source_type) override; - void treeland_capture_context_v1_source_failed(uint32_t reason) override; - -private: - QRect m_captureRegion; - QtWayland::treeland_capture_context_v1::source_type m_sourceType; - TreelandCaptureFrame *m_frame{ nullptr }; - TreelandCaptureSession *m_session{ nullptr }; -}; - -class TreelandCaptureManager - : public QWaylandClientExtensionTemplate - , public QtWayland::treeland_capture_manager_v1 -{ - Q_OBJECT - QML_ELEMENT - QML_SINGLETON - QML_UNCREATABLE("Managed by C++.") - Q_PROPERTY(TreelandCaptureContext* context READ context NOTIFY contextChanged FINAL) - Q_PROPERTY(bool record READ record WRITE setRecord NOTIFY recordChanged FINAL) - Q_PROPERTY(bool recordStarted READ recordStarted NOTIFY recordStartedChanged FINAL) - -public: - static TreelandCaptureManager *instance(); - static TreelandCaptureManager *create(QQmlEngine *, QJSEngine *); - - inline TreelandCaptureContext *context() const - { - return m_context; - } - - TreelandCaptureContext *ensureContext(); - - inline bool record() const - { - return m_record; - } - - void setRecord(bool newRecord); - bool recordStarted() const; - -Q_SIGNALS: - void contextChanged(); - void recordChanged(); - void finishSelect(); - void recordStartedChanged(); - -private: - TreelandCaptureManager(); - - TreelandCaptureContext *m_context{ nullptr }; - bool m_record{ false }; -}; diff --git a/examples/test_capture/images/watermark.png b/examples/test_capture/images/watermark.png deleted file mode 100644 index e0b538e45316426b6430ce9d5cace3b4a1e720d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2681 zcmV-<3WoKGP)EX>4Tx04R}tkv&MmKp2MKriw)?4(%Y~kfA!+MMWHI6^c+H)C#RSm|Xe?O&XFE z7e~Rh;NZ_<)xpJCR|i)?5c~mgc5qU3krKa43N2#1dj$A?7w1|2b$^aQwO}zIAQC5-VcNtS#50?= z!FiuJ%F41zd`>)J(glehxvqHp#<}FOz%!#}HZxBgB^FCvtaLFen;P*naZJ^8$`^7T ztDLtuYt=ey-;=*ET+mmRxlU^oDJ)_M5=1Ddqk<}I#A(+_v5=wjq>q2Z^-JVZ$W;L& z#{z25AiI9>Klt5St2j03C54kf?~CJni~)VSK&#<6-^Y&AIst;uz?I(d*P6iWC+Urj z7CQolwteSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00>b@L_t(|+U=cta8%_P#(&v_WCKYcBD9JM1}dOVQK`{N zu@{T9)q1~;Xt7hQ+G1rKN5?9otxmmkS}#-4(ayA;LZ{xNt>R@=l%h~BTBHhS)S@8X z5KMx^gv39dXPbzQ{`H;Rv*&we&aiv3!=C-#^WL8KeZK<{5fKp)5fKp)5fKre!eF2w z)PG(X*Y^vN2YP^BU;ywlU=Xl2#FypbdVemy0o)8Ud9Ct5BQV;+i2_L&{{^~$zXDT$ zZr~-L-qt1Wf2)CtXtKg%8}LuB)1}smb-)?clKp_M0h59Efp?-n!lMS*3Oody>&36O z0JZ>o0v`g4fe+$xg~u%5GQS=M+ycA`90%0(J7ScT0Gfaaz=Oa>Ys!3}6S&apx7ZpI zkwCuD0xY!8$nWs?Nx*Mxz9JGrGw_rh``$oRv@pffj>dj|<2K+K;Ct4TVCh1X@TV~q zINeEME%2;0q#C%yQP*U`O_%%%SOEOX=4&Fb$eI%jRZ$NA7e)v0wBLW% zw8_23yA_z@^`Fhj|IS+KpXI7;QRI&b@m^@k#&<*kLdLra44{kg|QW} zx08Y4z-(Z15*JjFae*8T9D$hJJa9JfRp0@{>b_yqwJxa(%Cd&+XRW9Mo&z@6f%gI@ z+MGWFtWWZyvMP{$fg7BWH(LWXS{qh5^xeq<>WKmgi-Kud30&*Q=}h~RHxP5X9#{cv zPx|7rD3J4j-4Sj6ycd2Ya2l%qy@D!#J;k`Vtku*9TO+18q}|y!<#OOBj-;B3p&41k zcvB+vz(I(cnhSiTUp|&A_Dz}<4XFWsi1?^BUGx!hB+rLSh1$4!mq_*yA&9#9iVtC1(K&{^1~0j@{93l-vcED;iMs z?@(0hSYZ=28Tcvix)Z|fQ6R|#D_2-6@~F3}-jP*{{l?7bHC;5%rRV;gXSKidL4 zTMr2uwBBrK^&!=0wy)LNG0q|F^{5Is)z16*D3D|_@mbEo7x*{)!ms}f zdkos=$mMoZa;DlA`lzU#8X?Em1wA@3e`5&0w1M4 zu}TB-Hcjs#Zt7v@8J)l=U<{h=c&i*}hf>E_G`#;BYG|8;8l|sC#Nr}ag^Yf(|@0aSi{8Mw)rctu*Bc-l->uOq7e`DjKhpqo(#dn;;G=%GxI1|QOEEm&vA z{jPn=2%D+ZL?q(dK5xgiMhm3uP>LE-sEZ$FAF_$QJr4`mmgZa|O#-O~en($t;rTxI ztm%{((wY%H}k}I)mX-C9hw`q|swD-tRv?0vU=w6e-&ZzBk z4C1M7M-#B){P$U?aiO-{7orq=$N(iYz&vYAL}2WS`lkvbW;Ok^rV3U624`tOim4&>sKIS9@Ex>y*mBb%-OfEa ztRa~}u823nsrC)Mh(LS`_>&XDJQ>}GrI;Er#F5oVM5E8MHq54Pc6@sjNOE`uHA7GE zQ4c(d<^UT>NvC1ju{-c1KDqgD18S6BhPFxCiwr-tp)#TGsPH0>LbHDdJF;rGnQBE< zzi!|$G((`w*(_o2FZL?8@ABgprnvE&IpUx%37 zRls!?#3gaL!kerv=M`u8yH=u#Ups0}Sb;X1TnhY=j8N>w6iCil_ZqZ~-b_33*HMwF zux4Cbhk8pC zLmxs_3>(pI3O7308;kZ&pNKY_yw;{`Qxrk?u(t+1v#9~iaCD zOTlCn#*j}(T-0+;{x+jV=mSx0BX0VVfItRV>y{xd>c^-Tw;k0!W}q>%hvL$Q$4JCe z%|P8E(D%5YSJB2{=Q>%82&^G>Xs75=h_yY+W~%VGpu+YRFFG0QqD-2jOBH9K;?ht= z94-VNagH&>pE(3=-8~oWdl(TY%BpktItM+DbSkR;-RX$xYSiZQG;lm>0o^SweHaWx zENz1|p&1c{V^BNj6{t*G9XQmE8e(5@IilCw9L5f@?-+`n!!a6-pMA7LSR4@`Q@;Q) zwHx{^bgSQAWT9MXGZqmTO~BuPukUca*3Ns1kB9^`q{3#Y_cNSx&e{1=#PCKWl!tVn zvg~Poy~bJkvFNc6_mQy%w^B$9HzIQIMBtk?W7B}&6X*VlB9ISIr}}EsCLL(=;`wn| ni!anl=OZ!4h=_=Y$PPXR8L7*UCa2Sh00000NkvXXu0mjf;b`m) diff --git a/examples/test_capture/main.cpp b/examples/test_capture/main.cpp deleted file mode 100644 index e06be5dcab..0000000000 --- a/examples/test_capture/main.cpp +++ /dev/null @@ -1,109 +0,0 @@ -// 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 "capture.h" - -#include - -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) -{ - QGuiApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - QQuickStyle::setStyle("Material"); - QGuiApplication app(argc, argv); - QQmlApplicationEngine engine; - auto manager = TreelandCaptureManager::instance(); - QObject::connect( - &engine, - &QQmlApplicationEngine::objectCreationFailed, - &app, - []() { - QCoreApplication::exit(-1); - }, - Qt::QueuedConnection); - auto captureWithMask = [&app, manager](::wl_surface *mask) { - auto captureContext = manager->ensureContext(); - if (!captureContext) { - app.exit(-1); - Q_UNREACHABLE(); - } - captureContext->selectSource(TreelandCaptureContext::source_type_output - | TreelandCaptureContext::source_type_window - | TreelandCaptureContext::source_type_region, - true, - false, - mask); - - QObject::connect( - manager, - &TreelandCaptureManager::finishSelect, - manager, - [&app, captureContext, manager] { - if (manager->record()) { - auto session = captureContext->ensureSession(); - session->start(); - QTimer::singleShot(1000, manager, [manager] { - Q_EMIT manager->recordStartedChanged(); - }); - } else { - auto frame = captureContext->ensureFrame(); - QImage result; - QEventLoop loop; - QObject::connect(frame, - &TreelandCaptureFrame::ready, - &app, - [&result, &loop](QImage image) { - result = image; - loop.quit(); - }); - QObject::connect(frame, &TreelandCaptureFrame::failed, &app, [&loop] { - loop.quit(); - }); - loop.exec(); - if (result.isNull()) - app.exit(-1); - auto saveBasePath = - QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); - QDir saveBaseDir(saveBasePath); - if (!saveBaseDir.exists()) - app.exit(-1); - QString picName = - "portal screenshot - " + QDateTime::currentDateTime().toString() + ".png"; - if (result.save(saveBaseDir.absoluteFilePath(picName), "PNG")) { - qDebug() << saveBaseDir.absoluteFilePath(picName); - delete TreelandCaptureManager::instance()->context(); - app.quit(); - } else { - delete TreelandCaptureManager::instance()->context(); - app.exit(-1); - } - } - }); - }; - QObject::connect(&engine, - &QQmlApplicationEngine::objectCreated, - manager, - [manager, captureWithMask](QObject *object, const QUrl &) { - if (auto canvasWindow = qobject_cast(object)) { - auto waylandWindow = static_cast( - canvasWindow->handle()); - auto mask = waylandWindow ? waylandWindow->surface() : nullptr; - if (manager->isActive()) { - captureWithMask(mask); - } else { - QObject::connect(manager, - &TreelandCaptureManager::activeChanged, - manager, - std::bind(captureWithMask, mask)); - } - } - }); - engine.loadFromModule("capture", "Main"); - return app.exec(); -} diff --git a/examples/test_capture/player.cpp b/examples/test_capture/player.cpp deleted file mode 100644 index d50633cdd0..0000000000 --- a/examples/test_capture/player.cpp +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright (C) 2024 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 "player.h" - -#include "capture.h" - -#include -#include -#include -#include -#include - -#include -#include - -#include - -Player::Player() -{ - setFlag(QQuickItem::ItemHasContents); -} - -TreelandCaptureContext *Player::captureContext() const -{ - return m_captureContext; -} - -void Player::setCaptureContext(TreelandCaptureContext *context) -{ - if (context == m_captureContext) - return; - if (m_captureContext) { - m_captureContext->disconnect(this); - } - m_captureContext = context; - if (m_captureContext) { - connect(m_captureContext.data(), - &TreelandCaptureContext::destroyed, - this, - std::bind(&Player::setCaptureContext, this, nullptr)); - if (m_captureContext->session()) { - connect(m_captureContext->session(), - &TreelandCaptureSession::ready, - this, - &Player::update); - } else { - connect(m_captureContext, &TreelandCaptureContext::sessionChanged, this, [this] { - if (m_captureContext->session()) - connect(m_captureContext->session(), - &TreelandCaptureSession::ready, - this, - &Player::update); - }); - } - connect(m_captureContext.data(), - &TreelandCaptureContext::sourceReady, - this, - &Player::updateGeometry); - } - Q_EMIT captureContextChanged(); -} - -QSGNode *Player::updatePaintNode(QSGNode *old, [[maybe_unused]] UpdatePaintNodeData *data) -{ - ensureDebugLogger(); - if (!m_captureContext || !m_captureContext->session() - || !m_captureContext->session()->started()) - return nullptr; - updateTexture(); - auto node = static_cast(old); - if (Q_UNLIKELY(!node) && m_rhiTexture != nullptr) { - node = window()->createImageNode(); - node->setOwnsTexture(false); - node->setTexture(&m_texture); - } else if (m_rhiTexture != nullptr) { - node->markDirty(QSGNode::DirtyMaterial); - } else { - return nullptr; - } - QRectF sourceRect; - if (m_captureContext->sourceType() - == QtWayland::treeland_capture_context_v1::source_type_window) { - sourceRect = QRectF({ 0, 0 }, m_texture.textureSize()); - } else { - sourceRect = m_captureContext->captureRegion(); - } - qInfo() << "sourceRect" << sourceRect << "textureSize" << m_texture.textureSize(); - node->setSourceRect(sourceRect); - node->setRect(boundingRect()); - node->setFiltering(QSGTexture::Linear); - node->setMipmapFiltering(QSGTexture::None); - return node; -} - -#define CASE_STR(value) \ - case value: \ - return #value; - -static const char *eglGetErrorString(EGLint error) -{ - switch (error) { - CASE_STR(EGL_SUCCESS) - CASE_STR(EGL_NOT_INITIALIZED) - CASE_STR(EGL_BAD_ACCESS) - CASE_STR(EGL_BAD_ALLOC) - CASE_STR(EGL_BAD_ATTRIBUTE) - CASE_STR(EGL_BAD_CONTEXT) - CASE_STR(EGL_BAD_CONFIG) - CASE_STR(EGL_BAD_CURRENT_SURFACE) - CASE_STR(EGL_BAD_DISPLAY) - CASE_STR(EGL_BAD_SURFACE) - CASE_STR(EGL_BAD_MATCH) - CASE_STR(EGL_BAD_PARAMETER) - CASE_STR(EGL_BAD_NATIVE_PIXMAP) - CASE_STR(EGL_BAD_NATIVE_WINDOW) - CASE_STR(EGL_CONTEXT_LOST) - default: - return "Unknown"; - } -} - -[[maybe_unused]] static const char *drmFormatString(int fourcc) -{ - switch (fourcc) { - CASE_STR(DRM_FORMAT_R8) - CASE_STR(DRM_FORMAT_RG88) - CASE_STR(DRM_FORMAT_GR88) - CASE_STR(DRM_FORMAT_R16) - CASE_STR(DRM_FORMAT_GR1616) - CASE_STR(DRM_FORMAT_RGB332) - CASE_STR(DRM_FORMAT_BGR233) - CASE_STR(DRM_FORMAT_XRGB4444) - CASE_STR(DRM_FORMAT_XBGR4444) - CASE_STR(DRM_FORMAT_RGBX4444) - CASE_STR(DRM_FORMAT_BGRX4444) - CASE_STR(DRM_FORMAT_ARGB4444) - CASE_STR(DRM_FORMAT_ABGR4444) - CASE_STR(DRM_FORMAT_RGBA4444) - CASE_STR(DRM_FORMAT_BGRA4444) - CASE_STR(DRM_FORMAT_XRGB1555) - CASE_STR(DRM_FORMAT_XBGR1555) - CASE_STR(DRM_FORMAT_RGBX5551) - CASE_STR(DRM_FORMAT_BGRX5551) - CASE_STR(DRM_FORMAT_ARGB1555) - CASE_STR(DRM_FORMAT_ABGR1555) - CASE_STR(DRM_FORMAT_RGBA5551) - CASE_STR(DRM_FORMAT_BGRA5551) - CASE_STR(DRM_FORMAT_RGB565) - CASE_STR(DRM_FORMAT_BGR565) - CASE_STR(DRM_FORMAT_RGB888) - CASE_STR(DRM_FORMAT_BGR888) - CASE_STR(DRM_FORMAT_XRGB8888) - CASE_STR(DRM_FORMAT_XBGR8888) - CASE_STR(DRM_FORMAT_RGBX8888) - CASE_STR(DRM_FORMAT_BGRX8888) - CASE_STR(DRM_FORMAT_ARGB8888) - CASE_STR(DRM_FORMAT_ABGR8888) - CASE_STR(DRM_FORMAT_RGBA8888) - CASE_STR(DRM_FORMAT_BGRA8888) - CASE_STR(DRM_FORMAT_XRGB2101010) - CASE_STR(DRM_FORMAT_XBGR2101010) - CASE_STR(DRM_FORMAT_RGBX1010102) - CASE_STR(DRM_FORMAT_BGRX1010102) - CASE_STR(DRM_FORMAT_ARGB2101010) - CASE_STR(DRM_FORMAT_ABGR2101010) - CASE_STR(DRM_FORMAT_RGBA1010102) - CASE_STR(DRM_FORMAT_BGRA1010102) - CASE_STR(DRM_FORMAT_ABGR16161616) - CASE_STR(DRM_FORMAT_XBGR16161616) - CASE_STR(DRM_FORMAT_XBGR16161616F) - CASE_STR(DRM_FORMAT_ABGR16161616F) - CASE_STR(DRM_FORMAT_YUYV) - CASE_STR(DRM_FORMAT_YVYU) - CASE_STR(DRM_FORMAT_UYVY) - CASE_STR(DRM_FORMAT_VYUY) - CASE_STR(DRM_FORMAT_AYUV) - CASE_STR(DRM_FORMAT_XYUV8888) - CASE_STR(DRM_FORMAT_Y210) - CASE_STR(DRM_FORMAT_Y212) - CASE_STR(DRM_FORMAT_Y216) - CASE_STR(DRM_FORMAT_Y410) - CASE_STR(DRM_FORMAT_Y412) - CASE_STR(DRM_FORMAT_Y416) - CASE_STR(DRM_FORMAT_NV12) - CASE_STR(DRM_FORMAT_NV21) - CASE_STR(DRM_FORMAT_NV16) - CASE_STR(DRM_FORMAT_NV61) - CASE_STR(DRM_FORMAT_P010) - CASE_STR(DRM_FORMAT_P012) - CASE_STR(DRM_FORMAT_P016) - CASE_STR(DRM_FORMAT_P030) - CASE_STR(DRM_FORMAT_YUV410) - CASE_STR(DRM_FORMAT_YVU410) - CASE_STR(DRM_FORMAT_YUV411) - CASE_STR(DRM_FORMAT_YVU411) - CASE_STR(DRM_FORMAT_YUV420) - CASE_STR(DRM_FORMAT_YVU420) - CASE_STR(DRM_FORMAT_YUV422) - CASE_STR(DRM_FORMAT_YVU422) - CASE_STR(DRM_FORMAT_YUV444) - CASE_STR(DRM_FORMAT_YVU444) - default: - return "Unknown"; - } -} - -#undef CASE_STR - -void Player::updateTexture() -{ - if (!m_captureContext->session()->frameValid()) - return; - auto glContext = QOpenGLContext::currentContext(); - Q_ASSERT(glContext); - auto eglContext = glContext->nativeInterface(); - Q_ASSERT(eglContext); - EGLImage eglImage; - EGLAttrib attribs[47]; - int atti = 0; - attribs[atti++] = EGL_WIDTH; - attribs[atti++] = m_captureContext->session()->bufferWidth(); - attribs[atti++] = EGL_HEIGHT; - attribs[atti++] = m_captureContext->session()->bufferHeight(); - attribs[atti++] = EGL_LINUX_DRM_FOURCC_EXT; - attribs[atti++] = m_captureContext->session()->bufferFormat(); - - auto objects = m_captureContext->session()->objects(); - auto nPlanes = objects.size(); - if (nPlanes > 0) { - attribs[atti++] = EGL_DMA_BUF_PLANE0_FD_EXT; - attribs[atti++] = objects[0].fd; - attribs[atti++] = EGL_DMA_BUF_PLANE0_OFFSET_EXT; - attribs[atti++] = objects[0].offset; - attribs[atti++] = EGL_DMA_BUF_PLANE0_PITCH_EXT; - attribs[atti++] = objects[0].stride; - if (m_captureContext->session()->modifierUnion().modifier) { - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modLow; - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modHigh; - } - } - - if (nPlanes > 1) { - attribs[atti++] = EGL_DMA_BUF_PLANE1_FD_EXT; - attribs[atti++] = objects[1].fd; - attribs[atti++] = EGL_DMA_BUF_PLANE1_OFFSET_EXT; - attribs[atti++] = objects[1].offset; - attribs[atti++] = EGL_DMA_BUF_PLANE1_PITCH_EXT; - attribs[atti++] = objects[1].stride; - if (m_captureContext->session()->modifierUnion().modifier) { - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modLow; - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modHigh; - } - } - - if (nPlanes > 2) { - attribs[atti++] = EGL_DMA_BUF_PLANE2_FD_EXT; - attribs[atti++] = objects[2].fd; - attribs[atti++] = EGL_DMA_BUF_PLANE2_OFFSET_EXT; - attribs[atti++] = objects[2].offset; - attribs[atti++] = EGL_DMA_BUF_PLANE2_PITCH_EXT; - attribs[atti++] = objects[2].stride; - if (m_captureContext->session()->modifierUnion().modifier) { - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modLow; - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modHigh; - } - } - - if (nPlanes > 3) { - attribs[atti++] = EGL_DMA_BUF_PLANE3_FD_EXT; - attribs[atti++] = objects[3].fd; - attribs[atti++] = EGL_DMA_BUF_PLANE3_OFFSET_EXT; - attribs[atti++] = objects[3].offset; - attribs[atti++] = EGL_DMA_BUF_PLANE3_PITCH_EXT; - attribs[atti++] = objects[3].stride; - if (m_captureContext->session()->modifierUnion().modifier) { - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modLow; - attribs[atti++] = EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT; - attribs[atti++] = m_captureContext->session()->modifierUnion().modHigh; - } - } - - attribs[atti++] = EGL_NONE; - eglImage = - eglCreateImage(eglContext->display(), EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, 0, attribs); - if (eglImage == EGL_NO_IMAGE) { - qWarning() << eglGetErrorString(eglGetError()); - m_captureContext->session()->doneFrame(); - return; - } - Q_ASSERT_X(window(), __func__, "Window should be ready for rhi."); - if (m_rhiTexture) { - m_texture.setTexture(nullptr); - delete m_rhiTexture; - } - m_rhiTexture = - window()->rhi()->newTexture(QRhiTexture::RGBA8, - QSize{ int(m_captureContext->session()->bufferWidth()), - int(m_captureContext->session()->bufferHeight()) }, - 1, - QRhiTexture::TextureArray); - glBindTexture(GL_TEXTURE_2D, m_rhiTexture->nativeTexture().object); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage); - m_texture.setTexture(m_rhiTexture); - m_texture.setTextureSize(QSize{ int(m_captureContext->session()->bufferWidth()), - int(m_captureContext->session()->bufferHeight()) }); - m_texture.setOwnsTexture(false); - glBindTexture(GL_TEXTURE_2D, 0); - eglDestroyImage(eglContext->display(), eglImage); - m_captureContext->session()->doneFrame(); -} - -void Player::updateGeometry() -{ - Q_ASSERT(m_captureContext); - setWidth(m_captureContext->captureRegion().width()); - setHeight(m_captureContext->captureRegion().height()); -} - -static void EGLAPIENTRY debugCallback(EGLenum error, - [[maybe_unused]] const char *command, - [[maybe_unused]] EGLint messageType, - [[maybe_unused]] EGLLabelKHR threadLabel, - [[maybe_unused]] EGLLabelKHR objectLabel, - const char *message) -{ - qInfo() << "EGL Debug:" << message << "(Error Code:" << error << ")"; -} - -void Player::ensureDebugLogger() -{ - if (m_loggerInitialized) - return; - if (QOpenGLContext::currentContext()->hasExtension(QByteArrayLiteral("GL_KHR_debug"))) { - const EGLAttrib debugAttribs[] = { - EGL_DEBUG_MSG_CRITICAL_KHR, - EGL_TRUE, - EGL_DEBUG_MSG_ERROR_KHR, - EGL_TRUE, - EGL_DEBUG_MSG_WARN_KHR, - EGL_TRUE, - EGL_DEBUG_MSG_INFO_KHR, - EGL_TRUE, - EGL_NONE, - }; - PFNEGLDEBUGMESSAGECONTROLKHRPROC eglDebugMessageControlKHR = - (PFNEGLDEBUGMESSAGECONTROLKHRPROC)eglGetProcAddress("eglDebugMessageControlKHR"); - if (eglDebugMessageControlKHR) { - eglDebugMessageControlKHR(debugCallback, debugAttribs); - } else { - qCritical() << "Failed to get eglDebugMessageControlKHR function."; - } - } else { - qCritical() << "GL_KHR_debug is not supported."; - } - m_loggerInitialized = true; -} diff --git a/examples/test_capture/player.h b/examples/test_capture/player.h deleted file mode 100644 index 479afbe372..0000000000 --- a/examples/test_capture/player.h +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2024 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 QSGPlainTexture; -class QRhiTexture; -class TreelandCaptureContext; - -class Player : public QQuickItem -{ - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(TreelandCaptureContext* captureContext READ captureContext WRITE setCaptureContext NOTIFY captureContextChanged FINAL) -public: - Player(); - - TreelandCaptureContext *captureContext() const; - void setCaptureContext(TreelandCaptureContext *context); - -Q_SIGNALS: - void captureContextChanged(); - -protected: - QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *) override; - -private: - void updateTexture(); - void updateGeometry(); - void ensureDebugLogger(); - - QSGPlainTexture m_texture; - QRhiTexture *m_rhiTexture{ nullptr }; - QPointer m_captureContext{}; - bool m_loggerInitialized{ false }; -}; diff --git a/examples/test_capture/subwindow.cpp b/examples/test_capture/subwindow.cpp deleted file mode 100644 index b2f24485b4..0000000000 --- a/examples/test_capture/subwindow.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (C) 2024 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 "subwindow.h" - -SubWindow::SubWindow() { } - -QQuickWindow *SubWindow::parent() const -{ - return qobject_cast(QWindow::parent()); -} - -void SubWindow::setParent(QQuickWindow *w) -{ - QWindow::setParent(w); - Q_EMIT parentChanged(); -} diff --git a/examples/test_capture/subwindow.h b/examples/test_capture/subwindow.h deleted file mode 100644 index 039c618015..0000000000 --- a/examples/test_capture/subwindow.h +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2024 UnionTech Software Technology Co., Ltd. -// SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only -#pragma once - -#include -#include -#include - -class SubWindow : public QQuickWindow -{ - Q_OBJECT - QML_ELEMENT - Q_PROPERTY(QQuickWindow *parent READ parent WRITE setParent NOTIFY parentChanged FINAL) -Q_SIGNALS: - void parentChanged(); - -public: - SubWindow(); - QQuickWindow *parent() const; - void setParent(QQuickWindow *w); -}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1eaf0d2c23..780083f1f4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -270,7 +270,6 @@ qt_add_qml_module(libtreeland core/qml/SwitchViewHighlightDelegate.qml core/qml/DockPreview.qml core/qml/FadeBehavior.qml - core/qml/CaptureSelectorLayer.qml core/qml/WindowPickerLayer.qml core/qml/EdgeTilePreview.qml core/qml/LockScreenFallback.qml diff --git a/src/core/qml/CaptureSelectorLayer.qml b/src/core/qml/CaptureSelectorLayer.qml deleted file mode 100644 index e47304f291..0000000000 --- a/src/core/qml/CaptureSelectorLayer.qml +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (C) 2024 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 - -import QtQuick -import QtQuick.Controls -import QtQuick.Shapes -import Waylib.Server -import Treeland.Capture -import Treeland -import org.deepin.dtk as D - -CaptureSourceSelector { - id: captureSourceSelector - anchors.fill: parent - component SourceToolButton: Item { - required property string iconName - required property int selectionMode - width: 50 - height: 50 - Rectangle { - id: bg - anchors { - fill: parent - margins: 5 - } - color: captureSourceSelector.selectionMode === selectionMode ? Qt.rgba(0,0,0,0.2) : "transparent" - radius: 8 - D.DciIcon { - anchors.fill: bg - name: iconName - sourceSize { - width: 36 - height: 36 - } - } - TapHandler { - gesturePolicy: TapHandler.WithinBounds - onTapped: { - captureSourceSelector.selectionMode = selectionMode - } - } - } - } - - Shape { - id: bgShape - anchors.fill: parent - ShapePath { - strokeWidth: 0 - fillColor: Qt.rgba(0, 0, 0, 0.3) - startX: 0 - startY: 0 - - // TODO: Use PathRectangle in Qt 6.8 - PathLine { x: bgShape.width; y: 0} - PathLine { x: bgShape.width; y: bgShape.height} - PathLine { x: 0; y: bgShape.height } - PathLine { x: 0; y: 0} - - PathMove { - x: selectionRegion.x - y: selectionRegion.y - } - - PathLine { x: selectionRegion.x; y: selectionRegion.bottom} - PathLine { x: selectionRegion.right; y: selectionRegion.bottom} - PathLine { x: selectionRegion.right; y: selectionRegion.y } - PathLine { x: selectionRegion.x; y: selectionRegion.y} - } - } - - Control { - id: captureToolBar - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.margins: 10 - visible: toolBarModel.count > 1 - OpacityAnimator on opacity { - from: 0.0 - to: 1.0 - duration: 300 - easing.type: Easing.OutExpo - } - contentItem: Row { - Repeater { - model: toolBarModel - SourceToolButton { } - } - } - background: Item { - Rectangle { - id: bgRect - anchors.fill: parent - color: Qt.rgba(16, 16, 16, .1) - radius: 8 - HoverHandler { - cursorShape: Qt.ArrowCursor - blocking: true - } - } - Blur { - anchors.fill: bgRect - radius: 8 - } - D.BoxShadow { - anchors.fill: parent - visible: true - cornerRadius: 8 - shadowColor: Qt.rgba(0, 0, 0, 0.15) - shadowOffsetY: 8 - shadowBlur: 20 - hollow: true - } - Border { - anchors.fill: parent - radius: 8 - insideColor: Qt.rgba(255, 255, 255, 0.05) - } - } - } - - Shape { - width: selectionRegion.width - height: selectionRegion.height - x: selectionRegion.x - y: selectionRegion.y - ShapePath { - strokeWidth: 2 - strokeColor: "white" - fillColor: "transparent" - strokeStyle: ShapePath.DashLine - dashPattern: [ 1, 4 ] - startX: 0 - startY: 0 - PathLine { x: selectionRegion.width; y: 0 } - PathLine { x: selectionRegion.width; y: selectionRegion.height } - PathLine { x: 0; y: selectionRegion.height } - PathLine { x: 0; y: 0} - } - } -} diff --git a/src/core/qmlengine.cpp b/src/core/qmlengine.cpp index 3ddf1dbd93..bc06c7943f 100644 --- a/src/core/qmlengine.cpp +++ b/src/core/qmlengine.cpp @@ -5,7 +5,6 @@ #include "common/treelandlogging.h" #include "core/rootsurfacecontainer.h" -#include "modules/capture/capture.h" #include "output/output.h" #include "surface/surfacewrapper.h" #include "workspace/workspace.h" @@ -52,7 +51,6 @@ QmlEngine::QmlEngine(QObject *parent) , dockPreviewComponent(this, "Treeland", "DockPreview") , minimizeAnimationComponent(this, "Treeland", "MinimizeAnimation") , showDesktopAnimatioComponentn(this, "Treeland", "ShowDesktopAnimation") - , captureSelectorComponent(this, "Treeland", "CaptureSelectorLayer") , windowPickerComponent(this, "Treeland", "WindowPickerLayer") , edgeTilePreviewComponent(this, "Treeland", "EdgeTilePreview") , launchpadAnimationComponent(this, "Treeland", "LaunchpadAnimation") @@ -250,15 +248,6 @@ QQuickItem *QmlEngine::createShowDesktopAnimation(SurfaceWrapper *surface, }); } -QQuickItem *QmlEngine::createCaptureSelector(QQuickItem *parent, CaptureManagerV1 *captureManager) -{ - return createComponent( - captureSelectorComponent, - parent, - { { "captureManager", QVariant::fromValue(captureManager) }, - { "z", QVariant::fromValue(RootSurfaceContainer::CaptureLayerZOrder) } }); -} - QQuickItem *QmlEngine::createWindowPicker(QQuickItem *parent) { return createComponent(windowPickerComponent, parent); diff --git a/src/core/qmlengine.h b/src/core/qmlengine.h index 3492c14660..4b093eae1f 100644 --- a/src/core/qmlengine.h +++ b/src/core/qmlengine.h @@ -24,7 +24,6 @@ class SurfaceWrapper; class Output; class Workspace; class WorkspaceModel; -class CaptureManagerV1; class QmlEngine : public QQmlApplicationEngine { Q_OBJECT @@ -62,7 +61,6 @@ class QmlEngine : public QQmlApplicationEngine uint direction); QQuickItem *createDockPreview(QQuickItem *parent); QQuickItem *createShowDesktopAnimation(SurfaceWrapper *surface, QQuickItem *parent, bool show); - QQuickItem *createCaptureSelector(QQuickItem *parent, CaptureManagerV1 *captureManager); QQuickItem *createWindowPicker(QQuickItem *parent); QQuickItem *createEdgeTilePreview(QQuickItem *parent); QQuickItem *createLockScreenFallback(QQuickItem *parent, @@ -98,7 +96,6 @@ class QmlEngine : public QQmlApplicationEngine QQmlComponent dockPreviewComponent; QQmlComponent minimizeAnimationComponent; QQmlComponent showDesktopAnimatioComponentn; - QQmlComponent captureSelectorComponent; QQmlComponent windowPickerComponent; QQmlComponent edgeTilePreviewComponent; QQmlComponent launchpadAnimationComponent; diff --git a/src/core/rootsurfacecontainer.h b/src/core/rootsurfacecontainer.h index 8048c6e8c1..eaa65e81bf 100644 --- a/src/core/rootsurfacecontainer.h +++ b/src/core/rootsurfacecontainer.h @@ -56,10 +56,10 @@ class RootSurfaceContainer : public SurfaceContainer TaskBarZOrder = 4, MenuBarZOrder = 4, PopupZOrder = 5, - CaptureLayerZOrder = 6, LockScreenZOrder = 7, GlobalOverlayZOrder = 100, PrivilegedOverlayZOrder = 200, // Privileged overlay, above lock screen & lock screen popups + SnapMaskLayerZOrder = 210, }; void init(WServer *server); diff --git a/src/core/shellhandler.cpp b/src/core/shellhandler.cpp index 9294780ef6..70652d9339 100644 --- a/src/core/shellhandler.cpp +++ b/src/core/shellhandler.cpp @@ -9,6 +9,7 @@ #include "core/windowconfigstore.h" #include "layersurfacecontainer.h" #include "modules/app-id-resolver/appidresolver.h" +#include "modules/snap-target/snaphandler.h" #include "modules/dde-shell/ddeshellmanagerinterfacev1.h" #include "modules/foreign-toplevel/foreigntoplevelmanagerv2.h" #include "modules/layer-shell-extension/layershellextensionmanagerinterfacev1.h" @@ -70,6 +71,7 @@ ShellHandler::ShellHandler(RootSurfaceContainer *rootContainer, WServer *server) , m_overlayContainer(new LayerSurfaceContainer(rootContainer)) , m_popupContainer(new SurfaceContainer(rootContainer)) , m_privilegedOverlayContainer(new SurfaceContainer(rootContainer)) + , m_snapMaskContainer(new SurfaceContainer(rootContainer)) , m_windowConfigStore(new WindowConfigStore(this)) { m_treelandForeignToplevel = server->attach(); @@ -211,6 +213,8 @@ void ShellHandler::updateXWaylandDesktopProperties() Helper::instance()->showDesktopState() == ShowDesktopInterfaceV1::State::Show); } + m_snapMaskContainer->setZ(RootSurfaceContainer::SnapMaskLayerZOrder); + m_snapMaskContainer->setObjectName(QStringLiteral("SnapMaskContainer")); } void ShellHandler::updateWrapperContainer(SurfaceWrapper *wrapper, WSurface *parentSurface) @@ -405,6 +409,11 @@ SurfaceContainer *ShellHandler::popupContainer() const return m_popupContainer; } +SurfaceContainer *ShellHandler::snapMaskContainer() const +{ + return m_snapMaskContainer; +} + RootSurfaceContainer *ShellHandler::rootSurfaceContainer() const { return m_rootSurfaceContainer; @@ -660,15 +669,23 @@ void ShellHandler::ensureXdgWrapper(WXdgToplevelSurface *surface, const QString } Q_EMIT surfaceWrapperAdded(wrapper); + // Privileged overlay, IM candidate panel, and capture mask detection via xdg-toplevel-tag QPointer wrapperPtr(wrapper); - QObject::connect(surface, &WXdgToplevelSurface::tagChanged, this, [this, wrapperPtr]() { - if (wrapperPtr) { - if (checkAndApplyPrivilegedOverlay(wrapperPtr)) - return; - if (m_imCandidatePanelManager->checkAndApplyIMCandidatePanel(wrapperPtr)) - return; + auto applyIfTaggedSurface = [this, surface, wrapperPtr]() { + if (!wrapperPtr) + return; + if (checkAndApplyPrivilegedOverlay(wrapperPtr)) + return; + if (m_imCandidatePanelManager->checkAndApplyIMCandidatePanel(wrapperPtr)) + return; + if (m_snapTarget + && surface->tag() == QLatin1String("org.deepin.treeland.snap-mask") + && !wrapperPtr->isSnapMask()) { + applySnapMask(wrapperPtr); } - }); + }; + QObject::connect(surface, &WXdgToplevelSurface::tagChanged, this, applyIfTaggedSurface); + applyIfTaggedSurface(); } bool ShellHandler::checkAndApplyPrivilegedOverlay(SurfaceWrapper *wrapper) @@ -1016,6 +1033,49 @@ void ShellHandler::registerSurfaceToForeignToplevel(SurfaceWrapper *wrapper) }); } +void ShellHandler::setSnapTarget(SnapTargetV1 *snap) +{ + m_snapTarget = snap; +} + +void ShellHandler::applySnapMask(SurfaceWrapper *wrapper) +{ + wrapper->setSkipSwitcher(true); + wrapper->setSkipDockPreView(true); + wrapper->setSkipMutiTaskView(true); + wrapper->setNoTitleBar(true); + wrapper->setNoCornerRadius(true); + wrapper->setNoDecoration(true); + wrapper->disableWindowAnimation(); + wrapper->setPositionAutomatic(false); + + if (auto *oldContainer = wrapper->container()) + oldContainer->removeSurface(wrapper); + m_snapMaskContainer->addSurface(wrapper); + + wrapper->setHasInitializeContainer(true); + wrapper->setZ(RootSurfaceContainer::SnapMaskLayerZOrder); + wrapper->setSnapMask(true); + + // Position mask to cover the entire virtual desktop (all outputs) + QRectF desktopRect; + for (auto *out : m_rootSurfaceContainer->outputs()) { + desktopRect = desktopRect.united(out->geometry()); + } + + if (!desktopRect.isEmpty()) { + wrapper->setNormalGeometry(desktopRect); + wrapper->setPosition(desktopRect.topLeft()); + wrapper->resize(desktopRect.size()); + } else { + qCWarning(lcTlCapture) << "Failed to position capture mask" + << wrapper << "- no output geometry available"; + } + + if (m_snapTarget) + m_snapTarget->setSnapMaskSurface(wrapper->surface()); +} + void ShellHandler::setupDockPreview() { Q_ASSERT(m_dockPreview); diff --git a/src/core/shellhandler.h b/src/core/shellhandler.h index 96abac4d87..7b90e1537b 100644 --- a/src/core/shellhandler.h +++ b/src/core/shellhandler.h @@ -35,6 +35,7 @@ class ForeignToplevelManagerInterfaceV2; class PrelaunchSplash; class WineWindowStateManager; class WineWindowManager; +class SnapTargetV1; WAYLIB_SERVER_BEGIN_NAMESPACE class WServer; @@ -74,6 +75,7 @@ class ShellHandler : public QObject [[nodiscard]] Workspace *workspace() const; [[nodiscard]] SurfaceContainer *popupContainer() const; [[nodiscard]] SurfaceContainer *privilegedOverlayContainer() const; + [[nodiscard]] SurfaceContainer *snapMaskContainer() const; [[nodiscard]] RootSurfaceContainer *rootSurfaceContainer() const; [[nodiscard]] ForeignToplevelManagerInterfaceV2 *foreignToplevel() const; @@ -97,6 +99,13 @@ class ShellHandler : public QObject [[nodiscard]] WAYLIB_SERVER_NAMESPACE::WInputMethodHelper *inputMethodHelper() const { return m_inputMethodHelper; } + void setSnapTarget(SnapTargetV1 *snap); + + [[nodiscard]] SnapTargetV1 *snapTarget() const + { + return m_snapTarget; + } + Q_SIGNALS: void surfaceWrapperAdded(SurfaceWrapper *wrapper); void surfaceWrapperAboutToRemove(SurfaceWrapper *wrapper); @@ -135,6 +144,7 @@ private Q_SLOTS: void setupSurfaceWindowMenu(SurfaceWrapper *wrapper); void updateLayerSurfaceContainer(SurfaceWrapper *surface); void registerSurfaceToForeignToplevel(SurfaceWrapper *wrapper); + void applySnapMask(SurfaceWrapper *wrapper); void handleDdeShellSurfaceAdded(WAYLIB_SERVER_NAMESPACE::WSurface *surface, SurfaceWrapper *wrapper); void updateXWaylandDesktopProperties(); @@ -191,6 +201,7 @@ private Q_SLOTS: LayerSurfaceContainer *m_overlayContainer = nullptr; SurfaceContainer *m_popupContainer = nullptr; SurfaceContainer *m_privilegedOverlayContainer = nullptr; + SurfaceContainer *m_snapMaskContainer = nullptr; IMCandidatePanelManager *m_imCandidatePanelManager = nullptr; QObject *m_windowMenu = nullptr; // Prelaunch wrappers created before binding to a real shell surface @@ -208,4 +219,5 @@ private Q_SLOTS: AppIdResolverManager *m_appIdResolverManager = nullptr; LayerShellExtensionManagerInterfaceV1 *m_layerShellExtensionManagerInterfaceV1 = nullptr; WindowConfigStore *m_windowConfigStore = nullptr; + SnapTargetV1 *m_snapTarget = nullptr; }; diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index 3b9dba87bf..b20d574d63 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -32,7 +32,7 @@ add_subdirectory(wallpaper-color) add_subdirectory(show-desktop) add_subdirectory(virtual-output) add_subdirectory(dde-shell) -add_subdirectory(capture) +add_subdirectory(snap-target) add_subdirectory(item-selector) add_subdirectory(ddm) add_subdirectory(screensaver) diff --git a/src/modules/capture/capture.cpp b/src/modules/capture/capture.cpp deleted file mode 100644 index 3f2c190b38..0000000000 --- a/src/modules/capture/capture.cpp +++ /dev/null @@ -1,1282 +0,0 @@ -// 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 "capture.h" - -#include "modules/capture/impl/capturev1impl.h" -#include "modules/item-selector/itemselector.h" -#include "seat/helper.h" -#include "surface/surfacewrapper.h" -#include "surface/surfaceproxy.h" -#include "workspace/workspace.h" -#include "common/treelandlogging.h" - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include - -static inline QRectF scaledRect(const QRectF &rect, qreal devicePixelRatio) -{ - return { rect.x() * devicePixelRatio, - rect.y() * devicePixelRatio, - rect.width() * devicePixelRatio, - rect.height() * devicePixelRatio }; -} - -CaptureSource *CaptureContextV1::source() const -{ - return m_captureSource; -} - -static inline uint32_t captureSourceTypeToProtocol(CaptureSource::CaptureSourceType type) -{ - switch (type) { - case CaptureSource::Output: - return 0x1; - case CaptureSource::Window: - case CaptureSource::Surface: - return 0x2; - case CaptureSource::Region: - return 0x4; - default: - Q_UNREACHABLE_RETURN(0); - } -} - -void CaptureContextV1::setSource(CaptureSource *source, const QRect &captureRegion) -{ - if (m_captureSource == source && m_captureRegion == captureRegion) - return; - if (m_captureSource) { - m_captureSource->disconnect(this); - if (session()) { - m_captureSource->disconnect(session()); - } - } - m_captureSource = source; - m_captureRegion = captureRegion; - connect(m_captureSource, - &CaptureSource::targetDestroyed, - this, - &CaptureContextV1::handleSourceDestroyed); - m_handle->sendSourceReady(this->captureRegion(), - captureSourceTypeToProtocol(source->sourceType())); - Q_EMIT sourceChanged(); -} - -void CaptureContextV1::cancelSelect() -{ - sendSourceFailed(UserCancel); -} - -WSurface *CaptureContextV1::mask() const -{ - return m_handle->mask; -} - -bool CaptureContextV1::freeze() const -{ - return m_handle->freeze; -} - -bool CaptureContextV1::withCursor() const -{ - return m_handle->withCursor; -} - -CaptureSource::CaptureSourceHint CaptureContextV1::sourceHint() const -{ - return { m_handle->sourceHint }; -} - -CaptureContextV1::CaptureContextV1(treeland_capture_context_v1 *h, - WOutputRenderWindow *outputRenderWindow, - QObject *parent) - : QObject(parent) - , m_handle(h) - , m_outputRenderWindow(outputRenderWindow) -{ - connect(h, &treeland_capture_context_v1::selectSource, this, &CaptureContextV1::onSelectSource); - connect(h, &treeland_capture_context_v1::capture, this, &CaptureContextV1::onCapture); - connect(h, &treeland_capture_context_v1::newSession, this, &CaptureContextV1::onCreateSession); -} - -void CaptureContextV1::onSelectSource() -{ - auto context = qobject_cast(sender()); - Q_ASSERT(context); // Sender must be context. - Q_EMIT selectInfoReady(); -} - -void CaptureContextV1::onCapture(treeland_capture_frame_v1 *frame) -{ - if (m_frame) { - wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), - "Cannot capture frame twice!"); - return; - } - if (!m_captureSource) { - wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), - "Source is not ready."); - return; - } - m_frame = frame; - auto notifyBuffer = [this] { - m_frame->sendBuffer( - WTools::drmToShmFormat(WTools::toDrmFormat(m_captureSource->image().format())), - source()->cropRect().width(), - source()->cropRect().height(), - source()->cropRect().width() * 4); - m_frame->sendBufferDone(); - connect(m_frame, - &treeland_capture_frame_v1::copy, - this, - &CaptureContextV1::handleFrameCopy, - Qt::UniqueConnection); - connect(m_frame, &treeland_capture_frame_v1::beforeDestroy, this, [this] { - m_frame = nullptr; - }); - }; - if (m_captureSource->imageValid()) { - notifyBuffer(); - } else { - connect(m_captureSource, &CaptureSource::imageReady, this, notifyBuffer); - } - Q_EMIT finishSelect(); -} - -void CaptureContextV1::handleFrameCopy(wlr_buffer *buffer) -{ - if (m_captureSource) { - m_captureSource->copyBuffer(buffer); - m_frame->sendReady(); - } else { - wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), - "Source is not ready, cannot capture."); - } -} - -void CaptureContextV1::sendSourceFailed(SourceFailure failure) -{ - m_handle->sendSourceFailed(failure); -} - -void CaptureContextV1::onCreateSession(treeland_capture_session_v1 *session) -{ - if (m_session) { - wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), - "Cannot create session twice!"); - return; - } - if (!m_captureSource) { - wl_client_post_implementation_error(wl_resource_get_client(m_handle->resource), - "Source is not ready."); - return; - } - m_session = session; - connect(m_session, - &treeland_capture_session_v1::start, - this, - &CaptureContextV1::handleSessionStart); - connect(m_session, - &treeland_capture_session_v1::frameDone, - this, - &CaptureContextV1::handleFrameDone); - connect(m_session, &treeland_capture_session_v1::beforeDestroy, this, [this] { - disconnect(outputRenderWindow(), - &WOutputRenderWindow::renderEnd, - this, - &CaptureContextV1::handleRenderEnd); - }); - ensureSourceSessionConnection(); - Q_EMIT finishSelect(); -} - -void CaptureContextV1::ensureSourceSessionConnection() -{ - Q_ASSERT(session() && source()); - connect(source(), - &CaptureSource::bufferDestroyed, - session(), - &treeland_capture_session_v1::sendProduceMoreCancel, - Qt::UniqueConnection); - connect(source(), - &CaptureSource::targetDestroyed, - session(), - &treeland_capture_session_v1::sendSourceDestroyCancel, - Qt::UniqueConnection); - connect(source(), - &CaptureSource::targetResized, - session(), - &treeland_capture_session_v1::sendSourceResizeCancel, - Qt::UniqueConnection); -} - -void CaptureContextV1::handleSourceDestroyed() -{ - sendSourceFailed(SourceDestroyed); -} - -void CaptureContextV1::handleSessionStart() -{ - m_currentFrameData.acked = true; - auto conn = connect(outputRenderWindow(), - &WOutputRenderWindow::renderEnd, - this, - &CaptureContextV1::handleRenderEnd, - Qt::AutoConnection); - if (!conn) { - qCWarning(lcTlCapture) << "Cannot connect to render end of output render window."; - } - if (!outputRenderWindow()->inRendering()) { - QMetaObject::invokeMethod(this, &CaptureContextV1::handleRenderEnd, Qt::AutoConnection); - } -} - -void CaptureContextV1::handleFrameDone(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvUsec) -{ - if (m_currentFrameData.readyAt.tvSecHi == tvSecHi - && m_currentFrameData.readyAt.tvSecLo == tvSecLo - && m_currentFrameData.readyAt.tvUsec == tvUsec) { - // Note: dmabuf attributes is exported from output backing buffer, fds will be - // closed as soon as backing buffer is destroyed. We should not close fd here. - m_currentFrameData.acked = true; - } else { - qCWarning(lcTlCapture) - << "Receive a frame done event that is not corresponding to current frame timestamp."; - } -} - -QPointer CaptureContextV1::session() const -{ - return m_session; -} - -QPointer CaptureContextV1::captureSource() const -{ - return m_captureSource; -} - -QPointer CaptureContextV1::outputRenderWindow() const -{ - return m_outputRenderWindow; -} - -void CaptureContextV1::handleRenderEnd() -{ - if (!session() || !m_currentFrameData.acked) - return; - auto source = captureSource(); - Q_ASSERT(source); - auto dmabuf = source->sourceDMABuffer(); - if (!dmabuf) { - qCWarning(lcTlCapture) << "Source has been invalid while connection still exists."; - return; - } - m_currentFrameData = {}; - wlr_buffer_get_dmabuf(dmabuf, &m_currentFrameData.attribs); - - union - { - uint64_t modifier; - - struct - { - uint32_t mod_low; - uint32_t mod_high; - }; - } modifierUnion(m_currentFrameData.attribs.modifier); - - qCInfo(lcTlCapture) << "Session:" << session(); - qCInfo(lcTlCapture) << "Session resource:" << session()->resource; - treeland_capture_session_v1_send_frame(session()->resource, - source->cropRect().x(), - source->cropRect().y(), - m_currentFrameData.attribs.width, - m_currentFrameData.attribs.height, - 0, - 0, - m_currentFrameData.attribs.format, - modifierUnion.mod_high, - modifierUnion.mod_low, - m_currentFrameData.attribs.n_planes); - for (auto i = 0; i < m_currentFrameData.attribs.n_planes; ++i) { - treeland_capture_session_v1_send_object(session()->resource, - i, - m_currentFrameData.attribs.fd[i], - m_currentFrameData.attribs.stride[i] - * m_currentFrameData.attribs.height, - m_currentFrameData.attribs.offset[i], - m_currentFrameData.attribs.stride[i], - i); - } - gettimeofday(&m_currentFrameData.readyAt.tv, nullptr); - treeland_capture_session_v1_send_ready(session()->resource, - m_currentFrameData.readyAt.tvSecHi, - m_currentFrameData.readyAt.tvSecLo, - m_currentFrameData.readyAt.tvUsec); -} - -CaptureManagerV1::CaptureManagerV1(QObject *parent) - : QObject(parent) - , m_manager(nullptr) - , m_captureContextModel(new CaptureContextModel(this)) - , m_contextInSelection(nullptr) -{ -} - -void CaptureManagerV1::setSelector(CaptureSourceSelector *selector) -{ - if (selector == m_selector) - return; - m_selector = selector; - Q_EMIT selectorChanged(); -} - -WOutputRenderWindow *CaptureManagerV1::outputRenderWindow() const -{ - return m_outputRenderWindow; -} - -void CaptureManagerV1::setOutputRenderWindow(WOutputRenderWindow *renderWindow) -{ - if (m_outputRenderWindow == renderWindow) { - return; - } - m_outputRenderWindow = renderWindow; -} - -QByteArrayView CaptureManagerV1::interfaceName() const -{ - return treeland_capture_manager_v1_interface.name; -} - -QPointer CaptureManagerV1::maskShellSurface() const -{ - return m_maskShellSurface; -} - -QPointer CaptureManagerV1::maskSurfaceWrapper() const -{ - return m_maskSurfaceWrapper; -} - -void CaptureManagerV1::create(WServer *server) -{ - m_manager = new treeland_capture_manager_v1(server->handle(), this); - connect(m_manager, - &treeland_capture_manager_v1::newCaptureContext, - this, - [this](treeland_capture_context_v1 *context) { - auto quickContext = new CaptureContextV1(context, outputRenderWindow(), this); - m_captureContextModel->addContext(quickContext); - connect(context, - &treeland_capture_context_v1::beforeDestroy, - quickContext, - [this, quickContext] { - m_captureContextModel->removeContext(quickContext); - handleContextBeforeDestroy(quickContext); - quickContext->deleteLater(); - }); - connect(quickContext, - &CaptureContextV1::selectInfoReady, - this, - &CaptureManagerV1::onCaptureContextSelectSource); - }); -} - -void CaptureManagerV1::destroy([[maybe_unused]] WServer *server) -{ - this->disconnect(); -} - -void CaptureManagerV1::onCaptureContextSelectSource() -{ - CaptureContextV1 *context = qobject_cast(sender()); - Q_ASSERT(context); // Sender must be context. - if (contextInSelection()) { - context->sendSourceFailed(CaptureContextV1::SelectorBusy); - return; - } - m_contextInSelection = context; - if (context->freeze()) { - freezeAllCapturedSurface(true, context->mask()); - } - Q_EMIT contextInSelectionChanged(); -} - -void CaptureManagerV1::clearContextInSelection(CaptureContextV1 *context) -{ - if (m_contextInSelection == context) { - if (m_contextInSelection->freeze()) { - freezeAllCapturedSurface(false, context->mask()); - } - m_contextInSelection = nullptr; - Q_EMIT contextInSelectionChanged(); - } -} - -WSurfaceItem *closestSurfaceItem(QQuickItem *content) -{ - while (content) { - auto item = qobject_cast(content); - if (item) - return item; - content = content->parentItem(); - } - return nullptr; -} - -void CaptureManagerV1::freezeAllCapturedSurface(bool freeze, WSurface *mask) -{ - // Exclude cursor surface item and the mask - Q_ASSERT(m_outputRenderWindow); - const auto isMaskOrSubsurface = [mask](WSurface *surface) { - if (!mask) - return false; - if (surface == mask) - return true; - const auto subsurfaces = mask->subsurfaces(); - return std::any_of(subsurfaces.cbegin(), subsurfaces.cend(), [surface](WSubsurface *subsurface) { - return subsurface->surface() == surface; - }); - }; - - QQueue nodes; - nodes.enqueue(m_outputRenderWindow->contentItem()); - while (!nodes.isEmpty()) { - auto node = nodes.dequeue(); - if (auto content = qobject_cast(node)) { - if (auto cursor = qobject_cast(node->parentItem())) { - if (freeze) - m_frozenCursorPos = cursor->position(); // Just store position for cursor - } else if (!isMaskOrSubsurface(content->surface())) { - content->setLive(!freeze); - } else if (content->surface() == mask) { - auto surfaceItem = closestSurfaceItem(content); - auto parentItem = surfaceItem->parentItem(); - // If parentItem is m_proxySurface (created by SurfaceProxy), skip it. - // The actual surface will be found when we traverse to it later. - // - // Note: Surface capture/record windows should not appear in multitask view, - // but currently we can't filter them at protocol level (e.g., through - // appId or surface type). We need to adjust the protocol so window - // manager can identify them before xdg-toplevel is shown. - if (auto wrapper = qobject_cast(parentItem)) { - if (qobject_cast(wrapper->parent())) { - // This is m_proxySurface, skip it - qCWarning(lcTlCapture) - << "Surface capture/record window found in multitask view," - << "protocol needs adjustment to filter it earlier"; - continue; - } - } - m_maskSurfaceWrapper = qobject_cast(parentItem); - if (m_maskSurfaceWrapper) { - // TODO: Set skipSwitcher, skipDockPreView, skipMutiTaskView here - // after adjusting protocol to identify capture windows early - m_maskSurfaceWrapper->setNoTitleBar(true); - m_maskSurfaceWrapper->setNoCornerRadius(true); - m_maskSurfaceWrapper->setNoDecoration(true); - m_maskSurfaceWrapper->disableWindowAnimation(); - m_maskSurfaceWrapper->setPositionAutomatic(false); - } - m_maskShellSurface = surfaceItem->shellSurface(); - } - } - auto childItems = node->childItems(); - for (const auto &child : std::as_const(childItems)) { - nodes.enqueue(child); - } - } -} - -void CaptureManagerV1::handleContextBeforeDestroy(CaptureContextV1 *context) -{ - clearContextInSelection(context); -} - -CaptureContextModel::CaptureContextModel(QObject *parent) - : QAbstractListModel(parent) -{ -} - -int CaptureContextModel::rowCount([[maybe_unused]] const QModelIndex &parent) const -{ - return m_captureContexts.size(); -} - -QVariant CaptureContextModel::data(const QModelIndex &index, int role) const -{ - if (index.row() < 0 || index.row() >= m_captureContexts.size()) - return {}; - switch (role) { - case ContextRole: - return QVariant::fromValue(m_captureContexts.at(index.row())); - } - return {}; -} - -QHash CaptureContextModel::roleNames() const -{ - return QHash{ { ContextRole, QByteArrayLiteral("context") } }; -} - -void CaptureContextModel::addContext(CaptureContextV1 *context) -{ - beginInsertRows(QModelIndex(), m_captureContexts.size(), m_captureContexts.size() + 1); - m_captureContexts.push_back(context); - endInsertRows(); -} - -void CaptureContextModel::removeContext(CaptureContextV1 *context) -{ - auto index = m_captureContexts.indexOf(context); - beginRemoveRows(QModelIndex(), index, index); - m_captureContexts.remove(index); - endRemoveRows(); -} - -CaptureSourceSelector::CaptureSourceSelector(QQuickItem *parent) - : SurfaceContainer(parent) - , m_internalContentItem(new QQuickItem(this)) - , m_itemSelector(new ItemSelector(m_internalContentItem)) - , m_canvasContainer(new SurfaceContainer(this)) - , m_toolBarModel(new ToolBarModel(this)) -{ - QQuickItemPrivate::get(m_internalContentItem)->anchors()->setFill(this); - m_internalContentItem->setZ(1); - QQuickItemPrivate::get(m_canvasContainer)->anchors()->setFill(this); - m_canvasContainer->setZ(2); - updateCursorShape(); - setAcceptedMouseButtons(Qt::LeftButton); - setActiveFocusOnTab(false); - connect(m_itemSelector, - &ItemSelector::hoveredItemChanged, - this, - &CaptureSourceSelector::hoveredItemChanged, - Qt::UniqueConnection); - connect(m_itemSelector, - &ItemSelector::selectionRegionChanged, - this, - &CaptureSourceSelector::handleItemSelectorSelectionRegionChanged, - Qt::UniqueConnection); - m_itemSelector->addCustomFilter([this](QQuickItem *item, - [[maybe_unused]] ItemSelector::ItemTypes selectionHint) -> bool { - if (auto surfaceItemContent = qobject_cast(item)) { - return surfaceItemContent->surface() != captureManager()->contextInSelection()->mask(); - } else if (auto surfaceItem = qobject_cast(item)) { - auto layerSurface = qobject_cast(surfaceItem->shellSurface()); - if (layerSurface) { - if (QString(layerSurface->handle()->scope) == "dde-shell/desktop") { - return false; - } - } - return surfaceItem->surface() != captureManager()->contextInSelection()->mask(); - } else { - return true; - } - }); -} - -CaptureSourceSelector::~CaptureSourceSelector() { } - -void CaptureSourceSelector::doneSelection() -{ - // Selection is done, begin to construct selection source - connect(renderWindow(), - &WOutputRenderWindow::renderEnd, - this, - &CaptureSourceSelector::createImage); - m_internalContentItem->setVisible(false); - // m_canvas may be null when no mask surface exists (e.g. capture via - // xdg-desktop-portal without a mask). Guard against null dereference. - if (m_canvas && m_canvas->surfaceItem()) - m_canvas->surfaceItem()->setSubsurfacesVisible(false); -} - -void CaptureSourceSelector::cancelSelection() -{ - if (captureManager() && captureManager()->contextInSelection()) { - releaseMaskSurface(); - captureManager()->contextInSelection()->sendSourceFailed(CaptureContextV1::UserCancel); - captureManager()->clearContextInSelection(captureManager()->contextInSelection()); - } -} - -void CaptureSourceSelector::updateCursorShape() -{ - if (m_selectionMode == SelectionMode::SelectOutput - || m_selectionMode == SelectionMode::SelectWindow) { - setCursor(Qt::PointingHandCursor); - } else if (m_selectionMode == SelectionMode::SelectRegion) { - setCursor(Qt::CrossCursor); - } else { - setCursor(Qt::ArrowCursor); - } -} - -wl_global *CaptureManagerV1::global() const -{ - return m_manager->global; -} - -QQuickItem *CaptureSourceSelector::hoveredItem() const -{ - return m_itemSelector->hoveredItem(); -} - -bool CaptureSourceSelector::itemSelectionMode() const -{ - return m_itemSelectionMode; -} - -void CaptureSourceSelector::setItemSelectionMode(bool itemSelection) -{ - if (m_itemSelectionMode == itemSelection) - return; - m_itemSelectionMode = itemSelection; - if (m_itemSelectionMode) { - // Let item selector do the job - connect(m_itemSelector, - &ItemSelector::selectionRegionChanged, - this, - &CaptureSourceSelector::handleItemSelectorSelectionRegionChanged, - Qt::UniqueConnection); - } else { - disconnect(m_itemSelector, - &ItemSelector::selectionRegionChanged, - this, - &CaptureSourceSelector::handleItemSelectorSelectionRegionChanged); - } - updateItemSelectorItemTypes(); -} - -void CaptureSourceSelector::updateItemSelectorItemTypes() -{ - if (m_itemSelectionMode) { - m_itemSelector->setSelectionTypeHint(selectionModeToItemTypes(m_selectionMode)); - } else { - // Always item select output in non-item selection mode - m_itemSelector->setSelectionTypeHint(ItemSelector::Output); - } -} - -WOutputRenderWindow *CaptureSourceSelector::renderWindow() const -{ - return qobject_cast(window()); -} - -void CaptureSourceSelector::createImage() -{ - disconnect(renderWindow(), - &WOutputRenderWindow::renderEnd, - this, - &CaptureSourceSelector::createImage); - if (m_selectedSource) { - m_selectedSource->createImage(); - if (m_selectedSource->imageValid()) { - releaseMaskSurface(); - } else { - connect(m_selectedSource, - &CaptureSource::imageReady, - this, - &CaptureSourceSelector::releaseMaskSurface); - } - } else { - releaseMaskSurface(); - } - captureManager()->clearContextInSelection(captureManager()->contextInSelection()); -} - -CaptureManagerV1 *CaptureSourceSelector::captureManager() const -{ - return m_captureManager; -} - -void CaptureSourceSelector::setCaptureManager(CaptureManagerV1 *newCaptureManager) -{ - if (m_captureManager == newCaptureManager) - return; - m_captureManager = newCaptureManager; - if (captureSourceHint().toInt() == 0 || captureSourceHint().testFlag(CaptureSource::Region)) { - // Default case, no hint means all hint internally. - doSetSelectionMode(SelectionMode::SelectRegion); - } else if (captureSourceHint().testAnyFlags( - { CaptureSource::Window | CaptureSource::Surface })) { - doSetSelectionMode(SelectionMode::SelectWindow); - } else { - doSetSelectionMode(SelectionMode::SelectOutput); - } - m_captureManager->setSelector(this); - m_toolBarModel->updateModel(); - Q_ASSERT(captureManager()->contextInSelection()); - connect(captureManager()->contextInSelection(), - &CaptureContextV1::finishSelect, - this, - &CaptureSourceSelector::doneSelection, - Qt::UniqueConnection); - Q_EMIT captureManagerChanged(); -} - -void CaptureSourceSelector::mouseMoveEvent(QMouseEvent *event) -{ - if (m_selectionMode == SelectionMode::SelectRegion) { - auto pos = event->position(); - auto distance = pos - m_selectionAnchor; - if (distance.manhattanLength() > 2) { - setItemSelectionMode(false); - m_doNotFinish = true; - QRectF newRegion; - newRegion.setLeft(qMin(m_selectionAnchor.x(), pos.x())); - newRegion.setRight(qMax(m_selectionAnchor.x(), pos.x())); - newRegion.setTop(qMin(m_selectionAnchor.y(), pos.y())); - newRegion.setBottom(qMax(m_selectionAnchor.y(), pos.y())); - setSelectionRegion(newRegion); - } - } -} - -[[maybe_unused]] static inline WSurfaceItemContent *findItemContent(QQuickItem *item) -{ - QQueue q; - q.enqueue(item); - while (!q.empty()) { - auto node = q.dequeue(); - if (auto content = qobject_cast(node)) - return content; - q.append(node->childItems()); - } - return nullptr; -} - -CaptureSourceSurface::CaptureSourceSurface(WSurfaceItemContent *surfaceItemContent, - qreal devicePixelRatio) - : CaptureSource(surfaceItemContent, devicePixelRatio, nullptr) - , m_surfaceItemContent(surfaceItemContent) -{ -} - -wlr_buffer *CaptureSourceSurface::internalBuffer() -{ - Q_ASSERT(m_sourceList.size() == 1); - if (!m_surfaceItemContent) { - // The captured window was destroyed while the capture session is - // still active; treat the source as invalidated. - qCWarning(lcTlCapture) - << "CaptureSourceSurface: surface item content destroyed while session is active"; - return nullptr; - } - auto *surface = m_surfaceItemContent->surface(); - auto *buffer = surface ? surface->buffer() : nullptr; - if (m_sourceList.first().first && surface && buffer) { - if (auto clientBuffer = wlr_client_buffer_get(buffer)) - return clientBuffer->source; - return buffer; - } - qCWarning(lcTlCapture) - << "CaptureSourceSurface: no usable buffer" - << "(sourceValid=" << bool(m_sourceList.first().first) - << ", surface=" << surface - << ", buffer=" << buffer << ")"; - return nullptr; -} - -CaptureSource::CaptureSourceType CaptureSourceSurface::sourceType() -{ - return CaptureSource::Surface; -} - -QRect CaptureSourceSurface::cropRect() const -{ - return m_surfaceItemContent - ? scaledRect(m_surfaceItemContent->boundingRect(), m_devicePixelRatio).toRect() - : QRect{}; -} - -QSize CaptureSourceSurface::sourceSize() const -{ - return m_surfaceItemContent ? (m_surfaceItemContent->size() * m_devicePixelRatio).toSize() - : QSize{}; -} - -CaptureSource *CaptureSourceSelector::selectedSource() const -{ - return m_selectedSource; -} - -void CaptureSourceSelector::selectSurface(WSurfaceItemContent *surfaceItemContent) -{ - if (!surfaceItemContent) - return; - - const QRect region = surfaceItemContent - ->mapRectToItem(this, surfaceItemContent->boundingRect()) - .toRect(); - setSelectedSource(new CaptureSourceSurface(surfaceItemContent, - surfaceItemContent->devicePixelRatio()), - region); -} - -void CaptureSourceSelector::setSelectedSource(CaptureSource *newSelectedSource, const QRect ®ion) -{ - if (m_selectedSource == newSelectedSource) - return; - qCDebug(lcTlCapture) << "Set selected source to" << newSelectedSource; - m_selectedSource = newSelectedSource; - if (m_selectedSource) { - m_captureManager->contextInSelection()->setSource(m_selectedSource, region); - } - Q_EMIT selectedSourceChanged(); -} - -QDebug operator<<(QDebug debug, CaptureSource &captureSource) -{ - debug << "CaptureSource(" << captureSource.sourceType() << "," << &captureSource << ")"; - return debug; -} - -void CaptureSourceSelector::componentComplete() -{ - // Notify mask size now - if (captureManager()->maskShellSurface() && captureManager()->maskSurfaceWrapper()) { - m_canvas = captureManager()->maskSurfaceWrapper(); - // QPointer only clears when the wrapper QObject is destroyed; the - // wrapper can be invalidated (native surface gone) earlier. Forget it - // at aboutToBeInvalidated so an invalidated canvas is never - // re-enqueued into m_savedContainer by releaseMaskSurface(). - connect(m_canvas, &SurfaceWrapper::aboutToBeInvalidated, this, [this]() { - m_canvas = nullptr; - }); - m_captureManager->maskShellSurface()->resize(size().toSize()); - if (m_captureManager->maskSurfaceWrapper()->container()) { - m_savedContainer = m_captureManager->maskSurfaceWrapper()->container(); - m_captureManager->maskSurfaceWrapper()->container()->removeSurface( - m_captureManager->maskSurfaceWrapper()); - } - m_canvasContainer->addSurface(m_captureManager->maskSurfaceWrapper()); - m_canvas->setX(0); - m_canvas->setY(0); - } - QQuickItem::componentComplete(); -} - -void CaptureSourceSelector::mousePressEvent(QMouseEvent *event) -{ - // Only handle pressed event in SelectRegion selection. - if (selectionMode() == SelectionMode::SelectRegion && event->button() == Qt::LeftButton) { - m_selectionAnchor = event->position(); - } -} - -void CaptureSourceSelector::mouseReleaseEvent([[maybe_unused]] QMouseEvent *event) -{ - switch (selectionMode()) { - case SelectionMode::SelectRegion: { - auto viewport = - m_itemSelector->outputItem()->property("screenViewport").value(); - if (viewport) { - setSelectedSource( - new CaptureSourceRegion(viewport, - mapRectToItem(viewport, selectionRegion()).toRect()), - selectionRegion().toRect()); - } - // Exit item selection mode after first click - setItemSelectionMode(false); - break; - } - case SelectionMode::SelectWindow: { - if (auto surfaceItemContent = qobject_cast(hoveredItem())) { - selectSurface(surfaceItemContent); - } - break; - } - case SelectionMode::SelectOutput: { - if (auto outputItem = qobject_cast(hoveredItem())) { - auto viewport = outputItem->property("screenViewport").value(); - if (viewport) { - setSelectedSource(new CaptureSourceOutput(viewport), selectionRegion().toRect()); - } - } - break; - } - } -} - -QRectF CaptureSourceSelector::selectionRegion() const -{ - return m_selectionRegion; -} - -void CaptureSourceSelector::setSelectionRegion(const QRectF &newSelectionRegion) -{ - if (m_selectionRegion == newSelectionRegion) - return; - m_selectionRegion = newSelectionRegion; - Q_EMIT selectionRegionChanged(); -} - -void CaptureSourceSelector::handleItemSelectorSelectionRegionChanged() -{ - setSelectionRegion(m_itemSelector->selectionRegion()); -} - -bool CaptureSource::imageValid() const -{ - return !m_image.isNull(); -} - -QImage CaptureSource::image() const -{ - return m_image; -} - -void CaptureSource::createImage() -{ - if (m_sourceList.size() == 1 && m_sourceList.first().first) { - auto grabber = new WTextureCapturer(m_sourceList.first().second, this); - grabber->grabToImage() - .then([this](QImage image) { - m_image = std::move(image); - Q_EMIT imageReady(); - }) - .onFailed([](const std::exception &e) { - qCCritical(lcTlCapture) << e.what(); - }); - } else { - // TODO: support multiple sources - } -} - -wlr_buffer *CaptureSource::sourceDMABuffer() -{ - auto buffer = internalBuffer(); - // Re-bind the destroy listener whenever the buffer identity changes; - // a stale binding would stop bufferDestroyed -> sendProduceMoreCancel - // from firing and leave the capture session stuck. - if (buffer != m_sourceBuffer) { - m_bufferDestroyListener.disconnect(); - m_sourceBuffer = nullptr; - if (buffer) { - m_sourceBuffer = buffer; - m_bufferDestroyListener.init(&buffer->events.destroy, [this] { - m_sourceBuffer = nullptr; - m_bufferDestroyListener.disconnect(); - Q_EMIT bufferDestroyed(); - }); - } - } - return buffer; -} - -void CaptureSource::copyBuffer(wlr_buffer *buffer) -{ - Q_ASSERT(imageValid()); - auto width = cropRect().width(); - auto height = cropRect().height(); - uint32_t format; - size_t stride; - void *data; - wlr_buffer_begin_data_ptr_access(buffer, WLR_BUFFER_DATA_PTR_ACCESS_WRITE, &data, &format, &stride); - Q_ASSERT(stride == static_cast(width) * 4); // For QImage - QImage img = image().copy(cropRect()); - auto bufFormat = WTools::toImageFormat(format); - if (image().format() != bufFormat) { - img = image().convertToFormat(bufFormat); - } - memcpy(data, img.constBits(), stride * height); - wlr_buffer_end_data_ptr_access(buffer); -} - -CaptureSourceOutput::CaptureSourceOutput(WOutputViewport *viewport) - : CaptureSource(viewport, viewport->devicePixelRatio(), nullptr) - , m_outputViewport(viewport) -{ -} - -wlr_buffer *CaptureSourceOutput::internalBuffer() -{ - Q_ASSERT(m_sourceList.size() == 1); - if (m_sourceList.first().first && m_outputViewport->wTextureProvider()) - return m_outputViewport->wTextureProvider()->wlrBuffer(); - else - return nullptr; -} - -QRect CaptureSourceOutput::cropRect() const -{ - return m_outputViewport - ? scaledRect(m_outputViewport->boundingRect(), m_outputViewport->devicePixelRatio()) - .toRect() - : QRect{}; -} - -QSize CaptureSourceOutput::sourceSize() const -{ - return m_outputViewport ? (m_outputViewport->size() * m_devicePixelRatio).toSize() : QSize{}; -} - -CaptureSource::CaptureSourceType CaptureSourceOutput::sourceType() -{ - return CaptureSource::Output; -} - -CaptureSourceRegion::CaptureSourceRegion(WOutputViewport *viewport, const QRect ®ion) - : CaptureSource(viewport, viewport->devicePixelRatio(), nullptr) -{ - m_viewportRegions.push_back({ viewport, region }); -} - -wlr_buffer *CaptureSourceRegion::internalBuffer() -{ - if (m_sourceList.size() == 1 && m_sourceList.first().first - && m_sourceList.first().second->wTextureProvider()) { - return m_sourceList.first().second->wTextureProvider()->wlrBuffer(); - } else { - return nullptr; - } -} - -CaptureSource::CaptureSourceType CaptureSourceRegion::sourceType() -{ - return CaptureSource::Region; -} - -QRect CaptureSourceRegion::cropRect() const -{ - QRect result{}; - WOutputViewport *ref = nullptr; - for (const auto &[viewport, region] : std::as_const(m_viewportRegions)) { - if (!viewport) - continue; - if (!ref) { - result = scaledRect(region, viewport->devicePixelRatio()).toRect(); - ref = viewport; - } else { - auto mapped = - scaledRect(ref->mapRectFromItem(viewport, region), viewport->devicePixelRatio()) - .toRect(); // FIXME: firstly map or firstly scale? - result = result.united(mapped); - } - } - return result; -} - -QSize CaptureSourceRegion::sourceSize() const -{ - QRect result{}; - WOutputViewport *ref = nullptr; - for (const auto &[viewport, region] : std::as_const(m_viewportRegions)) { - if (!viewport) - continue; - if (!ref) { - result = scaledRect(viewport->boundingRect(), viewport->devicePixelRatio()).toRect(); - ref = viewport; - } else { - auto mapped = ref->mapRectFromItem( - viewport, - scaledRect(viewport->boundingRect(), viewport->devicePixelRatio())) - .toRect(); // FIXME: firstly map or firstly scale? - result = result.united(mapped); - } - } - return result.size(); -} - -bool CaptureSourceRegion::addViewportRegion(WOutputViewport *viewport, const QRect ®ion) -{ - auto scenePos = viewport->mapToScene({ 0, 0 }); - int insertIndex; - for (insertIndex = 0; insertIndex < m_viewportRegions.size(); ++insertIndex) { - auto &[v, _] = m_viewportRegions[insertIndex]; - auto vScenePos = v->mapToScene({ 0, 0 }); - if (vScenePos.x() > scenePos.x()) { - break; - } - } - m_viewportRegions.insert(insertIndex, { viewport, region }); - return true; -} - -void CaptureSourceSelector::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) -{ - if (m_captureManager->maskShellSurface()) { - m_captureManager->maskShellSurface()->resize(newGeometry.size().toSize()); - } - SurfaceContainer::geometryChange(newGeometry, oldGeometry); -} - -CaptureSourceSelector::SelectionMode CaptureSourceSelector::selectionMode() const -{ - return m_selectionMode; -} - -void CaptureSourceSelector::setSelectionMode(const SelectionMode &newSelectionMode) -{ - if (captureSourceHint().toInt() == 0 - || captureSourceHint().testAnyFlags(selectionModeHint(newSelectionMode))) { - doSetSelectionMode(newSelectionMode); - } else { - qCWarning(lcTlCapture) << "Trying to set selection mode not support, discarded."; - } -} - -void CaptureSourceSelector::doSetSelectionMode(const SelectionMode &newSelectionMode) -{ - if (m_selectionMode == newSelectionMode) - return; - m_selectionMode = newSelectionMode; - updateCursorShape(); - setItemSelectionMode(true); - updateItemSelectorItemTypes(); - Q_EMIT selectionModeChanged(); -} - -CaptureSource::CaptureSourceHint CaptureSourceSelector::selectionModeHint( - const SelectionMode &selectionMode) -{ - switch (selectionMode) { - case SelectionMode::SelectOutput: - return CaptureSource::Output; - case SelectionMode::SelectRegion: - return CaptureSource::Region; - case SelectionMode::SelectWindow: - return { CaptureSource::Window | CaptureSource::Surface }; - default: - Q_UNREACHABLE(); - } -} - -ItemSelector::ItemTypes CaptureSourceSelector::selectionModeToItemTypes( - const SelectionMode &selectionMode) const -{ - switch (selectionMode) { - case SelectionMode::SelectOutput: - return ItemSelector::Output; - case SelectionMode::SelectRegion: - return ItemSelector::Output | ItemSelector::Window | ItemSelector::Surface; - case SelectionMode::SelectWindow: - return ItemSelector::Surface; - default: - Q_UNREACHABLE(); - } -} - -QQmlListProperty CaptureSourceSelector::contents() const -{ - return QQuickItemPrivate::get(m_internalContentItem)->data(); -} - -void CaptureSourceSelector::itemChange(ItemChange change, const ItemChangeData &data) -{ - switch (change) { - case QQuickItem::ItemChange::ItemParentHasChanged: - Q_ASSERT_X(parentContainer(), - __func__, - "CaptureSourceSelector must be attached to a SurfaceContainer."); - ensureQmlContext(); - break; - default: - break; - } - - SurfaceContainer::itemChange(change, data); -} - -ToolBarModel::ToolBarModel(CaptureSourceSelector *selector) - : QAbstractListModel(selector) -{ - updateModel(); -} - -void ToolBarModel::updateModel() -{ - beginResetModel(); - m_data.clear(); - auto sourceHint = selector()->captureSourceHint(); - if (sourceHint.testFlag(CaptureSource::Region)) { - m_data.push_back({ "select_region", CaptureSourceSelector::SelectionMode::SelectRegion }); - } - if (sourceHint.testAnyFlags({ CaptureSource::Surface | CaptureSource::Window })) { - m_data.push_back({ "select_window", CaptureSourceSelector::SelectionMode::SelectWindow }); - } - if (sourceHint.testFlag(CaptureSource::Output)) { - m_data.push_back({ "select_output", CaptureSourceSelector::SelectionMode::SelectOutput }); - } - Q_EMIT countChanged(); - endResetModel(); -} - -CaptureSourceSelector *ToolBarModel::selector() const -{ - return static_cast(parent()); -} - -int ToolBarModel::rowCount(const QModelIndex &) const -{ - return m_data.size(); -} - -QVariant ToolBarModel::data(const QModelIndex &index, int role) const -{ - if (index.row() < 0 || index.row() >= rowCount()) - return {}; - switch (role) { - case IconNameRole: - return QVariant::fromValue(m_data[index.row()].first); - case SelectionModeRole: - return QVariant::fromValue(m_data[index.row()].second); - default: - return {}; - } -} - -QHash ToolBarModel::roleNames() const -{ - return { { IconNameRole, "iconName" }, { SelectionModeRole, "selectionMode" } }; -} - -ToolBarModel *CaptureSourceSelector::toolBarModel() const -{ - return m_toolBarModel; -} - -void CaptureSourceSelector::releaseMaskSurface() -{ - // Mask surface should be reparented before destruction. - // If reparent in destructor, it's already marked as deleted in qml - disconnect(m_selectedSource, - &CaptureSource::imageReady, - this, - &CaptureSourceSelector::releaseMaskSurface); - if (m_savedContainer) { - QQueue> q; - q.enqueue(m_canvas); - while (!q.isEmpty()) { - auto node = q.dequeue(); - if (node) { - m_canvasContainer->removeSurface(node); - m_savedContainer->addSurface(node); - for (const auto &child : std::as_const(node->subSurfaces())) { - q.enqueue(child); - } - } - } - } -} diff --git a/src/modules/capture/capture.h b/src/modules/capture/capture.h deleted file mode 100644 index 98ce679896..0000000000 --- a/src/modules/capture/capture.h +++ /dev/null @@ -1,474 +0,0 @@ -// 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 "modules/capture/impl/capturev1impl.h" -#include "modules/item-selector/itemselector.h" -#include "surface/surfacecontainer.h" - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include - -WAYLIB_SERVER_BEGIN_NAMESPACE -class WOutputRenderWindow; -class WOutputViewport; -class WToplevelSurface; -WAYLIB_SERVER_END_NAMESPACE - -WAYLIB_SERVER_USE_NAMESPACE -class SurfaceWrapper; -class ItemSelector; - -template -concept IsCaptureSourceTarget = - std::is_base_of_v && std::is_base_of_v; - -class CaptureSource : public QObject -{ - Q_OBJECT -public: - enum CaptureSourceType - { - Output = 0x1, - Window = 0x2, - Region = 0x4, - Surface = 0x8, - }; - Q_FLAG(CaptureSourceType) - Q_DECLARE_FLAGS(CaptureSourceHint, CaptureSourceType) - - template - CaptureSource(T *ptr, qreal devicePixelRatio, QObject *parent) - : QObject(parent) - , m_devicePixelRatio(devicePixelRatio) - { - m_sourceList.push_back( - { { static_cast(ptr) }, static_cast(ptr) }); - connect(m_sourceList.first().first.data(), - &QQuickItem::destroyed, - this, - &CaptureSource::targetDestroyed); - connect(m_sourceList.first().first.data(), - &QQuickItem::widthChanged, - this, - &CaptureSource::targetResized); - connect(m_sourceList.first().first.data(), - &QQuickItem::heightChanged, - this, - &CaptureSource::targetResized); - } - -Q_SIGNALS: - void imageReady(); - void bufferDestroyed(); - void targetDestroyed(); - void targetResized(); - -public: - bool imageValid() const; - - // Get an image that is already cropped. - QImage image() const; - - void createImage(); - - /** - * @brief DMA buffer of source, there are three cases - * 1. output - output's dma buffer - * 2. window - window's dma buffer - * 3. region - output's dma buffer - * - * @return wlr_buffer* - */ - wlr_buffer *sourceDMABuffer(); - - /** - * @brief copyBuffer render captured contents to a buffer - * @param buffer buffer prepared by client - */ - void copyBuffer(wlr_buffer *buffer); - - // Cropped area of source - virtual QRect cropRect() const = 0; - - // Buffer size of the source - virtual QSize sourceSize() const = 0; - - virtual CaptureSourceType sourceType() = 0; - -protected: - virtual wlr_buffer *internalBuffer() = 0; - - template - void addTarget(T *target) - { - m_sourceList.push_back({ { static_cast(target) }, - static_cast(target) }); - connect(m_sourceList.last().first.data(), - &QQuickItem::destroyed, - this, - &CaptureSource::targetDestroyed); - connect(m_sourceList.last().first.data(), - &QQuickItem::widthChanged, - this, - &CaptureSource::targetResized); - connect(m_sourceList.last().first.data(), - &QQuickItem::heightChanged, - this, - &CaptureSource::targetResized); - } - - friend QDebug operator<<(QDebug debug, CaptureSource &captureSource); - QImage m_image; - // Destroy listener for the buffer currently tracked by m_sourceBuffer - // (observer; the buffer itself is owned by the surface). WPointer only - // nulls the handle; this listener emits bufferDestroyed(). - WAYLIB_SERVER_NAMESPACE::WScopedListener m_bufferDestroyListener; - WPointer m_sourceBuffer; - QList, WTextureProviderProvider *>> m_sourceList; - qreal m_devicePixelRatio; -}; - -#define CaptureSource_iid "org.deepin.treeland.CaptureSource" -Q_DECLARE_INTERFACE(CaptureSource, CaptureSource_iid) - -class CaptureContextV1; - -class CaptureContextModel : public QAbstractListModel -{ - Q_OBJECT -public: - CaptureContextModel(QObject *parent = nullptr); - - enum CaptureContextRole - { - ContextRole = Qt::UserRole + 1 - }; - Q_ENUM(CaptureContextRole) - int rowCount(const QModelIndex &parent) const override; - QVariant data(const QModelIndex &index, int role) const override; - QHash roleNames() const override; - void addContext(CaptureContextV1 *context); - void removeContext(CaptureContextV1 *context); - -private: - QList m_captureContexts; -}; - -class CaptureContextV1 : public QObject -{ - Q_OBJECT - Q_PROPERTY(WSurface *mask READ mask NOTIFY selectInfoReady FINAL) - Q_PROPERTY(bool freeze READ freeze NOTIFY selectInfoReady FINAL) - Q_PROPERTY(bool withCursor READ withCursor NOTIFY selectInfoReady FINAL) - Q_PROPERTY(CaptureSource::CaptureSourceHint sourceHint READ sourceHint NOTIFY selectInfoReady FINAL) - -public: - union FrameTime - { - timeval tv; - - struct - { - uint32_t tvSecLo; - uint32_t tvSecHi; - uint32_t tvUsec; - }; - }; - - struct FrameData - { - FrameTime readyAt{}; - wlr_dmabuf_attributes attribs{}; - bool acked{ false }; - bool canceled{ false }; - }; - - CaptureSource *source() const; - void setSource(CaptureSource *source, const QRect &captureRegion); - - void cancelSelect(); - - WSurface *mask() const; - bool freeze() const; - bool withCursor() const; - CaptureSource::CaptureSourceHint sourceHint() const; - QPointer session() const; - QPointer captureSource() const; - QPointer outputRenderWindow() const; - - enum SourceFailure - { - SelectorBusy = 1, - UserCancel, - SourceDestroyed, - Other, - }; - Q_ENUM(SourceFailure) - - CaptureContextV1(treeland_capture_context_v1 *h, - WOutputRenderWindow *outputRenderWindow, - QObject *parent = nullptr); - void sendSourceFailed(SourceFailure failure); - - inline QRect captureRegion() const - { - return m_captureRegion; - } - -Q_SIGNALS: - void sourceChanged(); - void finishSelect(); - void selectInfoReady(); - -private: - void onSelectSource(); - void onCapture(treeland_capture_frame_v1 *frame); - void onCreateSession(treeland_capture_session_v1 *session); - void handleFrameCopy(wlr_buffer *buffer); - void handleSessionStart(); - void handleFrameDone(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvUsec); - void handleRenderEnd(); - - void ensureSourceSessionConnection(); - void handleSourceDestroyed(); - - treeland_capture_context_v1 *const m_handle; - CaptureSource *m_captureSource{ nullptr }; - QPointer m_frame{ nullptr }; - QPointer m_session{ nullptr }; - const QPointer m_outputRenderWindow; - FrameData m_currentFrameData{}; - QRect m_captureRegion; -}; -class CaptureSourceSelector; - -class CaptureManagerV1 - : public QObject - , public WServerInterface -{ - Q_OBJECT - Q_PROPERTY(CaptureContextV1 *contextInSelection READ contextInSelection NOTIFY contextInSelectionChanged FINAL) - -public: - explicit CaptureManagerV1(QObject *parent = nullptr); - - CaptureContextModel *contextModel() const - { - return m_captureContextModel; - } - - CaptureContextV1 *contextInSelection() const - { - return m_contextInSelection; - } - - CaptureSourceSelector *selector() const - { - return m_selector; - } - - void setSelector(CaptureSourceSelector *selector); - - WOutputRenderWindow *outputRenderWindow() const; - void setOutputRenderWindow(WOutputRenderWindow *renderWindow); - QByteArrayView interfaceName() const override; - QPointer maskShellSurface() const; - QPointer maskSurfaceWrapper() const; - void clearContextInSelection(CaptureContextV1 *context); -Q_SIGNALS: - void contextInSelectionChanged(); - void newCaptureContext(CaptureContextV1 *context); - void selectorChanged(); - -protected: - void create(WServer *server) override; - void destroy(WServer *server) override; - wl_global *global() const override; - -private Q_SLOTS: - void onCaptureContextSelectSource(); - void freezeAllCapturedSurface(bool freeze, WAYLIB_SERVER_NAMESPACE::WSurface *maskItem); - void handleContextBeforeDestroy(CaptureContextV1 *context); - -private: - treeland_capture_manager_v1 *m_manager; - CaptureContextModel *m_captureContextModel; - CaptureContextV1 *m_contextInSelection; - WOutputRenderWindow *m_outputRenderWindow; - QPointF m_frozenCursorPos; - QPointer m_maskShellSurface; - QPointer m_maskSurfaceWrapper; - CaptureSourceSelector *m_selector; -}; - -class CaptureSourceSurface : public CaptureSource -{ - Q_OBJECT -public: - explicit CaptureSourceSurface(WSurfaceItemContent *surfaceItemContent, qreal devicePixelRatio); - wlr_buffer *internalBuffer() override; - CaptureSourceType sourceType() override; - QRect cropRect() const override; - QSize sourceSize() const override; - -private: - const QPointer m_surfaceItemContent; -}; - -class CaptureSourceOutput : public CaptureSource -{ - Q_OBJECT -public: - explicit CaptureSourceOutput(WOutputViewport *viewport); - wlr_buffer *internalBuffer() override; - CaptureSourceType sourceType() override; - QRect cropRect() const override; - QSize sourceSize() const override; - -private: - const QPointer m_outputViewport; -}; - -class CaptureSourceRegion : public CaptureSource -{ - Q_OBJECT -public: - CaptureSourceRegion(WOutputViewport *viewport, const QRect ®ion); - wlr_buffer *internalBuffer() override; - CaptureSourceType sourceType() override; - QRect cropRect() const override; - QSize sourceSize() const override; - bool addViewportRegion(WOutputViewport *viewport, const QRect ®ion); - -private: - QList, QRect>> m_viewportRegions; -}; -class ToolBarModel; - -class CaptureSourceSelector : public SurfaceContainer -{ - Q_OBJECT - Q_PROPERTY(CaptureManagerV1* captureManager READ captureManager WRITE setCaptureManager NOTIFY captureManagerChanged REQUIRED FINAL) - Q_PROPERTY(QRectF selectionRegion READ selectionRegion NOTIFY selectionRegionChanged FINAL) - Q_PROPERTY(SelectionMode selectionMode READ selectionMode WRITE setSelectionMode NOTIFY selectionModeChanged FINAL) - Q_PROPERTY(QQmlListProperty contents READ contents CONSTANT DESIGNABLE false) - Q_PROPERTY(ToolBarModel* toolBarModel READ toolBarModel CONSTANT FINAL) - Q_CLASSINFO("DefaultProperty", "contents") - QML_ELEMENT - -public: - enum class SelectionMode - { - SelectOutput, - SelectWindow, - SelectRegion - }; - Q_ENUM(SelectionMode); - explicit CaptureSourceSelector(QQuickItem *parent = nullptr); - ~CaptureSourceSelector() override; - CaptureManagerV1 *captureManager() const; - void setCaptureManager(CaptureManagerV1 *newCaptureManager); - - SelectionMode selectionMode() const; - void setSelectionMode(const SelectionMode &newSelectionMode); - void doSetSelectionMode(const SelectionMode &newSelectionMode); - static CaptureSource::CaptureSourceHint selectionModeHint(const SelectionMode &selectionMode); - ItemSelector::ItemTypes selectionModeToItemTypes(const SelectionMode &selectionMode) const; - QQmlListProperty contents() const; - - inline CaptureSource::CaptureSourceHint captureSourceHint() const - { - return captureManager() ? captureManager()->contextInSelection()->sourceHint() - : CaptureSource::CaptureSourceHint(); - } - - ToolBarModel *toolBarModel() const; - void doneSelection(); - void cancelSelection(); - void selectSurface(WSurfaceItemContent *surfaceItemContent); - -Q_SIGNALS: - void hoveredItemChanged(); - void selectedSourceChanged(); - void captureManagerChanged(); - void selectionRegionChanged(); - void selectionModeChanged(); - -protected: - void componentComplete() override; - void mouseMoveEvent(QMouseEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; - void itemChange(ItemChange change, const ItemChangeData &data) override; - -private: - QQuickItem *hoveredItem() const; - QRectF selectionRegion() const; - void setSelectionRegion(const QRectF &newSelectionRegion); - bool itemSelectionMode() const; - void setItemSelectionMode(bool itemSelection); - CaptureSource *selectedSource() const; - void setSelectedSource(CaptureSource *newSelectedSource, const QRect ®ion); - void handleItemSelectorSelectionRegionChanged(); - WOutputRenderWindow *renderWindow() const; - void createImage(); - void releaseMaskSurface(); - - void updateItemSelectorItemTypes(); - void updateCursorShape(); - - QPointer m_internalContentItem{}; - QPointer m_itemSelector{}; - QPointer m_canvasContainer{}; - CaptureSource *m_selectedSource{ nullptr }; - QPointer m_captureManager{ nullptr }; - QRectF m_selectionRegion{}; - QPointF m_selectionAnchor{}; - bool m_itemSelectionMode{ true }; - SelectionMode m_selectionMode = SelectionMode::SelectRegion; - bool m_doNotFinish{ false }; - QPointer m_savedContainer{}; - QPointer m_canvas{}; - ToolBarModel *m_toolBarModel{ nullptr }; -}; - -class ToolBarModel : public QAbstractListModel -{ - Q_OBJECT - Q_PROPERTY(uint count READ rowCount NOTIFY countChanged FINAL) -public: - enum ToolBarRole - { - IconNameRole, - SelectionModeRole - }; - Q_ENUM(ToolBarRole) - explicit ToolBarModel(CaptureSourceSelector *selector); - void updateModel(); - CaptureSourceSelector *selector() const; - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - QHash roleNames() const override; -Q_SIGNALS: - void countChanged(); - -private: - QList> m_data; -}; diff --git a/src/modules/capture/impl/capturev1impl.cpp b/src/modules/capture/impl/capturev1impl.cpp deleted file mode 100644 index ed4830d95b..0000000000 --- a/src/modules/capture/impl/capturev1impl.cpp +++ /dev/null @@ -1,362 +0,0 @@ -// 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 "capturev1impl.h" - -#include -#include - -#include - -#include - -WAYLIB_SERVER_USE_NAMESPACE - -static const struct treeland_capture_session_v1_interface session_impl = { - .destroy = handle_treeland_capture_session_v1_destroy, - .start = handle_treeland_capture_session_v1_start, - .frame_done = handle_treeland_capture_session_v1_frame_done -}; - -static const struct treeland_capture_manager_v1_interface manager_impl = { - .destroy = handle_treeland_capture_manager_v1_destroy, - .get_context = handle_treeland_capture_manager_v1_get_context -}; - -static const struct treeland_capture_context_v1_interface context_impl = { - .destroy = handle_treeland_capture_context_v1_destroy, - .select_source = handle_treeland_capture_context_v1_select_source, - .capture = handle_treeland_capture_context_v1_capture, - .create_session = handle_treeland_capture_context_v1_create_session -}; - -static const struct treeland_capture_frame_v1_interface frame_impl = { - .destroy = handle_treeland_capture_frame_v1_destroy, - .copy = handle_treeland_capture_frame_v1_copy -}; - -void handle_treeland_capture_context_v1_destroy([[maybe_unused]] wl_client *client, - wl_resource *resource) -{ - wl_resource_destroy(resource); -} -struct treeland_capture_context_v1 *capture_context_from_resource(struct wl_resource *resource); - -treeland_capture_manager_v1 *capture_manager_from_resource(wl_resource *resource) -{ - Q_ASSERT( - wl_resource_instance_of(resource, &treeland_capture_manager_v1_interface, &manager_impl)); - return static_cast(wl_resource_get_user_data(resource)); -} - -void handle_treeland_capture_manager_v1_destroy([[maybe_unused]] struct wl_client *client, - struct wl_resource *resource) -{ - auto manager = capture_manager_from_resource(resource); - manager->destroyClientResource(WClient::get(client), resource); -} - -treeland_capture_session_v1 *capture_session_from_resource(wl_resource *resource) -{ - Q_ASSERT( - wl_resource_instance_of(resource, &treeland_capture_session_v1_interface, &session_impl)); - return static_cast(wl_resource_get_user_data(resource)); -} - -treeland_capture_context_v1 *capture_context_from_resource(wl_resource *resource) -{ - Q_ASSERT( - wl_resource_instance_of(resource, &treeland_capture_context_v1_interface, &context_impl)); - return static_cast(wl_resource_get_user_data(resource)); -} - -treeland_capture_frame_v1 *capture_frame_from_resource(wl_resource *resource) -{ - Q_ASSERT(wl_resource_instance_of(resource, &treeland_capture_frame_v1_interface, &frame_impl)); - return static_cast(wl_resource_get_user_data(resource)); -} - -void capture_session_resource_destroy(struct wl_resource *resource) -{ - auto session = capture_session_from_resource(resource); - if (!session) { - return; - } - Q_EMIT session->beforeDestroy(); - // Use deleteLater() instead of delete to avoid re-entrant QObject destruction - // when this callback is triggered from within a WClient::destroyed slot (which - // fires from ~QObject of the WClient). Immediate deletion would free the Qt - // Connection object that doActivate() is currently iterating, causing a UAF - // crash in QBasicAtomicPointer::loadRelaxed (qobject.cpp:4317). - session->resource = nullptr; // resource is being freed; prevent double-destroy - session->deleteLater(); -} - -void capture_context_resource_destroy(struct wl_resource *resource) -{ - struct treeland_capture_context_v1 *context = capture_context_from_resource(resource); - if (!context) { - return; - } - Q_EMIT context->beforeDestroy(); - context->resource = nullptr; - context->deleteLater(); -} - -void capture_frame_resource_destroy(struct wl_resource *resource) -{ - struct treeland_capture_frame_v1 *frame = capture_frame_from_resource(resource); - if (!frame) { - return; - } - Q_EMIT frame->beforeDestroy(); - frame->resource = nullptr; - frame->deleteLater(); -} - -void treeland_capture_manager_bind(wl_client *client, void *data, uint32_t version, uint32_t id) -{ - auto *manager = static_cast(data); - Q_ASSERT(client && manager); - wl_resource *resource = - wl_resource_create(client, &treeland_capture_manager_v1_interface, version, id); - if (!resource) { - wl_client_post_no_memory(client); - return; - } - manager->addClientResource(client, resource); - wl_resource_set_implementation(resource, &manager_impl, manager, nullptr); -} - -treeland_capture_manager_v1::treeland_capture_manager_v1(wl_display *display, QObject *parent) - : QObject(parent) - , global(wl_global_create(display, - &treeland_capture_manager_v1_interface, - 1, - this, - treeland_capture_manager_bind)) -{ -} - -void treeland_capture_manager_v1::addClientResource(wl_client *client, wl_resource *resource) -{ - WClient *wClient = WClient::get(client); - clientResources.push_back({ wClient, resource }); -} - -void treeland_capture_manager_v1::destroyClientResource(WClient *client, wl_resource *resource) -{ - for (const auto &pair : clientResources) { - if (pair.first == client && pair.second == resource) { - wl_resource_destroy(pair.second); - clientResources.removeOne(pair); - } - } -} - -void handle_treeland_capture_session_v1_destroy([[maybe_unused]] wl_client *client, wl_resource *resource) -{ - wl_resource_destroy(resource); -} - -void handle_treeland_capture_session_v1_start([[maybe_unused]] wl_client *client, - wl_resource *resource) -{ - struct treeland_capture_session_v1 *session = capture_session_from_resource(resource); - Q_ASSERT(session); - Q_EMIT session->start(); -} - -void handle_treeland_capture_manager_v1_get_context(wl_client *client, - wl_resource *resource, - uint32_t context) -{ - struct treeland_capture_manager_v1 *manager = capture_manager_from_resource(resource); - Q_ASSERT(manager); - auto *capture_context = new treeland_capture_context_v1; - - int version = wl_resource_get_version(resource); - - struct wl_resource *context_resource = - wl_resource_create(client, &treeland_capture_context_v1_interface, version, context); - - if (!context_resource) { - wl_client_post_no_memory(client); - delete capture_context; - return; - } - wl_resource_set_implementation(context_resource, - &context_impl, - capture_context, - capture_context_resource_destroy); - - capture_context->resource = context_resource; - Q_EMIT manager->newCaptureContext(capture_context); -} - -void handle_treeland_capture_context_v1_create_session(wl_client *client, - wl_resource *resource, - uint32_t session) -{ - struct treeland_capture_context_v1 *context = capture_context_from_resource(resource); - Q_ASSERT(context); - - auto *capture_session = new treeland_capture_session_v1; - - int version = wl_resource_get_version(resource); - - struct wl_resource *capture_session_resource = - wl_resource_create(client, &treeland_capture_session_v1_interface, version, session); - if (!capture_session_resource) { - wl_client_post_no_memory(client); - delete capture_session; - return; - } - capture_session->resource = capture_session_resource; - wl_resource_set_implementation(capture_session_resource, - &session_impl, - capture_session, - capture_session_resource_destroy); - - Q_EMIT context->newSession(capture_session); -} - -void handle_treeland_capture_context_v1_select_source([[maybe_unused]] wl_client *client, - wl_resource *resource, - uint32_t source_hint, - uint32_t freeze, - uint32_t with_cursor, - wl_resource *mask) -{ - struct treeland_capture_context_v1 *context = capture_context_from_resource(resource); - Q_ASSERT(context); - if (source_hint) { - context->sourceHint = source_hint; - } else { - context->sourceHint = 0x7; // Contains all source type - } - context->freeze = freeze; - context->withCursor = with_cursor; - if (mask) { - context->mask = WSurface::fromHandle(wlr_surface_from_resource(mask)); - Q_ASSERT(context->mask); - } - Q_EMIT context->selectSource(); -} - -void handle_treeland_capture_context_v1_capture(wl_client *client, - wl_resource *resource, - uint32_t frame) -{ - treeland_capture_context_v1 *context = capture_context_from_resource(resource); - Q_ASSERT(context); - auto *capture_frame = new treeland_capture_frame_v1; - - int version = wl_resource_get_version(resource); - struct wl_resource *capture_frame_resource = - wl_resource_create(client, &treeland_capture_frame_v1_interface, version, frame); - - if (!capture_frame_resource) { - wl_client_post_no_memory(client); - delete capture_frame; - return; - } - - capture_frame->resource = capture_frame_resource; - - wl_resource_set_implementation(capture_frame_resource, - &frame_impl, - capture_frame, - capture_frame_resource_destroy); - - Q_EMIT context->capture(capture_frame); -} - -void treeland_capture_context_v1::sendSourceFailed(uint32_t reason) -{ - Q_ASSERT(resource); - treeland_capture_context_v1_send_source_failed(resource, reason); -} - -void treeland_capture_context_v1::sendSourceReady(QRect region, uint32_t source_type) -{ - Q_ASSERT(resource); - treeland_capture_context_v1_send_source_ready(resource, - region.x(), - region.y(), - region.width(), - region.height(), - source_type); -} - -void handle_treeland_capture_frame_v1_destroy([[maybe_unused]] wl_client *client, - wl_resource *resource) -{ - wl_resource_destroy(resource); -} - -void handle_treeland_capture_frame_v1_copy(wl_client *client, - wl_resource *resource, - wl_resource *buffer) -{ - treeland_capture_frame_v1 *frame = capture_frame_from_resource(resource); - Q_ASSERT(frame); - wlr_buffer *wlrBuffer = wlr_buffer_try_from_resource(buffer); - if (!wlrBuffer) { - wl_client_post_implementation_error(client, "Buffer not created!"); - return; - } - Q_EMIT frame->copy(wlrBuffer); -} - -void treeland_capture_session_v1::sendProduceMoreCancel() -{ - treeland_capture_session_v1_send_cancel(resource, - TREELAND_CAPTURE_SESSION_V1_CANCEL_REASON_TEMPORARY); -} - -void treeland_capture_session_v1::sendSourceDestroyCancel() -{ - treeland_capture_session_v1_send_cancel(resource, - TREELAND_CAPTURE_SESSION_V1_CANCEL_REASON_PERMANENT); -} - -void treeland_capture_session_v1::sendSourceResizeCancel() -{ - treeland_capture_session_v1_send_cancel(resource, - TREELAND_CAPTURE_SESSION_V1_CANCEL_REASON_RESIZING); -} - -void treeland_capture_frame_v1::sendBuffer(uint32_t format, - uint32_t width, - uint32_t height, - uint32_t stride) -{ - treeland_capture_frame_v1_send_buffer(resource, format, width, height, stride); -} - -void treeland_capture_frame_v1::sendBufferDone() -{ - treeland_capture_frame_v1_send_buffer_done(resource); -} - -void treeland_capture_frame_v1::sendReady() -{ - treeland_capture_frame_v1_send_ready(resource); -} - -void treeland_capture_frame_v1::sendFailed() -{ - treeland_capture_frame_v1_send_failed(resource); -} - -void handle_treeland_capture_session_v1_frame_done(wl_client *, - wl_resource *resource, - uint32_t tv_sec_hi, - uint32_t tv_sec_lo, - uint32_t tv_usec) -{ - auto session = capture_session_from_resource(resource); - Q_ASSERT(session); - Q_EMIT session->frameDone(tv_sec_hi, tv_sec_lo, tv_usec); -} diff --git a/src/modules/capture/impl/capturev1impl.h b/src/modules/capture/impl/capturev1impl.h deleted file mode 100644 index fa030d458b..0000000000 --- a/src/modules/capture/impl/capturev1impl.h +++ /dev/null @@ -1,124 +0,0 @@ -// 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 "treeland-capture-unstable-v1-protocol.h" - -#include -#include - -#include -#include - -Q_MOC_INCLUDE() -WAYLIB_SERVER_BEGIN_NAMESPACE -class WSurface; -class WClient; -WAYLIB_SERVER_END_NAMESPACE - -void handle_treeland_capture_context_v1_destroy([[maybe_unused]] wl_client *client, - wl_resource *resource); - -void handle_treeland_capture_context_v1_capture(wl_client *client, - wl_resource *resource, - uint32_t frame); -void handle_treeland_capture_context_v1_create_session(wl_client *client, - wl_resource *resource, - uint32_t session); -void handle_treeland_capture_context_v1_select_source(wl_client *client, - wl_resource *resource, - uint32_t source_hint, - uint32_t freeze, - uint32_t with_cursor, - wl_resource *mask); - -struct treeland_capture_context_v1 : public QObject -{ - Q_OBJECT -public: - struct wl_resource *resource{ nullptr }; - bool withCursor{ false }; - bool freeze{ false }; - uint32_t sourceHint{ 0 }; - QPointer mask{ nullptr }; - // mask should be created so there must exist a wsurface - - void sendSourceFailed(uint32_t reason); - void sendSourceReady(QRect region, uint32_t source_type); - void setResource(wl_client *client, wl_resource *resource); - -Q_SIGNALS: - void beforeDestroy(); - void newSession(treeland_capture_session_v1 *session); - void selectSource(); - void capture(treeland_capture_frame_v1 *frame); -}; - -void handle_treeland_capture_frame_v1_destroy([[maybe_unused]] wl_client *client, - wl_resource *resource); -void handle_treeland_capture_frame_v1_copy(wl_client *client, - wl_resource *resource, - wl_resource *buffer); - -struct treeland_capture_frame_v1 : public QObject -{ - Q_OBJECT -public: - wl_resource *resource{ nullptr }; - void setResource(wl_client *client, wl_resource *resource); - void sendBuffer(uint32_t format, uint32_t width, uint32_t height, uint32_t stride); - void sendBufferDone(); - void sendReady(); - void sendFailed(); - -Q_SIGNALS: - void copy(wlr_buffer *buffer); - void beforeDestroy(); -}; - -void handle_treeland_capture_session_v1_destroy(struct wl_client *client, - struct wl_resource *resource); -void handle_treeland_capture_session_v1_start([[maybe_unused]] struct wl_client *client, - struct wl_resource *resource); -void handle_treeland_capture_session_v1_frame_done(wl_client *client, - wl_resource *resource, - uint32_t tv_sec_hi, - uint32_t tv_sec_lo, - uint32_t tv_usec); - -struct treeland_capture_session_v1 : public QObject -{ - Q_OBJECT -public: - wl_resource *resource{ nullptr }; - - void setResource(wl_client *client, wl_resource *resource); - void sendProduceMoreCancel(); - void sendSourceDestroyCancel(); - void sendSourceResizeCancel(); - -Q_SIGNALS: - void beforeDestroy(); - void start(); - void frameDone(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvUsec); -}; - -void handle_treeland_capture_manager_v1_destroy([[maybe_unused]] wl_client *client, - wl_resource *resource); -void handle_treeland_capture_manager_v1_get_context(wl_client *client, - wl_resource *resource, - uint32_t context); - -struct treeland_capture_manager_v1 : public QObject -{ - Q_OBJECT -public: - wl_global *global; - QList> clientResources; - explicit treeland_capture_manager_v1(wl_display *display, QObject *parent = nullptr); - void addClientResource(wl_client *client, wl_resource *resource); - void destroyClientResource(WAYLIB_SERVER_NAMESPACE::WClient *client, wl_resource *resource); -Q_SIGNALS: - void newCaptureContext(treeland_capture_context_v1 *context); -}; diff --git a/src/modules/capture/CMakeLists.txt b/src/modules/snap-target/CMakeLists.txt similarity index 60% rename from src/modules/capture/CMakeLists.txt rename to src/modules/snap-target/CMakeLists.txt index 69452a862a..e8b331029c 100644 --- a/src/modules/capture/CMakeLists.txt +++ b/src/modules/snap-target/CMakeLists.txt @@ -1,46 +1,41 @@ -set(MODULE_NAME capture) +set(MODULE_NAME snap-target) find_package(TreelandProtocols REQUIRED) if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10) find_package(Qt6 REQUIRED COMPONENTS QuickPrivate) endif() -ws_generate_local(server ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-capture-unstable-v1.xml treeland-capture-unstable-v1-protocol) - -ws_generate( - server - wayland-protocols - staging/color-management/color-management-v1.xml - color-management-v1-protocol -) +ws_generate_local(server ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-snap-target-unstable-v1.xml treeland-snap-target-unstable-v1-protocol) qt_add_library(${MODULE_NAME} SHARED) set_target_properties(${MODULE_NAME} PROPERTIES - OUTPUT_NAME "treeland-protocol-capture-v1" + OUTPUT_NAME "treeland-protocol-snap-target-v1" VERSION ${CMAKE_PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) qt_add_qml_module(${MODULE_NAME} - URI Treeland.Capture + URI Treeland.SnapTarget VERSION 1.0 NO_PLUGIN SOURCES - ${CMAKE_SOURCE_DIR}/src/modules/capture/capture.h - ${CMAKE_SOURCE_DIR}/src/modules/capture/capture.cpp - ${CMAKE_SOURCE_DIR}/src/modules/capture/impl/capturev1impl.h - ${CMAKE_SOURCE_DIR}/src/modules/capture/impl/capturev1impl.cpp - ${WAYLAND_PROTOCOLS_OUTPUTDIR}/treeland-capture-unstable-v1-protocol.c + ${CMAKE_SOURCE_DIR}/src/modules/snap-target/snaphandler.h + ${CMAKE_SOURCE_DIR}/src/modules/snap-target/snaphandler.cpp + ${CMAKE_SOURCE_DIR}/src/modules/snap-target/snapdetector.h + ${CMAKE_SOURCE_DIR}/src/modules/snap-target/snapdetector.cpp + ${CMAKE_SOURCE_DIR}/src/modules/snap-target/impl/snapv1impl.h + ${CMAKE_SOURCE_DIR}/src/modules/snap-target/impl/snapv1impl.cpp + ${WAYLAND_PROTOCOLS_OUTPUTDIR}/treeland-snap-target-unstable-v1-protocol.c RESOURCE_PREFIX /qt/qml OUTPUT_DIRECTORY - ${PROJECT_BINARY_DIR}/qt/qml/Treeland/Capture + ${PROJECT_BINARY_DIR}/qt/qml/Treeland/SnapTarget ) target_compile_definitions(${MODULE_NAME} PRIVATE - WLR_USE_UNSTABLE + WLR_USE_UNSTABLE ) target_include_directories(${MODULE_NAME} @@ -61,7 +56,7 @@ target_link_libraries(${MODULE_NAME} impl_treeland( NAME - module_capture + module_snap_target LINK ${MODULE_NAME} ) diff --git a/src/modules/snap-target/impl/snapv1impl.cpp b/src/modules/snap-target/impl/snapv1impl.cpp new file mode 100644 index 0000000000..4210dc8446 --- /dev/null +++ b/src/modules/snap-target/impl/snapv1impl.cpp @@ -0,0 +1,87 @@ +// 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 "snapv1impl.h" + +static const struct treeland_snap_target_v1_interface snap_impl = { + .destroy = handle_treeland_snap_target_v1_destroy, + .start = handle_treeland_snap_target_v1_start, + .stop = handle_treeland_snap_target_v1_stop, +}; + +static treeland_snap_target_v1 *snap_target_from_resource(wl_resource *resource) +{ + Q_ASSERT(wl_resource_instance_of(resource, &treeland_snap_target_v1_interface, &snap_impl)); + return static_cast(wl_resource_get_user_data(resource)); +} + +treeland_snap_target_v1 *treeland_snap_target_v1_create_resource(wl_client *client, + uint32_t version, + uint32_t id) +{ + auto *snap = new treeland_snap_target_v1; + + wl_resource *resource = + wl_resource_create(client, &treeland_snap_target_v1_interface, version, id); + if (!resource) { + wl_client_post_no_memory(client); + delete snap; + return nullptr; + } + wl_resource_set_implementation(resource, + &snap_impl, + snap, + treeland_snap_target_v1_resource_destroy); + snap->resource = resource; + return snap; +} + +void treeland_snap_target_v1_resource_destroy(wl_resource *resource) +{ + auto snap = snap_target_from_resource(resource); + if (!snap) + return; + Q_EMIT snap->beforeDestroy(); + snap->resource = nullptr; + snap->deleteLater(); +} + +void handle_treeland_snap_target_v1_destroy([[maybe_unused]] wl_client *client, + wl_resource *resource) +{ + wl_resource_destroy(resource); +} + +void handle_treeland_snap_target_v1_start([[maybe_unused]] wl_client *client, + wl_resource *resource, + uint32_t events) +{ + auto snap = snap_target_from_resource(resource); + Q_ASSERT(snap); + Q_EMIT snap->startRequested(events); +} + +void handle_treeland_snap_target_v1_stop([[maybe_unused]] wl_client *client, wl_resource *resource) +{ + auto snap = snap_target_from_resource(resource); + Q_ASSERT(snap); + Q_EMIT snap->stopRequested(); +} + +void treeland_snap_target_v1::sendSnapRegion(int32_t x, int32_t y, uint32_t width, uint32_t height) +{ + Q_ASSERT(resource); + treeland_snap_target_v1_send_snap_region(resource, x, y, width, height); +} + +void treeland_snap_target_v1::sendPidfd(int pidfd) +{ + Q_ASSERT(resource); + treeland_snap_target_v1_send_pidfd(resource, pidfd); +} + +void treeland_snap_target_v1::sendFailed(uint32_t reason) +{ + Q_ASSERT(resource); + treeland_snap_target_v1_send_failed(resource, reason); +} diff --git a/src/modules/snap-target/impl/snapv1impl.h b/src/modules/snap-target/impl/snapv1impl.h new file mode 100644 index 0000000000..c480a89720 --- /dev/null +++ b/src/modules/snap-target/impl/snapv1impl.h @@ -0,0 +1,38 @@ +// 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 "treeland-snap-target-unstable-v1-protocol.h" + +#include + +void handle_treeland_snap_target_v1_destroy([[maybe_unused]] wl_client *client, + wl_resource *resource); +void handle_treeland_snap_target_v1_start([[maybe_unused]] wl_client *client, + wl_resource *resource, + uint32_t events); +void handle_treeland_snap_target_v1_stop([[maybe_unused]] wl_client *client, + wl_resource *resource); + +treeland_snap_target_v1 *treeland_snap_target_v1_create_resource(wl_client *client, + uint32_t version, + uint32_t id); + +void treeland_snap_target_v1_resource_destroy(wl_resource *resource); + +struct treeland_snap_target_v1 : public QObject +{ + Q_OBJECT +public: + wl_resource *resource{ nullptr }; + + void sendSnapRegion(int32_t x, int32_t y, uint32_t width, uint32_t height); + void sendPidfd(int pidfd); + void sendFailed(uint32_t reason); + +Q_SIGNALS: + void startRequested(uint32_t events); + void stopRequested(); + void beforeDestroy(); +}; diff --git a/src/modules/snap-target/snapdetector.cpp b/src/modules/snap-target/snapdetector.cpp new file mode 100644 index 0000000000..fd5e8270b8 --- /dev/null +++ b/src/modules/snap-target/snapdetector.cpp @@ -0,0 +1,69 @@ +// 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 "snapdetector.h" + +#include + +#include +#include +#include +#include + +WAYLIB_SERVER_USE_NAMESPACE + +QList SnapDetector::collect(WOutputRenderWindow *renderWindow, WSurface *excludeSurface) +{ + QList result; + if (!renderWindow || !renderWindow->contentItem()) + return result; + + QList> outputItems; + + auto items = + WOutputRenderWindow::paintOrderItemList(renderWindow->contentItem(), + [&outputItems](QQuickItem *item) -> bool { + if (!item->isVisible()) + return false; + if (qobject_cast(item)) { + outputItems.append(item); + return false; + } + if (qobject_cast(item)) + return true; + return false; + }); + + for (auto it = items.crbegin(); it != items.crend(); ++it) { + if (!*it) + continue; + auto surfaceItem = qobject_cast(*it); + if (!surfaceItem) + continue; + if (excludeSurface && surfaceItem->surface() == excludeSurface) + continue; + SnapTarget target; + target.rect = surfaceItem->mapRectToScene(surfaceItem->boundingRect()); + target.shellSurface = surfaceItem->shellSurface(); + result.append(target); + } + + for (const auto &item : std::as_const(outputItems)) { + if (!item) + continue; + SnapTarget target; + target.rect = item->mapRectToScene(item->boundingRect()); + result.append(target); + } + + return result; +} + +SnapTarget SnapDetector::hitTest(const QPointF &cursorPos, const QList &snapshot) +{ + for (const auto &target : snapshot) { + if (target.rect.contains(cursorPos)) + return target; + } + return { }; +} diff --git a/src/modules/snap-target/snapdetector.h b/src/modules/snap-target/snapdetector.h new file mode 100644 index 0000000000..d61e82071b --- /dev/null +++ b/src/modules/snap-target/snapdetector.h @@ -0,0 +1,37 @@ +// 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 + +WAYLIB_SERVER_BEGIN_NAMESPACE +class WOutputRenderWindow; +class WSurface; +WAYLIB_SERVER_END_NAMESPACE + +WAYLIB_SERVER_USE_NAMESPACE + +struct SnapTarget +{ + QRectF rect; + QPointer shellSurface; + + bool operator==(const SnapTarget &other) const + { + return rect == other.rect && shellSurface == other.shellSurface; + } +}; + +class SnapDetector +{ +public: + static QList collect(WOutputRenderWindow *renderWindow, WSurface *excludeSurface); + static SnapTarget hitTest(const QPointF &cursorPos, const QList &snapshot); +}; diff --git a/src/modules/snap-target/snaphandler.cpp b/src/modules/snap-target/snaphandler.cpp new file mode 100644 index 0000000000..70c2090181 --- /dev/null +++ b/src/modules/snap-target/snaphandler.cpp @@ -0,0 +1,200 @@ +// 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 "snaphandler.h" + +#include +#include +#include +#include + +SnapTargetV1::SnapTargetV1(QObject *parent) + : QObject(parent) + , WServerInterface() +{ +} + +QByteArrayView SnapTargetV1::interfaceName() const +{ + return treeland_snap_target_v1_interface.name; +} + +void SnapTargetV1::create(WServer *server) +{ + m_global = wl_global_create(server->handle(), + &treeland_snap_target_v1_interface, + 1, + this, + SnapTargetV1::bind); +} + +void SnapTargetV1::destroy([[maybe_unused]] WServer *server) +{ + this->disconnect(); + if (m_global) { + wl_global_remove(m_global); + m_global = nullptr; + } +} + +wl_global *SnapTargetV1::global() const +{ + return m_global; +} + +void SnapTargetV1::bind(wl_client *client, void *data, uint32_t version, uint32_t id) +{ + auto *snapSession = static_cast(data); + auto *snap = treeland_snap_target_v1_create_resource(client, version, id); + if (!snap) + return; + snapSession->onNewResource(snap); +} + +void SnapTargetV1::onNewResource(treeland_snap_target_v1 *snap) +{ + connect(snap, &treeland_snap_target_v1::startRequested, this, [this, snap](uint32_t events) { + onStartRequested(snap, events); + }); + connect(snap, &treeland_snap_target_v1::stopRequested, this, [this, snap]() { + onStopRequested(snap); + }); + connect(snap, &treeland_snap_target_v1::beforeDestroy, this, [this, snap]() { + onResourceDestroyed(snap); + }); +} + +void SnapTargetV1::onStartRequested(treeland_snap_target_v1 *snap, uint32_t events) +{ + if (!snap || !snap->resource) + return; + + // Only one snap session may be active across all clients. start on an + // already active snap object is ignored; start by another client fails + // with the snap_busy reason. + if (m_activeResource) { + if (m_activeResource != snap) + snap->sendFailed(TREELAND_SNAP_TARGET_V1_FAILURE_REASON_SNAP_BUSY); + return; + } + + m_activeResource = snap; + m_pidfdEnabled = events & TREELAND_SNAP_TARGET_V1_EVENT_TYPE_PIDFD; + + m_snapshot = SnapDetector::collect(m_renderWindow, m_snapMaskSurface); + + auto seat = m_seat; + if (seat && seat->cursor()) { + m_cursorConn = + connect(seat->cursor(), &WCursor::positionChanged, this, &SnapTargetV1::onCursorMoved); + } + + onCursorMoved(); +} + +void SnapTargetV1::onStopRequested(treeland_snap_target_v1 *snap) +{ + if (m_activeResource != snap) + return; + stopSnapping(); +} + +void SnapTargetV1::onResourceDestroyed(treeland_snap_target_v1 *snap) +{ + if (m_activeResource == snap) + stopSnapping(); +} + +void SnapTargetV1::stopSnapping() +{ + if (!m_activeResource) + return; + + m_activeResource = nullptr; + m_pidfdEnabled = false; + + if (m_cursorConn) + disconnect(m_cursorConn); + + m_snapshot.clear(); + m_lastTarget = SnapTarget(); +} + +void SnapTargetV1::onCursorMoved() +{ + updateSnapTarget(); +} + +void SnapTargetV1::updateSnapTarget() +{ + if (!m_activeResource) + return; + + auto seat = m_seat; + if (!seat || !seat->cursor()) + return; + + auto cursorPos = seat->cursor()->position(); + auto target = SnapDetector::hitTest(cursorPos, m_snapshot); + + if (target == m_lastTarget) + return; + + m_lastTarget = target; + sendSnapTarget(target); +} + +void SnapTargetV1::sendSnapTarget(const SnapTarget &target) +{ + if (!m_activeResource || !m_activeResource->resource) + return; + + // The pidfd event is sent only when the client enabled it and the target + // has a determinable owning process; it always precedes the snap_region + // event that commits it. + if (m_pidfdEnabled && !target.rect.isEmpty() && target.shellSurface) { + int pidfd = target.shellSurface->pidFD(); + if (pidfd >= 0) + m_activeResource->sendPidfd(pidfd); + } + + const QRectF ®ion = target.rect; + if (region.isValid() && !region.isEmpty()) { + m_activeResource->sendSnapRegion(static_cast(region.x()), + static_cast(region.y()), + static_cast(region.width()), + static_cast(region.height())); + } else { + m_activeResource->sendSnapRegion(0, 0, 0, 0); + } +} + +WSurface *SnapTargetV1::snapMaskSurface() const +{ + return m_snapMaskSurface; +} + +void SnapTargetV1::setSnapMaskSurface(WSurface *surface) +{ + m_snapMaskSurface = surface; +} + +WOutputRenderWindow *SnapTargetV1::outputRenderWindow() const +{ + return m_renderWindow; +} + +void SnapTargetV1::setOutputRenderWindow(WOutputRenderWindow *renderWindow) +{ + m_renderWindow = renderWindow; +} + +WSeat *SnapTargetV1::seat() const +{ + return m_seat; +} + +void SnapTargetV1::setSeat(WSeat *seat) +{ + m_seat = seat; +} diff --git a/src/modules/snap-target/snaphandler.h b/src/modules/snap-target/snaphandler.h new file mode 100644 index 0000000000..328d7a855f --- /dev/null +++ b/src/modules/snap-target/snaphandler.h @@ -0,0 +1,72 @@ +// 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 "impl/snapv1impl.h" +#include "snapdetector.h" + +#include +#include +#include + +#include +#include + +WAYLIB_SERVER_BEGIN_NAMESPACE +class WOutputRenderWindow; +class WSeat; +class WCursor; +WAYLIB_SERVER_END_NAMESPACE + +WAYLIB_SERVER_USE_NAMESPACE + +class SnapTargetV1 + : public QObject + , public WServerInterface +{ + Q_OBJECT +public: + explicit SnapTargetV1(QObject *parent = nullptr); + + WSurface *snapMaskSurface() const; + void setSnapMaskSurface(WSurface *surface); + + WOutputRenderWindow *outputRenderWindow() const; + void setOutputRenderWindow(WOutputRenderWindow *renderWindow); + + WSeat *seat() const; + void setSeat(WSeat *seat); + + QByteArrayView interfaceName() const override; + +protected: + void create(WServer *server) override; + void destroy(WServer *server) override; + wl_global *global() const override; + +private: + static void bind(wl_client *client, void *data, uint32_t version, uint32_t id); + + void onNewResource(treeland_snap_target_v1 *snap); + void onStartRequested(treeland_snap_target_v1 *snap, uint32_t events); + void onStopRequested(treeland_snap_target_v1 *snap); + void onResourceDestroyed(treeland_snap_target_v1 *snap); + + void onCursorMoved(); + void updateSnapTarget(); + void sendSnapTarget(const SnapTarget &target); + void stopSnapping(); + + wl_global *m_global{ nullptr }; + + QPointer m_snapMaskSurface; + QPointer m_renderWindow; + QPointer m_seat; + + QPointer m_activeResource; + bool m_pidfdEnabled{ false }; + QMetaObject::Connection m_cursorConn; + QList m_snapshot; + SnapTarget m_lastTarget; +}; diff --git a/src/seat/helper.cpp b/src/seat/helper.cpp index 4b40dea43e..179593829c 100644 --- a/src/seat/helper.cpp +++ b/src/seat/helper.cpp @@ -35,7 +35,7 @@ #include "input/inputdevice.h" #include "inputmanager.h" #include "interfaces/multitaskviewinterface.h" -#include "modules/capture/capture.h" +#include "modules/snap-target/snaphandler.h" #include "modules/dde-shell/ddeshellattached.h" #include "modules/dde-shell/ddeshellmanagerinterfacev1.h" #include "modules/ddm/ddminterfacev1.h" @@ -1933,21 +1933,9 @@ void Helper::init(Treeland::Treeland *treeland) m_showDesktopInterfaceV1 = m_server->attach(); m_virtualOutputInterfaceV1 = m_server->attach(); - auto captureManagerV1 = m_server->attach(); - captureManagerV1->setOutputRenderWindow(m_renderWindow); - - connect( - captureManagerV1, - &CaptureManagerV1::contextInSelectionChanged, - this, - [this, captureManagerV1] { - if (captureManagerV1->contextInSelection()) { - m_captureSelector = qobject_cast( - qmlEngine()->createCaptureSelector(m_rootSurfaceContainer, captureManagerV1)); - } else if (m_captureSelector) { - m_captureSelector->deleteLater(); - } - }); + auto snapTargetV1 = m_server->attach(); + snapTargetV1->setOutputRenderWindow(m_renderWindow); + m_shellHandler->setSnapTarget(snapTargetV1); m_personalizationInterfaceV1 = m_server->attach(); auto updateCurrentUser = [this] { @@ -2026,14 +2014,6 @@ void Helper::init(Treeland::Treeland *treeland) 0, "DDEShellHelper", "Only for attached"); - qmlRegisterUncreatableType("Treeland.Protocols", - 1, - 0, - "CaptureSource", - "An abstract class"); - qmlRegisterType("Treeland.Protocols", 1, 0, "CaptureContextV1"); - qmlRegisterType("Treeland.Protocols", 1, 0, "CaptureSourceSelector"); - m_server->attach(); m_server->start(); @@ -2053,6 +2033,10 @@ void Helper::init(Treeland::Treeland *treeland) this, m_rootSurfaceContainer->cursor()); + // Set seat on snap target session now that primary seat is available + if (auto *snap = m_shellHandler->snapTarget()) + snap->setSeat(m_primarySeat); + // Connect device signals and handle device lifecycle m_seatManager->connectBackendSignals(m_backend); connect(m_seatManager, &SeatsManager::deviceAdded, this, [this](WInputDevice *device) { @@ -2590,9 +2574,9 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent } if (event->type() == QEvent::KeyPress) { +#ifndef QT_NO_DEBUG auto kevent = static_cast(event); -#ifndef QT_NO_DEBUG if (QKeySequence(kevent->keyCombination()) == QKeySequence(Qt::MetaModifier | Qt::Key_F12)) { std::terminate(); @@ -2605,13 +2589,9 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent } #endif - if (m_captureSelector) { - if (event->modifiers() == Qt::NoModifier && kevent->key() == Qt::Key_Escape) - m_captureSelector->cancelSelection(); - } } - if (event->type() == QEvent::KeyRelease && !m_captureSelector) { + if (event->type() == QEvent::KeyRelease) { auto kevent = static_cast(event); const int key = kevent->key(); if (key == Qt::Key_Alt || key == Qt::Key_Control || key == Qt::Key_Shift @@ -2683,7 +2663,7 @@ bool Helper::beforeDisposeEvent(WSeat *seat, QWindow *targetWindow, QInputEvent if (m_shortcutManager->isCaptureActive() && m_shortcutManager->tryHandleCaptureEvent(seat, event)) return true; - if (seat == m_primarySeat && !m_captureSelector && m_currentMode != CurrentMode::LockScreen + if (seat == m_primarySeat && m_currentMode != CurrentMode::LockScreen && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease)) { auto kevent = static_cast(event); auto *seatContainer = m_rootSurfaceContainer->getSeatContainer(seat); diff --git a/src/seat/helper.h b/src/seat/helper.h index 8c4cf104a5..02ab6bd1a3 100644 --- a/src/seat/helper.h +++ b/src/seat/helper.h @@ -43,7 +43,6 @@ Q_MOC_INCLUDE() Q_MOC_INCLUDE() Q_MOC_INCLUDE() Q_MOC_INCLUDE("core/rootsurfacecontainer.h") -Q_MOC_INCLUDE("modules/capture/capture.h") Q_MOC_INCLUDE("surface/surfacewrapper.h") Q_MOC_INCLUDE("workspace/workspace.h") Q_MOC_INCLUDE("treelandconfig.hpp") @@ -92,7 +91,6 @@ class PointerConstraintsManager; WAYLIB_SERVER_USE_NAMESPACE -class CaptureSourceSelector; class DDEShellManagerInterfaceV1; class DDMInterfaceV1; class ForeignToplevelManagerInterfaceV2; @@ -497,8 +495,6 @@ private Q_SLOTS: OutputMode m_mode = OutputMode::Extension; std::optional m_fakelastPressedPosition; - QPointer m_captureSelector; - QPropertyAnimation *m_workspaceScaleAnimation{ nullptr }; QPropertyAnimation *m_workspaceOpacityAnimation{ nullptr }; diff --git a/src/surface/seatsurfacemanager.cpp b/src/surface/seatsurfacemanager.cpp index 5af4bcf94c..a7d7ba39cc 100644 --- a/src/surface/seatsurfacemanager.cpp +++ b/src/surface/seatsurfacemanager.cpp @@ -180,6 +180,8 @@ void SeatSurfaceManager::restoreShowDesktopFocus() void SeatSurfaceManager::beginMoveResize(SurfaceWrapper *surface, Qt::Edges edges) { + if (!surface || surface->isSnapMask()) + return; if (m_moveResizeState.surface) endMoveResize(); // Move of a tiled/maximized window: instantly de-tile to Normal BEFORE diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 46e7c95baf..08ef67e569 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -66,6 +66,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_isActivated(false) , m_attention(false) , m_isIMCandidatePanel(false) + , m_isSnapMask(false) , m_resizable(false) , m_maximizable(false) , m_modal(false) @@ -104,6 +105,7 @@ SurfaceWrapper::SurfaceWrapper(SurfaceWrapper *original, QQuickItem *parent) , m_isActivated(false) , m_attention(false) , m_isIMCandidatePanel(false) + , m_isSnapMask(false) , m_resizable(false) , m_maximizable(false) , m_modal(false) @@ -175,6 +177,7 @@ SurfaceWrapper::SurfaceWrapper(QmlEngine *qmlEngine, , m_isActivated(false) , m_attention(false) , m_isIMCandidatePanel(false) + , m_isSnapMask(false) , m_resizable(false) , m_maximizable(false) , m_modal(false) @@ -1131,6 +1134,13 @@ bool SurfaceWrapper::checkSetSurfaceState(State newSurfaceState, bool allowRetar if (currentState == newSurfaceState) return false; + // Capture mask is a fixed-size overlay: never let maximize/fullscreen/ + // tiling change its geometry. + if (m_isSnapMask + && (newSurfaceState == State::Maximized || newSurfaceState == State::Fullscreen + || newSurfaceState == State::Tiling)) + return false; + if (container()->filterSurfaceStateChange(this, newSurfaceState, currentState)) return false; @@ -1360,6 +1370,20 @@ void SurfaceWrapper::setIMCandidatePanel(bool isIMCandidatePanel) Q_EMIT isIMCandidatePanelChanged(); } +bool SurfaceWrapper::isSnapMask() const +{ + return m_isSnapMask; +} + +void SurfaceWrapper::setSnapMask(bool snapMask) +{ + if (m_isSnapMask == snapMask) + return; + m_isSnapMask = snapMask; + Q_EMIT snapMaskChanged(); + updateSizeCapabilities(); +} + void SurfaceWrapper::setNoDecoration(bool newNoDecoration) { if (m_wrapperAboutToRemove) @@ -2060,6 +2084,9 @@ void SurfaceWrapper::enterFullscreen(WOutput *targetOutput) return; } + if (m_isSnapMask) + return; + if (targetOutput) { auto *helper = Helper::instance(); auto *target = helper ? helper->getOutput(targetOutput) : nullptr; @@ -2442,8 +2469,9 @@ void SurfaceWrapper::setAlwaysOnBottom(bool alwaysOnBottom) bool SurfaceWrapper::showOnAllWorkspace() const { - if (m_type == Type::Layer || m_type == Type::XdgPopup || isInputPopupLike() - || surfaceRole() == SurfaceWrapper::SurfaceRole::PrivilegedOverlay) [[unlikely]] + if (m_type == Type::Layer || m_type == Type::XdgPopup || isInputPopupLike() || isSnapMask() + || surfaceRole() == SurfaceWrapper::SurfaceRole::PrivilegedOverlay) + [[unlikely]] return true; return m_workspaceId == Workspace::ShowOnAllWorkspaceId; } @@ -2631,8 +2659,10 @@ void SurfaceWrapper::updateSizeCapabilities() return; } - const bool resizable = m_shellSurface->hasCapability(WToplevelSurface::Capability::Resize); - const bool maximizable = m_shellSurface->hasCapability(WToplevelSurface::Capability::Maximized); + const bool resizable = !m_isSnapMask + && m_shellSurface->hasCapability(WToplevelSurface::Capability::Resize); + const bool maximizable = !m_isSnapMask + && m_shellSurface->hasCapability(WToplevelSurface::Capability::Maximized); if (m_resizable != resizable) { m_resizable = resizable; @@ -2696,6 +2726,13 @@ bool SurfaceWrapper::hasCapability(WToplevelSurface::Capability cap) const // Focus, Maximized, FullScreen, Resize return false; } + // Capture mask is a fixed-size overlay; never report window-manipulation + // capabilities so it cannot be resized by maximize/tile/fullscreen/resize. + if (m_isSnapMask + && (cap == WToplevelSurface::Capability::Maximized + || cap == WToplevelSurface::Capability::FullScreen + || cap == WToplevelSurface::Capability::Resize)) + return false; return m_shellSurface && m_shellSurface->hasCapability(cap); } diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index 0409ea5ab5..9b36167289 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -78,6 +78,7 @@ class SurfaceWrapper : public QQuickItem Q_PROPERTY(bool acceptKeyboardFocus READ acceptKeyboardFocus FINAL) Q_PROPERTY(bool isActivated READ isActivated NOTIFY isActivatedChanged FINAL) Q_PROPERTY(bool isIMCandidatePanel READ isIMCandidatePanel NOTIFY isIMCandidatePanelChanged FINAL) + Q_PROPERTY(bool isSnapMask READ isSnapMask NOTIFY snapMaskChanged 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) @@ -322,6 +323,8 @@ class SurfaceWrapper : public QQuickItem bool isActivated() const; bool isIMCandidatePanel() const; void setIMCandidatePanel(bool isIMCandidatePanel); + bool isSnapMask() const; + void setSnapMask(bool snapMask); bool isInputPopupLike() const; bool isLaunchpad() const; bool isQuickLaunchpad() const; @@ -392,6 +395,7 @@ public Q_SLOTS: void aboutToBeInvalidated(); void isActivatedChanged(); void isIMCandidatePanelChanged(); + void snapMaskChanged(); void resizableChanged(); void maximizableChanged(); void modalChanged(); @@ -544,6 +548,7 @@ public Q_SLOTS: uint m_isActivated : 1; uint m_attention : 1; uint m_isIMCandidatePanel : 1; + uint m_isSnapMask : 1; uint m_resizable : 1; uint m_maximizable : 1; uint m_modal : 1; From c02cb8516f4547be3fede47f7a282f08ea3945fc Mon Sep 17 00:00:00 2001 From: guolin Date: Fri, 14 Aug 2026 15:13:59 +0800 Subject: [PATCH 3/3] example --- examples/CMakeLists.txt | 1 + examples/test_snap_target/CMakeLists.txt | 56 ++++ examples/test_snap_target/Main.qml | 171 ++++++++++ examples/test_snap_target/main.cpp | 98 ++++++ examples/test_snap_target/screencapture.cpp | 334 ++++++++++++++++++++ examples/test_snap_target/screencapture.h | 115 +++++++ examples/test_snap_target/snapproxy.cpp | 269 ++++++++++++++++ examples/test_snap_target/snapproxy.h | 125 ++++++++ 8 files changed, 1169 insertions(+) create mode 100644 examples/test_snap_target/CMakeLists.txt create mode 100644 examples/test_snap_target/Main.qml create mode 100644 examples/test_snap_target/main.cpp create mode 100644 examples/test_snap_target/screencapture.cpp create mode 100644 examples/test_snap_target/screencapture.h create mode 100644 examples/test_snap_target/snapproxy.cpp create mode 100644 examples/test_snap_target/snapproxy.h diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9ab704e622..a892481621 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -10,6 +10,7 @@ add_subdirectory(test_pinch_handler) add_subdirectory(test_hide_window) add_subdirectory(test_data_control_manager) add_subdirectory(test_window_overlapped) +add_subdirectory(test_snap_target) add_subdirectory(test_window_appearance) add_subdirectory(test_super_overlay_surface) add_subdirectory(test_monitor_active_event) diff --git a/examples/test_snap_target/CMakeLists.txt b/examples/test_snap_target/CMakeLists.txt new file mode 100644 index 0000000000..559f44f350 --- /dev/null +++ b/examples/test_snap_target/CMakeLists.txt @@ -0,0 +1,56 @@ +find_package(PkgConfig REQUIRED) +find_package(Qt6 REQUIRED COMPONENTS Core Gui WaylandClient Quick) +if(Qt6_VERSION VERSION_GREATER_EQUAL 6.10) + find_package(Qt6 REQUIRED COMPONENTS WaylandClientPrivate) +endif() +find_package(TreelandProtocols REQUIRED) +pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir) + +set(BIN_NAME test-snap-target) + +qt_add_executable(${BIN_NAME} + main.cpp + screencapture.cpp + snapproxy.cpp +) + +qt_generate_wayland_protocol_client_sources(${BIN_NAME} + FILES + ${WAYLAND_PROTOCOLS_DATADIR}/staging/ext-foreign-toplevel-list/ext-foreign-toplevel-list-v1.xml + ${WAYLAND_PROTOCOLS_DATADIR}/staging/ext-image-capture-source/ext-image-capture-source-v1.xml + ${WAYLAND_PROTOCOLS_DATADIR}/staging/ext-image-copy-capture/ext-image-copy-capture-v1.xml +) + +qt_generate_wayland_protocol_client_sources(${BIN_NAME} + FILES + ${TREELAND_PROTOCOLS_DATA_DIR}/treeland-snap-target-unstable-v1.xml +) + +qt_generate_wayland_protocol_client_sources(${BIN_NAME} + FILES + ${WAYLAND_PROTOCOLS_DATADIR}/staging/xdg-toplevel-tag/xdg-toplevel-tag-v1.xml +) + +# xdg-toplevel-tag references xdg_toplevel_interface from xdg-shell +qt_generate_wayland_protocol_client_sources(${BIN_NAME} + FILES + ${WAYLAND_PROTOCOLS_DATADIR}/stable/xdg-shell/xdg-shell.xml +) + +qt_add_qml_module(${BIN_NAME} + URI TestSnapTarget + VERSION 1.0 + QML_FILES + Main.qml +) + +target_link_libraries(${BIN_NAME} + PRIVATE + Qt6::Core + Qt6::Gui + Qt6::WaylandClient + Qt6::WaylandClientPrivate + Qt6::Quick +) + +install(TARGETS ${BIN_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") diff --git a/examples/test_snap_target/Main.qml b/examples/test_snap_target/Main.qml new file mode 100644 index 0000000000..9740baa0af --- /dev/null +++ b/examples/test_snap_target/Main.qml @@ -0,0 +1,171 @@ +import QtQuick + +Window { + id: root + visible: true + flags: Qt.FramelessWindowHint + color: "transparent" + width: snapController.canvasWidth + height: snapController.canvasHeight + + // Frozen snapshot of the whole canvas, captured before the mask window was + // mapped. Only present when started with --background. + Image { + id: backgroundImage + anchors.fill: parent + fillMode: Image.Stretch + source: "image://captureBackground/canvas" + visible: snapController.backgroundEnabled + } + + Rectangle { + id: snapHighlight + color: Qt.rgba(0.2, 0.6, 1.0, 0.1) + border.color: "#3399FF" + border.width: 3 + visible: snapController.snapVisible && !snapController.confirmed + x: snapController.snapX + y: snapController.snapY + width: snapController.snapWidth + height: snapController.snapHeight + radius: 4 + } + + // Frozen selection highlight after confirmation + Rectangle { + id: frozenSelection + color: Qt.rgba(0.2, 0.6, 1.0, 0.15) + border.color: "#3399FF" + border.width: 4 + visible: snapController.confirmed + x: snapController.snapX + y: snapController.snapY + width: snapController.snapWidth + height: snapController.snapHeight + radius: 4 + } + + // Full-screen click area — only active before confirmation + MouseArea { + id: clickCatcher + anchors.fill: parent + enabled: !snapController.confirmed + cursorShape: Qt.CrossCursor + onClicked: snapController.confirmSelection() + } + + // Demo toolbar (non-functional) shown after confirmation + Rectangle { + id: toolbar + z: 9 + visible: snapController.confirmed + color: "#2B2B2B" + border.color: "#555555" + border.width: 1 + radius: 6 + height: 48 + width: 260 + + // Center toolbar above the frozen selection, clamped to screen + x: snapController.confirmed + ? Math.max(8, Math.min(root.width - width - 8, + snapController.snapX + snapController.snapWidth / 2 - width / 2)) + : 0 + y: snapController.confirmed + ? (snapController.snapY - height - 8 >= 8 + ? snapController.snapY - height - 8 + : Math.min(root.height - height - 8, + snapController.snapY + snapController.snapHeight + 8)) + : 0 + + Row { + anchors.centerIn: parent + spacing: 10 + + Repeater { + model: [ + { icon: "✎", label: "Annotate" }, + { icon: "⌨", label: "Text" }, + { icon: "⏲", label: "Delay" }, + { icon: "💾", label: "Save" } + ] + + Rectangle { + width: 52 + height: 36 + color: "transparent" + radius: 4 + + Text { + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: 2 + text: modelData.icon + font.pixelSize: 18 + color: "#FFFFFF" + } + + Text { + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottomMargin: 2 + text: modelData.label + font.pixelSize: 9 + color: "#AAAAAA" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + // Demo only - no actual functionality + } + } + } + } + } + + // Close button on the toolbar's right edge + Rectangle { + anchors.right: parent.right + anchors.rightMargin: 6 + anchors.verticalCenter: parent.verticalCenter + width: 24 + height: 24 + radius: 12 + color: "transparent" + + Text { + anchors.centerIn: parent + text: "×" + font.pixelSize: 18 + color: "#FFFFFF" + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: snapController.quit() + } + } + } + + // Debug overlay - one instance at the top-left corner of each screen + Repeater { + model: Qt.application.screens + Text { + z: 10 + x: modelData.virtualX - snapController.canvasX + 10 + y: modelData.virtualY - snapController.canvasY + 10 + text: "confirmed=" + snapController.confirmed + + " vis=" + snapController.snapVisible + + " region=(" + snapController.snapX + "," + snapController.snapY + + " " + snapController.snapWidth + "x" + snapController.snapHeight + ")" + + " proc=" + snapController.processName + + " surface=(" + snapController.canvasX + "," + snapController.canvasY + " " + + root.width + "x" + root.height + ")" + color: "red" + font.pixelSize: 14 + } + } +} diff --git a/examples/test_snap_target/main.cpp b/examples/test_snap_target/main.cpp new file mode 100644 index 0000000000..d870900650 --- /dev/null +++ b/examples/test_snap_target/main.cpp @@ -0,0 +1,98 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "screencapture.h" +#include "snapproxy.h" + +class BackgroundImageProvider : public QQuickImageProvider +{ +public: + BackgroundImageProvider() + : QQuickImageProvider(QQuickImageProvider::Image) + { + } + + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize) override + { + Q_UNUSED(id); + Q_UNUSED(requestedSize); + if (size) + *size = m_image.size(); + return m_image; + } + + void setImage(const QImage &image) + { + m_image = image; + } + +private: + QImage m_image; +}; + +int main(int argc, char *argv[]) +{ + QGuiApplication app(argc, argv); + + QCommandLineParser parser; + parser.setApplicationDescription("Treeland snap-target demo client"); + parser.addHelpOption(); + const QCommandLineOption backgroundOption( + QStringLiteral("background"), + QStringLiteral("Capture the whole canvas with ext-image-copy-capture before showing the " + "selection UI and use it as the window background.")); + parser.addOption(backgroundOption); + parser.process(app); + + SnapController controller; + + auto *backgroundProvider = new BackgroundImageProvider; + QQmlApplicationEngine engine; + engine.addImageProvider("captureBackground", backgroundProvider); + engine.rootContext()->setContextProperty("snapController", &controller); + + { + ScreenCapture sizeCapture; + const QRect desktop = sizeCapture.desktopRect(); + if (desktop.isValid() && !desktop.isEmpty()) { + controller.setCanvasX(desktop.x()); + controller.setCanvasY(desktop.y()); + controller.setCanvasWidth(desktop.width()); + controller.setCanvasHeight(desktop.height()); + } + } + + if (parser.isSet(backgroundOption)) { + ScreenCapture capture; + const QImage canvas = capture.captureFullCanvas(); + if (!canvas.isNull()) { + backgroundProvider->setImage(canvas); + controller.setBackgroundEnabled(true); + } else { + qWarning() << "Background capture failed, falling back to a transparent background"; + } + } + + engine.loadFromModule("TestSnapTarget", "Main"); + + if (engine.rootObjects().isEmpty()) + return -1; + + auto *window = qobject_cast(engine.rootObjects().first()); + if (!window) + return -1; + + window->show(); + + QTimer::singleShot(0, [&]() { + controller.setup(window); + }); + + return app.exec(); +} diff --git a/examples/test_snap_target/screencapture.cpp b/examples/test_snap_target/screencapture.cpp new file mode 100644 index 0000000000..e1d1f1a203 --- /dev/null +++ b/examples/test_snap_target/screencapture.cpp @@ -0,0 +1,334 @@ +// 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 "screencapture.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +QImage::Format ScreenCapture::qimageFormatForShm(uint32_t format) +{ + switch (format) { + case WL_SHM_FORMAT_ARGB8888: + case WL_SHM_FORMAT_BGRA8888: + return QImage::Format_ARGB32; + case WL_SHM_FORMAT_XRGB8888: + case WL_SHM_FORMAT_BGRX8888: + return QImage::Format_RGB32; + case WL_SHM_FORMAT_XBGR8888: + return QImage::Format_RGBX8888; + default: + return QImage::Format_Invalid; + } +} + +ScreenCapture::~ScreenCapture() +{ + for (Output *output : std::as_const(m_outputs)) { + cleanupOutput(output); + if (output->xdgOutput.isInitialized()) + output->xdgOutput.destroy(); + if (output->wlOutput.isInitialized()) + output->wlOutput.release(); + delete output; + } +} + +void ScreenCapture::cleanupOutput(Output *output) +{ + if (output->frame.isInitialized()) + output->frame.destroy(); + if (output->buffer.isInitialized()) + output->buffer.destroy(); + if (output->shmPool.isInitialized()) + output->shmPool.destroy(); + if (output->shmData) { + munmap(output->shmData, output->bufferH * output->bufferW * 4); + output->shmData = nullptr; + } + if (output->shmFd >= 0) { + close(output->shmFd); + output->shmFd = -1; + } + if (output->session.isInitialized()) + output->session.destroy(); +} + +void ScreenCapture::Registry::registry_global(uint32_t name, const QString &interface, + uint32_t version) +{ + auto *self = owner; + if (interface == QLatin1String("wl_shm")) { + self->m_shm.init(self->m_registry.object(), name, version); + } else if (interface == QLatin1String("zxdg_output_manager_v1")) { + self->m_xdgOutputManager.init(self->m_registry.object(), name, version); + } else if (interface == QLatin1String("ext_output_image_capture_source_manager_v1")) { + self->m_extSourceManager.init(self->m_registry.object(), name, version); + } else if (interface == QLatin1String("ext_image_copy_capture_manager_v1")) { + self->m_extCopyManager.init(self->m_registry.object(), name, version); + } else if (interface == QLatin1String("wl_output")) { + auto *output = new Output; + output->wlOutput.init(self->m_registry.object(), name, version); + self->m_outputs.append(output); + } +} + +void ScreenCapture::Registry::registry_global_remove(uint32_t name) +{ + Q_UNUSED(name); +} + +void ScreenCapture::XdgOutput::zxdg_output_v1_logical_position(int32_t x, int32_t y) +{ + output->logicalX = x; + output->logicalY = y; +} + +void ScreenCapture::XdgOutput::zxdg_output_v1_logical_size(int32_t width, int32_t height) +{ + output->logicalW = width; + output->logicalH = height; +} + +void ScreenCapture::Session::ext_image_copy_capture_session_v1_buffer_size(uint32_t width, + uint32_t height) +{ + output->bufferW = width; + output->bufferH = height; +} + +void ScreenCapture::Session::ext_image_copy_capture_session_v1_shm_format(uint32_t format) +{ + if (output->imageFormat != QImage::Format_Invalid) + return; + output->imageFormat = qimageFormatForShm(format); + if (output->imageFormat != QImage::Format_Invalid) + output->shmFormat = format; +} + +void ScreenCapture::Session::ext_image_copy_capture_session_v1_done() +{ + output->constraintsDone = true; +} + +void ScreenCapture::Frame::ext_image_copy_capture_frame_v1_ready() +{ + auto *output = this->output; + if (!output->shmData || output->bufferW <= 0 || output->bufferH <= 0 + || output->imageFormat == QImage::Format_Invalid) { + output->captureFailed = true; + } else { + QImage image(static_cast(output->shmData), output->bufferW, output->bufferH, + output->bufferW * 4, output->imageFormat); + output->image = image.copy(); + output->captureReady = true; + } + destroy(); +} + +void ScreenCapture::Frame::ext_image_copy_capture_frame_v1_failed(uint32_t reason) +{ + Q_UNUSED(reason); + output->captureFailed = true; + destroy(); +} + +QImage ScreenCapture::captureFullCanvas() +{ + m_outputs.clear(); + + auto *app = qGuiApp; + auto *waylandApp = app ? app->nativeInterface() : nullptr; + if (!waylandApp) + return { }; + m_display = waylandApp->display(); + if (!m_display) + return { }; + + m_registry.owner = this; + m_registry.init(wl_display_get_registry(m_display)); + wl_display_roundtrip(m_display); + + if (!m_shm.isInitialized() || !m_extSourceManager.isInitialized() + || !m_extCopyManager.isInitialized() || m_outputs.isEmpty()) { + return { }; + } + + if (m_xdgOutputManager.isInitialized()) { + for (Output *output : std::as_const(m_outputs)) { + output->xdgOutput.output = output; + output->xdgOutput.init(m_xdgOutputManager.get_xdg_output(output->wlOutput.object())); + } + wl_display_roundtrip(m_display); + } + + for (Output *output : std::as_const(m_outputs)) { + output->session.output = output; + auto *source = m_extSourceManager.create_source(output->wlOutput.object()); + auto *session = m_extCopyManager.create_session(source, 0); + ext_image_capture_source_v1_destroy(source); + output->session.init(session); + } + wl_display_roundtrip(m_display); + + static int serial = 0; + int framesSent = 0; + for (Output *output : std::as_const(m_outputs)) { + if (!output->constraintsDone + || output->imageFormat == QImage::Format_Invalid) + continue; + + const int stride = output->bufferW * 4; + const int size = stride * output->bufferH; + + QByteArray name = QByteArrayLiteral("/treeland-snap-target-") + + QByteArray::number(static_cast(getpid())) + + QByteArrayLiteral("-") + QByteArray::number(serial++); + int fd = shm_open(name.constData(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600); + if (fd < 0) + continue; + shm_unlink(name.constData()); + if (ftruncate(fd, size) < 0) { + close(fd); + continue; + } + auto *data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) { + close(fd); + continue; + } + + output->shmFd = fd; + output->shmData = data; + output->shmPool.init(m_shm.create_pool(fd, size)); + output->buffer.init(output->shmPool.create_buffer( + 0, output->bufferW, output->bufferH, stride, output->shmFormat)); + + output->frame.output = output; + auto *frame = output->session.create_frame(); + output->frame.init(frame); + output->frame.attach_buffer(output->buffer.object()); + output->frame.damage_buffer(0, 0, INT32_MAX, INT32_MAX); + output->frame.capture(); + ++framesSent; + } + + if (framesSent > 0) { + bool pending = true; + while (pending) { + if (wl_display_dispatch(m_display) < 0) + break; + pending = false; + for (Output *output : std::as_const(m_outputs)) { + if (!output->captureReady && !output->captureFailed) { + pending = true; + break; + } + } + } + } + + int minX = 0, minY = 0, maxX = 0, maxY = 0; + bool haveImage = false; + for (Output *output : std::as_const(m_outputs)) { + if (!output->captureReady || output->image.isNull()) + continue; + const int w = output->logicalW > 0 ? output->logicalW : output->bufferW; + const int h = output->logicalH > 0 ? output->logicalH : output->bufferH; + if (!haveImage) { + minX = output->logicalX; + minY = output->logicalY; + maxX = output->logicalX + w; + maxY = output->logicalY + h; + haveImage = true; + continue; + } + minX = qMin(minX, output->logicalX); + minY = qMin(minY, output->logicalY); + maxX = qMax(maxX, output->logicalX + w); + maxY = qMax(maxY, output->logicalY + h); + } + if (!haveImage) + return { }; + + double scale = 1.0; + for (Output *output : std::as_const(m_outputs)) { + if (!output->captureReady || output->image.isNull()) + continue; + if (output->logicalW > 0 && output->bufferW > 0) + scale = qMax(scale, double(output->bufferW) / output->logicalW); + } + + QImage canvas(qMax(1, qRound((maxX - minX) * scale)), + qMax(1, qRound((maxY - minY) * scale)), + QImage::Format_ARGB32); + canvas.fill(Qt::transparent); + QPainter painter(&canvas); + for (Output *output : std::as_const(m_outputs)) { + if (!output->captureReady || output->image.isNull()) + continue; + const QRectF target((output->logicalX - minX) * scale, + (output->logicalY - minY) * scale, + (output->logicalW > 0 ? output->logicalW : output->bufferW) * scale, + (output->logicalH > 0 ? output->logicalH : output->bufferH) * scale); + painter.drawImage(target, output->image); + } + painter.end(); + + return canvas; +} + +QRect ScreenCapture::desktopRect() +{ + auto *app = qGuiApp; + auto *waylandApp = app ? app->nativeInterface() : nullptr; + if (!waylandApp) + return {}; + auto *display = waylandApp->display(); + if (!display) + return {}; + + m_registry.owner = this; + m_registry.init(wl_display_get_registry(display)); + wl_display_roundtrip(display); + + if (m_outputs.isEmpty()) + return {}; + + if (m_xdgOutputManager.isInitialized()) { + for (Output *output : std::as_const(m_outputs)) { + output->xdgOutput.output = output; + output->xdgOutput.init(m_xdgOutputManager.get_xdg_output(output->wlOutput.object())); + } + wl_display_roundtrip(display); + } + + int minX = 0, minY = 0, maxX = 0, maxY = 0; + bool have = false; + for (Output *output : std::as_const(m_outputs)) { + if (output->logicalW <= 0 || output->logicalH <= 0) + continue; + if (!have) { + minX = output->logicalX; + minY = output->logicalY; + maxX = output->logicalX + output->logicalW; + maxY = output->logicalY + output->logicalH; + have = true; + continue; + } + minX = qMin(minX, output->logicalX); + minY = qMin(minY, output->logicalY); + maxX = qMax(maxX, output->logicalX + output->logicalW); + maxY = qMax(maxY, output->logicalY + output->logicalH); + } + + return have ? QRect(minX, minY, maxX - minX, maxY - minY) : QRect{}; +} diff --git a/examples/test_snap_target/screencapture.h b/examples/test_snap_target/screencapture.h new file mode 100644 index 0000000000..2ec1375921 --- /dev/null +++ b/examples/test_snap_target/screencapture.h @@ -0,0 +1,115 @@ +// 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 SCREENCAPTURE_H +#define SCREENCAPTURE_H + +#include +#include +#include + +#include "qwayland-ext-image-capture-source-v1.h" +#include "qwayland-ext-image-copy-capture-v1.h" + +#include +#include + +class ScreenCapture +{ +public: + ScreenCapture() = default; + ~ScreenCapture(); + + // Captures the whole canvas (all outputs, composed by logical position) + // via ext-image-copy-capture. Returns an empty image on failure. + QImage captureFullCanvas(); + + // Enumerates outputs via xdg-output and returns the logical desktop + // rect (union of all output logical geometries). No capture is performed. + QRect desktopRect(); + +private: + struct Output; + + class Registry : public QtWayland::wl_registry + { + public: + ScreenCapture *owner = nullptr; + + protected: + void registry_global(uint32_t name, const QString &interface, uint32_t version) override; + void registry_global_remove(uint32_t name) override; + }; + + class XdgOutput : public QtWayland::zxdg_output_v1 + { + public: + Output *output = nullptr; + + protected: + void zxdg_output_v1_logical_position(int32_t x, int32_t y) override; + void zxdg_output_v1_logical_size(int32_t width, int32_t height) override; + }; + + class Session : public QtWayland::ext_image_copy_capture_session_v1 + { + public: + Output *output = nullptr; + + protected: + void ext_image_copy_capture_session_v1_buffer_size(uint32_t width, uint32_t height) override; + void ext_image_copy_capture_session_v1_shm_format(uint32_t format) override; + void ext_image_copy_capture_session_v1_done() override; + }; + + class Frame : public QtWayland::ext_image_copy_capture_frame_v1 + { + public: + Output *output = nullptr; + + protected: + void ext_image_copy_capture_frame_v1_ready() override; + void ext_image_copy_capture_frame_v1_failed(uint32_t reason) override; + }; + + struct Output + { + QtWayland::wl_output wlOutput; + XdgOutput xdgOutput; + int logicalX = 0; + int logicalY = 0; + int logicalW = 0; + int logicalH = 0; + + Session session; + Frame frame; + QtWayland::wl_shm_pool shmPool; + QtWayland::wl_buffer buffer; + + int bufferW = 0; + int bufferH = 0; + uint32_t shmFormat = 0; + QImage::Format imageFormat = QImage::Format_Invalid; + bool constraintsDone = false; + + int shmFd = -1; + void *shmData = nullptr; + + QImage image; + bool captureReady = false; + bool captureFailed = false; + }; + + static QImage::Format qimageFormatForShm(uint32_t format); + void cleanupOutput(Output *output); + + Registry m_registry; + QtWayland::wl_shm m_shm; + QtWayland::zxdg_output_manager_v1 m_xdgOutputManager; + QtWayland::ext_output_image_capture_source_manager_v1 m_extSourceManager; + QtWayland::ext_image_copy_capture_manager_v1 m_extCopyManager; + QVector m_outputs; + wl_display *m_display = nullptr; +}; + +#endif // SCREENCAPTURE_H diff --git a/examples/test_snap_target/snapproxy.cpp b/examples/test_snap_target/snapproxy.cpp new file mode 100644 index 0000000000..cdc31ffca4 --- /dev/null +++ b/examples/test_snap_target/snapproxy.cpp @@ -0,0 +1,269 @@ +#include "snapproxy.h" + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +QString processNameForPidfd(int pidfd) +{ + QFile fdinfo(QStringLiteral("/proc/self/fdinfo/%1").arg(pidfd)); + if (!fdinfo.open(QIODevice::ReadOnly)) + return {}; + + qint64 pid = 0; + const QByteArray content = fdinfo.readAll(); + const QList lines = content.split('\n'); + for (const QByteArray &line : lines) { + if (line.startsWith("Pid:")) { + pid = line.mid(4).trimmed().toLongLong(); + break; + } + } + if (pid <= 0) + return {}; + + QFile comm(QStringLiteral("/proc/%1/comm").arg(pid)); + if (!comm.open(QIODevice::ReadOnly)) + return {}; + + return QString::fromLocal8Bit(comm.readAll()).trimmed(); +} + +} // namespace + +ToplevelTagManager::ToplevelTagManager() + : QWaylandClientExtensionTemplate(1) +{ +} + +ToplevelTagManager::~ToplevelTagManager() +{ + if (isActive()) + destroy(); +} + +Snap::Snap() + : QWaylandClientExtensionTemplate(1) +{ +} + +Snap::~Snap() +{ + if (isActive()) + destroy(); +} + +void Snap::treeland_snap_target_v1_snap_region(int32_t x, int32_t y, + uint32_t width, uint32_t height) +{ + emit snapRegion(static_cast(x), static_cast(y), + static_cast(width), static_cast(height)); +} + +void Snap::treeland_snap_target_v1_pidfd(int32_t pidfd) +{ + emit this->pidfd(static_cast(pidfd)); +} + +void Snap::treeland_snap_target_v1_failed(uint32_t reason) +{ + emit failed(static_cast(reason)); +} + +struct ::xdg_toplevel *getXdgToplevel(QWindow *window) +{ + if (!window || !window->handle()) + return nullptr; + + auto *ww = static_cast(window->handle()); + if (!ww) + return nullptr; + + auto *ss = ww->shellSurface(); + if (!ss) + return nullptr; + + std::any role = ss->surfaceRole(); + try { + return std::any_cast(role); + } catch (const std::bad_any_cast &) { + } + return nullptr; +} + +SnapController::SnapController(QObject *parent) + : QObject(parent) +{ + connect(&m_tagManager, &QWaylandClientExtension::activeChanged, + this, &SnapController::maybeInitialize); + connect(&m_snap, &QWaylandClientExtension::activeChanged, + this, &SnapController::maybeInitialize); +} + +SnapController::~SnapController() +{ + clearPendingPidfd(); +} + +void SnapController::setBackgroundEnabled(bool enabled) +{ + if (m_backgroundEnabled == enabled) + return; + m_backgroundEnabled = enabled; + emit backgroundEnabledChanged(); +} + +void SnapController::setCanvasWidth(int w) +{ + if (m_canvasWidth == w) + return; + m_canvasWidth = w; + emit canvasSizeChanged(); +} + +void SnapController::setCanvasHeight(int h) +{ + if (m_canvasHeight == h) + return; + m_canvasHeight = h; + emit canvasSizeChanged(); +} + +void SnapController::setCanvasX(int x) +{ + if (m_canvasX == x) + return; + m_canvasX = x; + emit canvasSizeChanged(); +} + +void SnapController::setCanvasY(int y) +{ + if (m_canvasY == y) + return; + m_canvasY = y; + emit canvasSizeChanged(); +} + +void SnapController::setup(QWindow *window) +{ + m_window = window; + maybeInitialize(); +} + +void SnapController::maybeInitialize() +{ + if (m_initialized) + return; + if (!m_window || !m_window->handle()) + return; + if (!m_tagManager.isActive() || !m_snap.isActive()) + return; + + m_initialized = true; + + auto *toplevel = getXdgToplevel(m_window); + if (toplevel) + m_tagManager.set_toplevel_tag(toplevel, "org.deepin.treeland.snap-mask"); + else + qWarning() << "Could not obtain xdg_toplevel from window surface role"; + + connect(&m_snap, &Snap::snapRegion, this, &SnapController::onSnapRegion); + connect(&m_snap, &Snap::pidfd, this, &SnapController::onPidfd); + connect(&m_snap, &Snap::failed, this, &SnapController::onFailed); + m_snap.start(TREELAND_SNAP_TARGET_V1_EVENT_TYPE_PIDFD); +} + +void SnapController::onPidfd(int pidfd) +{ + clearPendingPidfd(); + if (pidfd < 0) + return; + + // The fd is owned by the connection and invalidated after the event is + // dispatched, so keep our own descriptor until the committing + // snap_region event arrives. + m_pendingPidfd = ::dup(pidfd); +} + +void SnapController::onSnapRegion(int x, int y, quint32 width, quint32 height) +{ + // snap_region commits the target: consume the pidfd received before it. + if (m_pendingPidfd >= 0) + setProcessName(processNameForPidfd(m_pendingPidfd)); + else + setProcessName(QString()); + clearPendingPidfd(); + + // The compositor reports the snap target in global scene coordinates; + // translate it into this window's local space by subtracting the canvas + // (union of all output logical geometries) origin. + const int localX = x - m_canvasX; + const int localY = y - m_canvasY; + + // Only remember the geometry of non-empty snap targets so that the + // frozen selection / toolbar after confirmation still reflects the last + // valid region the user hovered over. Empty regions (cursor over blank + // desktop) just hide the live highlight without clobbering coordinates. + if (width != 0 && height != 0) { + m_x = localX; + m_y = localY; + m_w = width; + m_h = height; + } + m_visible = (width != 0 && height != 0); + emit snapRegionChanged(); +} + +void SnapController::onFailed(quint32 reason) +{ + const char *reasonStr = "unknown"; + switch (reason) { + case QtWayland::treeland_snap_target_v1::failure_reason_snap_busy: + reasonStr = "snap_busy"; + break; + } + qWarning() << "Snap failed:" << reasonStr; + QCoreApplication::quit(); +} + +void SnapController::setProcessName(const QString &name) +{ + if (m_processName == name) + return; + m_processName = name; + emit processNameChanged(); +} + +void SnapController::clearPendingPidfd() +{ + if (m_pendingPidfd < 0) + return; + ::close(m_pendingPidfd); + m_pendingPidfd = -1; +} + +void SnapController::confirmSelection() +{ + if (m_confirmed) + return; + m_confirmed = true; + emit confirmedChanged(); + + m_snap.stop(); +} + +void SnapController::quit() +{ + QCoreApplication::quit(); +} diff --git a/examples/test_snap_target/snapproxy.h b/examples/test_snap_target/snapproxy.h new file mode 100644 index 0000000000..9394beb1e7 --- /dev/null +++ b/examples/test_snap_target/snapproxy.h @@ -0,0 +1,125 @@ +#ifndef SNAPPROXY_H +#define SNAPPROXY_H + +#include +#include +#include +#include + +#include "qwayland-treeland-snap-target-unstable-v1.h" +#include "qwayland-xdg-toplevel-tag-v1.h" +#include "qwayland-xdg-shell.h" + +class QWindow; + +class ToplevelTagManager + : public QWaylandClientExtensionTemplate + , public QtWayland::xdg_toplevel_tag_manager_v1 +{ + Q_OBJECT +public: + ToplevelTagManager(); + ~ToplevelTagManager() override; +}; + +class Snap + : public QWaylandClientExtensionTemplate + , public QtWayland::treeland_snap_target_v1 +{ + Q_OBJECT +public: + Snap(); + ~Snap() override; + +signals: + void snapRegion(int x, int y, quint32 width, quint32 height); + void pidfd(int pidfd); + void failed(quint32 reason); + +protected: + void treeland_snap_target_v1_snap_region(int32_t x, int32_t y, + uint32_t width, uint32_t height) override; + void treeland_snap_target_v1_pidfd(int32_t pidfd) override; + void treeland_snap_target_v1_failed(uint32_t reason) override; +}; + +struct ::xdg_toplevel *getXdgToplevel(QWindow *window); + +class SnapController : public QObject +{ + Q_OBJECT + Q_PROPERTY(int snapX READ snapX NOTIFY snapRegionChanged) + Q_PROPERTY(int snapY READ snapY NOTIFY snapRegionChanged) + Q_PROPERTY(int snapWidth READ snapWidth NOTIFY snapRegionChanged) + Q_PROPERTY(int snapHeight READ snapHeight NOTIFY snapRegionChanged) + Q_PROPERTY(bool snapVisible READ snapVisible NOTIFY snapRegionChanged) + Q_PROPERTY(QString processName READ processName NOTIFY processNameChanged) + Q_PROPERTY(bool confirmed READ confirmed NOTIFY confirmedChanged) + Q_PROPERTY(bool backgroundEnabled READ backgroundEnabled WRITE setBackgroundEnabled + NOTIFY backgroundEnabledChanged) + Q_PROPERTY(int canvasX READ canvasX WRITE setCanvasX NOTIFY canvasSizeChanged) + Q_PROPERTY(int canvasY READ canvasY WRITE setCanvasY NOTIFY canvasSizeChanged) + Q_PROPERTY(int canvasWidth READ canvasWidth WRITE setCanvasWidth NOTIFY canvasSizeChanged) + Q_PROPERTY(int canvasHeight READ canvasHeight WRITE setCanvasHeight NOTIFY canvasSizeChanged) + +public: + explicit SnapController(QObject *parent = nullptr); + ~SnapController() override; + + void setup(QWindow *window); + Q_INVOKABLE void confirmSelection(); + Q_INVOKABLE void quit(); + + int snapX() const { return m_x; } + int snapY() const { return m_y; } + int snapWidth() const { return static_cast(m_w); } + int snapHeight() const { return static_cast(m_h); } + bool snapVisible() const { return m_visible; } + QString processName() const { return m_processName; } + bool confirmed() const { return m_confirmed; } + bool backgroundEnabled() const { return m_backgroundEnabled; } + void setBackgroundEnabled(bool enabled); + int canvasX() const { return m_canvasX; } + void setCanvasX(int x); + int canvasY() const { return m_canvasY; } + void setCanvasY(int y); + int canvasWidth() const { return m_canvasWidth; } + void setCanvasWidth(int w); + int canvasHeight() const { return m_canvasHeight; } + void setCanvasHeight(int h); +signals: + void snapRegionChanged(); + void processNameChanged(); + void confirmedChanged(); + void backgroundEnabledChanged(); + void canvasSizeChanged(); + +private: + void maybeInitialize(); + void onSnapRegion(int x, int y, quint32 width, quint32 height); + void onPidfd(int pidfd); + void onFailed(quint32 reason); + void setProcessName(const QString &name); + void clearPendingPidfd(); + + ToplevelTagManager m_tagManager; + Snap m_snap; + QPointer m_window; + + int m_x = 0; + int m_y = 0; + quint32 m_w = 0; + quint32 m_h = 0; + bool m_visible = false; + bool m_initialized = false; + bool m_confirmed = false; + bool m_backgroundEnabled = false; + int m_canvasX = 0; + int m_canvasY = 0; + int m_canvasWidth = 0; + int m_canvasHeight = 0; + QString m_processName; + int m_pendingPidfd = -1; +}; + +#endif // SNAPPROXY_H