Keep the project picker reachable while the rail is collapsed

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) <noreply@anthropic.com>
This commit is contained in:
Nam Pham Dinh Thanh
2026-08-18 11:41:03 +09:00
co-authored by Claude Opus 5
parent ac29f519cc
commit bcdfe43c7c
3 changed files with 71 additions and 0 deletions
+30
View File
@@ -247,10 +247,23 @@ class MainWindow(QMainWindow):
self.nav_new_chat.setIcon(_icon("plus")) self.nav_new_chat.setIcon(_icon("plus"))
self.nav_new_chat.setCursor(Qt.PointingHandCursor) self.nav_new_chat.setCursor(Qt.PointingHandCursor)
self.nav_new_chat.clicked.connect(self._on_rail_new_chat) 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 = QVBoxLayout()
head.setContentsMargins(6, 0, 6, 6) head.setContentsMargins(6, 0, 6, 6)
head.setSpacing(6) head.setSpacing(6)
head.addWidget(self.nav_project) head.addWidget(self.nav_project)
head.addWidget(self.nav_project_btn)
head.addWidget(self.nav_new_chat) head.addWidget(self.nav_new_chat)
nvl.addLayout(head) nvl.addLayout(head)
self.workspace.project_selected.connect(self._sync_rail_project) self.workspace.project_selected.connect(self._sync_rail_project)
@@ -621,12 +634,28 @@ class MainWindow(QMainWindow):
self.nav_project.setCurrentIndex(idx) self.nav_project.setCurrentIndex(idx)
has = bool(choices) has = bool(choices)
self.nav_project.setEnabled(has) 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.setEnabled(has)
self.nav_new_chat.setToolTip( self.nav_new_chat.setToolTip(
"" if has else tr("app.nav.create_project_first")) "" if has else tr("app.nav.create_project_first"))
finally: finally:
self._syncing_rail_project = False 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: def _on_rail_project_pick(self, _idx: int) -> None:
if self._syncing_rail_project: if self._syncing_rail_project:
return return
@@ -701,6 +730,7 @@ class MainWindow(QMainWindow):
# picker would be a stub of a name, so it steps aside entirely and the # picker would be a stub of a name, so it steps aside entirely and the
# button keeps just its + icon. # button keeps just its + icon.
self.nav_project.setVisible(not self._nav_collapsed) self.nav_project.setVisible(not self._nav_collapsed)
self.nav_project_btn.setVisible(self._nav_collapsed)
self._refresh_rail_recents() self._refresh_rail_recents()
# Collapsed to 54px only the theme toggle still fits; the rest of the # 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 # account row would be clipped, so it steps aside (Settings, which opens
+10
View File
@@ -469,6 +469,16 @@ QComboBox#navProjectPick {
background: $surface_raised; border: 1px solid $nav_border; color: $text; background: $surface_raised; border: 1px solid $nav_border; color: $text;
padding: 4px 8px; border-radius: ${radius}px; 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 { QPushButton#navSettingsBtn {
background: transparent; border: none; color: $text_muted; background: transparent; border: none; color: $text_muted;
padding: 6px 8px 6px 4px; text-align: left; border-radius: ${radius}px; padding: 6px 8px 6px 4px; text-align: left; border-radius: ${radius}px;
+31
View File
@@ -289,6 +289,37 @@ def main() -> int:
print(f"provider dang chon : {win.provider_combo.currentText()!r}") print(f"provider dang chon : {win.provider_combo.currentText()!r}")
print(f"tai khoan : {win.account_lbl.text()!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()
print(f"tong dong dieu huong: {n_total} + nut Cai dat") print(f"tong dong dieu huong: {n_total} + nut Cai dat")
if fails: if fails: