改善 Studio 回放布局与传感器切换体验

缩短界面提示,保存上次视频目录,并防止旧结果绘入 Finger 3D。针对性测试及可见界面复测通过。
This commit is contained in:
ykcheungab
2026-10-06 20:18:50 +08:00
parent 425fa0d44d
commit 321e52f47a
11 changed files with 265 additions and 41 deletions
@@ -0,0 +1,54 @@
"""The two video pickers share the last directory chosen by the operator."""
from __future__ import annotations
import json
from pathlib import Path
from PySide6.QtWidgets import QApplication
import orisys_studio.replay_controller as replay_controller
def test_both_video_pickers_recall_the_last_chosen_directory(
make_window, monkeypatch, tmp_path
):
window = make_window(sensor_type="box")
window.show()
window.brand_bar.btn_playback.click()
QApplication.processEvents()
first = tmp_path / "first"
second = tmp_path / "second"
first.mkdir()
second.mkdir()
window.prefs["last_video_directory"] = str(first)
chosen = [second / "first.mp4", first / "second.mp4"]
starts: list[str] = []
def choose(_parent, _title, start, _filter):
starts.append(start)
return str(chosen[len(starts) - 1]), ""
monkeypatch.setattr(replay_controller.QFileDialog, "getOpenFileName", choose)
monkeypatch.setattr(window.replay_ctl, "load_video", lambda _video: True)
window.replay_ctl.on_open_recording()
assert starts == [str(first)]
assert window.prefs["last_video_directory"] == str(second)
window.replay_ctl.on_browse_video()
assert starts == [str(first), str(second)]
assert window.prefs["last_video_directory"] == str(first)
saved = json.loads((tmp_path / "preference.json").read_text(encoding="utf-8"))
assert saved["last_video_directory"] == str(first)
def test_missing_remembered_directory_falls_back_to_current_video(make_window, tmp_path):
window = make_window(sensor_type="box")
current = tmp_path / "clip.mp4"
current.write_bytes(b"fixture")
window.prefs["last_video_directory"] = str(tmp_path / "removed")
window.toolbar.set_video_path(str(current))
assert window.replay_ctl._video_dialog_directory(Path.home()) == str(tmp_path)
@@ -51,6 +51,45 @@ def _pick(window, sensor_type: str) -> None:
combo.setCurrentIndex(combo.findData(sensor_type))
def test_switching_to_finger_enables_3d_after_an_earlier_off_choice(qt_app, make_window):
window = make_window(sensor_type="box", show_finger_3d=False)
window.show()
qt_app.processEvents()
try:
assert not window._show_finger_3d
_pick(window, "finger")
qt_app.processEvents()
assert window._show_finger_3d
assert window.prefs["show_finger_3d"] is True
assert window.mesh3d_panel.isVisible()
assert window.act_finger_3d.isChecked()
window.layout_ctl.on_finger_3d_toggled(False)
assert not window._show_finger_3d
_pick(window, "box")
_pick(window, "finger")
qt_app.processEvents()
assert window.mesh3d_panel.isVisible()
assert window.act_finger_3d.isChecked()
finally:
window.close()
def test_replay_keeps_the_marker_image_readable_in_a_1197px_window(qt_app, make_window):
window = make_window(sensor_type="finger", show_finger_3d=True)
window.resize(1197, 820)
window.show()
qt_app.processEvents()
try:
window.brand_bar.btn_playback.click()
qt_app.processEvents()
assert window.mesh3d_panel.isVisible()
assert window.arrow_panel.width() >= 380
assert window.force_plots.width() > window.arrow_panel.width()
finally:
window.close()
def test_reserving_the_native_handle_costs_nothing_at_construction(qt_app, make_window):
"""The handle is reserved, and reserving it does not build the pane.
+82
View File
@@ -2830,6 +2830,88 @@ def test_the_rail_reads_the_rate_and_what_it_cost(qt_app, make_window):
window.close()
def test_replay_without_a_video_names_the_visible_picker(qt_app, make_window):
from orisys_app_shared.i18n import t
window = make_window(sensor_type="box")
window.brand_bar.btn_playback.click()
qt_app.processEvents()
assert window.arrow_panel.image_label.text() == t("panels.replay_empty")
assert window.toolbar.btn_replay_video.text() in t("panels.replay_empty")
window.close()
def test_a_remembered_video_is_not_described_as_open(make_window):
from orisys_app_shared.i18n import t
window = make_window(sensor_type="finger", video_path="D:/clips/old.mp4")
window.brand_bar.btn_playback.click()
window._refresh_status_rail()
assert not window.replay.has_video()
assert window.toolbar.source_summary.text() == t(
"status.rail_recent_video", name="old.mp4"
)
def test_the_rail_puts_result_identity_before_a_long_video_name(
qt_app, make_window, monkeypatch
):
from types import SimpleNamespace
from orisys_app_shared.i18n import t
window = make_window(sensor_type="finger")
window.resize(1197, 752)
window.show()
window.brand_bar.btn_playback.click()
window.toolbar.set_video_path("D:/very-long-recording-name-for-a-box-sensor.mp4")
session = SimpleNamespace(
reader=SimpleNamespace(provenance=SimpleNamespace(sensor_type="box"))
)
monkeypatch.setattr(window.replay_ctl, "available_result", lambda: session)
window._refresh_status_rail()
qt_app.processEvents()
prefix = t("status.rail_result_profile", type="box")
label = window.toolbar.source_summary
assert label.text().startswith(prefix), label.text()
assert label.fontMetrics().horizontalAdvance(prefix) <= label.width(), (
"the result's sensor disappears from the visible rail"
)
window.close()
def test_old_result_depth_is_not_sent_to_the_new_sensor_scene(make_window, monkeypatch):
from types import SimpleNamespace
window = make_window(sensor_type="finger")
session = SimpleNamespace(
reader=SimpleNamespace(provenance=SimpleNamespace(sensor_type="box")),
paint_policy=lambda: None,
)
monkeypatch.setattr(window.replay_ctl, "available_result", lambda: session)
submitted = []
fake_view = SimpleNamespace(
set_overlay_policy=lambda _policy: None,
submit_packet=submitted.append,
)
panel = window.mesh3d_panel
real_view = panel._view
panel._view = fake_view
monkeypatch.setattr(window, "_connect_analysis_display", lambda _panel: True)
monkeypatch.setattr(window, "_tick_analysis_3d", lambda: None)
try:
window._show_analysis_frame("old box packet")
assert submitted == []
session.reader.provenance.sensor_type = "finger"
window._show_analysis_frame("finger packet")
assert submitted == ["finger packet"]
finally:
panel._view = real_view
def test_the_camera_probe_is_usable_without_a_camera(capsys):
"""The probe has to be safe to run on a machine with no camera attached."""
import camera_probe
+16 -2
View File
@@ -304,9 +304,23 @@ def test_the_shipped_live_layout_makes_the_3d_pane_the_widest_panel():
assert mesh > charts, DEFAULT_SPLITTER_SIZES
live_mesh, live_image, live_charts, _cal_live = LIVE["live"]
assert live_mesh > live_image and live_mesh > live_charts, LIVE["live"]
assert LIVE["replay"] == LIVE["live"], (
"the replay view is the live layout with another source"
assert LIVE["replay"][2] > LIVE["replay"][0], (
"history browsing needs more chart width than 3D width"
)
assert LIVE["live"][1] >= 400 and LIVE["replay"][1] >= 400, (
"the 2D marker picture is too narrow to inspect"
)
def test_panel_stretch_keeps_each_views_priority_after_resizing():
controller, parts = _controller(prefs={"view_preset": "live"}, sensor_type="finger")
controller.show_finger_3d = True
controller.apply_panel_visibility()
assert dict(parts["splitter"].stretches) == {0: 5, 1: 4, 2: 4, 3: 0}
controller.apply_preset("replay")
assert dict(parts["splitter"].stretches) == {0: 4, 1: 4, 2: 5, 3: 0}
def test_the_calibration_panel_toggle_is_the_calibration_view_switch():