修正 Studio 力曲线原点、运行不可用提示与禁用按钮样式
FN 轴视图留 8% 余量使 0 刻度与静止曲线离开边框;实时视图在轨道上直接写出运行不可用的原因;禁用按钮改为虚线无填充,复选框加勾,单选框选中不再变方,并放大勾选框、微调框箭头、滚动条与菜单行以便触屏操作。另修 ui_check 桩 worker 签名与英文回放提示。在 Linux 离屏环境对比未改动的 HEAD 验证:studio 失败集合为 HEAD 的子集,ui_check 结果一致。
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user