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:
+142
-1
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user