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:
@@ -45,7 +45,8 @@
|
|||||||
"run": "Run / Stop",
|
"run": "Run / Stop",
|
||||||
"stop": "Cancel acquisition",
|
"stop": "Cancel acquisition",
|
||||||
"preview": "Preview / Stop preview",
|
"preview": "Preview / Stop preview",
|
||||||
"reset": "Reset tracker",
|
"reset": "Reset sensor tracker",
|
||||||
|
"reset_display": "Reset display",
|
||||||
"reset_view": "Reset views",
|
"reset_view": "Reset views",
|
||||||
"reset_view_tip": "Put the chart window, cursor and 3D camera back to their defaults (F8). Touches neither the device nor the zero estimate.",
|
"reset_view_tip": "Put the chart window, cursor and 3D camera back to their defaults (F8). Touches neither the device nor the zero estimate.",
|
||||||
"pause": "Pause / resume (F7)",
|
"pause": "Pause / resume (F7)",
|
||||||
@@ -133,8 +134,11 @@
|
|||||||
"playback_disabled": "Open a recorded video first (“Choose a recorded video…” at the top left)",
|
"playback_disabled": "Open a recorded video first (“Choose a recorded video…” at the top left)",
|
||||||
"playback_needs_recording": "This video has no recording beside it (no forces.csv): replay needs a recording; to just watch the video use View ▸ Recompute forces from the video",
|
"playback_needs_recording": "This video has no recording beside it (no forces.csv): replay needs a recording; to just watch the video use View ▸ Recompute forces from the video",
|
||||||
"playback_video_only": "Play this video (no recording beside it: the picture plays, the force charts are empty)",
|
"playback_video_only": "Play this video (no recording beside it: the picture plays, the force charts are empty)",
|
||||||
"reset": "Reset",
|
"reset_sensor": "Reset sensor",
|
||||||
"reset_tip": "Zero the device tracker. This affects the zero point only, never the view — to put the views back use View → Reset views.",
|
"reset_sensor_tip": "Zero the sensor tracker (the device's own zero). Zeroing it also restarts the display baseline, so the curves and the value cards re-learn their zero from the next quiet samples; the curve history is kept — View ▸ Reset views (F8) clears the views.",
|
||||||
|
"reset_sensor_unavailable": "Zero the sensor tracker — available while a session is running (start a run first).",
|
||||||
|
"reset_display": "Reset display",
|
||||||
|
"reset_display_tip": "Clear the accumulated display: the curves, cursor and value cards go back to their defaults and the replay returns to the start of the recording. There is no device session in this view, so nothing of the device is touched; the 3D camera is View ▸ Reset views (F8).",
|
||||||
"pause": "⏸ Pause",
|
"pause": "⏸ Pause",
|
||||||
"pause_resume": "▶",
|
"pause_resume": "▶",
|
||||||
"pause_tip": "Pause the computation and the display (F7): no flow, depth or contact is computed, the picture holds its last frame, and the device stays connected for an immediate resume. A recording that spans a pause has a gap.",
|
"pause_tip": "Pause the computation and the display (F7): no flow, depth or contact is computed, the picture holds its last frame, and the device stays connected for an immediate resume. A recording that spans a pause has a gap.",
|
||||||
@@ -424,8 +428,9 @@
|
|||||||
"capture_slow": "The camera delivers less than the configured frame rate: {detail}. The recording keeps the real rate. This is a capture-side (camera/driver) matter that software cannot change: use a camera that really does deliver 30 fps — after checking that the cable or port is not slowing this one down — and measure again.",
|
"capture_slow": "The camera delivers less than the configured frame rate: {detail}. The recording keeps the real rate. This is a capture-side (camera/driver) matter that software cannot change: use a camera that really does deliver 30 fps — after checking that the cable or port is not slowing this one down — and measure again.",
|
||||||
"video_ended": "Video ended",
|
"video_ended": "Video ended",
|
||||||
"camera_disconnected": "Camera disconnected",
|
"camera_disconnected": "Camera disconnected",
|
||||||
"reset_ok": "Tracker reset",
|
"reset_ok": "Sensor tracker reset (the display baseline restarts)",
|
||||||
"reset_pending": "Zeroing requested; waiting for the acquisition thread to confirm…",
|
"reset_pending": "Sensor zero requested; the display baseline restarts once the acquisition thread confirms…",
|
||||||
|
"reset_display": "Display reset: curves back to their defaults, replay at the start",
|
||||||
"view_reset": "Views reset",
|
"view_reset": "Views reset",
|
||||||
"paused": "Paused: nothing is computed, the device stays connected",
|
"paused": "Paused: nothing is computed, the device stays connected",
|
||||||
"resumed": "Resumed",
|
"resumed": "Resumed",
|
||||||
|
|||||||
@@ -45,7 +45,8 @@
|
|||||||
"run": "运行 / 停止",
|
"run": "运行 / 停止",
|
||||||
"stop": "取消采集",
|
"stop": "取消采集",
|
||||||
"preview": "预览 / 停止预览",
|
"preview": "预览 / 停止预览",
|
||||||
"reset": "重置追踪器",
|
"reset": "重置传感器追踪器",
|
||||||
|
"reset_display": "复位显示",
|
||||||
"reset_view": "复位视图",
|
"reset_view": "复位视图",
|
||||||
"reset_view_tip": "把曲线窗口、游标与三维视角恢复为默认(F8)。不改动设备,也不清除零点估计。",
|
"reset_view_tip": "把曲线窗口、游标与三维视角恢复为默认(F8)。不改动设备,也不清除零点估计。",
|
||||||
"pause": "暂停 / 继续(F7)",
|
"pause": "暂停 / 继续(F7)",
|
||||||
@@ -133,8 +134,11 @@
|
|||||||
"playback_disabled": "先打开一段录制视频(左上「选择录制视频…」)",
|
"playback_disabled": "先打开一段录制视频(左上「选择录制视频…」)",
|
||||||
"playback_needs_recording": "这段视频旁边没有记录(缺 forces.csv):回放需要一条记录;只想看视频请用 视图 ▸ 从视频重算力值",
|
"playback_needs_recording": "这段视频旁边没有记录(缺 forces.csv):回放需要一条记录;只想看视频请用 视图 ▸ 从视频重算力值",
|
||||||
"playback_video_only": "播放这段视频(没有配套记录:可以看画面,力曲线为空)",
|
"playback_video_only": "播放这段视频(没有配套记录:可以看画面,力曲线为空)",
|
||||||
"reset": "重置",
|
"reset_sensor": "重置传感器",
|
||||||
"reset_tip": "把设备追踪器归零(清零)。只影响零点,不动界面;想恢复视图请用「视图 → 复位视图」。",
|
"reset_sensor_tip": "把传感器追踪器归零(清零),这是设备自己的零点。归零同时会重新估计显示基线:曲线与数字卡按之后的静默样本重新学习零点;曲线历史保留 —— 要清空视图请用「视图 → 复位视图」(F8)。",
|
||||||
|
"reset_sensor_unavailable": "把传感器追踪器归零 —— 仅在采集会话运行时可用(先点「运行」)。",
|
||||||
|
"reset_display": "复位显示",
|
||||||
|
"reset_display_tip": "清空累积的显示:曲线、游标与数字卡恢复默认,回放回到录像开头。本视图没有设备会话,不会改动设备;三维视角请用「视图 → 复位视图」(F8)。",
|
||||||
"pause": "⏸ 暂停",
|
"pause": "⏸ 暂停",
|
||||||
"pause_resume": "▶",
|
"pause_resume": "▶",
|
||||||
"pause_tip": "暂停计算与显示(F7):不再做光流/深度/接触计算,画面停在最后一帧,设备保持连接,随时可继续。跨过暂停的记录会有一段空档。",
|
"pause_tip": "暂停计算与显示(F7):不再做光流/深度/接触计算,画面停在最后一帧,设备保持连接,随时可继续。跨过暂停的记录会有一段空档。",
|
||||||
@@ -424,8 +428,9 @@
|
|||||||
"capture_slow": "摄像头交付的帧率低于配置值:{detail}。记录会按实际帧率保存。这是采集端(摄像头/驱动)的问题,软件改不了:换一颗真能交付 30 fps 的摄像头,或先确认线材/端口没有把它降速,然后再测。",
|
"capture_slow": "摄像头交付的帧率低于配置值:{detail}。记录会按实际帧率保存。这是采集端(摄像头/驱动)的问题,软件改不了:换一颗真能交付 30 fps 的摄像头,或先确认线材/端口没有把它降速,然后再测。",
|
||||||
"video_ended": "视频已结束",
|
"video_ended": "视频已结束",
|
||||||
"camera_disconnected": "摄像机已断开",
|
"camera_disconnected": "摄像机已断开",
|
||||||
"reset_ok": "追踪器已重置",
|
"reset_ok": "传感器追踪器已重置(显示基线重新估计)",
|
||||||
"reset_pending": "已请求归零,等待采集线程确认…",
|
"reset_pending": "已请求传感器归零,采集线程确认后显示基线重新估计…",
|
||||||
|
"reset_display": "显示已复位:曲线恢复默认,回放回到开头",
|
||||||
"view_reset": "视图已复位",
|
"view_reset": "视图已复位",
|
||||||
"paused": "已暂停:不再计算,设备保持连接",
|
"paused": "已暂停:不再计算,设备保持连接",
|
||||||
"resumed": "已继续",
|
"resumed": "已继续",
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
{ "id": "stop", "path": "toolbar.btn_stop", "objectName": null, "style": "dangerOutline / dangerButton", "class": "QPushButton",
|
{ "id": "stop", "path": "toolbar.btn_stop", "objectName": null, "style": "dangerOutline / dangerButton", "class": "QPushButton",
|
||||||
"purpose": "end the live session, leave the replay, or cancel a connect that has not returned" },
|
"purpose": "end the live session, leave the replay, or cancel a connect that has not returned" },
|
||||||
{ "id": "reset", "path": "toolbar.btn_reset", "objectName": null, "class": "QPushButton",
|
{ "id": "reset", "path": "toolbar.btn_reset", "objectName": null, "class": "QPushButton",
|
||||||
"purpose": "zero the device tracker; enabled by the session state, never by which panel is on screen" },
|
"purpose": "one control, one meaning per view: in the live view it zeroes the sensor tracker (enabled by the session state, never by which panel is on screen, and the display baseline restarts with it); in the replay view, which owns no device, it resets the display" },
|
||||||
{ "id": "settings_toggle", "path": "toolbar.btn_settings", "objectName": null, "style": "ghostButton", "class": "QPushButton",
|
{ "id": "settings_toggle", "path": "toolbar.btn_settings", "objectName": null, "style": "ghostButton", "class": "QPushButton",
|
||||||
"purpose": "expand or collapse the settings area" }
|
"purpose": "expand or collapse the settings area" }
|
||||||
]
|
]
|
||||||
@@ -201,7 +201,7 @@
|
|||||||
{ "id": "act_preview", "path": "act_preview", "objectName": null, "class": "QAction",
|
{ "id": "act_preview", "path": "act_preview", "objectName": null, "class": "QAction",
|
||||||
"purpose": "F6: camera preview on/off" },
|
"purpose": "F6: camera preview on/off" },
|
||||||
{ "id": "act_reset", "path": "act_reset", "objectName": null, "class": "QAction",
|
{ "id": "act_reset", "path": "act_reset", "objectName": null, "class": "QAction",
|
||||||
"purpose": "Ctrl+R: zero the device tracker, the same action as the rail's reset" },
|
"purpose": "Ctrl+R: the rail's reset on a second surface, named from the view it is in (reset sensor tracker while measuring, reset display while replaying)" },
|
||||||
{ "id": "act_reset_view", "path": "act_reset_view", "objectName": null, "class": "QAction",
|
{ "id": "act_reset_view", "path": "act_reset_view", "objectName": null, "class": "QAction",
|
||||||
"purpose": "F8: reset the views (curves, window, cursor, 3D camera) without touching the device" },
|
"purpose": "F8: reset the views (curves, window, cursor, 3D camera) without touching the device" },
|
||||||
{ "id": "act_pause", "path": "act_pause", "objectName": null, "class": "QAction",
|
{ "id": "act_pause", "path": "act_pause", "objectName": null, "class": "QAction",
|
||||||
|
|||||||
@@ -1187,13 +1187,18 @@ class StudioMainWindow(QMainWindow):
|
|||||||
or self._recompute_running
|
or self._recompute_running
|
||||||
or self._session_active
|
or self._session_active
|
||||||
)
|
)
|
||||||
# The tracker reset belongs to a device session; this view has none.
|
# The rail's reset in *this* view is the display reset: there is no
|
||||||
self.toolbar.set_reset_enabled(False)
|
# 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.toolbar.set_record_enabled(
|
||||||
self._running or self.recorder.is_recording
|
self._running or self.recorder.is_recording
|
||||||
)
|
)
|
||||||
if not self._running:
|
if not self._running:
|
||||||
self.toolbar.refresh_labels()
|
self.toolbar.refresh_labels()
|
||||||
|
self._sync_reset_action()
|
||||||
self._sync_shortcut_actions()
|
self._sync_shortcut_actions()
|
||||||
return
|
return
|
||||||
sensor_ok = self.sensor_picker.sensor_type() in RUNNABLE_TYPES
|
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)
|
self.toolbar.set_reset_enabled(self.session.reset_available)
|
||||||
if not self._running:
|
if not self._running:
|
||||||
self.toolbar.refresh_labels()
|
self.toolbar.refresh_labels()
|
||||||
|
self._sync_reset_action()
|
||||||
self._sync_shortcut_actions()
|
self._sync_shortcut_actions()
|
||||||
|
|
||||||
def retranslate(self) -> None:
|
def retranslate(self) -> None:
|
||||||
@@ -1249,7 +1255,9 @@ class StudioMainWindow(QMainWindow):
|
|||||||
self.act_run.setText(t("menu.run"))
|
self.act_run.setText(t("menu.run"))
|
||||||
self.act_stop.setText(t("menu.stop"))
|
self.act_stop.setText(t("menu.stop"))
|
||||||
self.act_preview.setText(t("menu.preview"))
|
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.setText(t("menu.reset_view"))
|
||||||
self.act_reset_view.setToolTip(t("menu.reset_view_tip"))
|
self.act_reset_view.setToolTip(t("menu.reset_view_tip"))
|
||||||
self.act_pause.setText(t("menu.pause"))
|
self.act_pause.setText(t("menu.pause"))
|
||||||
@@ -1823,6 +1831,23 @@ class StudioMainWindow(QMainWindow):
|
|||||||
self._persist_prefs()
|
self._persist_prefs()
|
||||||
|
|
||||||
# ----------------------------------------------------------- shortcuts
|
# ----------------------------------------------------------- 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:
|
def _sync_shortcut_actions(self) -> None:
|
||||||
if getattr(self, "act_run", None) is None:
|
if getattr(self, "act_run", None) is None:
|
||||||
return
|
return
|
||||||
@@ -2239,7 +2264,7 @@ class StudioMainWindow(QMainWindow):
|
|||||||
self._notify(t("toolbar.nothing_to_stop"))
|
self._notify(t("toolbar.nothing_to_stop"))
|
||||||
|
|
||||||
def _on_reset(self) -> None:
|
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
|
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
|
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
|
a new device zero means the old idle estimate is stale, not that the operator
|
||||||
wants to lose what they were looking at.
|
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
|
Which of the two this button means is the *view's* answer, and the label says
|
||||||
button (the same act as Esc), so this button is disabled wherever there is no
|
it (``toolbar.reset_target``): a device session can be zeroed, a replay cannot
|
||||||
device session to zero.
|
— 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:
|
if self._session_phase != "running" or self._active_worker is None:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
# A request, not the zeroing itself: the acquisition thread applies it
|
# A request, not the zeroing itself: the acquisition thread applies it
|
||||||
# between two frames and reports the frame and time it took effect.
|
# between two frames and reports the frame and time it took effect.
|
||||||
self._active_worker.reset()
|
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.force_plots.request_presentation_reestimate()
|
||||||
self._last_plot_packet_key = None
|
self._last_plot_packet_key = None
|
||||||
self.statusBar().showMessage(t("status.reset_pending"))
|
self.statusBar().showMessage(t("status.reset_pending"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.statusBar().showMessage(t("status.run_failed", error=str(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:
|
def _on_pause_toggled(self, paused: bool) -> None:
|
||||||
"""Pause the pipeline and the display; the device stays open.
|
"""Pause the pipeline and the display; the device stays open.
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
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
|
collapsed by default. Both live inside one scroll area so a narrow window
|
||||||
scrolls sideways instead of squeezing the controls into an unreadable row.
|
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
|
from __future__ import annotations
|
||||||
@@ -118,6 +123,9 @@ class ToolbarWidget(QWidget):
|
|||||||
self._video_path = ""
|
self._video_path = ""
|
||||||
#: ``acquire`` while measuring, ``playback`` while replaying a recording.
|
#: ``acquire`` while measuring, ``playback`` while replaying a recording.
|
||||||
self._run_role = "acquire"
|
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 = QVBoxLayout(self)
|
||||||
outer.setContentsMargins(2, 2, 2, 2)
|
outer.setContentsMargins(2, 2, 2, 2)
|
||||||
@@ -421,6 +429,9 @@ class ToolbarWidget(QWidget):
|
|||||||
self.btn_run.blockSignals(False)
|
self.btn_run.blockSignals(False)
|
||||||
self._refresh_run_label()
|
self._refresh_run_label()
|
||||||
self._refresh_stop_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:
|
def run_role(self) -> str:
|
||||||
return self._run_role
|
return self._run_role
|
||||||
@@ -490,7 +501,28 @@ class ToolbarWidget(QWidget):
|
|||||||
self.btn_stop.setEnabled(bool(enabled))
|
self.btn_stop.setEnabled(bool(enabled))
|
||||||
|
|
||||||
def set_reset_enabled(self, enabled: bool) -> None:
|
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:
|
def set_camera_controls_enabled(self, enabled: bool) -> None:
|
||||||
live = self._source_view == "live"
|
live = self._source_view == "live"
|
||||||
@@ -553,6 +585,7 @@ class ToolbarWidget(QWidget):
|
|||||||
self._refresh_preview_label()
|
self._refresh_preview_label()
|
||||||
self._refresh_run_label()
|
self._refresh_run_label()
|
||||||
self._refresh_stop_label()
|
self._refresh_stop_label()
|
||||||
|
self._refresh_reset_label()
|
||||||
|
|
||||||
# ------------------------------------------------------------- internals
|
# ------------------------------------------------------------- internals
|
||||||
def _populate_units_combo(self) -> None:
|
def _populate_units_combo(self) -> None:
|
||||||
@@ -797,11 +830,24 @@ class ToolbarWidget(QWidget):
|
|||||||
self.btn_run.setToolTip(tip)
|
self.btn_run.setToolTip(tip)
|
||||||
|
|
||||||
def _refresh_reset_label(self) -> None:
|
def _refresh_reset_label(self) -> None:
|
||||||
self.btn_reset.setText(t("toolbar.reset"))
|
"""Say which of the two things this button will do, where it is on screen.
|
||||||
# The button zeroes the *device* tracker; it no longer clears the curves.
|
|
||||||
# That is now 视图 → 复位视图, because one button doing both meant an
|
The operator's complaint was that 重置 read as "reset the charts" when it zeroes
|
||||||
# operator who wanted a fresh baseline also lost the history on screen.
|
the *sensor tracker* — and that in the replay view, where it is the display that
|
||||||
self.btn_reset.setToolTip(t("toolbar.reset_tip"))
|
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:
|
def retranslate(self) -> None:
|
||||||
self.cam_id_label.setText(t("toolbar.cam_id"))
|
self.cam_id_label.setText(t("toolbar.cam_id"))
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ Written against the requirement rather than the implementation:
|
|||||||
* the primary button is a play/pause toggle in **both** views, and pressing it while a
|
* the primary button is a play/pause toggle in **both** views, and pressing it while a
|
||||||
session runs pauses without stopping — the device stays connected;
|
session runs pauses without stopping — the device stays connected;
|
||||||
* stop ends the session (live) or leaves the replay (playback);
|
* stop ends the session (live) or leaves the replay (playback);
|
||||||
* reset is disabled in the replay view, where there is no tracker to zero.
|
* reset is one control with one meaning *per view*: the sensor tracker's in the live
|
||||||
|
view (which owns a device session), the display's in the replay view (which owns
|
||||||
|
none).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import ui_check
|
import ui_check
|
||||||
|
from orisys_app_shared.i18n import t
|
||||||
from tests.conftest import finish_run, running_window, show_window
|
from tests.conftest import finish_run, running_window, show_window
|
||||||
|
|
||||||
|
|
||||||
@@ -77,28 +80,81 @@ def test_stop_ends_the_session(make_window):
|
|||||||
stuck.release()
|
stuck.release()
|
||||||
|
|
||||||
|
|
||||||
def test_reset_is_for_the_device_and_is_disabled_in_the_replay_view(make_window):
|
def test_a_paused_session_still_offers_both_stop_and_reset(make_window):
|
||||||
"""Reset stays a *device* control, and it is the thing the replay view cannot offer.
|
"""Pause holds the session; the device is still open, so both controls stay offered.
|
||||||
|
|
||||||
|
This is the state the two rules are easiest to get wrong in: the transport is
|
||||||
|
unchecked, which is not "stopped", so Stop must still end the session and the reset
|
||||||
|
must still reach the device it is holding.
|
||||||
|
"""
|
||||||
|
window = show_window(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
|
||||||
|
stuck = ui_check._StuckWorker()
|
||||||
|
window._ensure_workers = lambda: None
|
||||||
|
window._box_worker = stuck
|
||||||
|
window._finger_worker = stuck
|
||||||
|
window._start_run()
|
||||||
|
window._run_timer.stop()
|
||||||
|
try:
|
||||||
|
# A first frame is the application's own path to ``running``, and the moment
|
||||||
|
# the rail recomputes what the session can do (the reset keys off it), so the
|
||||||
|
# session is reached through that path rather than stated directly.
|
||||||
|
stuck.emit(ui_check._fake_packet(1))
|
||||||
|
window._on_run_tick()
|
||||||
|
assert window._session_phase == "running"
|
||||||
|
assert window.toolbar.btn_reset.isEnabled(), (
|
||||||
|
"a running session must offer the tracker reset before it is paused"
|
||||||
|
)
|
||||||
|
window.toolbar.btn_run.setChecked(False)
|
||||||
|
assert window._display_paused is True and window._running
|
||||||
|
|
||||||
|
assert window.toolbar.btn_stop.isEnabled(), "no way out of the paused session"
|
||||||
|
assert window.toolbar.btn_reset.isEnabled(), (
|
||||||
|
"pausing took the device's reset away: the device is still open"
|
||||||
|
)
|
||||||
|
assert window.toolbar.reset_target() == "sensor"
|
||||||
|
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
|
||||||
|
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_tip")
|
||||||
|
|
||||||
|
window.toolbar.btn_reset.click()
|
||||||
|
assert stuck.reset_requests == 1, (
|
||||||
|
"the paused session's reset did not reach the device"
|
||||||
|
)
|
||||||
|
|
||||||
|
window.toolbar.btn_stop.click()
|
||||||
|
assert not window._running, "Stop did not end the paused session"
|
||||||
|
finally:
|
||||||
|
stuck.release()
|
||||||
|
|
||||||
|
|
||||||
|
def test_reset_is_the_sensor_in_live_and_the_display_in_the_replay_view(make_window):
|
||||||
|
"""Reset stays one control, and each view gets the meaning it can act on.
|
||||||
|
|
||||||
The enabled state of reset once frames are flowing is covered by
|
The enabled state of reset once frames are flowing is covered by
|
||||||
``test_ui_smoke.py::test_a_reset_reaches_the_device_and_is_recorded`` (which drives
|
``test_ui_smoke.py::test_a_zeroing_is_recorded_only_after_the_worker_confirms_it``
|
||||||
``_on_reset``); this test pins what the unification is about: reset and stop are two
|
(which drives ``_on_reset``); this test pins what the unification is about: reset and
|
||||||
different controls, and the replay view disables the one that needs a device.
|
stop are two different controls, and the view decides which of the two things the
|
||||||
|
reset acts on — the sensor tracker where a session owns a device, the display where
|
||||||
|
there is nothing of the device to act on.
|
||||||
"""
|
"""
|
||||||
window, stuck = running_window(make_window)
|
window, stuck = running_window(make_window)
|
||||||
try:
|
try:
|
||||||
assert window.toolbar.btn_reset.text() != window.toolbar.btn_stop.text(), (
|
assert window.toolbar.btn_reset.text() != window.toolbar.btn_stop.text(), (
|
||||||
"reset and stop must be distinguishable controls"
|
"reset and stop must be distinguishable controls"
|
||||||
)
|
)
|
||||||
|
assert window.toolbar.reset_target() == "sensor"
|
||||||
assert window.toolbar.btn_stop.isEnabled(), "stop is not reachable during a session"
|
assert window.toolbar.btn_stop.isEnabled(), "stop is not reachable during a session"
|
||||||
finally:
|
finally:
|
||||||
finish_run(window, stuck)
|
finish_run(window, stuck)
|
||||||
|
|
||||||
replay = show_window(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
|
replay = show_window(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
|
||||||
replay.brand_bar.btn_playback.click()
|
replay.brand_bar.btn_playback.click()
|
||||||
assert not replay.toolbar.btn_reset.isEnabled(), (
|
assert replay.toolbar.reset_target() == "display", (
|
||||||
"reset is offered in the replay view, where there is no tracker to zero"
|
"the replay view owns no device: its reset is the display's"
|
||||||
)
|
)
|
||||||
|
assert replay.toolbar.btn_reset.isEnabled(), (
|
||||||
|
"the display reset is unavailable in the replay view"
|
||||||
|
)
|
||||||
|
assert replay.toolbar.btn_reset.text() == t("toolbar.reset_display")
|
||||||
assert hasattr(replay.toolbar, "btn_stop"), "the replay view has no stop control"
|
assert hasattr(replay.toolbar, "btn_stop"), "the replay view has no stop control"
|
||||||
assert replay.toolbar.btn_run.text().startswith("▶"), (
|
assert replay.toolbar.btn_run.text().startswith("▶"), (
|
||||||
f"the replay transport idle reads {replay.toolbar.btn_run.text()!r}, not play"
|
f"the replay transport idle reads {replay.toolbar.btn_run.text()!r}, not play"
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ elements are supposed to exist*. These tests make that claim falsifiable against
|
|||||||
permanently skipped;
|
permanently skipped;
|
||||||
* the transport group the manifest declares is the behaviour of a *live* session: a running
|
* the transport group the manifest declares is the behaviour of a *live* session: a running
|
||||||
session keeps the tracker reset available and reads "pause", and a paused session reads
|
session keeps the tracker reset available and reads "pause", and a paused session reads
|
||||||
"resume". That is the defect class this artifact exists to prevent — a state assertion
|
"resume"; in the replay view the same declared reset is the display reset and says so.
|
||||||
|
That is the defect class this artifact exists to prevent — a state assertion
|
||||||
about widgets passing while the visible result is wrong.
|
about widgets passing while the visible result is wrong.
|
||||||
|
|
||||||
A deliberate asymmetry: the existence assertions are hard (a missing element fails), while
|
A deliberate asymmetry: the existence assertions are hard (a missing element fails), while
|
||||||
@@ -248,12 +249,18 @@ def test_the_transport_group_matches_the_live_session_states(make_window):
|
|||||||
assert reset is window.toolbar.btn_reset
|
assert reset is window.toolbar.btn_reset
|
||||||
assert window._session_phase == "running", window._session_phase
|
assert window._session_phase == "running", window._session_phase
|
||||||
|
|
||||||
# A running session: the transport reads pause and the reset is available.
|
# A running session: the transport reads pause and the reset is available —
|
||||||
|
# and it says which of the two things it acts on (the sensor tracker).
|
||||||
assert transport.isChecked(), (
|
assert transport.isChecked(), (
|
||||||
"a running session must show the transport engaged"
|
"a running session must show the transport engaged"
|
||||||
)
|
)
|
||||||
assert transport.text() == t("toolbar.pause"), transport.text()
|
assert transport.text() == t("toolbar.pause"), transport.text()
|
||||||
assert reset.isEnabled(), "a running session must offer the tracker reset"
|
assert reset.isEnabled(), "a running session must offer the tracker reset"
|
||||||
|
assert window.toolbar.reset_target() == "sensor", (
|
||||||
|
"the live view's reset is the sensor tracker's"
|
||||||
|
)
|
||||||
|
assert reset.text() == t("toolbar.reset_sensor"), reset.text()
|
||||||
|
assert reset.toolTip() == t("toolbar.reset_sensor_tip"), reset.toolTip()
|
||||||
assert stop.isEnabled(), "a running session must offer stop"
|
assert stop.isEnabled(), "a running session must offer stop"
|
||||||
|
|
||||||
# Paused: the transport reads resume, and the device action stays available.
|
# Paused: the transport reads resume, and the device action stays available.
|
||||||
@@ -286,7 +293,13 @@ def test_the_transport_group_matches_the_live_session_states(make_window):
|
|||||||
|
|
||||||
|
|
||||||
def test_the_transport_of_the_replay_view_is_not_the_device_reset(make_window):
|
def test_the_transport_of_the_replay_view_is_not_the_device_reset(make_window):
|
||||||
"""The same controls, the other view: an idle transport and no device to zero."""
|
"""The same controls, the other view: an idle transport, and a reset that resets the display.
|
||||||
|
|
||||||
|
The manifest declares one ``reset`` element for both views, so this is where the
|
||||||
|
claim that it means *one* thing per view is checked: in the replay view it is the
|
||||||
|
display reset (named as such, and there is no device for it to touch), not a
|
||||||
|
disabled control and not a tracker zero.
|
||||||
|
"""
|
||||||
window = _window(make_window, "box", "replay")
|
window = _window(make_window, "box", "replay")
|
||||||
try:
|
try:
|
||||||
by_id = elements.manifest().by_id()
|
by_id = elements.manifest().by_id()
|
||||||
@@ -294,8 +307,16 @@ def test_the_transport_of_the_replay_view_is_not_the_device_reset(make_window):
|
|||||||
reset = elements.resolve(window, by_id["reset"])
|
reset = elements.resolve(window, by_id["reset"])
|
||||||
assert not transport.isChecked()
|
assert not transport.isChecked()
|
||||||
assert transport.text() == t("toolbar.playback"), transport.text()
|
assert transport.text() == t("toolbar.playback"), transport.text()
|
||||||
assert not reset.isEnabled(), (
|
assert window.toolbar.reset_target() == "display", (
|
||||||
"reset is offered where there is no tracker to zero"
|
"the replay view's reset is the display's: this view owns no device"
|
||||||
|
)
|
||||||
|
assert reset.isEnabled(), (
|
||||||
|
"the display reset is available in the replay view (the reported defect)"
|
||||||
|
)
|
||||||
|
assert reset.text() == t("toolbar.reset_display"), reset.text()
|
||||||
|
assert reset.toolTip() == t("toolbar.reset_display_tip"), reset.toolTip()
|
||||||
|
assert window.act_reset.text() == t("menu.reset_display"), (
|
||||||
|
"Ctrl+R is the same control on a second surface and must be named the same way"
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
window.close()
|
window.close()
|
||||||
|
|||||||
@@ -1532,7 +1532,6 @@ def test_the_nav_items_switch_the_main_window_layout(make_window):
|
|||||||
assert not window.replay.is_loaded()
|
assert not window.replay.is_loaded()
|
||||||
assert window.force_plots.mode() == "follow", "browsing starts with no recording"
|
assert window.force_plots.mode() == "follow", "browsing starts with no recording"
|
||||||
assert window.toolbar.btn_replay_video.isVisible(), "no video picker in the replay"
|
assert window.toolbar.btn_replay_video.isVisible(), "no video picker in the replay"
|
||||||
live_sizes = tuple(window.content_splitter.sizes())
|
|
||||||
|
|
||||||
window.brand_bar.btn_live.click()
|
window.brand_bar.btn_live.click()
|
||||||
assert window.view_preset() == "live"
|
assert window.view_preset() == "live"
|
||||||
@@ -2050,7 +2049,8 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
|
|||||||
wrong. So the rail's Stop button — the same one the live view uses to end a session
|
wrong. So the rail's Stop button — the same one the live view uses to end a session
|
||||||
— is an unconditional stop in this view, and it covers both things that can run
|
— is an unconditional stop in this view, and it covers both things that can run
|
||||||
there: plain playback and a recompute pass. The tracker reset owns no device here,
|
there: plain playback and a recompute pass. The tracker reset owns no device here,
|
||||||
so it is disabled rather than repurposed.
|
so it is the *display* reset and says so (see
|
||||||
|
``test_the_replay_reset_is_the_display_reset_and_never_the_device``).
|
||||||
"""
|
"""
|
||||||
from orisys_app_shared.i18n import t
|
from orisys_app_shared.i18n import t
|
||||||
from PySide6.QtWidgets import QApplication
|
from PySide6.QtWidgets import QApplication
|
||||||
@@ -2061,10 +2061,10 @@ def test_the_replay_view_offers_a_stop_that_always_works(make_window, replay_rec
|
|||||||
assert window._load_recording(replay_recording)
|
assert window._load_recording(replay_recording)
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|
||||||
assert not window.toolbar.btn_reset.isEnabled(), (
|
assert window.toolbar.btn_reset.isEnabled(), (
|
||||||
"the replay view offers to zero a device tracker it has not got"
|
"the replay view's display reset is unavailable"
|
||||||
)
|
)
|
||||||
assert window.toolbar.btn_reset.text() == t("toolbar.reset")
|
assert window.toolbar.btn_reset.text() == t("toolbar.reset_display")
|
||||||
assert window.toolbar.btn_stop.text() == t("toolbar.stop_label")
|
assert window.toolbar.btn_stop.text() == t("toolbar.stop_label")
|
||||||
assert not window.toolbar.btn_stop.isEnabled(), (
|
assert not window.toolbar.btn_stop.isEnabled(), (
|
||||||
"a stop is offered while nothing is running"
|
"a stop is offered while nothing is running"
|
||||||
@@ -2136,12 +2136,131 @@ def test_the_live_view_keeps_the_tracker_reset(make_window):
|
|||||||
window = _show(make_window(), (1440, 880))
|
window = _show(make_window(), (1440, 880))
|
||||||
window.brand_bar.btn_live.click()
|
window.brand_bar.btn_live.click()
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
assert window.toolbar.btn_reset.text() == t("toolbar.reset"), (
|
assert window.toolbar.reset_target() == "sensor", (
|
||||||
|
"the live view's reset is the sensor tracker's"
|
||||||
|
)
|
||||||
|
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor"), (
|
||||||
"the rail's reset slot is not the tracker reset in the live view"
|
"the rail's reset slot is not the tracker reset in the live view"
|
||||||
)
|
)
|
||||||
|
# Idle, the tip explains what is missing instead of leaving a dead button.
|
||||||
|
assert not window.toolbar.btn_reset.isEnabled()
|
||||||
|
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_unavailable"), (
|
||||||
|
window.toolbar.btn_reset.toolTip()
|
||||||
|
)
|
||||||
window.close()
|
window.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_replay_reset_is_the_display_reset_and_never_the_device(
|
||||||
|
make_window, replay_recording
|
||||||
|
):
|
||||||
|
"""The reported defect: "the reset button cannot be used in replay mode".
|
||||||
|
|
||||||
|
The replay view owns no device, so the tracker reset is meaningless there — but the
|
||||||
|
display reset is exactly what an operator wants. The control is the same one, and
|
||||||
|
these are the facts that have to hold together: it is available, it puts the display
|
||||||
|
back (curves redrawn at their default framing, playhead at the start), and it never
|
||||||
|
asks a device worker to zero anything — the window is armed with one, and its request
|
||||||
|
counter stays at zero.
|
||||||
|
"""
|
||||||
|
from orisys_app_shared.i18n import t
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
window = _show(make_window(), (1440, 880))
|
||||||
|
stuck = ui_check._StuckWorker()
|
||||||
|
window._ensure_workers = lambda: None
|
||||||
|
window._box_worker = stuck
|
||||||
|
window._finger_worker = stuck
|
||||||
|
window.brand_bar.btn_playback.click()
|
||||||
|
QApplication.processEvents()
|
||||||
|
assert window._load_recording(replay_recording)
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
assert window.toolbar.reset_target() == "display"
|
||||||
|
assert window.toolbar.btn_reset.isEnabled(), (
|
||||||
|
"the replay view must offer the display reset"
|
||||||
|
)
|
||||||
|
assert window.toolbar.btn_reset.text() == t("toolbar.reset_display")
|
||||||
|
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_display_tip")
|
||||||
|
assert window.act_reset.text() == t("menu.reset_display"), (
|
||||||
|
"Ctrl+R is the same control and must be named the same way"
|
||||||
|
)
|
||||||
|
assert window.act_reset.isEnabled()
|
||||||
|
|
||||||
|
# What the reset has to undo: a moved playhead and a browsed window.
|
||||||
|
window.replay.set_position(0.9)
|
||||||
|
window.force_plots.set_window(0.4, 1.1)
|
||||||
|
QApplication.processEvents()
|
||||||
|
assert window.force_plots.mode() == "browse"
|
||||||
|
assert window.replay.position() > 0.5
|
||||||
|
|
||||||
|
window.toolbar.btn_reset.click()
|
||||||
|
QApplication.processEvents()
|
||||||
|
assert stuck.reset_requests == 0, (
|
||||||
|
"the replay's reset asked a device worker to zero the tracker"
|
||||||
|
)
|
||||||
|
assert window.replay.position() == 0.0, (
|
||||||
|
f"the reset did not start the replay over: {window.replay.position()}"
|
||||||
|
)
|
||||||
|
assert window.force_plots.cursor_time() == 0.0
|
||||||
|
assert window.force_plots.mode() == "browse", (
|
||||||
|
"the reset left the charts out of the replay's own mode"
|
||||||
|
)
|
||||||
|
sizes = [
|
||||||
|
0 if curve.getData()[0] is None else int(curve.getData()[0].size)
|
||||||
|
for curve in window.force_plots.curves
|
||||||
|
]
|
||||||
|
assert all(size > 0 for size in sizes), (
|
||||||
|
f"the reset left the replay with blank charts: {sizes}"
|
||||||
|
)
|
||||||
|
assert window.statusBar().currentMessage() == t("status.reset_display"), (
|
||||||
|
window.statusBar().currentMessage()
|
||||||
|
)
|
||||||
|
window.close()
|
||||||
|
|
||||||
|
|
||||||
|
def test_the_live_reset_zeroes_the_device_and_restarts_the_display_baseline(make_window):
|
||||||
|
"""The live half of the same control: the device is asked, and the tip's side
|
||||||
|
effect (the display baseline restarting) really happens."""
|
||||||
|
from orisys_app_shared.i18n import t
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
window = _show(make_window(sensor_type="box", source_mode="camera"), (1440, 880))
|
||||||
|
stuck = ui_check._StuckWorker()
|
||||||
|
window._ensure_workers = lambda: None
|
||||||
|
window._box_worker = stuck
|
||||||
|
window._finger_worker = stuck
|
||||||
|
window._start_run()
|
||||||
|
window._run_timer.stop()
|
||||||
|
try:
|
||||||
|
# The first frame is the application's own path to ``running`` — and the moment
|
||||||
|
# the rail recomputes what a session can do (see ``_update_run_enabled``), which
|
||||||
|
# is why the state is reached through it rather than stated directly.
|
||||||
|
stuck.emit(ui_check._fake_packet(1))
|
||||||
|
window._on_run_tick()
|
||||||
|
QApplication.processEvents()
|
||||||
|
assert window._session_phase == "running"
|
||||||
|
assert window.toolbar.btn_reset.isEnabled()
|
||||||
|
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
|
||||||
|
assert window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_tip")
|
||||||
|
restarts: list = []
|
||||||
|
window.force_plots.request_presentation_reestimate = lambda: restarts.append(True)
|
||||||
|
|
||||||
|
window.toolbar.btn_reset.click()
|
||||||
|
assert stuck.reset_requests == 1, "the live reset did not reach the device"
|
||||||
|
assert restarts == [True], (
|
||||||
|
"the display baseline was not restarted with the zero (the tooltip's side effect)"
|
||||||
|
)
|
||||||
|
assert window.statusBar().currentMessage() == t("status.reset_pending"), (
|
||||||
|
window.statusBar().currentMessage()
|
||||||
|
)
|
||||||
|
assert window.act_reset.text() == t("menu.reset"), (
|
||||||
|
"Ctrl+R must be named as the sensor reset in the live view"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
window._stop_run(reason="user_stop")
|
||||||
|
stuck.release()
|
||||||
|
|
||||||
|
|
||||||
def test_the_tracker_reset_follows_the_session_across_a_view_switch(make_window):
|
def test_the_tracker_reset_follows_the_session_across_a_view_switch(make_window):
|
||||||
"""Zeroing the tracker is a *device* action: which panel is on screen is not a fact
|
"""Zeroing the tracker is a *device* action: which panel is on screen is not a fact
|
||||||
about the session, so switching live → calibration → live must not take the reset
|
about the session, so switching live → calibration → live must not take the reset
|
||||||
@@ -2171,7 +2290,7 @@ def test_the_tracker_reset_follows_the_session_across_a_view_switch(make_window)
|
|||||||
assert window.toolbar.btn_reset.isEnabled() and window.act_reset.isEnabled(), (
|
assert window.toolbar.btn_reset.isEnabled() and window.act_reset.isEnabled(), (
|
||||||
"a running session must offer the tracker reset before any view switch"
|
"a running session must offer the tracker reset before any view switch"
|
||||||
)
|
)
|
||||||
assert window.toolbar.btn_reset.text() == t("toolbar.reset")
|
assert window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
|
||||||
|
|
||||||
window.brand_bar.btn_calibration.click()
|
window.brand_bar.btn_calibration.click()
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
|
|||||||
@@ -190,8 +190,6 @@ def _fake_packet(
|
|||||||
|
|
||||||
nan = float("nan")
|
nan = float("nan")
|
||||||
blank = np.zeros((8, 8, 3), np.uint8) if frame is None else frame
|
blank = np.zeros((8, 8, 3), np.uint8) if frame is None else frame
|
||||||
# A single tall sample makes it easy to prove decimation keeps peaks.
|
|
||||||
fn_raw = 99.0 if frame_id == 700 else (9.0 + frame_id * 0.01)
|
|
||||||
return SdkPacket(
|
return SdkPacket(
|
||||||
raw_frame=blank,
|
raw_frame=blank,
|
||||||
processed_frame=blank,
|
processed_frame=blank,
|
||||||
@@ -952,6 +950,26 @@ def main() -> int:
|
|||||||
window.toolbar.record_chip.toolTip() != "",
|
window.toolbar.record_chip.toolTip() != "",
|
||||||
"the record chip does not explain its state",
|
"the record chip does not explain its state",
|
||||||
)
|
)
|
||||||
|
# The reset names its target where it is (the reported confusion was that 重置
|
||||||
|
# read as "reset the charts" while it zeroes the *sensor*), and an idle live
|
||||||
|
# view says in the tip that there is no session to zero yet rather than
|
||||||
|
# leaving a dead button.
|
||||||
|
check(
|
||||||
|
window.toolbar.reset_target() == "sensor",
|
||||||
|
f"{theme}: the live view's reset is not the sensor tracker's",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
window.toolbar.btn_reset.text() == t("toolbar.reset_sensor"),
|
||||||
|
f"{theme}: the reset does not say it zeroes the sensor: "
|
||||||
|
f"{window.toolbar.btn_reset.text()!r}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
not window.toolbar.btn_reset.isEnabled()
|
||||||
|
and window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_unavailable"),
|
||||||
|
f"{theme}: the idle reset does not explain itself: "
|
||||||
|
f"enabled={window.toolbar.btn_reset.isEnabled()} "
|
||||||
|
f"{window.toolbar.btn_reset.toolTip()!r}",
|
||||||
|
)
|
||||||
check(
|
check(
|
||||||
not rail_squeezed(window),
|
not rail_squeezed(window),
|
||||||
f"{theme}: the status rail squeezes {rail_squeezed(window)}",
|
f"{theme}: the status rail squeezes {rail_squeezed(window)}",
|
||||||
@@ -1079,6 +1097,63 @@ def main() -> int:
|
|||||||
app.processEvents()
|
app.processEvents()
|
||||||
set_locale("zh_CN")
|
set_locale("zh_CN")
|
||||||
|
|
||||||
|
# The reset is now the rail's longest action label ("Reset sensor" / "Reset
|
||||||
|
# display" against the old bare "Reset"), so it is the one to measure rather than
|
||||||
|
# assume: the rail shares one line with the whole session state, 1280x720 is the
|
||||||
|
# tightest window and English is the widest locale. Both views are measured,
|
||||||
|
# because the label follows the view. The widths printed are what this check is
|
||||||
|
# judging — the threshold is "no elision", not a number.
|
||||||
|
for language in ("en_US", "zh_CN"):
|
||||||
|
set_locale(language)
|
||||||
|
app.setStyleSheet(build_qss("dark"))
|
||||||
|
measured = StudioMainWindow(
|
||||||
|
prefs={"language": language, "theme": "dark", "sensor_type": "finger"}
|
||||||
|
)
|
||||||
|
measured.resize(1280, 720)
|
||||||
|
measured.show()
|
||||||
|
for _ in range(3):
|
||||||
|
app.processEvents()
|
||||||
|
button = measured.toolbar.btn_reset
|
||||||
|
live_width = button.sizeHint().width()
|
||||||
|
check(
|
||||||
|
button.text() == t("toolbar.reset_sensor"),
|
||||||
|
f"{language}: the live reset reads {button.text()!r}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
button.width() >= button.sizeHint().width() - 1,
|
||||||
|
f"{language} at 1280x720: the live reset is elided "
|
||||||
|
f"({button.width()} < {button.sizeHint().width()})",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
not rail_squeezed(measured),
|
||||||
|
f"{language} at 1280x720 (live): the rail squeezes {rail_squeezed(measured)}",
|
||||||
|
)
|
||||||
|
measured.brand_bar.btn_playback.click()
|
||||||
|
for _ in range(3):
|
||||||
|
app.processEvents()
|
||||||
|
replay_width = button.sizeHint().width()
|
||||||
|
check(
|
||||||
|
button.text() == t("toolbar.reset_display"),
|
||||||
|
f"{language}: the replay reset reads {button.text()!r}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
button.width() >= button.sizeHint().width() - 1,
|
||||||
|
f"{language} at 1280x720: the replay reset is elided "
|
||||||
|
f"({button.width()} < {button.sizeHint().width()})",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
not rail_squeezed(measured),
|
||||||
|
f"{language} at 1280x720 (replay): the rail squeezes "
|
||||||
|
f"{rail_squeezed(measured)}",
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f" reset label at 1280x720, {language}: live {live_width}px, "
|
||||||
|
f"replay {replay_width}px (button width {button.width()}px)"
|
||||||
|
)
|
||||||
|
measured.close()
|
||||||
|
app.processEvents()
|
||||||
|
set_locale("zh_CN")
|
||||||
|
|
||||||
# Exactly one *filled* action at a time: a red Stop dominating an otherwise
|
# Exactly one *filled* action at a time: a red Stop dominating an otherwise
|
||||||
# idle top area was the original complaint.
|
# idle top area was the original complaint.
|
||||||
app.setStyleSheet(build_qss("dark"))
|
app.setStyleSheet(build_qss("dark"))
|
||||||
@@ -1162,6 +1237,22 @@ def main() -> int:
|
|||||||
and window.toolbar.btn_reset.isEnabled(),
|
and window.toolbar.btn_reset.isEnabled(),
|
||||||
"pausing the transport did not leave a running, stoppable session",
|
"pausing the transport did not leave a running, stoppable session",
|
||||||
)
|
)
|
||||||
|
# The paused session is where the two labels are easiest to get wrong: the
|
||||||
|
# transport is unchecked ("paused", not "stopped"), so the reset must still name
|
||||||
|
# the sensor tracker it can act on and keep the tip that says what happens.
|
||||||
|
check(
|
||||||
|
window.toolbar.reset_target() == "sensor"
|
||||||
|
and window.toolbar.btn_reset.text() == t("toolbar.reset_sensor")
|
||||||
|
and window.toolbar.btn_reset.toolTip() == t("toolbar.reset_sensor_tip"),
|
||||||
|
f"the paused session's reset does not say what it does: "
|
||||||
|
f"{window.toolbar.btn_reset.text()!r} / "
|
||||||
|
f"{window.toolbar.btn_reset.toolTip()!r}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
window.act_reset.isEnabled() and window.act_reset.text() == t("menu.reset"),
|
||||||
|
f"Ctrl+R does not follow the rail in a paused session: "
|
||||||
|
f"{window.act_reset.text()!r}",
|
||||||
|
)
|
||||||
window.toolbar.btn_run.setChecked(True)
|
window.toolbar.btn_run.setChecked(True)
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
check(
|
check(
|
||||||
@@ -3217,11 +3308,80 @@ def main() -> int:
|
|||||||
# The replay view stops through the rail's own Stop button — the same control the
|
# The replay view stops through the rail's own Stop button — the same control the
|
||||||
# live view uses — because the transport is a play/pause toggle and "which one is
|
# live view uses — because the transport is a play/pause toggle and "which one is
|
||||||
# stuck?" is not a question to ask an operator in front of a frozen picture. The
|
# stuck?" is not a question to ask an operator in front of a frozen picture. The
|
||||||
# tracker reset owns no device here, so it is disabled rather than repurposed.
|
# tracker reset owns no device here, so the same button is the *display* reset and
|
||||||
|
# says so: a control that cannot be used in the view the operator is working in is
|
||||||
|
# the reported defect ("the reset button cannot be used in replay mode").
|
||||||
check(
|
check(
|
||||||
not replay_window.toolbar.btn_reset.isEnabled(),
|
replay_window.toolbar.reset_target() == "display"
|
||||||
"the replay view offers to zero a device tracker it has not got",
|
and replay_window.toolbar.btn_reset.isEnabled(),
|
||||||
|
"the replay view does not offer its display reset",
|
||||||
)
|
)
|
||||||
|
check(
|
||||||
|
replay_window.toolbar.btn_reset.text() == t("toolbar.reset_display")
|
||||||
|
and replay_window.toolbar.btn_reset.toolTip() == t("toolbar.reset_display_tip"),
|
||||||
|
f"the replay's reset does not say what it does: "
|
||||||
|
f"{replay_window.toolbar.btn_reset.text()!r} / "
|
||||||
|
f"{replay_window.toolbar.btn_reset.toolTip()!r}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
replay_window.act_reset.text() == t("menu.reset_display"),
|
||||||
|
f"Ctrl+R is not named as the display reset in the replay: "
|
||||||
|
f"{replay_window.act_reset.text()!r}",
|
||||||
|
)
|
||||||
|
# The display reset itself, on a browsed window and a moved playhead: the charts
|
||||||
|
# go back to the recording's own framing, the replay starts over, and no device
|
||||||
|
# worker is asked to zero anything — the window is armed with a controllable one
|
||||||
|
# here, and its request counter has to stay at zero.
|
||||||
|
replay_devices = _StuckWorker()
|
||||||
|
kept = (
|
||||||
|
replay_window._box_worker,
|
||||||
|
replay_window._finger_worker,
|
||||||
|
replay_window._ensure_workers,
|
||||||
|
)
|
||||||
|
replay_window._ensure_workers = lambda: None
|
||||||
|
replay_window._box_worker = replay_devices
|
||||||
|
replay_window._finger_worker = replay_devices
|
||||||
|
try:
|
||||||
|
replay_plots.set_window(0.2, 0.6)
|
||||||
|
replay.set_position(replay.duration_s() * 0.5)
|
||||||
|
app.processEvents()
|
||||||
|
check(
|
||||||
|
replay_plots.mode() == "browse" and replay.position() > 0.0,
|
||||||
|
"the state the display reset has to undo was not set up",
|
||||||
|
)
|
||||||
|
replay_window.toolbar.btn_reset.click()
|
||||||
|
app.processEvents()
|
||||||
|
check(
|
||||||
|
replay_devices.reset_requests == 0,
|
||||||
|
"the replay's reset asked a device worker to zero the tracker",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
replay.position() == 0.0 and replay_plots.cursor_time() == 0.0,
|
||||||
|
f"the display reset did not start the replay over: "
|
||||||
|
f"{replay.position()} / {replay_plots.cursor_time()}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
replay_plots.mode() == "browse" and replay_plots.window()[0] == 0.0,
|
||||||
|
f"the display reset left the charts out of the recording's framing: "
|
||||||
|
f"{replay_plots.mode()} {replay_plots.window()}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
all(int(curve.getData()[0].size) == 40 for curve in replay_plots.curves),
|
||||||
|
"the display reset left the replay with blank charts: "
|
||||||
|
f"{[curve.getData()[0] for curve in replay_plots.curves]}",
|
||||||
|
)
|
||||||
|
check(
|
||||||
|
replay_window.statusBar().currentMessage() == t("status.reset_display"),
|
||||||
|
f"the display reset did not say what it did: "
|
||||||
|
f"{replay_window.statusBar().currentMessage()!r}",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
(
|
||||||
|
replay_window._box_worker,
|
||||||
|
replay_window._finger_worker,
|
||||||
|
replay_window._ensure_workers,
|
||||||
|
) = kept
|
||||||
|
replay_devices.release()
|
||||||
check(
|
check(
|
||||||
replay_window.toolbar.btn_stop.text() == t("toolbar.stop_label"),
|
replay_window.toolbar.btn_stop.text() == t("toolbar.stop_label"),
|
||||||
f"the replay's stop button is not labelled as one: "
|
f"the replay's stop button is not labelled as one: "
|
||||||
|
|||||||
Reference in New Issue
Block a user