fix(studio): 回放视图里选了不是记录的视频时,明确说明原因并给出可行路径
- What: `_load_replay_video()` 先用与 `list_recordings()` 相同的判据确认"这是一条记录"(视频同目录 必须有 `forces.csv`);不是记录时不再把该目录当成记录打开,而是给出原因(状态栏 + 2D 面板常驻文案)、 在 rail 上保留该视频路径,并把「从视频重算力值」置为可用;`_can_recompute()` 统一"重算有没有东西可读" (记录或已选视频),`_sync_menu_checks`/`_refresh_replay_state`/`_on_recompute_toggled` 共用它; 回放按钮在"已选视频但不是记录"时给出对应 tooltip;`_start_playback()` 遇到零长度时间轴时改为明确提示, 不再静默取消勾选。 - Why: 用户报告"历史回放面板,选择视频后按回放键没有反应"。复现确认:`RecordingReader` 容忍空目录, 所以随便一段视频所在目录都会被"成功"打开成一条空记录,`duration_s()==0`,按回放→`_start_playback` 直接静默 return——按钮点了像没点。一条记录的定义(录像 + forces.csv + session.json)此前只写在 `list_recordings` 里,回放入口没有用同一个判据。 - Verification: 复现脚本三种状态(未选/裸视频/记录内视频)行为符合预期;新增三项 UI 用例 (裸视频被拒且说明原因并能重算、记录内视频可打开且能播、缺视频段的记录给出提示); apps/studio 全量通过;ui_check 576 项 0 失败(新增 6 项覆盖同一行为);locale_check 0 缺失。 - Not in this change: 让回放直接播放"非记录"的视频(那需要一条没有力值的纯视频通道;现在用「从视频重算」 覆盖这个需求);`RecordingReader` 对空目录的容忍行为(它要允许正在写入、尚未写 session.json 的记录)。
This commit is contained in:
@@ -901,15 +901,46 @@ class StudioMainWindow(QMainWindow):
|
||||
self._load_replay_video(Path(chosen))
|
||||
|
||||
def _load_replay_video(self, video: Path) -> bool:
|
||||
"""Open the recording a recorded video belongs to."""
|
||||
"""Open the recording a recorded video belongs to.
|
||||
|
||||
A **recording** is a directory that holds ``forces.csv`` beside the video —
|
||||
the same rule ``list_recordings`` uses, so the two cannot disagree about what
|
||||
a recording is. A video on its own is not one: it has no force rows and no
|
||||
timeline. Opening its folder anyway "succeeded" (the reader tolerates a
|
||||
directory with nothing in it) and then left the transport button playing a
|
||||
zero-length timeline, which is why choosing a video and pressing 回放 did
|
||||
nothing at all. So a bare video is refused with what is missing, and the rail
|
||||
keeps its path so 「从视频重算」 can still run the chain over it.
|
||||
"""
|
||||
video = Path(video)
|
||||
folder = video.parent if video.is_file() else video
|
||||
if not (folder / "forces.csv").is_file():
|
||||
if video.is_file():
|
||||
self.toolbar.set_video_path(str(video))
|
||||
self.prefs["video_path"] = str(video)
|
||||
self._set_source_mode("video")
|
||||
if not self.replay.is_loaded():
|
||||
# Nothing else is on screen, so the reason belongs in the panel that
|
||||
# would have shown the frames — not only in a status line that expires.
|
||||
self.arrow_panel.clear_preview(t("playback.video_only_panel"))
|
||||
self._refresh_replay_state()
|
||||
self._notify(t("playback.not_a_recording", name=video.name))
|
||||
return False
|
||||
if not self._load_recording(folder):
|
||||
return False
|
||||
self.toolbar.set_video_path(str(video))
|
||||
self._refresh_status_rail()
|
||||
return True
|
||||
|
||||
def _can_recompute(self) -> bool:
|
||||
"""Whether 「从视频重算」 has something to read: a recording, or a video file."""
|
||||
if self._view_preset != "replay":
|
||||
return False
|
||||
if self.replay.is_loaded():
|
||||
return True
|
||||
named = self.toolbar.video_path()
|
||||
return bool(named) and Path(named).is_file()
|
||||
|
||||
def _load_recording(self, directory: Path) -> bool:
|
||||
if self._view_preset != "replay":
|
||||
self._apply_view_preset("replay")
|
||||
@@ -951,10 +982,10 @@ class StudioMainWindow(QMainWindow):
|
||||
if segments:
|
||||
self.toolbar.set_video_path(str(segments[0]))
|
||||
self.toolbar.set_run_checked(self._play_timer.isActive())
|
||||
# Loading a recording is what makes a recompute pass possible at all.
|
||||
self.act_recompute.setEnabled(
|
||||
self._view_preset == "replay" and self.replay.is_loaded()
|
||||
)
|
||||
# Loading a recording is what makes a recompute pass possible at all — but a
|
||||
# chosen video file is enough to recompute *from*, even when it is not a
|
||||
# recording: that is the way to look at a video that has no forces.csv.
|
||||
self.act_recompute.setEnabled(self._can_recompute())
|
||||
self._update_run_enabled()
|
||||
self._refresh_status_rail()
|
||||
|
||||
@@ -987,7 +1018,10 @@ class StudioMainWindow(QMainWindow):
|
||||
def _start_playback(self) -> None:
|
||||
duration = self.replay.duration_s()
|
||||
if duration <= 0:
|
||||
# A zero-length timeline is not something to play, and silently un-checking
|
||||
# the button is how "pressing 回放 does nothing" felt to the operator.
|
||||
self.toolbar.set_run_checked(False)
|
||||
self._notify(t("playback.empty_recording"))
|
||||
return
|
||||
position = self.replay.position()
|
||||
if position >= duration - 0.02:
|
||||
@@ -1045,7 +1079,7 @@ class StudioMainWindow(QMainWindow):
|
||||
run *forward*, because the chain is stateful, so it is a pass rather than
|
||||
a scrub.
|
||||
"""
|
||||
wanted = bool(checked) and self._view_preset == "replay" and self.replay.is_loaded()
|
||||
wanted = bool(checked) and self._can_recompute()
|
||||
if self.act_recompute.isChecked() != wanted:
|
||||
self.act_recompute.blockSignals(True)
|
||||
self.act_recompute.setChecked(wanted)
|
||||
@@ -1338,9 +1372,7 @@ class StudioMainWindow(QMainWindow):
|
||||
self.act_finger_3d.blockSignals(True)
|
||||
self.act_finger_3d.setChecked(bool(self._show_finger_3d) if is_finger else False)
|
||||
self.act_finger_3d.blockSignals(False)
|
||||
self.act_recompute.setEnabled(
|
||||
self._view_preset == "replay" and self.replay.is_loaded()
|
||||
)
|
||||
self.act_recompute.setEnabled(self._can_recompute())
|
||||
self._apply_arrow_rotation_for_sensor()
|
||||
|
||||
def _current_source_kind(self) -> str:
|
||||
@@ -1486,9 +1518,16 @@ class StudioMainWindow(QMainWindow):
|
||||
# not a runnable sensor. While it plays it stays enabled so it can be
|
||||
# paused.
|
||||
loaded = self.replay.is_loaded()
|
||||
self.toolbar.set_run_tooltip_reason(
|
||||
"" if loaded else t("toolbar.playback_disabled")
|
||||
)
|
||||
if loaded:
|
||||
reason = ""
|
||||
elif self._can_recompute():
|
||||
# A video was chosen but it is not a recording: the transport cannot
|
||||
# play it, and the tooltip says which button can show it instead of
|
||||
# leaving the operator pressing a dead button.
|
||||
reason = t("toolbar.playback_needs_recording")
|
||||
else:
|
||||
reason = t("toolbar.playback_disabled")
|
||||
self.toolbar.set_run_tooltip_reason(reason)
|
||||
self.toolbar.set_run_enabled(loaded or self._play_timer.isActive())
|
||||
self.toolbar.set_record_enabled(
|
||||
self._running or self.recorder.is_recording
|
||||
|
||||
Reference in New Issue
Block a user