From bcdfe43c7c8fb84c983588c5145e15dcd8ae3970 Mon Sep 17 00:00:00 2001 From: Nam Pham Dinh Thanh Date: Tue, 18 Aug 2026 11:41:03 +0900 Subject: [PATCH] Keep the project picker reachable while the rail is collapsed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The picker was hidden outright at 54px, on the grounds that a combo box there could only show a stub of a name. True, but it left the collapsed rail with no way to change project at all — and the audit page's own inventory of the 11 collapsible controls says the rail's fold "giữ nguyên toàn bộ". A folder QToolButton stands in for it: same items, and choosing one moves the combo, so _on_rail_project_pick stays the single code path. Its tooltip carries the current project name, which the combo could not have shown anyway. It matches the + button's width and drops its menu arrow — at 42px the arrow would take a third of the row to say nothing. check_nav now collapses the rail and requires a reachable picker whose menu matches the combo item for item, then picks a row and asserts the project actually changed. Hiding the button fails it. 15/15 checkers pass. Co-Authored-By: Claude Opus 5 (1M context) --- app.py | 30 ++++++++++++++++++++++++++++++ theme.py | 10 ++++++++++ tools/check_nav.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/app.py b/app.py index 9360703..b7bd920 100644 --- a/app.py +++ b/app.py @@ -247,10 +247,23 @@ class MainWindow(QMainWindow): self.nav_new_chat.setIcon(_icon("plus")) self.nav_new_chat.setCursor(Qt.PointingHandCursor) self.nav_new_chat.clicked.connect(self._on_rail_new_chat) + # At 54px the picker cannot show a name, but dropping it altogether left + # the collapsed rail with no way to change project at all. This stands in + # for it: same list, same handler, just the folder icon and a tooltip. + self.nav_project_btn = QToolButton() + self.nav_project_btn.setObjectName("navProjectPickMini") + self.nav_project_btn.setIcon(_icon("folder")) + self.nav_project_btn.setCursor(Qt.PointingHandCursor) + self.nav_project_btn.setPopupMode(QToolButton.InstantPopup) + self.nav_project_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.nav_project_btn.setMenu(QMenu(self.nav_project_btn)) + self.nav_project_btn.menu().aboutToShow.connect(self._fill_rail_project_menu) + self.nav_project_btn.setVisible(False) head = QVBoxLayout() head.setContentsMargins(6, 0, 6, 6) head.setSpacing(6) head.addWidget(self.nav_project) + head.addWidget(self.nav_project_btn) head.addWidget(self.nav_new_chat) nvl.addLayout(head) self.workspace.project_selected.connect(self._sync_rail_project) @@ -621,12 +634,28 @@ class MainWindow(QMainWindow): self.nav_project.setCurrentIndex(idx) has = bool(choices) self.nav_project.setEnabled(has) + self.nav_project_btn.setEnabled(has) + self.nav_project_btn.setToolTip( + self.nav_project.currentText().replace("📁 ", "") + if has else tr("app.nav.create_project_first")) self.nav_new_chat.setEnabled(has) self.nav_new_chat.setToolTip( "" if has else tr("app.nav.create_project_first")) finally: self._syncing_rail_project = False + def _fill_rail_project_menu(self) -> None: + """Mirror the picker's items. Choosing one moves the picker, which runs + _on_rail_project_pick — the collapsed rail adds no second code path.""" + menu = self.nav_project_btn.menu() + menu.clear() + for i in range(self.nav_project.count()): + act = menu.addAction(self.nav_project.itemText(i)) + act.setCheckable(True) + act.setChecked(i == self.nav_project.currentIndex()) + act.triggered.connect( + lambda _checked=False, row=i: self.nav_project.setCurrentIndex(row)) + def _on_rail_project_pick(self, _idx: int) -> None: if self._syncing_rail_project: return @@ -701,6 +730,7 @@ class MainWindow(QMainWindow): # picker would be a stub of a name, so it steps aside entirely and the # button keeps just its + icon. self.nav_project.setVisible(not self._nav_collapsed) + self.nav_project_btn.setVisible(self._nav_collapsed) self._refresh_rail_recents() # Collapsed to 54px only the theme toggle still fits; the rest of the # account row would be clipped, so it steps aside (Settings, which opens diff --git a/theme.py b/theme.py index c334972..5bbe87b 100644 --- a/theme.py +++ b/theme.py @@ -469,6 +469,16 @@ QComboBox#navProjectPick { background: $surface_raised; border: 1px solid $nav_border; color: $text; padding: 4px 8px; border-radius: ${radius}px; } +/* Stand-in for the picker while the rail is 54px wide: icon only, no arrow — + the arrow would eat a third of the width for no information. */ +QToolButton#navProjectPickMini { + background: $surface_raised; border: 1px solid $nav_border; border-radius: ${radius}px; + padding: 4px; qproperty-iconSize: 16px 16px; +} +QToolButton#navProjectPickMini:hover { background: $nav_hover; } +QToolButton#navProjectPickMini:disabled { background: transparent; border-color: $border; } +QToolButton#navProjectPickMini::menu-indicator { image: none; width: 0; } + QPushButton#navSettingsBtn { background: transparent; border: none; color: $text_muted; padding: 6px 8px 6px 4px; text-align: left; border-radius: ${radius}px; diff --git a/tools/check_nav.py b/tools/check_nav.py index d5cd6da..8fe0a41 100644 --- a/tools/check_nav.py +++ b/tools/check_nav.py @@ -289,6 +289,37 @@ def main() -> int: print(f"provider dang chon : {win.provider_combo.currentText()!r}") print(f"tai khoan : {win.account_lbl.text()!r}") + # Collapsing the rail must not take the project picker away with it: at + # 54px the combo cannot show a name, so a folder button stands in for it. + if not win._nav_collapsed: + win._toggle_nav() + app.processEvents() + mini = getattr(win, "nav_project_btn", None) + if mini is None or mini.isHidden(): + fails.append("thu gon rail xong khong con cach nao doi project") + else: + mini.menu().aboutToShow.emit() + app.processEvents() + menu_items = [a.text() for a in mini.menu().actions()] + combo_items = [win.nav_project.itemText(i) + for i in range(win.nav_project.count())] + if menu_items != combo_items: + fails.append(f"menu project khi thu gon lech voi combo: " + f"{menu_items} vs {combo_items}") + elif len(menu_items) > 1: + before = win.workspace.selected_project_id() + # Any row but the one already selected, or nothing would change. + row = (win.nav_project.currentIndex() + 1) % len(menu_items) + mini.menu().actions()[row].trigger() + app.processEvents() + after = win.workspace.selected_project_id() + if after == before: + fails.append("chon project tu menu thu gon khong doi project") + else: + print(f"doi project khi thu gon: {before} -> {after}") + win._toggle_nav() + app.processEvents() + print() print(f"tong dong dieu huong: {n_total} + nut Cai dat") if fails: