fix(ui): sections 17 and 18 were checked against the wrong text

check_design_parity read its checklist from build_audit_page.ANALYSIS. Eight
sections of the audit page are hand-written, and for those the generator's
text is NOT what the page says — so Settings and the Task editor were reported
as matching a design they had never been compared against. The Task editor was
in fact built backwards.

  * The checker now reads the "Thay đổi" bullets straight out of
    docs/ui-audit.html, and reports how many bullets on the page still have no
    probe (57 on the page, 32 probed) instead of implying full coverage.

  * Task editor: reverted from three step tabs to a left list + right panel,
    five rows matching the five real group boxes — which is what the page asks
    for, in as many words ("thay vì chia tab"), for consistency with Settings.

  * Settings now switches panels rather than scrolling, so both dialogs are
    navigated identically and neither is a long scroll any more.

  * Settings field presentation, as the page's second bullet asks: the six
    checkboxes became switches, and Language/Theme became segmented controls.
    ToggleSwitch subclasses QCheckBox and SegmentedControl exposes the slice
    of the QComboBox API this dialog uses, so no save/load path changed —
    verified by round-tripping language/theme/tray through _save().

All 22 task-editor fields and 14 settings fields verified present after the
move; 7 suites green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
NamPDT
2026-08-17 12:49:52 +09:00
co-authored by Claude Opus 5
parent cb68f87b35
commit 62bec1f984
7 changed files with 365 additions and 132 deletions
+142 -1
View File
@@ -8,7 +8,7 @@ from pathlib import Path
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, Qt, Signal
from PySide6.QtGui import QColor, QPainter, QPen
from PySide6.QtWidgets import (
QAbstractSpinBox, QComboBox, QDoubleSpinBox, QFrame,
QAbstractSpinBox, QCheckBox, QComboBox, QDoubleSpinBox, QFrame,
QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
QVBoxLayout, QWidget,
)
@@ -209,6 +209,147 @@ def narrow_guard(owner: QWidget, threshold: int, apply):
return _NarrowGuard(owner, threshold, apply)
class ToggleSwitch(QCheckBox):
"""A checkbox drawn as an on/off switch.
Subclasses QCheckBox rather than replacing it, so every ``isChecked()`` /
``setChecked()`` / ``stateChanged`` call site keeps working untouched — only
the painting changes. A switch reads as "this is on or off" where a tick box
reads as "this is selected", which is what these settings actually mean.
"""
_W, _H = 34, 18
def __init__(self, text: str = "", parent=None):
super().__init__(text, parent)
self.setCursor(Qt.PointingHandCursor)
def sizeHint(self): # noqa: N802 - Qt override
base = super().sizeHint()
base.setWidth(base.width() + self._W)
base.setHeight(max(base.height(), self._H + 4))
return base
def paintEvent(self, _e): # noqa: N802 - Qt override
from ..theme import current_palette
p = current_palette()
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
y = (self.height() - self._H) // 2
track = QRectF(0, y, self._W, self._H)
on = self.isChecked()
enabled = self.isEnabled()
fill = QColor(p.accent_solid if on else p.border_strong)
if not enabled:
fill.setAlpha(110)
painter.setPen(Qt.NoPen)
painter.setBrush(fill)
painter.drawRoundedRect(track, self._H / 2, self._H / 2)
knob = self._H - 4
kx = self._W - knob - 2 if on else 2
painter.setBrush(QColor("#FFFFFF" if enabled else p.text_faint))
painter.drawEllipse(QRectF(kx, y + 2, knob, knob))
if self.text():
painter.setPen(QColor(p.text if enabled else p.text_faint))
painter.drawText(
QRectF(self._W + 8, 0, self.width() - self._W - 8, self.height()),
int(Qt.AlignLeft | Qt.AlignVCenter), self.text())
painter.end()
class SegmentedControl(QWidget):
"""Two-to-four choices shown side by side instead of hidden in a drop-list.
Exposes the slice of the QComboBox API this app's settings code uses
(addItem / findData / currentData / setCurrentIndex / currentIndexChanged),
so it drops into an existing form without touching the save/load paths.
"""
currentIndexChanged = Signal(int)
def __init__(self, parent=None):
super().__init__(parent)
self._data: list = []
self._buttons: list = []
self._current = -1
lay = QHBoxLayout(self)
lay.setContentsMargins(0, 0, 0, 0)
lay.setSpacing(0)
self._lay = lay
lay.addStretch(1)
def addItem(self, text: str, data=None) -> None: # noqa: N802 - Qt-style name
from PySide6.QtWidgets import QPushButton
btn = QPushButton(text)
btn.setObjectName("segItem")
btn.setCheckable(True)
btn.setCursor(Qt.PointingHandCursor)
index = len(self._buttons)
btn.clicked.connect(lambda _c=False, i=index: self.setCurrentIndex(i))
self._lay.insertWidget(index, btn)
self._buttons.append(btn)
self._data.append(data)
if self._current < 0:
self.setCurrentIndex(0)
def findData(self, value) -> int: # noqa: N802
return self._data.index(value) if value in self._data else -1
def currentData(self): # noqa: N802
return self._data[self._current] if 0 <= self._current < len(self._data) else None
def currentIndex(self) -> int: # noqa: N802
return self._current
def count(self) -> int:
return len(self._buttons)
def setItemText(self, index: int, text: str) -> None: # noqa: N802
if 0 <= index < len(self._buttons):
self._buttons[index].setText(text)
def setCurrentIndex(self, index: int) -> None: # noqa: N802
if not (0 <= index < len(self._buttons)) or index == self._current:
for i, b in enumerate(self._buttons):
b.setChecked(i == self._current)
return
self._current = index
for i, b in enumerate(self._buttons):
b.setChecked(i == index)
self.currentIndexChanged.emit(index)
def section_panels(sections, width: int = 260):
"""Left list + right panel: pick a section, see that section only.
``sections`` is [(label, widget)]. Returns (list_widget, stack) for the
caller to place side by side. Used by Settings and the Task editor so both
are navigated the same way, instead of one long scroll where you cannot
tell which group you are in or how many are left.
"""
from PySide6.QtWidgets import QListWidget, QListWidgetItem, QStackedWidget
index = QListWidget()
index.setObjectName("sectionIndex")
index.setFrameShape(QListWidget.NoFrame)
index.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
index.setTextElideMode(Qt.ElideRight)
index.setWordWrap(False)
stack = QStackedWidget()
for label, widget in sections:
item = QListWidgetItem(label)
item.setToolTip(label)
index.addItem(item)
stack.addWidget(widget)
index.currentRowChanged.connect(stack.setCurrentIndex)
index.setCurrentRow(0)
natural = max(index.fontMetrics().horizontalAdvance(lab) for lab, _w in sections) + 36
index.setFixedWidth(max(120, min(width, natural)))
return index, stack
def section_index(scroll, sections, width: int = 260):
"""A clickable table of contents for a long scrolling dialog.