diff --git a/apps/studio/config/locales/en_US.json b/apps/studio/config/locales/en_US.json index 613657f..74a23b7 100644 --- a/apps/studio/config/locales/en_US.json +++ b/apps/studio/config/locales/en_US.json @@ -209,13 +209,13 @@ "view_mode_tip": "Image display mode while running", "replay_pipeline": "Recorded video (raw camera image)", "replay_pipeline_tip": "Replay shows the recorded raw camera image with no live arrow overlay; to get arrows and depth from analysis, use View → Analyze video", - "replay_raw_note": "A recording stores only the raw camera image: there is no flow or depth to draw, because this recording has no analysis result yet. Opening a recording runs the pass automatically, or press “Analyze video”; once a result exists this pane can draw arrows and the depth map", + "replay_raw_note": "No analysis result yet. Opening a recording runs analysis, or choose Analyze video… above.", "frame_missing_note": "This frame carries no {fields}: there is nothing for this view to draw", "field_flow": "flow", "field_depth": "depth", "recompute_pipeline": "Recomputed from the recording (not the recorded value)", "recompute_pipeline_tip": "This frame is what the current algorithm version computes for that footage — not the measurement that was recorded at the time.", - "replay_empty": "No recording open\nPress “Open recording…” below", + "replay_empty": "No video open\nChoose “Recorded video…” above", "arrow_empty": "No image yet\nPick a video or camera, then press Run", "arrow_empty_tip": "The deformation arrow image appears here while running or previewing", "arrow_render_failed": "Flow arrows could not be drawn for this frame\nTry another view, or reload the video", @@ -244,7 +244,7 @@ "mesh3d_reset_tip": "Reset view: back to the framing the scene itself uses", "replay_analysis_running": "this recording is being analysed: there are no arrows or depth to draw until it finishes. Progress and Cancel are on the analysis panel.", "replay_analysis_failed": "there is no analysis result to draw: {reason}. Only the raw camera image can be shown here; once that is dealt with, Analyze video runs the pass again by hand.", - "replay_analysis_unavailable": "this is not a recording that can be analysed (its folder has no session.json, or no raw_*.mp4 segment): no pass can run, so this panel shows the raw camera image only. For forces and deformation from this video use View > Analyze video." + "replay_analysis_unavailable": "This video has no analysis setup. Choose Analyze video… above to select one and generate results." }, "chart": { "fn": "FN", @@ -295,7 +295,7 @@ "no_video": "This recording has no usable video segment", "open_failed": "Could not open that recording: {error}", "not_a_recording": "“{name}” is not a recording: its folder has no forces.csv (a recording is the video plus forces.csv and session.json). Replay needs a recording; to just watch this video, open it as a video or use View ▸ Analyze video.", - "video_only_opened": "“{name}” has no recording beside it (no forces.csv), so it is playing as a video: the picture and the timeline work, the force charts are empty. For forces and deformation use View ▸ Analyze video.", + "video_only_opened": "Opened “{name}”; no analysis result yet. Choose Analyze video… above to generate one.", "video_only_notice": "This video has no recording beside it (no forces.csv): it plays, but the force charts and the recording details are empty.", "video_only_panel": "This is not a recording, and this video cannot be decoded (no forces.csv beside it, and it does not read as playable video)\nYou can still try View ▸ Analyze video", "empty_recording": "This recording has no playable picture (no raw_*.mp4 segment), so it cannot be replayed", @@ -449,8 +449,9 @@ "phase_connecting": "Connecting", "phase_running": "Acquiring", "phase_error": "Error", - "rail_video_name": "Video file: {name}", - "rail_result_profile": "Result profile: {type}", + "rail_video_name": "Video: {name}", + "rail_recent_video": "Previous video: {name}", + "rail_result_profile": "Result: {type}", "rail_result_reused": "Stored result", "rail_result_complete": "Analysis complete", "rail_result_running": "Analysing…", diff --git a/apps/studio/config/locales/zh_CN.json b/apps/studio/config/locales/zh_CN.json index 9a3a2e7..8fe8532 100644 --- a/apps/studio/config/locales/zh_CN.json +++ b/apps/studio/config/locales/zh_CN.json @@ -209,13 +209,13 @@ "view_mode_tip": "运行时的图像显示模式", "replay_pipeline": "录制视频(原始相机图像)", "replay_pipeline_tip": "回放显示的是录制下来的原始相机图像,不含实时箭头叠加;要看分析得到的箭头与深度请用「视图 → 分析视频」", - "replay_raw_note": "录像只保存原始相机图像:这里没有光流/深度可画,因为这个录像还没有分析结果。打开录像会自动跑一遍分析,也可以点「分析视频」手动生成;有结果后本面板就能画箭头和深度图", + "replay_raw_note": "尚无分析结果。打开录像会自动分析,或点顶部「分析视频…」。", "frame_missing_note": "本帧没有{fields}:这个视图这一帧没有数据可画", "field_flow": "光流", "field_depth": "深度", "recompute_pipeline": "由录像重算(非记录值)", "recompute_pipeline_tip": "这一帧是当前算法版本对录像重新计算得到的,不是当初记录下来的测量值。", - "replay_empty": "尚未打开录制\n点击下方「打开录制…」", + "replay_empty": "尚未打开录像\n点击顶部「选择录制视频…」", "arrow_empty": "尚未采集图像\n请选择视频或摄像机后点击「运行」", "arrow_empty_tip": "运行或预览后在此显示形变箭头图", "arrow_render_failed": "本帧无法绘制形变箭头\n请换一种视图,或重新加载视频", @@ -244,7 +244,7 @@ "mesh3d_reset_tip": "复位视角:回到场景自带的取景", "replay_analysis_running": "正在分析这个录像:跑完之前这里没有箭头/深度可画,进度与取消在分析面板上。", "replay_analysis_failed": "没有可画的分析结果:{reason}。这里只能显示原始相机图像;处理完之后可以点「分析视频」再跑一遍。", - "replay_analysis_unavailable": "这不是一条可分析的录像(同目录没有 session.json,或没有 raw_*.mp4 分段):没有能跑的分析,本面板只能显示原始相机图像。要从这段视频得到力值/形变,请用 视图 ▸ 分析视频。" + "replay_analysis_unavailable": "视频尚无分析配置。点顶部「分析视频…」选择配置并生成结果。" }, "chart": { "fn": "FN", @@ -295,7 +295,7 @@ "no_video": "这段录制没有可用的视频分段", "open_failed": "无法打开该录制: {error}", "not_a_recording": "「{name}」不是一条记录:同目录没有 forces.csv(一条记录 = 录像 + forces.csv + session.json)。回放需要记录;只想看这段视频请直接打开视频,或用「分析视频」。", - "video_only_opened": "「{name}」没有配套记录(无 forces.csv),已按视频播放:可以看画面与拖时间轴,力曲线为空。要得到力值/形变,请使用 视图 ▸ 分析视频。", + "video_only_opened": "已打开「{name}」;尚无分析结果。点顶部「分析视频…」生成。", "video_only_notice": "这段视频没有配套记录(同目录无 forces.csv):可以播放,但力曲线与记录信息为空。", "video_only_panel": "这不是一条记录,而且无法解码这段视频(没有 forces.csv,也不像可播放的视频文件)\n可以试试 视图 ▸ 分析视频", "empty_recording": "这条记录没有可播放的画面(没有 raw_*.mp4 视频分段),因此无法回放", @@ -449,8 +449,9 @@ "phase_connecting": "连接中", "phase_running": "采集中", "phase_error": "出错", - "rail_video_name": "视频文件: {name}", - "rail_result_profile": "结果配置: {type}", + "rail_video_name": "视频:{name}", + "rail_recent_video": "上次视频:{name}", + "rail_result_profile": "结果:{type}", "rail_result_reused": "已复用缓存结果", "rail_result_complete": "分析完成", "rail_result_running": "正在分析…", diff --git a/apps/studio/config/preferences.default.json b/apps/studio/config/preferences.default.json index a14fd58..e0a96ee 100644 --- a/apps/studio/config/preferences.default.json +++ b/apps/studio/config/preferences.default.json @@ -6,6 +6,7 @@ "camera_id": "0", "source_mode": "camera", "video_path": "", + "last_video_directory": "", "show_cal_panel": false, "show_finger_3d": true, "show_fps": false, @@ -35,5 +36,5 @@ "finger_2d_orientation": "rotate180", "settings_expanded": false, "view_preset": "custom", - "splitter_sizes": [340, 480, 400] + "splitter_sizes": [560, 400, 480, 0] } diff --git a/apps/studio/src/orisys_studio/app_window.py b/apps/studio/src/orisys_studio/app_window.py index 51ca051..c9d1780 100644 --- a/apps/studio/src/orisys_studio/app_window.py +++ b/apps/studio/src/orisys_studio/app_window.py @@ -84,14 +84,14 @@ RUNNABLE_TYPES = frozenset({"box", "mini", "finger", "finger-mems"}) #: Types whose 2D arrow view is rotated 90° (the sensor is mounted that way). finger-mems is #: a box-style stitched camera, so it rotates like box until the device says otherwise. ARROW_ROTATE_TYPES = frozenset({"box", "mini", "finger-mems"}) -DEFAULT_SPLITTER_SIZES = (620, 320, 500) +DEFAULT_SPLITTER_SIZES = (560, 400, 480) MIN_PANEL_WIDTHS = { # The 3D pane used to be the one panel whose content was a stack of form fields, so # its minimum was measured under the stylesheet in both languages (241 px in English) # and could not go below 260. The controls now live in the settings dialog and what # is left wraps, so the pane can be dragged narrow again — the picture is the point. "mesh3d": 200, - "arrow": 300, + "arrow": 380, "plots": 340, "cal": 300, } @@ -101,13 +101,11 @@ VIEW_PRESETS = ("custom", "live", "replay", "calibration") #: Splitter sizes per view, in panel order (3D, 2D, charts, calibration). #: ``custom`` keeps whatever the user arranged (see ``_restore_splitter_sizes``). -#: Replay is the live layout on purpose: the two views differ in their source -#: (a camera, or a recorded video), not in their furniture. -#: The 3D pane is the widest of the three: it is the view's subject when it is on, and -#: the 2D panel is a reading of the same measurement rather than a second subject. +#: Live gives the 3D picture the most width; replay gives the history chart the +#: most width. Both keep the 2D picture large enough to inspect marker detail. VIEW_SPLITTER_SIZES = { - "live": (620, 300, 520, 0), - "replay": (620, 300, 520, 0), + "live": (560, 400, 480, 0), + "replay": (480, 400, 560, 0), "calibration": (0, 0, 560, 880), } #: The 3D pane belongs to the live view and to history browsing, and only for a @@ -1488,7 +1486,12 @@ class StudioMainWindow(QMainWindow): if self._view_preset == "replay": path = self.toolbar.video_path() if path: - detail = t("status.rail_video_name", name=Path(path).name) + key = ( + "status.rail_video_name" + if self.replay.has_video() + else "status.rail_recent_video" + ) + detail = t(key, name=Path(path).name) # Name the *result's* profile and whether it was reused or just finished, so # changing the sensor picker cannot look like the open figures came from the # newly selected product. @@ -1497,11 +1500,8 @@ class StudioMainWindow(QMainWindow): provenance = getattr(getattr(session, "reader", None), "provenance", None) result_type = str(getattr(provenance, "sensor_type", "") or "").strip() if result_type: - detail = ( - f"{detail} · {t('status.rail_result_profile', type=result_type)}" - if detail - else t("status.rail_result_profile", type=result_type) - ) + result_label = t("status.rail_result_profile", type=result_type) + detail = f"{result_label} · {detail}" if detail else result_label progress = getattr(self.replay_ctl, "_progress", None) progress_state = ( str(progress.state()) if progress is not None and hasattr(progress, "state") else "" @@ -1534,7 +1534,7 @@ class StudioMainWindow(QMainWindow): # The full path and the replay's notes do not fit on a 30 px rail, so # they live in the summary's tooltip (the bar that used to show them is # gone: the two views share one layout). - tip_parts = [self.toolbar.video_path()] + tip_parts = [detail, self.toolbar.video_path()] if self._view_preset == "replay" and self.replay.has_video(): # Includes the "this is a video, not a recording" note for a bare video. tip_parts.append(self.replay.warnings_text()) @@ -1999,6 +1999,11 @@ class StudioMainWindow(QMainWindow): def _on_sensor_type_changed(self, sensor_type: str) -> None: self.prefs["sensor_type"] = str(sensor_type) self.prefs["config_name"] = self.sensor_picker.config_name() + if sensor_type == "finger": + # Entering Finger opens its principal view even if an earlier session + # left the optional pane turned off. The user can still turn it off. + self._show_finger_3d = True + self.prefs["show_finger_3d"] = True self.cal_panel.set_meta_context( sensor_type=str(sensor_type), config_ref=self._resolve_config_ref(), @@ -2006,6 +2011,7 @@ class StudioMainWindow(QMainWindow): # A different product is a different UV island; the previous latch must not # paint the new canvas (a box result has no mask and would inherit a finger's). self._domain_mask.reset() + self.mesh3d_panel.show_reference_only() # The pane follows the sensor: a box sensor has no 3D scene, and a finger's # missing package must not be reported while looking at a box. self.layout_ctl.sync_3d_source() @@ -2941,6 +2947,13 @@ class StudioMainWindow(QMainWindow): policy is the open session's ``paint_policy``, so 3D arrows share the 2D canvas's grid and threshold (including the box painter's 2 px floor). """ + session = self.replay_ctl.available_result() + provenance = getattr(getattr(session, "reader", None), "provenance", None) + result_type = str(getattr(provenance, "sensor_type", "") or "").strip() + if result_type and result_type != self.sensor_picker.sensor_type(): + # The old replay stays inspectable after a sensor switch, but its + # depth grid must never be submitted to the new sensor's 3D scene. + return view = self.mesh3d_panel.view if view is None: return diff --git a/apps/studio/src/orisys_studio/replay_controller.py b/apps/studio/src/orisys_studio/replay_controller.py index 0f5d9b3..71c8d1e 100644 --- a/apps/studio/src/orisys_studio/replay_controller.py +++ b/apps/studio/src/orisys_studio/replay_controller.py @@ -238,7 +238,7 @@ class ReplayController(QObject): *video* (what the operator recognises) and the charts pair it with the ``forces.csv`` written beside it. """ - start = str(recordings_dir()) + start = self._video_dialog_directory(recordings_dir()) chosen, _filter = QFileDialog.getOpenFileName( self._dialog_parent(), t("playback.open"), @@ -247,6 +247,7 @@ class ReplayController(QObject): ) if not chosen: return + self._remember_video_directory(Path(chosen)) self.load_video(Path(chosen)) def on_browse_video(self) -> None: @@ -258,7 +259,7 @@ class ReplayController(QObject): """ if self._host.running or self._host.preview_open: return - start = self._toolbar.video_path() or str(Path.home()) + start = self._video_dialog_directory(Path.home()) path, _ = QFileDialog.getOpenFileName( self._dialog_parent(), t("menu.open_video"), @@ -267,8 +268,25 @@ class ReplayController(QObject): ) if not path: return + self._remember_video_directory(Path(path)) self.load_video(Path(path)) + def _video_dialog_directory(self, fallback: Path) -> str: + """Start both video pickers in the last directory the operator chose.""" + saved = str(self._host.prefs.get("last_video_directory") or "") + if saved and Path(saved).is_dir(): + return saved + current = self._toolbar.video_path() + if current and Path(current).parent.is_dir(): + return str(Path(current).parent) + return str(fallback) + + def _remember_video_directory(self, video: Path) -> None: + directory = video.parent + if directory.is_dir(): + self._host.prefs["last_video_directory"] = str(directory) + self._host.persist_prefs() + def on_recompute_button(self) -> None: """The replay view's own entry for the processed picture.""" if self._actions is None: @@ -1415,12 +1433,14 @@ class ReplayController(QObject): if self._charts.is_recording_source(): self._charts.clear_recording_source() self._charts.follow_latest() - self._image_panel.set_source_kind("live") + self._image_panel.set_source_kind("replay" if active else "live") # The panel held a recorded frame: the next live packet must repaint it, so the # "already rendered" marker is dropped. self._host.drop_render_marker() if not self._host.running and not self._host.preview_open: - self._image_panel.clear_preview(t("panels.arrow_empty")) + self._image_panel.clear_preview( + t("panels.replay_empty" if active else "panels.arrow_empty") + ) def _serve_curves(self, session: AnalysisSession | None = None) -> str: """Point the charts at the force trace this timeline really has, and label it. diff --git a/apps/studio/src/orisys_studio/view_layout.py b/apps/studio/src/orisys_studio/view_layout.py index 53a0a50..c45b545 100644 --- a/apps/studio/src/orisys_studio/view_layout.py +++ b/apps/studio/src/orisys_studio/view_layout.py @@ -193,12 +193,10 @@ class ViewLayoutController(QObject): self._cal_panel.setMinimumWidth(self._min_panel_widths["cal"]) self._set_check(self._actions.finger_3d if self._actions else None, show_3d, enabled=is_finger) self._set_check(self._actions.cal_panel if self._actions else None, show_cal) - # Panel weights: the charts are the widest panel in a view without 3D, the 3D - # pane is the widest one when it is on (it is the subject, not an inset), and - # the 2D panel is deliberately the smaller one. - arrow_weight = 3 - plots_weight = 4 - mesh_weight = 5 + # Keep each preset's panel priority when the window is resized. + arrow_weight = 4 + plots_weight = 5 if preset == "replay" else 4 + mesh_weight = 4 if preset == "replay" else 5 for i in range(self._splitter.count()): widget = self._splitter.widget(i) if widget is self._mesh_panel: diff --git a/apps/studio/tests/test_replay_video_directory.py b/apps/studio/tests/test_replay_video_directory.py new file mode 100644 index 0000000..95d4519 --- /dev/null +++ b/apps/studio/tests/test_replay_video_directory.py @@ -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) diff --git a/apps/studio/tests/test_sensor_switch_window.py b/apps/studio/tests/test_sensor_switch_window.py index 2aed373..e5ee545 100644 --- a/apps/studio/tests/test_sensor_switch_window.py +++ b/apps/studio/tests/test_sensor_switch_window.py @@ -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. diff --git a/apps/studio/tests/test_ui_smoke.py b/apps/studio/tests/test_ui_smoke.py index 6fa1b0c..e8951e8 100644 --- a/apps/studio/tests/test_ui_smoke.py +++ b/apps/studio/tests/test_ui_smoke.py @@ -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 diff --git a/apps/studio/tests/test_view_layout.py b/apps/studio/tests/test_view_layout.py index 7289730..96e5233 100644 --- a/apps/studio/tests/test_view_layout.py +++ b/apps/studio/tests/test_view_layout.py @@ -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(): diff --git a/shared/src/orisys_app_shared/prefs/store.py b/shared/src/orisys_app_shared/prefs/store.py index 9a38c15..65035d8 100644 --- a/shared/src/orisys_app_shared/prefs/store.py +++ b/shared/src/orisys_app_shared/prefs/store.py @@ -38,6 +38,7 @@ _DEFAULTS: dict[str, Any] = { "camera_id": "0", "source_mode": "camera", "video_path": "", + "last_video_directory": "", "show_cal_panel": False, "show_finger_3d": True, "sensor_type": "box", @@ -84,7 +85,7 @@ _DEFAULTS: dict[str, Any] = { #: Top-level view: "custom" (a hand-arranged layout), "live", "replay" or #: "calibration". Each view is a layout of the same window. "view_preset": "custom", - "splitter_sizes": [340, 480, 400], + "splitter_sizes": [560, 400, 480, 0], "window_default_size": [1440, 880], }