From d37a0c15789b750576a57126bd943eccca649b21 Mon Sep 17 00:00:00 2001 From: Nam Pham Dinh Thanh Date: Tue, 18 Aug 2026 11:55:35 +0900 Subject: [PATCH] Let the splitter handle beside the rail actually resize it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rail was setFixedWidth, so the handle QSplitter draws between it and the content was inert — it looked draggable and did nothing when dragged. The expanded rail is now a range (132–360px) instead of one number, so the handle has something to give. Collapsing still pins it at 54px exactly, and a width dragged to is remembered across the fold rather than snapping back to the 150px default. Long project names and RECENTS threads are what a wider rail buys. check_rail_resize drives the splitter the way the handle does and asserts the rail follows, clamps at both ends, folds to 54px and comes back to the chosen width. Restoring setFixedWidth fails it — the first attempt at that mutation passed, because setMaximumWidth on the next line quietly undid it. 16/16 checkers pass. Co-Authored-By: Claude Opus 5 (1M context) --- app.py | 32 ++++++++++- tools/check_rail_resize.py | 110 +++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 tools/check_rail_resize.py diff --git a/app.py b/app.py index b7bd920..de07c55 100644 --- a/app.py +++ b/app.py @@ -37,6 +37,12 @@ ASSETS = Path(__file__).resolve().parent / "assets" # shows icon-only (still fully clickable, just narrower). _NAV_EXPANDED_WIDTH = 150 _NAV_COLLAPSED_WIDTH = 54 +# The splitter between rail and content draws a drag handle. It only means +# something if the rail can actually take a width from it, so the expanded rail +# is a range rather than one number; long project and thread names in RECENTS +# are the reason someone would widen it. +_NAV_MIN_WIDTH = 132 +_NAV_MAX_WIDTH = 360 def app_icon() -> QIcon: @@ -213,7 +219,8 @@ class MainWindow(QMainWindow): self._collapse_right_icon = collapse_right_icon self._nav_wrap = QWidget() self._nav_wrap.setObjectName("navWrap") - self._nav_wrap.setFixedWidth(_NAV_EXPANDED_WIDTH) + self._nav_width = _NAV_EXPANDED_WIDTH # remembered across collapses + self._set_nav_width_range(_NAV_MIN_WIDTH, _NAV_MAX_WIDTH) nvl = QVBoxLayout(self._nav_wrap) nvl.setContentsMargins(0, 0, 0, 0) nvl.setSpacing(0) @@ -326,6 +333,7 @@ class MainWindow(QMainWindow): self.split.setStretchFactor(0, 0) self.split.setStretchFactor(1, 1) self.split.setSizes([_NAV_EXPANDED_WIDTH, 1000]) + self.split.splitterMoved.connect(self._on_split_moved) self.setCentralWidget(self.split) # Landing stays Workspace ▸ Project, exactly as before. Go through _goto # so the page is actually shown — selecting the row alone only moves the @@ -747,10 +755,28 @@ class MainWindow(QMainWindow): def _nav_new_chat_enabled(self) -> bool: return bool(self.workspace.project_choices()) + def _set_nav_width_range(self, lo: int, hi: int) -> None: + """setFixedWidth would leave the splitter handle inert — visible, and + doing nothing when dragged.""" + self._nav_wrap.setMinimumWidth(lo) + self._nav_wrap.setMaximumWidth(hi) + + def _on_split_moved(self, _pos: int, _index: int) -> None: + if not self._nav_collapsed: + self._nav_width = max(_NAV_MIN_WIDTH, + min(_NAV_MAX_WIDTH, self._nav_wrap.width())) + def _toggle_nav(self) -> None: + if not self._nav_collapsed: + self._nav_width = max(_NAV_MIN_WIDTH, + min(_NAV_MAX_WIDTH, self._nav_wrap.width())) self._nav_collapsed = not self._nav_collapsed - width = _NAV_COLLAPSED_WIDTH if self._nav_collapsed else _NAV_EXPANDED_WIDTH - self._nav_wrap.setFixedWidth(width) + if self._nav_collapsed: + width = _NAV_COLLAPSED_WIDTH + self._set_nav_width_range(width, width) + else: + width = self._nav_width + self._set_nav_width_range(_NAV_MIN_WIDTH, _NAV_MAX_WIDTH) self._apply_nav_labels() # Same chevron convention as every other collapsible panel: right- # pointing (fill-right) means "click to expand", left means "collapse". diff --git a/tools/check_rail_resize.py b/tools/check_rail_resize.py new file mode 100644 index 0000000..aa2df51 --- /dev/null +++ b/tools/check_rail_resize.py @@ -0,0 +1,110 @@ +"""The splitter handle beside the rail has to actually move the rail. + +setFixedWidth left it drawn but inert: it looked draggable and did nothing. +Also checks that a width the user drags to survives a collapse/expand, and +that collapsing still pins the rail at 54px. +""" +from __future__ import annotations + +import os +import sys + +sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO.parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent)) +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +from capture_screens import ( # noqa: E402 + _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts) + + +def main() -> int: + sandbox = _isolate_home() + from PySide6.QtWidgets import QApplication + + app = QApplication([]) + _load_fonts() + _freeze_schedulers() + _apply_theme(app) + + from cowork_local.config import CONFIG_DIR + assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}" + + from seed_demo_data import seed + seed() + + from cowork_local.app import ( + _NAV_COLLAPSED_WIDTH, _NAV_MAX_WIDTH, _NAV_MIN_WIDTH, MainWindow) + from cowork_local.config import AppConfig + from cowork_local.i18n import set_language + from cowork_local.state import AppContext + + set_language("vi") + win = MainWindow(AppContext(AppConfig.load()), user_name="local") + win.resize(1400, 900) + win.show() + app.processEvents() + + fails = [] + rail, split = win._nav_wrap, win.split + + def drag_to(px): + """What the splitter does when the handle is dragged.""" + total = sum(split.sizes()) + split.setSizes([px, max(1, total - px)]) + app.processEvents() + win._on_split_moved(px, 1) + app.processEvents() + return rail.width() + + start = rail.width() + wide = drag_to(300) + print(f"keo rong : {start} -> {wide}px") + if wide <= start: + fails.append(f"keo tay nam ra 300px ma rail van {wide}px") + + narrow = drag_to(_NAV_MIN_WIDTH) + print(f"keo hep : {wide} -> {narrow}px") + if narrow >= wide: + fails.append(f"keo hep lai khong an: {narrow}px") + + over = drag_to(_NAV_MAX_WIDTH + 200) + print(f"keo qua max: {over}px (tran {_NAV_MAX_WIDTH})") + if over > _NAV_MAX_WIDTH: + fails.append(f"rail vuot tran: {over} > {_NAV_MAX_WIDTH}") + + under = drag_to(20) + print(f"keo duoi min: {under}px (san {_NAV_MIN_WIDTH})") + if under < _NAV_MIN_WIDTH: + fails.append(f"rail thap hon san: {under} < {_NAV_MIN_WIDTH}") + + # a dragged width has to come back after a fold + chosen = drag_to(280) + win._toggle_nav() + app.processEvents() + folded = rail.width() + print(f"thu gon : {folded}px") + if folded != _NAV_COLLAPSED_WIDTH: + fails.append(f"thu gon phai la {_NAV_COLLAPSED_WIDTH}px, dang {folded}px") + win._toggle_nav() + app.processEvents() + back = rail.width() + print(f"mo lai : {back}px (da chon {chosen}px)") + if abs(back - chosen) > 4: + fails.append(f"mo lai quen be rong da keo: {back} thay vi {chosen}") + + print() + for f in fails: + print("FAIL " + f) + print("PASS tay nam keo duoc, nho be rong qua lan gap" if not fails + else f"{len(fails)} problem(s)") + sys.stdout.flush() + os._exit(1 if fails else 0) + + +if __name__ == "__main__": + raise SystemExit(main())