Make the rail reset zero the sensor in live and the display in replay

The replay view disabled a control whose display reset is exactly what an
operator wants there, and the bare "Reset" hid that the live one zeroes the
sensor tracker; the label, tooltip and status line now name the target, and
Ctrl+R follows the view. Verified with the studio suite, ui_check (628
checks) and real-GL rail snapshots of both views.
This commit is contained in:
ykcheungab
2026-10-04 11:09:22 +08:00
parent aa526c62a9
commit 6fb2f72c6e
9 changed files with 525 additions and 50 deletions
+70 -7
View File
@@ -1187,13 +1187,18 @@ class StudioMainWindow(QMainWindow):
or self._recompute_running
or self._session_active
)
# The tracker reset belongs to a device session; this view has none.
self.toolbar.set_reset_enabled(False)
# The rail's reset in *this* view is the display reset: there is no
# device session to zero, and a rail button that is dead in a view the
# operator is working in is what the report called out. The label says
# which target is live (``toolbar.reset_target``), and ``_on_reset``
# routes on the same answer.
self.toolbar.set_reset_enabled(True)
self.toolbar.set_record_enabled(
self._running or self.recorder.is_recording
)
if not self._running:
self.toolbar.refresh_labels()
self._sync_reset_action()
self._sync_shortcut_actions()
return
sensor_ok = self.sensor_picker.sensor_type() in RUNNABLE_TYPES
@@ -1222,6 +1227,7 @@ class StudioMainWindow(QMainWindow):
self.toolbar.set_reset_enabled(self.session.reset_available)
if not self._running:
self.toolbar.refresh_labels()
self._sync_reset_action()
self._sync_shortcut_actions()
def retranslate(self) -> None:
@@ -1249,7 +1255,9 @@ class StudioMainWindow(QMainWindow):
self.act_run.setText(t("menu.run"))
self.act_stop.setText(t("menu.stop"))
self.act_preview.setText(t("menu.preview"))
self.act_reset.setText(t("menu.reset"))
# Ctrl+R is the rail's reset on a second surface, so its wording follows the
# view the same way the button's does (see ``_sync_reset_action``).
self._sync_reset_action()
self.act_reset_view.setText(t("menu.reset_view"))
self.act_reset_view.setToolTip(t("menu.reset_view_tip"))
self.act_pause.setText(t("menu.pause"))
@@ -1823,6 +1831,23 @@ class StudioMainWindow(QMainWindow):
self._persist_prefs()
# ----------------------------------------------------------- shortcuts
def _sync_reset_action(self) -> None:
"""The View menu's Ctrl+R says what the rail's reset does in this view.
It is the *same* control on a second surface (``_on_reset`` is the one handler),
so it is named from the rail's own answer rather than keeping a second copy of
the rule. In the replay view it reads 复位显示 next to 复位视图 (F8), which stays
the camera-and-window reset it always was.
"""
if getattr(self, "act_reset", None) is None:
return
if self.toolbar.reset_target() == "display":
self.act_reset.setText(t("menu.reset_display"))
self.act_reset.setToolTip(t("toolbar.reset_display_tip"))
return
self.act_reset.setText(t("menu.reset"))
self.act_reset.setToolTip(t("toolbar.reset_sensor_tip"))
def _sync_shortcut_actions(self) -> None:
if getattr(self, "act_run", None) is None:
return
@@ -2239,7 +2264,7 @@ class StudioMainWindow(QMainWindow):
self._notify(t("toolbar.nothing_to_stop"))
def _on_reset(self) -> None:
"""Zero the device tracker. Graphics are reset separately (``_on_reset_view``).
"""Zero the *sensor tracker* — or, where there is no device, reset the display.
This used to call ``force_plots.reset()`` as well, so wanting a fresh baseline
also threw away the history, the window and the cursor on screen — two different
@@ -2248,22 +2273,60 @@ class StudioMainWindow(QMainWindow):
a new device zero means the old idle estimate is stale, not that the operator
wants to lose what they were looking at.
It is only ever this: ending a replay or a recompute pass is the rail's Stop
button (the same act as Esc), so this button is disabled wherever there is no
device session to zero.
Which of the two this button means is the *view's* answer, and the label says
it (``toolbar.reset_target``): a device session can be zeroed, a replay cannot
— its display can, and that is what the operator reached for. Neither branch
adds a second meaning: the live one is the tracker reset it always was, and the
replay one is the display reset ``_reset_display`` states in full.
"""
if self.toolbar.reset_target() == "display":
self._reset_display()
return
if self._session_phase != "running" or self._active_worker is None:
return
try:
# A request, not the zeroing itself: the acquisition thread applies it
# between two frames and reports the frame and time it took effect.
self._active_worker.reset()
# The side effect the tooltip states: a new zero makes the old idle
# estimate stale, so the display baseline is re-learned from the samples
# that follow. The curve history itself is kept.
self.force_plots.request_presentation_reestimate()
self._last_plot_packet_key = None
self.statusBar().showMessage(t("status.reset_pending"))
except Exception as exc:
self.statusBar().showMessage(t("status.run_failed", error=str(exc)))
def _reset_display(self) -> None:
"""The rail's reset in the replay view: clear the display and start the replay over.
Clearing the curves alone is not a display reset *here*: a recording is only
drawn when its source is (re)attached — measured, the curves stay empty through
playback — so the operator would be left with a blank chart where the trace used
to be. Re-attaching the recording's own source is the path opening it takes: the
history, curves, selection and window go back to their defaults, the whole
recording is drawn again, and the playhead returns to the start so the chart
cursor and the transport agree.
It touches nothing the device owns (there is no device session in this view) and
no preference: the unit the charts are in is kept, unlike ``force_plots.reset``,
which drops the display back to raw.
"""
reader = self.replay.reader()
if reader is not None:
self.force_plots.set_recording_source(reader)
self.force_plots.show_all()
# The chart cursor is this view's playhead: both go back to the start. The
# cursor is set even without a reader, because a data-only recording has no
# video for the engine to move to.
self.force_plots.set_cursor(0.0)
self.replay_ctl.set_position(0.0)
if self.replay_ctl.is_playing():
# Playing while the display starts over: restart the play clock, or the
# next tick would jump the playhead back to where it already was.
self.replay_ctl.start_playback()
self.statusBar().showMessage(t("status.reset_display"))
def _on_pause_toggled(self, paused: bool) -> None:
"""Pause the pipeline and the display; the device stays open.
+52 -6
View File
@@ -15,6 +15,11 @@ Everything that is *configuration* rather than state — source, camera number,
video file, units and the sensor/config pickers — lives in the settings area,
collapsed by default. Both live inside one scroll area so a narrow window
scrolls sideways instead of squeezing the controls into an unreadable row.
The reset is the one rail action whose meaning follows the view
(``reset_target``): the live view zeroes the sensor tracker, the replay view —
which owns no device — puts the display back. Its text and tooltip always name
the target that is live *now*, so one control never means two things at once.
"""
from __future__ import annotations
@@ -118,6 +123,9 @@ class ToolbarWidget(QWidget):
self._video_path = ""
#: ``acquire`` while measuring, ``playback`` while replaying a recording.
self._run_role = "acquire"
#: Whether the control's target can be acted on *now* (see
#: ``set_reset_enabled``): it decides which of the two live tooltips is shown.
self._reset_available = False
outer = QVBoxLayout(self)
outer.setContentsMargins(2, 2, 2, 2)
@@ -421,6 +429,9 @@ class ToolbarWidget(QWidget):
self.btn_run.blockSignals(False)
self._refresh_run_label()
self._refresh_stop_label()
# 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()
def run_role(self) -> str:
return self._run_role
@@ -490,7 +501,28 @@ class ToolbarWidget(QWidget):
self.btn_stop.setEnabled(bool(enabled))
def set_reset_enabled(self, enabled: bool) -> None:
self.btn_reset.setEnabled(bool(enabled))
"""Offer the reset, and say in its tooltip what it can act on right now.
The button's *target* is the view's (``reset_target``); this is only whether
that target is actionable — a live session for the tracker, always for the
display — so the tip explains a disabled live button rather than leaving the
operator to guess what is missing.
"""
self._reset_available = bool(enabled)
self.btn_reset.setEnabled(self._reset_available)
self._refresh_reset_label()
def reset_target(self) -> str:
"""What the rail's reset control acts on in this view: ``sensor`` or ``display``.
One control, one meaning *per view*, and this is the single place that decides
it: the live view has a device session, so its reset zeroes the tracker; the
replay view has none, so the same button puts the display back instead of being
a dead control ("the reset button cannot be used in replay mode"). The window
routes its click on this answer and ``_refresh_reset_label`` names it, so the
behaviour and the label cannot drift apart.
"""
return "display" if self._run_role == "playback" else "sensor"
def set_camera_controls_enabled(self, enabled: bool) -> None:
live = self._source_view == "live"
@@ -553,6 +585,7 @@ class ToolbarWidget(QWidget):
self._refresh_preview_label()
self._refresh_run_label()
self._refresh_stop_label()
self._refresh_reset_label()
# ------------------------------------------------------------- internals
def _populate_units_combo(self) -> None:
@@ -797,11 +830,24 @@ class ToolbarWidget(QWidget):
self.btn_run.setToolTip(tip)
def _refresh_reset_label(self) -> None:
self.btn_reset.setText(t("toolbar.reset"))
# The button zeroes the *device* tracker; it no longer clears the curves.
# That is now 视图 → 复位视图, because one button doing both meant an
# operator who wanted a fresh baseline also lost the history on screen.
self.btn_reset.setToolTip(t("toolbar.reset_tip"))
"""Say which of the two things this button will do, where it is on screen.
The operator's complaint was that 重置 read as "reset the charts" when it zeroes
the *sensor tracker* — and that in the replay view, where it is the display that
can be reset, it was unavailable. The label therefore names the target and the
tip states the side effect that is real but invisible: zeroing the tracker also
restarts the display baseline (the curve history is kept).
"""
if self.reset_target() == "display":
self.btn_reset.setText(t("toolbar.reset_display"))
self.btn_reset.setToolTip(t("toolbar.reset_display_tip"))
return
self.btn_reset.setText(t("toolbar.reset_sensor"))
self.btn_reset.setToolTip(
t("toolbar.reset_sensor_tip")
if self._reset_available
else t("toolbar.reset_sensor_unavailable")
)
def retranslate(self) -> None:
self.cam_id_label.setText(t("toolbar.cam_id"))