diff --git a/src/core/qml/Animations/MinimizeAnimation.qml b/src/core/qml/Animations/MinimizeAnimation.qml index 9bc25dbc9d..7301adc48b 100644 --- a/src/core/qml/Animations/MinimizeAnimation.qml +++ b/src/core/qml/Animations/MinimizeAnimation.qml @@ -24,9 +24,7 @@ Item { property int animationDuration: duration readonly property real minimizedRotation: -30 readonly property bool showShadow: !target.noDecoration - && (direction === MinimizeAnimation.Direction.Hide - ? target.previousSurfaceState === SurfaceWrapper.State.Normal - : target.surfaceState === SurfaceWrapper.State.Normal) + && target.surfaceState === SurfaceWrapper.State.Normal function start() { configureAnimation(direction === MinimizeAnimation.Direction.Hide diff --git a/src/core/qml/WorkspaceProxy.qml b/src/core/qml/WorkspaceProxy.qml index c71498ba6f..f0d0fab8ec 100644 --- a/src/core/qml/WorkspaceProxy.qml +++ b/src/core/qml/WorkspaceProxy.qml @@ -1,4 +1,4 @@ -// Copyright (C) 2024 UnionTech Software Technology Co., Ltd. +// Copyright (C) 2024-2026 UnionTech Software Technology Co., Ltd. // SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only import QtQuick @@ -25,7 +25,7 @@ Item { y: surface.y - output.outputItem.y z: orderIndex active: surface.ownsOutput === output - && surface.surfaceState !== SurfaceWrapper.State.Minimized + && !surface.minimized sourceComponent: SurfaceProxy { surface: loader.surface fullProxy: true @@ -44,7 +44,7 @@ Item { y: surface.y - output.outputItem.y z: orderIndex active: surface.ownsOutput === output - && surface.surfaceState !== SurfaceWrapper.State.Minimized + && !surface.minimized sourceComponent: SurfaceProxy { surface: allLoader.surface fullProxy: true diff --git a/src/modules/resource/treelandremotesource.cpp b/src/modules/resource/treelandremotesource.cpp index b0f306fe48..5408f8612c 100644 --- a/src/modules/resource/treelandremotesource.cpp +++ b/src/modules/resource/treelandremotesource.cpp @@ -332,6 +332,7 @@ WindowInfo TreelandRemoteSource::buildWindowInfo(SurfaceWrapper *surface, info.setPosition(surface->position()); info.setVisible(surface->isVisible()); info.setActive(surface->isActivated()); + info.setMinimized(surface->isMinimized()); info.setType(static_cast(surface->type())); info.setState(static_cast(surface->surfaceState())); diff --git a/src/modules/resource/treelandwindowtree.rep b/src/modules/resource/treelandwindowtree.rep index e845541928..c756eddf84 100644 --- a/src/modules/resource/treelandwindowtree.rep +++ b/src/modules/resource/treelandwindowtree.rep @@ -18,6 +18,7 @@ POD WindowInfo( int state, bool visible, bool active, + bool minimized, QRectF geometry, QRectF titlebarGeometry, QRectF boundingRect, diff --git a/src/plugins/multitaskview/multitaskview.cpp b/src/plugins/multitaskview/multitaskview.cpp index 2269a27cee..0eeca54dea 100644 --- a/src/plugins/multitaskview/multitaskview.cpp +++ b/src/plugins/multitaskview/multitaskview.cpp @@ -159,9 +159,9 @@ void MultitaskviewSurfaceModel::initializeModel() &MultitaskviewSurfaceModel::handleWrapperOutputChanged, Qt::UniqueConnection); connect(surface, - &SurfaceWrapper::surfaceStateChanged, + &SurfaceWrapper::minimizedChanged, this, - &MultitaskviewSurfaceModel::handleSurfaceStateChanged, + &MultitaskviewSurfaceModel::handleMinimizedChanged, Qt::UniqueConnection); } std::sort(m_data.begin(), @@ -498,7 +498,7 @@ void MultitaskviewSurfaceModel::handleWrapperOutputChanged() } } -void MultitaskviewSurfaceModel::handleSurfaceStateChanged() +void MultitaskviewSurfaceModel::handleMinimizedChanged() { auto surface = qobject_cast(sender()); Q_ASSERT(surface); @@ -559,9 +559,9 @@ void MultitaskviewSurfaceModel::handleSurfaceAdded(SurfaceWrapper *surface) &MultitaskviewSurfaceModel::handleWrapperOutputChanged, Qt::UniqueConnection); connect(surface, - &SurfaceWrapper::surfaceStateChanged, + &SurfaceWrapper::minimizedChanged, this, - &MultitaskviewSurfaceModel::handleSurfaceStateChanged, + &MultitaskviewSurfaceModel::handleMinimizedChanged, Qt::UniqueConnection); if (surface->ownsOutput() == output()) { if (surfaceReady(surface)) { @@ -701,9 +701,9 @@ void MultitaskviewSurfaceModel::disconnectSurface(SurfaceWrapper *surface) this, &MultitaskviewSurfaceModel::handleWrapperOutputChanged); disconnect(surface, - &SurfaceWrapper::surfaceStateChanged, + &SurfaceWrapper::minimizedChanged, this, - &MultitaskviewSurfaceModel::handleSurfaceStateChanged); + &MultitaskviewSurfaceModel::handleMinimizedChanged); disconnect(surface, &SurfaceWrapper::normalGeometryChanged, this, diff --git a/src/plugins/multitaskview/multitaskview.h b/src/plugins/multitaskview/multitaskview.h index 6bdd724579..a1d5e5c7b2 100644 --- a/src/plugins/multitaskview/multitaskview.h +++ b/src/plugins/multitaskview/multitaskview.h @@ -183,7 +183,7 @@ class MultitaskviewSurfaceModel : public QAbstractListModel std::pair commitAndGetUpdateRange(const QList &rawData); void handleWrapperGeometryChanged(); void handleWrapperOutputChanged(); - void handleSurfaceStateChanged(); + void handleMinimizedChanged(); void handleSurfaceMappedChanged(); void handleSurfaceAdded(SurfaceWrapper *surface); void handleSurfaceRemoved(SurfaceWrapper *surface); diff --git a/src/surface/surfacewrapper.cpp b/src/surface/surfacewrapper.cpp index 80b38d7cb6..7b72421e3d 100644 --- a/src/surface/surfacewrapper.cpp +++ b/src/surface/surfacewrapper.cpp @@ -1175,6 +1175,11 @@ void SurfaceWrapper::setSurfaceState(State newSurfaceState) // request until the visual animation finishes. abortGeometryAnimation(); + if (!isVisible()) { + setSurfaceStateDirectly(newSurfaceState); + return; + } + const QRectF targetGeometry = targetGeometryForState(newSurfaceState); if (targetGeometry.isValid()) { @@ -1254,7 +1259,7 @@ bool SurfaceWrapper::isMaximized() const bool SurfaceWrapper::isMinimized() const { - return m_surfaceState == State::Minimized; + return m_minimized; } bool SurfaceWrapper::isTiling() const @@ -1615,36 +1620,17 @@ void SurfaceWrapper::doSetSurfaceState(State newSurfaceState) return; } - const bool wasMinimized = (m_surfaceState == State::Minimized); - const bool willBeMinimized = (newSurfaceState == State::Minimized); - const bool needMinimizeLinkage = (wasMinimized != willBeMinimized); - - setVisibleDecoration(newSurfaceState == State::Minimized || newSurfaceState == State::Normal); + setVisibleDecoration(newSurfaceState == State::Normal); setNoCornerRadius(newSurfaceState == State::Maximized || newSurfaceState == State::Fullscreen || newSurfaceState == State::Tiling); m_previousSurfaceState.setValueBypassingBindings(m_surfaceState); m_surfaceState.setValueBypassingBindings(newSurfaceState); - // Keep modal/parent minimize linkage ahead of this surface's own state change - // so focus fallback never sees the parent in the old state first. - if (needMinimizeLinkage && modal() && m_parentSurface) { - if (willBeMinimized && !m_parentSurface->isMinimized()) { - m_parentSurface->minimize(false); - } else if (!willBeMinimized && m_parentSurface->isMinimized()) { - m_parentSurface->restoreFromMinimized(false); - } - } - switch (m_previousSurfaceState.value()) { case State::Maximized: m_shellSurface->setMaximize(false); break; - case State::Minimized: - m_shellSurface->setMinimize(false); - updateFocusControlState(FocusControlState::UnMinimized, true); - updateHasActiveCapability(ActiveControlState::UnMinimized, true); - break; case State::Fullscreen: m_shellSurface->setFullScreen(false); break; @@ -1661,11 +1647,6 @@ void SurfaceWrapper::doSetSurfaceState(State newSurfaceState) case State::Maximized: m_shellSurface->setMaximize(true); break; - case State::Minimized: - updateFocusControlState(FocusControlState::UnMinimized, false); - updateHasActiveCapability(ActiveControlState::UnMinimized, false); - m_shellSurface->setMinimize(true); - break; case State::Fullscreen: m_shellSurface->setFullScreen(true); break; @@ -1679,19 +1660,6 @@ void SurfaceWrapper::doSetSurfaceState(State newSurfaceState) m_surfaceState.notify(); updateTitleBar(); updateVisible(); - - if (needMinimizeLinkage) { - for (SurfaceWrapper *child : std::as_const(m_subSurfaces)) { - if (willBeMinimized && child->modal()) - continue; // Modal children stay visible when parent is minimized. - if (child->isMinimized() != willBeMinimized) { - if (willBeMinimized) - child->minimize(false); - else - child->restoreFromMinimized(false); - } - } - } } void SurfaceWrapper::onAnimationReady() @@ -1924,21 +1892,76 @@ void SurfaceWrapper::setRadius(qreal newRadius) void SurfaceWrapper::minimize(bool onAnimation) { - if (m_surfaceState == State::Minimized) + if (m_wrapperAboutToRemove) + return; + + if (m_minimized) + return; + + // The parent container may reject state changes during interactive edge resize. + if (container()->filterSurfaceStateChange(this, m_surfaceState, m_surfaceState)) + return; + + abortGeometryAnimation(); + + m_minimized = true; + + if (!m_shellSurface) { + updateVisible(); return; - setSurfaceState(State::Minimized); + } + + // Keep modal/parent minimize linkage ahead of this surface's own state change + // so focus fallback never sees the parent in the old state first. + if (modal() && m_parentSurface && !m_parentSurface->isMinimized()) + m_parentSurface->minimize(false); + + m_shellSurface->setMinimize(true); + updateFocusControlState(FocusControlState::UnMinimized, false); + updateHasActiveCapability(ActiveControlState::UnMinimized, false); + updateVisible(); + + for (SurfaceWrapper *child : std::as_const(m_subSurfaces)) { + if (child->modal()) + continue; // Modal children stay visible when parent is minimized. + if (!child->isMinimized()) + child->minimize(false); + } + if (onAnimation) startMinimizeAnimation(iconGeometry(), CLOSE_ANIMATION); } void SurfaceWrapper::restoreFromMinimized(bool onAnimation) { - if (m_surfaceState != State::Minimized && m_hideByshowDesk) + if (m_wrapperAboutToRemove) + return; + + if (!m_minimized && m_hideByshowDesk) return; if (!m_hideByshowDesk) setHideByShowDesk(true); - doSetSurfaceState(m_previousSurfaceState); + m_minimized = false; + + if (!m_shellSurface) { + updateVisible(); + } else { + if (modal() && m_parentSurface && m_parentSurface->isMinimized()) + m_parentSurface->restoreFromMinimized(false); + + updateFocusControlState(FocusControlState::UnMinimized, true); + updateHasActiveCapability(ActiveControlState::UnMinimized, true); + + m_shellSurface->setMinimize(false); + updateVisible(); + + for (SurfaceWrapper *child : std::as_const(m_subSurfaces)) { + if (child->isMinimized()) + child->restoreFromMinimized(false); + } + } + if (onAnimation) startMinimizeAnimation(iconGeometry(), OPEN_ANIMATION); } @@ -1952,8 +1975,7 @@ void SurfaceWrapper::maximize() return; } - if (m_surfaceState == State::Minimized || m_surfaceState == State::Fullscreen - || !isMaximizable()) + if (m_surfaceState == State::Fullscreen || !isMaximizable()) return; setSurfaceState(State::Maximized); @@ -2022,9 +2044,6 @@ void SurfaceWrapper::enterFullscreen(WOutput *targetOutput) return; } - if (m_surfaceState == State::Minimized) - return; - if (targetOutput) { auto *helper = Helper::instance(); auto *target = helper ? helper->getOutput(targetOutput) : nullptr; diff --git a/src/surface/surfacewrapper.h b/src/surface/surfacewrapper.h index 2269c432e8..97fac7e2d2 100644 --- a/src/surface/surfacewrapper.h +++ b/src/surface/surfacewrapper.h @@ -81,6 +81,7 @@ class SurfaceWrapper : public QQuickItem 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) + Q_PROPERTY(bool minimized READ isMinimized NOTIFY minimizedChanged FINAL) public: enum class Type @@ -99,7 +100,6 @@ class SurfaceWrapper : public QQuickItem { Normal, Maximized, - Minimized, Fullscreen, Tiling, }; @@ -354,6 +354,7 @@ public Q_SLOTS: void positionAutomaticChanged(); void previousSurfaceStateChanged(); void surfaceStateChanged(); + void minimizedChanged(); void radiusChanged(); void moveRequested(); void resizeRequested(Qt::Edges edges); @@ -498,6 +499,11 @@ public Q_SLOTS: m_surfaceState, State::Normal, &SurfaceWrapper::surfaceStateChanged) + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(SurfaceWrapper, + bool, + m_minimized, + false, + &SurfaceWrapper::minimizedChanged) int m_workspaceId = -1; int m_explicitAlwaysOnTop = 0; bool m_explicitAlwaysOnBottom = false; diff --git a/tests/protocols/INDEX.md b/tests/protocols/INDEX.md index fdeb71519a..67e49d9d27 100644 --- a/tests/protocols/INDEX.md +++ b/tests/protocols/INDEX.md @@ -28,7 +28,7 @@ Wayland 线上请求与事件;本文档规定发出请求后,测试必须观 | [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 | +| [foreign-toplevel-manager-v2](treeland-foreign-toplevel-manager-v2/README.md) | E | 真实 toplevel、dock preview、窗口状态(含最小化与布局正交)、激活/焦点与 icon rectangle | | [input-manager-unstable-v1](treeland-input-manager-unstable-v1/README.md) | I / E(可选) | 默认空设备 manager 生命周期;uinput 驱动真实 libinput capability 热插拔 | | [keyboard-state-notify-unstable-v1](treeland-keyboard-state-notify-unstable-v1/README.md) | P | watcher 配置与空键盘/空 modifier 边界 | | [output-manager-v1](treeland-output-manager-v1/README.md) | I / P | 真实 `wl_output` 的 primary-output 链路 | @@ -80,7 +80,7 @@ request stub 算作 request 覆盖;生成的 client-protocol 文件本身不 | 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 像素 | +| 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` 组合 | | input-manager-unstable-v1 | 1 / 22 | 默认测试仅证明空设备 manager 可绑定;uinput target 断言 Keyboard capability 热插拔 | settings/apply、真实 mouse/touchpad 配置生效、无设备 failed;uinput E 层需显式启用并实际执行 | | keyboard-state-notify-unstable-v1 | 6 / 6 | watcher 配置、`apply` 的空键盘/空 modifier 边界 | `current_state/state_changed`、多 watcher、初始 locked、seat 销毁、重复 apply 与物理键盘对照 | | output-manager-v1 | 4 / 7 | `primary_output`;未知 output 的 color-control 错误 | `set_color_temperature/set_brightness/commit` 成功路径及 `result/color_temperature/brightness`,真实 output/像素变化 | diff --git a/tests/protocols/treeland-foreign-toplevel-manager-v2/README.md b/tests/protocols/treeland-foreign-toplevel-manager-v2/README.md index 96a6a0d189..fb9eae7328 100644 --- a/tests/protocols/treeland-foreign-toplevel-manager-v2/README.md +++ b/tests/protocols/treeland-foreign-toplevel-manager-v2/README.md @@ -15,6 +15,7 @@ | 最小化与关闭 | 对 handle 发 `set_minimized`、`unset_minimized`、`close` | 真实 wrapper 的 minimized 状态变化;xdg client 收到 close | | 最大化 | 对 handle 发 `set_maximized`、`unset_maximized` | 同一真实 `SurfaceWrapper` 进入、退出 `Maximized` 状态 | | 全屏 | 对 handle 发 `set_fullscreen(NULL)`、`unset_fullscreen` | 同一真实 `SurfaceWrapper` 进入、退出 `Fullscreen` 状态 | +| 布局与最小化正交 | `set_maximized`/`set_fullscreen` 后发 `set_minimized`、`unset_minimized`,再 `unset_maximized`/`unset_fullscreen` | 最小化期间 `SurfaceWrapper::surfaceState()` 保持布局状态且窗口不可见;恢复后仍为该布局状态并重新可见;取消布局状态后回到 `Normal` 且不再最小化 | | 激活 | 对 handle 发 `activate(wl_seat)` | `Helper` 将 wrapper 设为 activated,且它成为该 seat 的真实 keyboard focus surface | | 窗口代表区域 | 对 handle 的 mapped xdg `wl_surface` 发 `set_icon_geometry(11,12,130,140)` | production rectangle handler 将局部坐标换算为 wrapper 的 `iconGeometry`:`wrapper.position + QRect(11,12,130,140)` | @@ -40,6 +41,12 @@ minimized;`set_maximized`/`unset_maximized` 分别改变 wrapper 的 `Maximize `set_fullscreen(NULL)`/`unset_fullscreen` 分别改变其 `Fullscreen` 状态。这里读取的是 compositor 的 `SurfaceWrapper::surfaceState()`,不是 foreign handle 自身缓存的 state event。 +最小化标志与布局状态正交:在 `Maximized` 或 `Fullscreen` 状态下发 `set_minimized`,测试断言 +`SurfaceWrapper::isMinimized()` 为真、`surfaceState()` 仍为原布局状态、且窗口不可见; +`unset_minimized` 后布局状态不变且窗口重新可见;随后 `unset_maximized`/`unset_fullscreen` +必须回到 `Normal`、可见且不再最小化。这覆盖了“全屏→最小化→恢复→取消全屏后卡在最小化”的 +回归路径,而不只是顺序地单独测试各状态。 + `activate(wl_seat)` 进入 manager 连接的 `Helper::forceActivateSurface(wrapper, ..., seat)`,测试读取 wrapper 的 activated 标记和 root surface container 默认 seat container 的 `keyboardFocusSurface()`,两者都必须指向同一 @@ -55,6 +62,9 @@ mapped wrapper。`set_icon_geometry` 则 `set/unset_fullscreen`、`activate`、`set_icon_geometry`、`close`,以及 manager/context 的 `stop`、`get_dock_preview_context`、`show`、`show_tooltip`、`close`、`destroy`。 +布局与最小化的组合已覆盖 `Maximized`、`Fullscreen` 两种布局;`Tiling` 没有对应的 foreign +handle 请求,仍未被组合覆盖。 + 仍未验证 dock UI 或 preview 的渲染像素;`output_enter` / `output_leave`、title、app-id、pid、 parent 和 foreign handle 对 state event 的线上内容也尚未逐项断言。`set_fullscreen` 当前传 `NULL` output,因此未覆盖“指定 `wl_output` 是 fullscreen hint”这一分支。fixture 的 diff --git a/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp b/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp index 7490bcbaab..37a4c29e9c 100644 --- a/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp +++ b/tests/protocols/treeland-foreign-toplevel-manager-v2/setup.cpp @@ -75,6 +75,10 @@ extern "C" void ftm_read_server_state(void *data) g_state.output_ready = !Helper::instance()->rootSurfaceContainer()->outputs().isEmpty() ? 1 : 0; g_state.wrapper_minimized = g_wrapper && g_wrapper->shellSurface() && g_wrapper->shellSurface()->isMinimized() ? 1 : 0; + g_state.wrapper_self_minimized = g_wrapper && g_wrapper->isMinimized() ? 1 : 0; + g_state.wrapper_state = + g_wrapper ? static_cast(g_wrapper->surfaceState()) : -1; + g_state.wrapper_visible = g_wrapper && g_wrapper->isVisible() ? 1 : 0; g_state.wrapper_maximized = g_wrapper && g_wrapper->isMaximized() ? 1 : 0; g_state.wrapper_fullscreen = g_wrapper && g_wrapper->surfaceState() == SurfaceWrapper::State::Fullscreen ? 1 : 0; diff --git a/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.c b/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.c index 72a7cd7dbc..47e2413187 100644 --- a/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.c +++ b/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.c @@ -17,6 +17,8 @@ struct test_case { int (*run)(struct test_ctx *ctx); }; +typedef void (*ftm_layout_request_fn)(struct treeland_foreign_toplevel_handle_v2 *handle); + void test_init(struct test_ctx *ctx) { memset(ctx, 0, sizeof(*ctx)); @@ -407,6 +409,86 @@ static int unfullscreen_real_toplevel(struct test_ctx *ctx) return render_ack_and_read_server_state(ctx, &state) && !state.wrapper_fullscreen; } +static void request_maximized(struct treeland_foreign_toplevel_handle_v2 *handle) +{ + treeland_foreign_toplevel_handle_v2_set_maximized(handle); +} + +static void request_unmaximized(struct treeland_foreign_toplevel_handle_v2 *handle) +{ + treeland_foreign_toplevel_handle_v2_unset_maximized(handle); +} + +static void request_fullscreen(struct treeland_foreign_toplevel_handle_v2 *handle) +{ + treeland_foreign_toplevel_handle_v2_set_fullscreen(handle, NULL); +} + +static void request_unfullscreen(struct treeland_foreign_toplevel_handle_v2 *handle) +{ + treeland_foreign_toplevel_handle_v2_unset_fullscreen(handle); +} + +static int minimize_keeps_layout_state(struct test_ctx *ctx, + ftm_layout_request_fn request_layout, + ftm_layout_request_fn request_unlayout, + int layout_state) +{ + if (!ctx->handle) + return 0; + + request_layout(ctx->handle); + if (wl_display_roundtrip(ctx->display) < 0) + return 0; + + struct ftm_server_state state; + if (!render_ack_and_read_server_state(ctx, &state) + || state.wrapper_state != layout_state) + return 0; + + treeland_foreign_toplevel_handle_v2_set_minimized(ctx->handle); + if (wl_display_roundtrip(ctx->display) < 0) + return 0; + if (!read_server_state(ctx, &state)) + return 0; + if (!state.wrapper_self_minimized || state.wrapper_state != layout_state + || state.wrapper_visible) + return 0; + + treeland_foreign_toplevel_handle_v2_unset_minimized(ctx->handle); + if (wl_display_roundtrip(ctx->display) < 0) + return 0; + if (!render_ack_and_read_server_state(ctx, &state)) + return 0; + if (state.wrapper_self_minimized || state.wrapper_state != layout_state + || !state.wrapper_visible) + return 0; + + request_unlayout(ctx->handle); + if (wl_display_roundtrip(ctx->display) < 0) + return 0; + if (!render_ack_and_read_server_state(ctx, &state)) + return 0; + return !state.wrapper_self_minimized && state.wrapper_visible + && state.wrapper_state == FTM_WRAPPER_STATE_NORMAL; +} + +static int minimize_keeps_maximized_state(struct test_ctx *ctx) +{ + return minimize_keeps_layout_state(ctx, + request_maximized, + request_unmaximized, + FTM_WRAPPER_STATE_MAXIMIZED); +} + +static int minimize_keeps_fullscreen_state(struct test_ctx *ctx) +{ + return minimize_keeps_layout_state(ctx, + request_fullscreen, + request_unfullscreen, + FTM_WRAPPER_STATE_FULLSCREEN); +} + static int activate_real_toplevel(struct test_ctx *ctx) { if (!ctx->handle || !ctx->seat) @@ -473,6 +555,8 @@ static const struct test_case cases[] = { { "handle.unmaximize_changes_wrapper", unmaximize_real_toplevel }, { "handle.fullscreen_changes_wrapper", fullscreen_real_toplevel }, { "handle.unfullscreen_changes_wrapper", unfullscreen_real_toplevel }, + { "handle.minimize_keeps_maximized_state", minimize_keeps_maximized_state }, + { "handle.minimize_keeps_fullscreen_state", minimize_keeps_fullscreen_state }, { "handle.activate_focuses_wrapper", activate_real_toplevel }, { "handle.set_icon_geometry_changes_icon_geometry", set_icon_geometry_changes_icon_geometry }, { "handle.close_requests_xdg_close", request_close }, diff --git a/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.h b/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.h index b164cc7427..b522de4b92 100644 --- a/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.h +++ b/tests/protocols/treeland-foreign-toplevel-manager-v2/treeland-foreign-toplevel-manager-v2.h @@ -14,6 +14,13 @@ int protocol_test_run(const char *socket_name); #define TEST_MSG_MAX 256 +enum ftm_wrapper_state { + FTM_WRAPPER_STATE_NORMAL = 0, + FTM_WRAPPER_STATE_MAXIMIZED = 1, + FTM_WRAPPER_STATE_FULLSCREEN = 2, + FTM_WRAPPER_STATE_TILING = 3, +}; + struct test_result { const char *name; int failed; @@ -26,6 +33,9 @@ struct ftm_server_state { int wrapper_in_workspace; int mapped_xdg_toplevel; int wrapper_minimized; + int wrapper_self_minimized; + int wrapper_state; + int wrapper_visible; int wrapper_maximized; int wrapper_fullscreen; int wrapper_activated; diff --git a/tests/test_treeland_debug/main.cpp b/tests/test_treeland_debug/main.cpp index ff09a9525f..52f1f83998 100644 --- a/tests/test_treeland_debug/main.cpp +++ b/tests/test_treeland_debug/main.cpp @@ -152,9 +152,8 @@ void TreelandDebugTest::testStateNameKnownStates() { QCOMPARE(stateName(0), QStringLiteral("Normal")); QCOMPARE(stateName(1), QStringLiteral("Maximized")); - QCOMPARE(stateName(2), QStringLiteral("Minimized")); - QCOMPARE(stateName(3), QStringLiteral("Fullscreen")); - QCOMPARE(stateName(4), QStringLiteral("Tiling")); + QCOMPARE(stateName(2), QStringLiteral("Fullscreen")); + QCOMPARE(stateName(3), QStringLiteral("Tiling")); } void TreelandDebugTest::testStateNameUnknown() diff --git a/tools/treeland-debug/debughelpers.cpp b/tools/treeland-debug/debughelpers.cpp index 52d9415339..e92ffc79f6 100644 --- a/tools/treeland-debug/debughelpers.cpp +++ b/tools/treeland-debug/debughelpers.cpp @@ -13,9 +13,8 @@ QString stateName(int state) switch (state) { case 0: return QStringLiteral("Normal"); case 1: return QStringLiteral("Maximized"); - case 2: return QStringLiteral("Minimized"); - case 3: return QStringLiteral("Fullscreen"); - case 4: return QStringLiteral("Tiling"); + case 2: return QStringLiteral("Fullscreen"); + case 3: return QStringLiteral("Tiling"); default: return QStringLiteral("Unknown(%1)").arg(state); } } diff --git a/tools/treeland-debug/debugsession.cpp b/tools/treeland-debug/debugsession.cpp index 00aee05607..cdbb028530 100644 --- a/tools/treeland-debug/debugsession.cpp +++ b/tools/treeland-debug/debugsession.cpp @@ -154,6 +154,7 @@ QJsonObject windowToJson(const WindowInfo &window) {"state", window.state()}, {"visible", window.visible()}, {"active", window.active()}, + {"minimized", window.minimized()}, {"geometry", rectToJson(window.geometry())}, {"titlebarGeometry", rectToJson(window.titlebarGeometry())}, {"boundingRect", rectToJson(window.boundingRect())}, diff --git a/tools/treeland-debug/main.cpp b/tools/treeland-debug/main.cpp index 8444c0679e..280c119a31 100644 --- a/tools/treeland-debug/main.cpp +++ b/tools/treeland-debug/main.cpp @@ -145,14 +145,15 @@ QString windowGeometryDetail(const WindowInfo &w) void printWindowsTable(const QList &windows) { QTextStream out(stdout); - out << QStringLiteral("ID APP-ID STATE ACTIVE OUTPUT GEOMETRY TITLE\n"); + out << QStringLiteral("ID APP-ID STATE ACTIVE MIN OUTPUT GEOMETRY TITLE\n"); for (const auto &window : windows) { const auto g = window.geometry(); - const QString line = QStringLiteral("%1 %2 %3 %4 %5 %6,%7 %8x%9 %10") + const QString line = QStringLiteral("%1 %2 %3 %4 %5 %6 %7,%8 %9x%10 %11") .arg(QString::number(window.id()).leftJustified(16)) .arg(window.appId().leftJustified(22)) .arg(stateName(window.state()).leftJustified(12)) .arg(window.active() ? QStringLiteral("yes") : QStringLiteral("no")) + .arg((window.minimized() ? QStringLiteral("yes") : QStringLiteral("no")).leftJustified(5)) .arg(window.output().leftJustified(8)) .arg(static_cast(g.x())) .arg(static_cast(g.y())) @@ -178,7 +179,7 @@ void printClientsTable(const QList &clients) out << QStringLiteral(" cmd: %1\n").arg(client.command()); for (const auto &window : client.windows()) { const auto g = window.geometry(); - out << QStringLiteral(" %1%2 id=%3 %4 %5,%6 %7x%8 [%9]\n") + out << QStringLiteral(" %1%2 id=%3 %4 %5,%6 %7x%8 [%9]%10\n") .arg(window.active() ? QStringLiteral("*") : QStringLiteral(" ")) .arg(window.appId().leftJustified(24)) .arg(window.id()) @@ -187,7 +188,8 @@ void printClientsTable(const QList &clients) .arg(static_cast(g.y())) .arg(static_cast(g.width())) .arg(static_cast(g.height())) - .arg(window.output()); + .arg(window.output()) + .arg(window.minimized() ? QStringLiteral(" (minimized)") : QString()); out << windowGeometryDetail(window); } out << Qt::endl; @@ -224,7 +226,8 @@ void printTree(const TreelandInfo &info) out << wsBranch << " " << conn << (w.active() ? QStringLiteral("* ") : QStringLiteral(" ")) << w.appId() << " id=" << w.id() << " " - << stateName(w.state()) << " " + << stateName(w.state()) + << (w.minimized() ? QStringLiteral("+Min") : QString()) << " " << static_cast(g.x()) << "," << static_cast(g.y()) << " " << static_cast(g.width()) << "x" << static_cast(g.height()) << " [" << w.output() << "]" @@ -243,7 +246,8 @@ void printTree(const TreelandInfo &info) out << branch2 << conn << (w.active() ? QStringLiteral("* ") : QStringLiteral(" ")) << w.appId() << " id=" << w.id() << " " - << stateName(w.state()) << " " + << stateName(w.state()) + << (w.minimized() ? QStringLiteral("+Min") : QString()) << " " << static_cast(g.x()) << "," << static_cast(g.y()) << " " << static_cast(g.width()) << "x" << static_cast(g.height()) << " [" << w.output() << "]" @@ -817,7 +821,7 @@ static int runTop(Session &session, int timeoutMs, int intervalMs) // Collect all windows with their frame deltas. struct WinRow { qint64 id; QString appId; QString title; QString output; - int state; QRectF geo; bool active; + int state; QRectF geo; bool active; bool minimized; qint64 frames; QRectF damage; int64_t framesDelta; }; @@ -827,6 +831,7 @@ static int runTop(Session &session, int timeoutMs, int intervalMs) WinRow r; r.id = w.id(); r.appId = w.appId(); r.title = w.title(); r.output = w.output(); r.state = w.state(); r.active = w.active(); + r.minimized = w.minimized(); r.geo = w.geometry(); r.frames = w.frames(); r.damage = w.damage(); const auto prev = prevFrames.value(w.id()); r.framesDelta = (prev > 0) ? (w.frames() - prev) : 0; @@ -846,16 +851,17 @@ static int runTop(Session &session, int timeoutMs, int intervalMs) out << "treeland-debug top — " << QDateTime::currentDateTime().toString(Qt::ISODate) << " (clients: " << clients.size() << " windows: " << rows.size() << ")\n" - << " ID APP-ID STATE FRAMES GEO MARKER\n"; + << " ID APP-ID STATE MIN FRAMES GEO MARKER\n"; for (const auto &r : rows) { QString marker; if (r.id == focusId) marker = QStringLiteral("F"); else if (r.id == cursorId) marker = QStringLiteral("C"); else marker = QStringLiteral(" "); - out << QStringLiteral(" %1 %2 %3 %4 %5,%6 %7x%8 %9\n") + out << QStringLiteral(" %1 %2 %3 %4 %5 %6,%7 %8x%9 %10\n") .arg(QString::number(r.id).leftJustified(16)) .arg(r.appId.leftJustified(22)) .arg(stateName(r.state).leftJustified(12)) + .arg((r.minimized ? QStringLiteral("yes") : QStringLiteral("no")).leftJustified(4)) .arg(r.framesDelta, 5) .arg(static_cast(r.geo.x())) .arg(static_cast(r.geo.y())) @@ -1057,6 +1063,8 @@ static int runWatch(Session &session, int timeoutMs, qint64 id, int intervalMs) .arg(stateName(prev.state())).arg(stateName(cur.state())); if (cur.active() != prev.active()) changes << (cur.active() ? QStringLiteral("activated") : QStringLiteral("deactivated")); + if (cur.minimized() != prev.minimized()) + changes << (cur.minimized() ? QStringLiteral("minimized") : QStringLiteral("unminimized")); } const qint64 frames = cur.frames(); if (frames != prevFrames)