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.QtCore import Qt, QTimer
|
||||||
from PySide6.QtGui import QGuiApplication, QIcon
|
from PySide6.QtGui import QGuiApplication, QIcon
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
|
QStyledItemDelegate,
|
||||||
QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu,
|
QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu,
|
||||||
QPushButton, QScrollArea, QSizePolicy, QSplitter, QStackedWidget,
|
QPushButton, QScrollArea, QSizePolicy, QSplitter, QStackedWidget,
|
||||||
QSystemTrayIcon, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
QSystemTrayIcon, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||||
@@ -79,6 +80,23 @@ class _Toast(QLabel):
|
|||||||
self._timer.start(ms)
|
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):
|
class MainWindow(QMainWindow):
|
||||||
# Nav rows (Dashboard/Schedule/Monitoring are lazy; Workspace is the eager home page).
|
# 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
|
_ROW_DASHBOARD, _ROW_SCHEDULE, _ROW_WORKSPACE, _ROW_MONITORING = 0, 1, 2, 3
|
||||||
@@ -448,13 +466,14 @@ class MainWindow(QMainWindow):
|
|||||||
tree.setIndentation(0)
|
tree.setIndentation(0)
|
||||||
tree.setRootIsDecorated(False)
|
tree.setRootIsDecorated(False)
|
||||||
tree.setUniformRowHeights(True)
|
tree.setUniformRowHeights(True)
|
||||||
# The column follows the viewport instead of the widest label. Left
|
# The column follows the viewport instead of the widest label. Left
|
||||||
# to size itself it stayed ~100px wide inside the 54px collapsed
|
# to size itself it stayed ~100px wide inside the 54px collapsed
|
||||||
# rail, so a horizontal scrollbar appeared and slid the icons out of
|
# rail, so a horizontal scrollbar appeared and slid the icons out of
|
||||||
# the position they hold while the rail is open.
|
# the position they hold while the rail is open.
|
||||||
from PySide6.QtWidgets import QHeaderView
|
from PySide6.QtWidgets import QHeaderView
|
||||||
tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
|
||||||
tree.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
tree.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
tree.setItemDelegate(_NavItemDelegate(tree))
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
def _nav_rows(self):
|
def _nav_rows(self):
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ QToolTip {
|
|||||||
|
|
||||||
/* Icons are drawn at text scale, not as decoration. */
|
/* Icons are drawn at text scale, not as decoration. */
|
||||||
QPushButton, QToolButton, QComboBox, QTabBar { qproperty-iconSize: 15px 15px; }
|
QPushButton, QToolButton, QComboBox, QTabBar { qproperty-iconSize: 15px 15px; }
|
||||||
QTreeWidget#navrail { qproperty-iconSize: 16px 16px; }
|
QTreeWidget#navrail { qproperty-iconSize: 22px 16px; }
|
||||||
|
|
||||||
/* ---- shell ------------------------------------------------------------ */
|
/* ---- shell ------------------------------------------------------------ */
|
||||||
QWidget#topbar { background: $bg; border: none; border-bottom: 1px solid $border; }
|
QWidget#topbar { background: $bg; border: none; border-bottom: 1px solid $border; }
|
||||||
@@ -471,7 +471,11 @@ QComboBox#navProjectPick {
|
|||||||
}
|
}
|
||||||
QPushButton#navSettingsBtn {
|
QPushButton#navSettingsBtn {
|
||||||
background: transparent; border: none; color: $text_muted;
|
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,
|
/* 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
|
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. */
|
enough the middle of the collapsed 54px rail to look centred. */
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ def main() -> int:
|
|||||||
win._toggle_nav()
|
win._toggle_nav()
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
fails += compare(theme_name, rail, opened, closed)
|
fails += compare(theme_name, rail, opened, closed)
|
||||||
|
fails += column_widths_do_not_move_icons(win, app, theme_name)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
for f in fails:
|
for f in fails:
|
||||||
@@ -149,6 +150,63 @@ def main() -> int:
|
|||||||
os._exit(1 if fails else 0)
|
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):
|
def compare(theme_name, rail, opened, closed):
|
||||||
print()
|
print()
|
||||||
print(f"theme={theme_name}")
|
print(f"theme={theme_name}")
|
||||||
|
|||||||
Reference in New Issue
Block a user