修正 Studio 力曲线原点、运行不可用提示与禁用按钮样式

FN 轴视图留 8% 余量使 0 刻度与静止曲线离开边框;实时视图在轨道上直接写出运行不可用的原因;禁用按钮改为虚线无填充,复选框加勾,单选框选中不再变方,并放大勾选框、微调框箭头、滚动条与菜单行以便触屏操作。另修 ui_check 桩 worker 签名与英文回放提示。在 Linux 离屏环境对比未改动的 HEAD 验证:studio 失败集合为 HEAD 的子集,ui_check 结果一致。
This commit is contained in:
ykcheungab
2026-10-18 14:39:13 +08:00
parent 7b5ffa74f6
commit 85d0b396e3
9 changed files with 226 additions and 49 deletions
+91 -24
View File
@@ -69,6 +69,51 @@ def _paint_arrow(kind: str, fill: str) -> str:
return ""
@lru_cache(maxsize=8)
def _paint_check(fill: str) -> str:
"""Draw a tick PNG for a checked checkbox; return its path ('' on failure).
A checked box used to be a solid square, which at 15 px reads as "disabled"
or "indeterminate" rather than "on".
"""
try:
from PySide6.QtCore import QPointF, Qt
from PySide6.QtGui import QColor, QImage, QPainter, QPainterPath, QPen
except Exception: # pragma: no cover - headless import failure
return ""
try:
size = 36 # drawn at 2x the indicator so the tick stays crisp when scaled
safe = fill.strip().lstrip("#").replace("/", "")
path = _arrow_icon_cache_dir() / f"check_{safe}.png"
if path.is_file():
return str(path)
image = QImage(size, size, QImage.Format.Format_ARGB32)
image.fill(Qt.GlobalColor.transparent)
painter = QPainter(image)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
pen = QPen(QColor(fill), size * 0.13)
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
painter.setPen(pen)
tick = QPainterPath(QPointF(size * 0.22, size * 0.52))
tick.lineTo(QPointF(size * 0.42, size * 0.72))
tick.lineTo(QPointF(size * 0.78, size * 0.30))
painter.drawPath(tick)
painter.end()
if not image.save(str(path), "PNG"):
return ""
return str(path)
except Exception: # pragma: no cover - never break theming
return ""
def _check_image(fill: str) -> str:
path = _paint_check(fill)
if not path:
return "none"
return 'url("' + path.replace("\\", "/") + '")'
def _arrow_image(kind: str, fill: str, size: int = 14) -> str:
path = _paint_arrow(kind, fill)
if not path:
@@ -86,6 +131,8 @@ def build_qss(theme: str = "dark") -> str:
arrow_up = _arrow_image("up", dim)
arrow_down_disabled = _arrow_image("down", muted)
arrow_up_disabled = _arrow_image("up", muted)
check_mark = _check_image(accent_text)
check_mark_disabled = _check_image(muted)
return f"""
QWidget {{
background-color: {c['window_bg']};
@@ -117,7 +164,7 @@ QMenu {{
padding: 4px;
}}
QMenu::item {{
padding: 6px 18px;
padding: 8px 20px;
border-radius: 4px;
}}
QMenu::item:selected {{
@@ -219,10 +266,13 @@ QPushButton:pressed {{
QPushButton:focus {{
border-color: {c['focus_ring']};
}}
/* Disabled = no fill and a dashed outline. A filled disabled button was the
same colour as an enabled neutral one, so a rail of unavailable actions
read as a rail of available ones. */
QPushButton:disabled {{
color: {c['text_muted']};
background-color: {c['window_bg']};
border: 1px solid {c['separator']};
background-color: transparent;
border: 1px dashed {c['input_border']};
}}
QPushButton#primaryButton {{
background-color: {c['accent']};
@@ -241,9 +291,9 @@ QPushButton#primaryButton:checked {{
border-color: {c['accent']};
}}
QPushButton#primaryButton:disabled {{
background-color: {c['panel_border']};
background-color: transparent;
color: {c['text_muted']};
border-color: {c['panel_border']};
border: 1px dashed {c['input_border']};
}}
QPushButton#dangerButton {{
background-color: {c['danger_fill']};
@@ -262,9 +312,9 @@ QPushButton#dangerButton:checked {{
border-color: {c['danger_fill']};
}}
QPushButton#dangerButton:disabled {{
background-color: {c['panel_border']};
background-color: transparent;
color: {c['text_muted']};
border-color: {c['panel_border']};
border: 1px dashed {c['input_border']};
}}
QPushButton#ghostButton {{
background-color: transparent;
@@ -310,8 +360,8 @@ QPushButton#dangerOutline:checked {{
}}
QPushButton#dangerOutline:disabled {{
color: {c['text_muted']};
background-color: {c['window_bg']};
border: 1px solid {c['separator']};
background-color: transparent;
border: 1px dashed {c['input_border']};
}}
QPushButton[compact="true"] {{
padding: 6px 9px;
@@ -428,9 +478,12 @@ QComboBox QAbstractItemView {{
selection-background-color: {c['menu_hover']};
selection-color: {c['text_primary']};
}}
QComboBox QAbstractItemView::item {{
min-height: 30px;
}}
QAbstractSpinBox::up-button, QAbstractSpinBox::down-button {{
subcontrol-origin: border;
width: 18px;
width: 22px;
background: transparent;
border: none;
}}
@@ -457,15 +510,17 @@ QAbstractSpinBox::down-arrow:disabled {{
image: {arrow_down_disabled};
}}
/* ---------- Check / radio ---------- */
/* 18 px boxes and a taller row: the label is part of the hit area, and the
whole row has to be easy to hit with a finger. */
QCheckBox, QRadioButton {{
background: transparent;
color: {c['text_primary']};
spacing: 7px;
padding: 2px 0;
spacing: 8px;
padding: 4px 0;
}}
QCheckBox::indicator, QRadioButton::indicator {{
width: 15px;
height: 15px;
width: 18px;
height: 18px;
background-color: {c['input_bg']};
border: 1px solid {c['input_border']};
}}
@@ -473,23 +528,34 @@ QCheckBox::indicator {{
border-radius: 4px;
}}
QRadioButton::indicator {{
border-radius: 8px;
border-radius: 10px;
}}
QCheckBox::indicator:hover, QRadioButton::indicator:hover {{
border-color: {c['accent_hover']};
}}
QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
QCheckBox::indicator:checked {{
background-color: {c['accent']};
border: 4px solid {c['accent']};
border: 1px solid {c['accent']};
image: {check_mark};
}}
/* QSS borders add to ``width``: a 5 px ring on an 18 px box grew the checked
radio to 28 px and turned the circle into a rounded square. Shrinking the
content keeps the outer size (20 px) equal to the unchecked one. */
QRadioButton::indicator:checked {{
width: 10px;
height: 10px;
background-color: {c['input_bg']};
border: 4px solid {c['accent']};
border: 5px solid {c['accent']};
}}
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {{
background-color: {c['window_bg']};
border-color: {c['separator']};
}}
QCheckBox::indicator:checked:disabled {{
background-color: {c['separator']};
border-color: {c['separator']};
image: {check_mark_disabled};
}}
/* ---------- Progress ---------- */
QProgressBar {{
background-color: {c['input_bg']};
@@ -549,26 +615,26 @@ QTableCornerButton::section {{
/* ---------- Scrollbars ---------- */
QScrollBar:vertical {{
background: {c['scrollbar_bg']};
width: 10px;
width: 12px;
margin: 0;
border: none;
}}
QScrollBar:horizontal {{
background: {c['scrollbar_bg']};
height: 10px;
height: 12px;
margin: 0;
border: none;
}}
QScrollBar::handle:vertical {{
background: {c['scrollbar_handle']};
min-height: 28px;
border-radius: 5px;
min-height: 32px;
border-radius: 6px;
margin: 2px;
}}
QScrollBar::handle:horizontal {{
background: {c['scrollbar_handle']};
min-width: 28px;
border-radius: 5px;
min-width: 32px;
border-radius: 6px;
margin: 2px;
}}
QScrollBar::handle:vertical:hover, QScrollBar::handle:horizontal:hover {{
@@ -707,3 +773,4 @@ def color(theme: str, key: str, default: str = "#888888") -> str:
def clear_theme_cache() -> None:
load_colors.cache_clear()
_paint_arrow.cache_clear()
_paint_check.cache_clear()