From e601225b0f32603f04f232f76d83b6f70f463360 Mon Sep 17 00:00:00 2001 From: Anh Tran Nguyen Minh Date: Mon, 7 Sep 2026 19:22:22 +0900 Subject: [PATCH] =?UTF-8?q?fix(ui):=20thanh=20menu=20tr=C3=A1i=20kh=C3=B4n?= =?UTF-8?q?g=20k=C3=A9o=20m=E1=BA=A5t=20=C4=91=C6=B0=E1=BB=A3c=20n?= =?UTF-8?q?=E1=BB=AFa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kéo thanh chia sang trái hết cỡ thì thanh menu biến mất hẳn, và khi đã mất thì không còn gì đủ rộng để nhận ra, nói gì tới bắt lại mà kéo ra. _nav_wrap.setMinimumWidth(132) chỉ chi phối việc BỐ TRÍ, không chi phối thao tác kéo: QSplitter mặc định cho phép người dùng kéo một ngăn vượt qua chính minimum của nó rồi đóng sập về 0. Tắt bằng setChildrenCollapsible(False). Thu gọn vẫn là việc của nút MENU, và nó dừng ở 54px chứ không về 0. Co-Authored-By: Claude Opus 5 (1M context) --- presentation/shell/nav_rail.py | 6 ++ tests/ui/test_nav_rail_min_width.py | 86 +++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/ui/test_nav_rail_min_width.py diff --git a/presentation/shell/nav_rail.py b/presentation/shell/nav_rail.py index eb4b9a4..94d080f 100644 --- a/presentation/shell/nav_rail.py +++ b/presentation/shell/nav_rail.py @@ -163,6 +163,12 @@ class NavRailMixin: self.split = QSplitter(Qt.Horizontal) self.split.addWidget(self._nav_wrap) self.split.addWidget(right) + # A QSplitter lets the user drag a pane past its own minimumWidth and + # snap it shut at zero — the minimum only governs LAYOUT, not the drag. + # So the rail set a 132px floor and still vanished entirely when dragged + # hard left, with no handle left wide enough to notice, let alone grab. + # Collapsing is what the MENU button is for, and that stops at 54px. + self.split.setChildrenCollapsible(False) self.split.setStretchFactor(0, 0) self.split.setStretchFactor(1, 1) self.split.setSizes([_NAV_EXPANDED_WIDTH, 1000]) diff --git a/tests/ui/test_nav_rail_min_width.py b/tests/ui/test_nav_rail_min_width.py new file mode 100644 index 0000000..fd674b2 --- /dev/null +++ b/tests/ui/test_nav_rail_min_width.py @@ -0,0 +1,86 @@ +"""Thanh menu trái không được kéo mất. + +Kéo thanh chia sang trái hết cỡ thì thanh menu biến mất hẳn, và khi đã mất thì +không còn gì đủ rộng để nhận ra, nói gì tới bắt lại mà kéo ra. + +Nguyên nhân: ``_nav_wrap.setMinimumWidth(132)`` chỉ chi phối việc BỐ TRÍ, không +chi phối thao tác kéo. ``QSplitter`` mặc định cho phép người dùng kéo một ngăn +vượt qua chính minimum của nó rồi đóng sập về 0 — phải tắt bằng +``setChildrenCollapsible(False)``. + +Thu gọn là việc của nút MENU, và nó dừng ở 54px chứ không về 0. +""" +from __future__ import annotations + +import pytest + +from cowork_local.presentation.shell.rail_metrics import ( + _NAV_COLLAPSED_WIDTH, _NAV_MIN_WIDTH, +) + +pytest.importorskip("PySide6", reason="cần PySide6 để dựng cửa sổ thật") + + +@pytest.fixture(scope="module") +def window(qapp, tmp_path_factory): + from cowork_local.presentation.shell.bootstrap import build_config, build_context + from cowork_local.presentation.shell.main_window import MainWindow + + config_path = tmp_path_factory.mktemp("cfg") / "config.json" + build_config(config_path) + win = MainWindow(build_context(config_path)) + win.resize(1280, 800) + yield win + win.close() + + +def test_splitter_khong_cho_dong_sap_ngan_nao(window): + """Chốt trực tiếp thứ đã thiếu. + + Cố ý KHÔNG khẳng định ``isCollapsible(0)``: hàm đó trả về cờ đặt RIÊNG cho + từng ngăn, và nó vẫn là ``True`` kể cả khi chính sách chung đã tắt — nên + khẳng định vào đó là chốt một chi tiết cài đặt của Qt, không phải hành vi ta + cần. Ba bài dưới kiểm bằng thao tác kéo thật. + """ + assert window.split.childrenCollapsible() is False + + +def test_thanh_menu_co_san_min_width(window): + """Không có sàn thì tắt collapsible cũng chẳng chặn được gì.""" + assert window._nav_wrap.minimumWidth() == _NAV_MIN_WIDTH + + +def test_keo_het_co_sang_trai_van_khong_mat_thanh_menu(window): + """Kéo thanh chia về 0 — Qt phải kẹp lại ở sàn, không cho về 0.""" + window.split.setSizes([0, 1280]) + + assert window.split.sizes()[0] >= _NAV_MIN_WIDTH, ( + f"thanh menu bị thu về {window.split.sizes()[0]}px") + + +def test_keo_qua_da_van_bi_kep_lai(window): + """Giá trị âm/cực nhỏ cũng phải bị kẹp, không chỉ đúng số 0.""" + window.split.setSizes([-500, 1780]) + + assert window.split.sizes()[0] >= _NAV_MIN_WIDTH + + +def test_nut_menu_van_thu_gon_duoc_ve_54(window): + """Bản vá không được chặn mất đường thu gọn hợp lệ. + + Nút MENU thu về ``_NAV_COLLAPSED_WIDTH`` (54px) bằng cách hạ CHÍNH minimum + của widget, nên ``setChildrenCollapsible(False)`` không cản — nó chỉ cấm đi + xuống dưới minimum đang có. + """ + assert window._nav_collapsed is False + window._toggle_nav() + try: + assert window._nav_collapsed is True + assert window._nav_wrap.minimumWidth() == _NAV_COLLAPSED_WIDTH + window.split.setSizes([0, 1280]) + assert window.split.sizes()[0] >= _NAV_COLLAPSED_WIDTH + finally: + window._toggle_nav() + + assert window._nav_collapsed is False + assert window._nav_wrap.minimumWidth() == _NAV_MIN_WIDTH