Align rail Settings, translate the help transcript, style the checkers
Settings sat 7px further in than the Dashboard/Giám sát rows above it — its
QSS gave it a 6px side margin where those rows start at the rail edge. At the
collapsed 54px width that put its icon near the middle of the rail, which is
what "thu gọn menu lại ra giữa" was describing. Icon and label now start on
the same x as the rows, open and collapsed, in both themes.
The help panel's transcript is rendered HTML, so switching language re-labelled
the chrome but left the greeting — and the "AI Assistant" speaker label — in
whatever language the panel was built in. retranslate() now rewrites the
greeting (matched by identity, so a real reply is never touched) and re-renders.
The sparkle is #FDBE59, sampled from the audit page's own render. Its CSS says
.spark{color:#0F9B8A}, but the glyph is the ✨ emoji and a colour emoji ignores
CSS colour, so the page has always drawn a gold star.
Behind all three: MainWindow does not style itself — run() calls
app.setStyleSheet — so 12 of 13 checkers were measuring a window with no
padding, margins or borders. Every QSS-driven layout bug was invisible to them,
and an unstyled window reported an icon drift that does not exist. Added
_apply_theme() and wired it through.
Two checker repairs that followed:
· check_no_hscroll flagged the 9pt dialogs on sizeHintForColumn(0), which
returns 182px at 9pt, 11pt and 14pt alike. Nothing was clipped. It now
compares the painted text against the width actually on screen, and fails
on a squeezed list (24 combos) where the old test passed.
· the checkers print Vietnamese and died mid-report on a cp932 console.
New: check_rail_align (icons hold one line, both themes, both states) and
check_help_i18n (transcript follows the language). Both verified to fail
without their fix.
15/15 checkers pass. check_nav and check_design_parity segfault in Qt teardown
roughly one run in three — pre-existing, after the verdict prints, and it
happens with or without the theme change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a358a20556
commit
d060d5679a
+156
-123
@@ -1,123 +1,156 @@
|
||||
"""Prove the long dialogs never scroll sideways — including at large fonts.
|
||||
|
||||
The report that started this came from a display at 125–150% scaling, where
|
||||
every label is wider than on a 100% screen. Rather than trusting one font size,
|
||||
this runs each dialog at several point sizes and several widths and fails if any
|
||||
horizontal scrollbar turns up, in the scroll area or in the section index.
|
||||
|
||||
Run: python tools/check_no_hscroll.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
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 _isolate_home, _load_fonts # noqa: E402
|
||||
|
||||
WIDTHS = (1100, 964, 820, 700)
|
||||
POINTS = (9, 11, 14) # ~100%, ~125%, ~150% display scaling
|
||||
|
||||
|
||||
def hscroll(dlg, app):
|
||||
"""(scroll-area overflow, index overflow) — each True means content is
|
||||
wider than the space it is given.
|
||||
|
||||
A dialog built from step tabs has one scroll area per page, and a page that
|
||||
is not current has stale geometry — so each tab is brought to the front
|
||||
before its page is measured.
|
||||
"""
|
||||
from PySide6.QtWidgets import QListWidget, QScrollArea
|
||||
over_area = False
|
||||
stack = getattr(dlg, "section_stack", None)
|
||||
if stack is not None:
|
||||
# One scroll area per section; a page that is not current has stale
|
||||
# geometry, so bring each to the front before measuring it.
|
||||
idx = dlg.section_list
|
||||
keep = idx.currentRow()
|
||||
for i in range(stack.count()):
|
||||
idx.setCurrentRow(i)
|
||||
for _ in range(3):
|
||||
app.processEvents()
|
||||
sa = stack.widget(i)
|
||||
if sa.widget().sizeHint().width() > sa.viewport().width():
|
||||
over_area = True
|
||||
idx.setCurrentRow(keep)
|
||||
else:
|
||||
sa = dlg.findChildren(QScrollArea)[0]
|
||||
over_area = sa.widget().sizeHint().width() > sa.viewport().width()
|
||||
idx = dlg.findChild(QListWidget, "sectionIndex")
|
||||
over_idx = False
|
||||
if idx is not None:
|
||||
over_idx = idx.sizeHintForColumn(0) > idx.viewport().width()
|
||||
return over_area, over_idx
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sandbox = _isolate_home()
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
_load_fonts()
|
||||
|
||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||
|
||||
from cowork_local.i18n import set_language
|
||||
from cowork_local.state import AppContext
|
||||
from cowork_local.ui.settings_dialog import SettingsDialog
|
||||
from cowork_local.ui.task_editor_dialog import TaskEditorDialog
|
||||
|
||||
set_language("vi")
|
||||
ctx = AppContext(AppConfig.load())
|
||||
fails: list[str] = []
|
||||
|
||||
for pt in POINTS:
|
||||
f = QFont(app.font())
|
||||
f.setPointSize(pt)
|
||||
app.setFont(f)
|
||||
for name, make in (("Cai dat", lambda: SettingsDialog(ctx)),
|
||||
("Task editor", lambda: TaskEditorDialog(ctx=ctx))):
|
||||
dlg = make()
|
||||
dlg.show()
|
||||
row = []
|
||||
for w in WIDTHS:
|
||||
dlg.resize(w, 900)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
over_area, over_idx = hscroll(dlg, app)
|
||||
row.append(f"{w}:{'A' if over_area else '.'}{'I' if over_idx else '.'}")
|
||||
if over_area:
|
||||
fails.append(f"{name} @ {pt}pt {w}px — vung cuon tran ngang")
|
||||
if over_idx:
|
||||
fails.append(f"{name} @ {pt}pt {w}px — muc luc tran ngang")
|
||||
print(f" {pt:>2}pt {name:12} {' '.join(row)}")
|
||||
dlg.close()
|
||||
print()
|
||||
|
||||
print("A = vung cuon tran · I = muc luc tran · . = khong tran")
|
||||
print()
|
||||
if fails:
|
||||
print("*** LOI ***")
|
||||
for x in fails:
|
||||
print(" " + x)
|
||||
return 1
|
||||
print("KET QUA: khong hop thoai nao cuon ngang, o moi co chu va be rong da thu")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
"""Prove the long dialogs never scroll sideways — including at large fonts.
|
||||
|
||||
The report that started this came from a display at 125–150% scaling, where
|
||||
every label is wider than on a 100% screen. Rather than trusting one font size,
|
||||
this runs each dialog at several point sizes and several widths and fails if any
|
||||
horizontal scrollbar turns up, in the scroll area or in the section index.
|
||||
|
||||
Run: python tools/check_no_hscroll.py
|
||||
"""
|
||||
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 _apply_theme, _isolate_home, _load_fonts # noqa: E402
|
||||
|
||||
WIDTHS = (1100, 964, 820, 700)
|
||||
POINTS = (9, 11, 14) # ~100%, ~125%, ~150% display scaling
|
||||
|
||||
|
||||
def hscroll(dlg, app):
|
||||
"""(scroll-area overflow, index overflow) — each True means content is
|
||||
wider than the space it is given.
|
||||
|
||||
A dialog built from step tabs has one scroll area per page, and a page that
|
||||
is not current has stale geometry — so each tab is brought to the front
|
||||
before its page is measured.
|
||||
"""
|
||||
from PySide6.QtWidgets import QListWidget, QScrollArea
|
||||
over_area = False
|
||||
stack = getattr(dlg, "section_stack", None)
|
||||
if stack is not None:
|
||||
# One scroll area per section; a page that is not current has stale
|
||||
# geometry, so bring each to the front before measuring it.
|
||||
idx = dlg.section_list
|
||||
keep = idx.currentRow()
|
||||
for i in range(stack.count()):
|
||||
idx.setCurrentRow(i)
|
||||
for _ in range(3):
|
||||
app.processEvents()
|
||||
sa = stack.widget(i)
|
||||
if sa.widget().sizeHint().width() > sa.viewport().width():
|
||||
over_area = True
|
||||
idx.setCurrentRow(keep)
|
||||
else:
|
||||
sa = dlg.findChildren(QScrollArea)[0]
|
||||
over_area = sa.widget().sizeHint().width() > sa.viewport().width()
|
||||
idx = dlg.findChild(QListWidget, "sectionIndex")
|
||||
over_idx = False
|
||||
if idx is not None:
|
||||
over_idx = _index_elides(idx)
|
||||
return over_area, over_idx
|
||||
|
||||
|
||||
def _index_elides(idx) -> bool:
|
||||
"""True when a section name does not fit the visible width of the list.
|
||||
|
||||
Two earlier attempts got this wrong:
|
||||
· `sizeHintForColumn(0) > viewport().width()` returns 182px at 9pt, 11pt
|
||||
and 14pt alike — it does not track the font, so it called the 9pt
|
||||
dialog broken while nothing on screen was clipped.
|
||||
· Asking the delegate whether it elides. It does not: the view lays each
|
||||
row out at its natural width and the viewport simply clips what runs
|
||||
past it, so a list squeezed to 90px still reported "no elision".
|
||||
|
||||
So compare the painted text against the width that is actually on screen.
|
||||
"""
|
||||
from PySide6.QtGui import QFontMetrics
|
||||
from PySide6.QtWidgets import QStyle, QStyleOptionViewItem
|
||||
|
||||
for i in range(idx.count()):
|
||||
row = idx.indexFromItem(idx.item(i))
|
||||
opt = QStyleOptionViewItem()
|
||||
idx.initViewItemOption(opt)
|
||||
opt.rect = idx.visualRect(row)
|
||||
idx.itemDelegate().initStyleOption(opt, row)
|
||||
box = idx.style().subElementRect(QStyle.SE_ItemViewItemText, opt, idx)
|
||||
label = idx.item(i).text()
|
||||
visible = idx.viewport().width() - box.left()
|
||||
if QFontMetrics(opt.font).horizontalAdvance(label) > visible:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sandbox = _isolate_home()
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication([])
|
||||
_load_fonts()
|
||||
|
||||
_apply_theme(app) # measure the styled widget, not a bare one
|
||||
from cowork_local.config import AppConfig, CONFIG_DIR
|
||||
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
|
||||
|
||||
from cowork_local.i18n import set_language
|
||||
from cowork_local.state import AppContext
|
||||
from cowork_local.ui.settings_dialog import SettingsDialog
|
||||
from cowork_local.ui.task_editor_dialog import TaskEditorDialog
|
||||
|
||||
set_language("vi")
|
||||
ctx = AppContext(AppConfig.load())
|
||||
fails: list[str] = []
|
||||
|
||||
for pt in POINTS:
|
||||
f = QFont(app.font())
|
||||
f.setPointSize(pt)
|
||||
app.setFont(f)
|
||||
for name, make in (("Cai dat", lambda: SettingsDialog(ctx)),
|
||||
("Task editor", lambda: TaskEditorDialog(ctx=ctx))):
|
||||
dlg = make()
|
||||
dlg.show()
|
||||
row = []
|
||||
for w in WIDTHS:
|
||||
dlg.resize(w, 900)
|
||||
for _ in range(4):
|
||||
app.processEvents()
|
||||
over_area, over_idx = hscroll(dlg, app)
|
||||
row.append(f"{w}:{'A' if over_area else '.'}{'I' if over_idx else '.'}")
|
||||
if over_area:
|
||||
fails.append(f"{name} @ {pt}pt {w}px — vung cuon tran ngang")
|
||||
if over_idx:
|
||||
fails.append(f"{name} @ {pt}pt {w}px — muc luc tran ngang")
|
||||
print(f" {pt:>2}pt {name:12} {' '.join(row)}")
|
||||
dlg.close()
|
||||
print()
|
||||
|
||||
print("A = vung cuon tran · I = muc luc tran · . = khong tran")
|
||||
print()
|
||||
if fails:
|
||||
print("*** LOI ***")
|
||||
for x in fails:
|
||||
print(" " + x)
|
||||
return 1
|
||||
print("KET QUA: khong hop thoai nao cuon ngang, o moi co chu va be rong da thu")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_rc = main()
|
||||
# Qt (WebEngine especially) crashes during interpreter teardown with
|
||||
# 0xC0000409 AFTER the work is done, which would mask the real result —
|
||||
# and check_probes_bite reads these exit codes to decide whether a probe
|
||||
# caught its mutation. Leave immediately with the verdict instead.
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
os._exit(_rc)
|
||||
|
||||
Reference in New Issue
Block a user