修正 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
+2 -5
View File
@@ -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)
+37 -1
View File
@@ -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.