修正 Studio 力曲线原点、运行不可用提示与禁用按钮样式
FN 轴视图留 8% 余量使 0 刻度与静止曲线离开边框;实时视图在轨道上直接写出运行不可用的原因;禁用按钮改为虚线无填充,复选框加勾,单选框选中不再变方,并放大勾选框、微调框箭头、滚动条与菜单行以便触屏操作。另修 ui_check 桩 worker 签名与英文回放提示。在 Linux 离屏环境对比未改动的 HEAD 验证:studio 失败集合为 HEAD 的子集,ui_check 结果一致。
This commit is contained in:
@@ -128,8 +128,7 @@
|
||||
"cancel_label": "■ Cancel",
|
||||
"cancel_tip": "Abort the connection (always available while connecting, shortcut Esc)",
|
||||
"run_unavailable": "Not available yet",
|
||||
"run_disabled_video": "Select a video file first",
|
||||
"run_video_in_replay": "Play a video from the History replay view (Play and the chart cursor)",
|
||||
"run_video_in_replay": "Live uses a camera: press Use camera",
|
||||
"run_disabled_sensor": "Run is not available for this sensor type",
|
||||
"playback": "▶ Play",
|
||||
"playback_pause": "⏸ Pause",
|
||||
@@ -227,8 +226,8 @@
|
||||
"field_depth": "depth",
|
||||
"recompute_pipeline": "Recomputed from the recording (not the recorded value)",
|
||||
"recompute_pipeline_tip": "This frame is what the current algorithm version computes for that footage — not the measurement that was recorded at the time.",
|
||||
"replay_empty": "No video open\nChoose “Recorded video…” above",
|
||||
"arrow_empty": "No image yet\nPick a video or camera, then press Run",
|
||||
"replay_empty": "No video open\nPress “Choose a recorded video…” above",
|
||||
"arrow_empty": "No image yet\nConnect a camera, then press Run above\nOpen recorded videos in Replay",
|
||||
"arrow_empty_tip": "The deformation arrow image appears here while running or previewing",
|
||||
"arrow_render_failed": "Flow arrows could not be drawn for this frame\nTry another view, or reload the video",
|
||||
"arrow_capture_unavailable": "The captured frame is not available for this overlay\nThe raw camera view is still one selection away",
|
||||
|
||||
@@ -128,8 +128,7 @@
|
||||
"cancel_label": "■ 断开",
|
||||
"cancel_tip": "中断连接(连接卡住时随时可用,快捷键 Esc)",
|
||||
"run_unavailable": "当前不可运行",
|
||||
"run_disabled_video": "请先选择一个视频文件",
|
||||
"run_video_in_replay": "请在「历史回放」视图里播放视频(用播放与曲线游标)",
|
||||
"run_video_in_replay": "实时视图只用摄像机:请点「使用摄像机」",
|
||||
"run_disabled_sensor": "此传感器类型暂不支持运行",
|
||||
"playback": "▶ 播放",
|
||||
"playback_pause": "⏸ 暂停",
|
||||
@@ -228,7 +227,7 @@
|
||||
"recompute_pipeline": "由录像重算(非记录值)",
|
||||
"recompute_pipeline_tip": "这一帧是当前算法版本对录像重新计算得到的,不是当初记录下来的测量值。",
|
||||
"replay_empty": "尚未打开录像\n点击顶部「选择录制视频…」",
|
||||
"arrow_empty": "尚未采集图像\n请选择视频或摄像机后点击「运行」",
|
||||
"arrow_empty": "尚未采集图像\n连接摄像机后点击上方「运行」\n录好的视频请到「回放」中打开",
|
||||
"arrow_empty_tip": "运行或预览后在此显示形变箭头图",
|
||||
"arrow_render_failed": "本帧无法绘制形变箭头\n请换一种视图,或重新加载视频",
|
||||
"arrow_capture_unavailable": "当前无法取得采集图像用于叠加\n原始相机画面仍可在同一选择器中查看",
|
||||
|
||||
@@ -1344,12 +1344,9 @@ class StudioMainWindow(QMainWindow):
|
||||
elif self.toolbar.source_mode() == "video":
|
||||
# Live acquires from a camera. A video file is played from the History
|
||||
# replay view (Play and the chart cursor), not by starting a chain here.
|
||||
# Choosing a file would not help here, so the reason never asks for one.
|
||||
source_ok = False
|
||||
reason = (
|
||||
t("toolbar.run_video_in_replay")
|
||||
if self.toolbar.video_path()
|
||||
else t("toolbar.run_disabled_video")
|
||||
)
|
||||
reason = t("toolbar.run_video_in_replay")
|
||||
self.toolbar.set_run_tooltip_reason(reason)
|
||||
# While a session exists the transport stays enabled — pausing and resuming
|
||||
# are the whole point of it being checked, not an exception to an idle rule.
|
||||
|
||||
@@ -60,6 +60,28 @@ CURSOR_READOUT_S = 0.5
|
||||
#: counts as a *selection* rather than the default "whole session" fit. 1% of the
|
||||
#: session absorbs the float noise of a fit without hiding a real zoom.
|
||||
SELECTION_EPSILON = 0.01
|
||||
#: Share of a fixed axis span left free beyond each configured bound. Without it
|
||||
#: the bound labels (FN's "0") sit on the plot frame and are not drawn, and a
|
||||
#: resting FN curve is painted on the border it shares with the FX subplot. 8 %
|
||||
#: is about half a label's height on the smallest subplot ui_check measures.
|
||||
AXIS_MARGIN_FRACTION = 0.08
|
||||
|
||||
|
||||
def _display_range(lower: float, upper: float) -> tuple[float, float]:
|
||||
"""The view range that shows ``lower…upper`` with a margin on both sides.
|
||||
|
||||
The configured range — and the ticks — stay exactly what the operator set;
|
||||
only the drawn view is wider. A margin never brings zero into view when the
|
||||
configured range excludes it, so the zero line keeps meaning "zero is here".
|
||||
"""
|
||||
pad = (float(upper) - float(lower)) * AXIS_MARGIN_FRACTION
|
||||
view_lower = lower - pad
|
||||
if lower > 0.0 >= view_lower:
|
||||
view_lower = lower
|
||||
view_upper = upper + pad
|
||||
if upper < 0.0 <= view_upper:
|
||||
view_upper = upper
|
||||
return view_lower, view_upper
|
||||
|
||||
|
||||
def _window_brush(theme: str):
|
||||
@@ -316,6 +338,9 @@ class ForcePlotsPanel(QFrame):
|
||||
for i, _name in enumerate(CHANNEL_LABELS):
|
||||
plot = self.plot_widget.addPlot(row=i, col=0)
|
||||
plot.showGrid(x=True, y=True, alpha=GRID_ALPHA)
|
||||
# A bound label (FN's 0) may reach past the axis end by a few pixels on
|
||||
# a short subplot; drawing it is better than losing the origin.
|
||||
plot.getAxis("left").setStyle(hideOverlappingLabels=False)
|
||||
# Panning and zooming are always available: a chart that ignores the
|
||||
# mouse until it is already browsing could never *enter* browsing by
|
||||
# being dragged. Y stays locked to the configured limits.
|
||||
@@ -719,7 +744,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self.plots[i].setXRange(*state.x_range, padding=0.0)
|
||||
if use_fixed:
|
||||
ymin, ymax = self._fixed_y_range(i)
|
||||
self.plots[i].setYRange(ymin, ymax, padding=0.0)
|
||||
self.plots[i].setYRange(*_display_range(ymin, ymax), padding=0.0)
|
||||
self.plots[i].getAxis("left").setTicks(
|
||||
[_nice_ticks(ymin, ymax, count=5)]
|
||||
)
|
||||
@@ -1363,7 +1388,7 @@ class ForcePlotsPanel(QFrame):
|
||||
for i, plot in enumerate(self.plots):
|
||||
if use_fixed:
|
||||
ymin, ymax = self._fixed_y_range(i)
|
||||
plot.setYRange(ymin, ymax, padding=0.0)
|
||||
plot.setYRange(*_display_range(ymin, ymax), padding=0.0)
|
||||
plot.getAxis("left").setTicks([_nice_ticks(ymin, ymax, count=5)])
|
||||
else:
|
||||
plot.getAxis("left").setTicks(None)
|
||||
|
||||
@@ -181,6 +181,16 @@ class ToolbarWidget(QWidget):
|
||||
self.data_state = ShrinkableLabel()
|
||||
self.data_state.setObjectName("panelMeta")
|
||||
self.data_state.setVisible(False)
|
||||
# Why the transport cannot start, as text on the rail. A tooltip alone was
|
||||
# the only place this was said, and a touch screen has no hover to show it.
|
||||
# Ignored horizontally, so it only ever gets the rail's spare width and
|
||||
# never squeezes a button; hidden when empty, so it costs no spacing.
|
||||
self.run_hint = ShrinkableLabel()
|
||||
self.run_hint.setObjectName("panelMeta")
|
||||
self.run_hint.setSizePolicy(
|
||||
QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred
|
||||
)
|
||||
self.run_hint.setVisible(False)
|
||||
self.record_chip = RecordStatusChip(theme=self._theme)
|
||||
|
||||
# --- capture source (one of the two groups is visible) -------------
|
||||
@@ -288,7 +298,9 @@ class ToolbarWidget(QWidget):
|
||||
rail.addWidget(self.source_summary)
|
||||
rail.addWidget(self.calibration_label)
|
||||
rail.addWidget(self.data_state)
|
||||
rail.addStretch(1)
|
||||
# Shown, the hint takes the spare width (stretch 1); hidden, the spacer does.
|
||||
rail.addWidget(self.run_hint, 1)
|
||||
rail.addStretch(0)
|
||||
rail.addWidget(self.record_chip)
|
||||
rail.addWidget(_rail(self.btn_record))
|
||||
rail.addWidget(_rail(self.btn_run))
|
||||
@@ -358,6 +370,7 @@ class ToolbarWidget(QWidget):
|
||||
def set_theme(self, theme: str) -> None:
|
||||
self._theme = str(theme)
|
||||
self.record_chip.set_theme(self._theme)
|
||||
self._apply_run_hint()
|
||||
|
||||
def set_sensor_summary(self, text: str) -> None:
|
||||
"""The rail's sensor button text (``BOX · box/sensor.json``)."""
|
||||
@@ -432,6 +445,7 @@ class ToolbarWidget(QWidget):
|
||||
# The reset is the one rail control whose *target* moves with the view (see
|
||||
# ``reset_target``), so its label is refreshed with the other two.
|
||||
self._refresh_reset_label()
|
||||
self._apply_run_hint()
|
||||
|
||||
def run_role(self) -> str:
|
||||
return self._run_role
|
||||
@@ -557,11 +571,33 @@ class ToolbarWidget(QWidget):
|
||||
self._run_available = bool(enabled)
|
||||
self.btn_run.setEnabled(bool(enabled))
|
||||
self._apply_run_tooltip()
|
||||
self._apply_run_hint()
|
||||
|
||||
def set_run_tooltip_reason(self, reason: str) -> None:
|
||||
"""Explain *why* Run is unavailable (shown in the button tooltip)."""
|
||||
self._run_tooltip_reason = str(reason or "")
|
||||
self._apply_run_tooltip()
|
||||
self._apply_run_hint()
|
||||
|
||||
def _apply_run_hint(self) -> None:
|
||||
"""Show the reason on the rail while acquisition cannot start.
|
||||
|
||||
Only the *blocking* reason is shown: an advisory one (a bare video plays
|
||||
with empty charts) stays in the tooltip, so the rail is not a second
|
||||
status bar. The replay view is left out on purpose: its empty 2D pane
|
||||
already says which button opens a recording, and the extra rail item
|
||||
would cost the width the replay rail does not have at 1280 px.
|
||||
"""
|
||||
blocked = not self._run_available and self._run_role != "playback"
|
||||
text = self._run_tooltip_reason if blocked else ""
|
||||
self.run_hint.setText(text)
|
||||
self.run_hint.setToolTip(text)
|
||||
self.run_hint.setVisible(bool(text))
|
||||
if text:
|
||||
self.run_hint.setStyleSheet(
|
||||
f"QLabel#panelMeta {{ color: {color(self._theme, 'warning')};"
|
||||
" background: transparent; }"
|
||||
)
|
||||
|
||||
def set_run_phase(self, phase: str) -> None:
|
||||
"""Track the session phase.
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
from orisys_app_shared.i18n import t
|
||||
from orisys_studio.ui.force_plots import ForcePlotsPanel, _nice_ticks
|
||||
from orisys_studio.ui.force_plots import ForcePlotsPanel, _display_range, _nice_ticks
|
||||
|
||||
|
||||
def _panel(unit: str = "raw") -> ForcePlotsPanel:
|
||||
@@ -23,6 +23,12 @@ def _ticks(panel: ForcePlotsPanel, channel: int) -> list[float]:
|
||||
return [float(value) for value, _text in levels[0]]
|
||||
|
||||
|
||||
def _view(panel: ForcePlotsPanel, channel: int) -> tuple[float, float]:
|
||||
"""The Y range the subplot actually draws."""
|
||||
low, high = panel.plots[channel].getViewBox().viewRange()[1]
|
||||
return float(low), float(high)
|
||||
|
||||
|
||||
def _curve(panel: ForcePlotsPanel, channel: int) -> np.ndarray:
|
||||
_times, values = panel.curves[channel].getData()
|
||||
return np.asarray(values, dtype=float)
|
||||
@@ -82,14 +88,15 @@ def test_the_configured_ranges_reach_every_axis(qt_app):
|
||||
)
|
||||
panel.set_fixed_newton(True)
|
||||
|
||||
# The panel starts in raw, so the raw limits are the ones on the axes.
|
||||
assert panel.plots[0].getViewBox().viewRange()[1] == [-100000.0, 1000000.0]
|
||||
assert panel.plots[1].getViewBox().viewRange()[1] == [-500000.0, 500000.0]
|
||||
# The panel starts in raw, so the raw limits are the ones on the axes (drawn
|
||||
# with a small margin beyond each bound; see ``_display_range``).
|
||||
assert _view(panel, 0) == _display_range(-100000.0, 1000000.0)
|
||||
assert _view(panel, 1) == _display_range(-500000.0, 500000.0)
|
||||
|
||||
panel.set_units("N")
|
||||
assert panel.plots[0].getViewBox().viewRange()[1] == [-5.0, 15.0]
|
||||
assert panel.plots[1].getViewBox().viewRange()[1] == [-3.0, 3.0]
|
||||
assert panel.plots[2].getViewBox().viewRange()[1] == [-3.0, 3.0]
|
||||
assert _view(panel, 0) == _display_range(-5.0, 15.0)
|
||||
assert _view(panel, 1) == _display_range(-3.0, 3.0)
|
||||
assert _view(panel, 2) == _display_range(-3.0, 3.0)
|
||||
assert panel._fixed_y_range(0) == (-5.0, 15.0)
|
||||
assert panel._fixed_y_range(1) == (-3.0, 3.0)
|
||||
# A range that starts below zero carries an explicit 0 tick: that is the
|
||||
@@ -164,3 +171,32 @@ def test_the_default_ranges_are_the_ones_they_always_were(qt_app):
|
||||
(15.0, "15"),
|
||||
(20.0, "20"),
|
||||
]
|
||||
|
||||
|
||||
def test_the_default_fn_origin_is_drawn_off_the_frame(qt_app):
|
||||
"""FN starts at 0, yet its 0 label and resting curve stay off the plot border.
|
||||
|
||||
With the view ending exactly at 0 the label was not drawn and an idle FN
|
||||
curve lay on the frame shared with FX — the customer's "缺少原点" report.
|
||||
"""
|
||||
for unit in ("raw", "N"):
|
||||
panel = ForcePlotsPanel(theme="dark")
|
||||
panel.set_units(unit)
|
||||
panel.set_fixed_newton(True)
|
||||
ymin, ymax = panel._fixed_y_range(0)
|
||||
assert ymin == 0.0, "the configured FN floor stays 0"
|
||||
view_min, view_max = _view(panel, 0)
|
||||
assert view_min < 0.0 < view_max, unit
|
||||
assert view_max > ymax, unit
|
||||
assert 0.0 in _ticks(panel, 0), unit
|
||||
assert panel.zero_lines[0].isVisibleTo(panel.plots[0].getViewBox()), unit
|
||||
|
||||
|
||||
def test_a_margin_never_brings_an_excluded_zero_into_view():
|
||||
# A margin that stays on its own side of zero is kept...
|
||||
assert _display_range(100.0, 1100.0) == (20.0, 1180.0)
|
||||
# ...one that would cross zero stops at the configured bound.
|
||||
assert _display_range(10.0, 1010.0) == (10.0, 1090.0)
|
||||
assert _display_range(-1010.0, -10.0) == (-1090.0, -10.0)
|
||||
# A range that already holds zero gets the full margin on both sides.
|
||||
assert _display_range(0.0, 20.0) == (-1.6, 21.6)
|
||||
|
||||
@@ -1576,6 +1576,8 @@ def test_live_run_refuses_a_video_source(make_window, monkeypatch):
|
||||
window._set_source_mode("video")
|
||||
QApplication.processEvents()
|
||||
assert not window.toolbar.btn_run.isEnabled(), "Run is offered for a video file in live"
|
||||
# The reason is on the rail as text, not only in a tooltip (no hover on touch).
|
||||
assert window.toolbar.run_hint.text() == t("toolbar.run_video_in_replay")
|
||||
called: list[str] = []
|
||||
monkeypatch.setattr(
|
||||
"orisys_app_shared.acquisition.sdk_worker.orisys_importable",
|
||||
@@ -3138,6 +3140,19 @@ def test_force_threshold_is_on_for_every_sensor_by_default(make_window):
|
||||
_finish(window, stuck)
|
||||
|
||||
|
||||
def test_the_run_hint_clears_once_run_is_available(make_window):
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
|
||||
|
||||
window = make_window(sensor_type="finger", source_mode="camera")
|
||||
try:
|
||||
window.brand_bar.btn_live.click()
|
||||
QApplication.processEvents()
|
||||
window._set_source_mode("video")
|
||||
QApplication.processEvents()
|
||||
assert window.toolbar.run_hint.text()
|
||||
window._set_source_mode("camera")
|
||||
QApplication.processEvents()
|
||||
assert window.toolbar.btn_run.isEnabled()
|
||||
assert window.toolbar.run_hint.text() == ""
|
||||
finally:
|
||||
window.close()
|
||||
|
||||
@@ -298,7 +298,10 @@ class _StuckWorker(RecordSinkMixin):
|
||||
mono_t = float(getattr(packet, "mono_t", 0.0) or 0.0)
|
||||
self.on_reset_applied(frame_id, mono_t, 1)
|
||||
|
||||
def start(self, _source, *, config_ref=None) -> None:
|
||||
def start(
|
||||
self, _source, *, config_ref=None, cal_path=None, device_id=None
|
||||
) -> None:
|
||||
# Same keywords as the real workers' ``start``; the stub ignores them.
|
||||
self._release.clear()
|
||||
self._thread = threading.Thread(target=self._hang, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user