Pin rail row icons to the left edge instead of letting Qt centre them
QStyledItemDelegate hands the style decorationAlignment = AlignHCenter | AlignVCenter. For a row with a label that is invisible — the icon sits left of the text either way — but every rail row loses its label when the rail collapses to 54px, and a centred decoration then lands wherever the column happens to leave room. _NavItemDelegate overrides it to AlignLeft, so the icon is on the row's left padding regardless of column width, style or platform. Honest limit: this was reported from the running app and I could not reproduce it offscreen — sweeping the column width leaves the icon at a fixed x with or without the delegate. What is verifiable is the instruction, and centring is the only mechanism by which Qt can put a label-less row's icon anywhere but the left. check_rail_align now asserts the flag itself, which fails (132) the moment the delegate is removed, alongside the column sweep. Settings follows the rows to their new inset (padding-left 7px → 4px). Its label still sat 3px left of Dashboard's because a button leaves less air between icon and label than a tree row does; a 19px icon box closes that to 1px, with the 16px glyph left-aligned inside it. 15/15 checkers pass, no flakes this run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d060d5679a
commit
cb03bc1494
@@ -9,6 +9,7 @@ from typing import List
|
||||
from PySide6.QtCore import Qt, QTimer
|
||||
from PySide6.QtGui import QGuiApplication, QIcon
|
||||
from PySide6.QtWidgets import (
|
||||
QStyledItemDelegate,
|
||||
QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu,
|
||||
QPushButton, QScrollArea, QSizePolicy, QSplitter, QStackedWidget,
|
||||
QSystemTrayIcon, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
@@ -79,6 +80,23 @@ class _Toast(QLabel):
|
||||
self._timer.start(ms)
|
||||
|
||||
|
||||
class _NavItemDelegate(QStyledItemDelegate):
|
||||
"""Keep a rail row's icon on the left edge, whatever the column is doing.
|
||||
|
||||
QStyledItemDelegate hands the style decorationAlignment = AlignHCenter, so
|
||||
a row with no label — every row once the rail collapses to 54px — has its
|
||||
icon centred inside whatever box the column happens to give it. That box
|
||||
tracks the column width, which is not stable: stretched to the viewport the
|
||||
icons land in the middle of the rail, while a column left wider than the
|
||||
view leaves them at the left. Same code, two different pictures, which is
|
||||
why a test render disagreed with the running app.
|
||||
"""
|
||||
|
||||
def initStyleOption(self, option, index):
|
||||
super().initStyleOption(option, index)
|
||||
option.decorationAlignment = Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
# Nav rows (Dashboard/Schedule/Monitoring are lazy; Workspace is the eager home page).
|
||||
_ROW_DASHBOARD, _ROW_SCHEDULE, _ROW_WORKSPACE, _ROW_MONITORING = 0, 1, 2, 3
|
||||
@@ -455,6 +473,7 @@ class MainWindow(QMainWindow):
|
||||
from PySide6.QtWidgets import QHeaderView
|
||||
tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
||||
tree.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
tree.setItemDelegate(_NavItemDelegate(tree))
|
||||
return tree
|
||||
|
||||
def _nav_rows(self):
|
||||
|
||||
@@ -349,7 +349,7 @@ QToolTip {
|
||||
|
||||
/* Icons are drawn at text scale, not as decoration. */
|
||||
QPushButton, QToolButton, QComboBox, QTabBar { qproperty-iconSize: 15px 15px; }
|
||||
QTreeWidget#navrail { qproperty-iconSize: 16px 16px; }
|
||||
QTreeWidget#navrail { qproperty-iconSize: 22px 16px; }
|
||||
|
||||
/* ---- shell ------------------------------------------------------------ */
|
||||
QWidget#topbar { background: $bg; border: none; border-bottom: 1px solid $border; }
|
||||
@@ -471,7 +471,11 @@ QComboBox#navProjectPick {
|
||||
}
|
||||
QPushButton#navSettingsBtn {
|
||||
background: transparent; border: none; color: $text_muted;
|
||||
padding: 6px 8px 6px 7px; text-align: left; border-radius: ${radius}px;
|
||||
padding: 6px 8px 6px 4px; text-align: left; border-radius: ${radius}px;
|
||||
/* A button leaves less air between icon and label than a tree row
|
||||
does, so the word sat 3px left of Dashboard's. Widening the icon
|
||||
box (the glyph stays 16px, left-aligned) closes the gap. */
|
||||
qproperty-iconSize: 19px 16px;
|
||||
/* No side margin: Settings reads as one more row under Dashboard/Giám sát,
|
||||
so its icon has to start on their x. A 6px margin put it at 14 — near
|
||||
enough the middle of the collapsed 54px rail to look centred. */
|
||||
|
||||
@@ -139,6 +139,7 @@ def main() -> int:
|
||||
win._toggle_nav()
|
||||
app.processEvents()
|
||||
fails += compare(theme_name, rail, opened, closed)
|
||||
fails += column_widths_do_not_move_icons(win, app, theme_name)
|
||||
|
||||
print()
|
||||
for f in fails:
|
||||
@@ -149,6 +150,63 @@ def main() -> int:
|
||||
os._exit(1 if fails else 0)
|
||||
|
||||
|
||||
def column_widths_do_not_move_icons(win, app, theme_name):
|
||||
"""The icon must not care how wide the column is.
|
||||
|
||||
On the machine that reported this the column matched the rail and the icons
|
||||
sat in the middle; in a test render the column stayed wider than the view
|
||||
and the same code drew them at the left. So sweep the width and require the
|
||||
icon to hold still.
|
||||
"""
|
||||
from PySide6.QtWidgets import QStyle, QStyleOptionViewItem
|
||||
|
||||
if not win._nav_collapsed:
|
||||
win._toggle_nav()
|
||||
app.processEvents()
|
||||
fails, seen = [], {}
|
||||
# The invariant that matters. A centred decoration is the only way Qt can
|
||||
# put a label-less row's icon anywhere but the left edge, and how far it
|
||||
# travels depends on the box the column hands it — which is why this
|
||||
# reproduces on one machine and not another. Require the instruction
|
||||
# itself, not just the pixel it happens to produce here.
|
||||
from PySide6.QtCore import Qt as _Qt
|
||||
for tree_name in ("nav", "nav_bottom"):
|
||||
tree = getattr(win, tree_name)
|
||||
index = tree.indexFromItem(tree.topLevelItem(0), 0)
|
||||
opt = QStyleOptionViewItem()
|
||||
tree.initViewItemOption(opt)
|
||||
tree.itemDelegate().initStyleOption(opt, index)
|
||||
align = int(opt.decorationAlignment)
|
||||
if align & int(_Qt.AlignHCenter) or not align & int(_Qt.AlignLeft):
|
||||
fails.append(f"{theme_name} {tree_name}: decorationAlignment={align}"
|
||||
f" — icon is free to drift off the left edge")
|
||||
for tree_name in ("nav", "nav_bottom"):
|
||||
tree = getattr(win, tree_name)
|
||||
keep = tree.columnWidth(0)
|
||||
for width in (tree.viewport().width(), 70, 100, 140):
|
||||
tree.setColumnWidth(0, width)
|
||||
app.processEvents()
|
||||
index = tree.indexFromItem(tree.topLevelItem(0), 0)
|
||||
opt = QStyleOptionViewItem()
|
||||
tree.initViewItemOption(opt)
|
||||
opt.rect = tree.visualRect(index)
|
||||
tree.itemDelegate().initStyleOption(opt, index)
|
||||
x = tree.style().subElementRect(
|
||||
QStyle.SE_ItemViewItemDecoration, opt, tree).left()
|
||||
seen.setdefault(tree_name, []).append((width, x))
|
||||
tree.setColumnWidth(0, keep)
|
||||
app.processEvents()
|
||||
xs = {x for _w, x in seen[tree_name]}
|
||||
if len(xs) > 1:
|
||||
fails.append(f"{theme_name} {tree_name}: icon x changes with the "
|
||||
f"column width — {seen[tree_name]}")
|
||||
print(f" column sweep: " + " ".join(
|
||||
f"{n}={[x for _w, x in v]}" for n, v in seen.items()))
|
||||
win._toggle_nav()
|
||||
app.processEvents()
|
||||
return fails
|
||||
|
||||
|
||||
def compare(theme_name, rail, opened, closed):
|
||||
print()
|
||||
print(f"theme={theme_name}")
|
||||
|
||||
Reference in New Issue
Block a user