改善 Studio 回放布局与传感器切换体验
缩短界面提示,保存上次视频目录,并防止旧结果绘入 Finger 3D。针对性测试及可见界面复测通过。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user