Give each tab its own full timeline and transport (PlayerPanel refactor)

Extracts a new PlayerPanel widget (video + its own seek slider/marker
strip + its own centered frame-step/play/mute transport row) into
player_panel.py. Both Source and Preview tabs now use one, so
switching tabs switches a complete, independent monitor -- its own
scrubber, its own play state, its own frame-stepping -- not just the
video frame with a single shared play button underneath.

main.py's global keyboard shortcuts (,/./space/left/right) now
dispatch to whichever panel is active via a small _active_panel()
helper, so they follow the same 'whichever monitor you're looking at'
rule as the on-screen controls.

MarkerStrip (clap detection) and the Mark Audio/Video/offset/export
row stay outside the tabs, at the MainWindow level -- marking and
exporting are inherently about the source file, not the preview clip,
so only the timeline+transport moved, not the whole marking workflow.

Updated every existing smoke test for the new win.source_panel.* /
win.preview_panel.* structure (previously flat win.player,
win.seek_slider, etc.), and added a new check to
smoke_test_preview.py confirming the Preview tab's slider is a
genuinely separate, functional widget -- dragging it seeks the preview
player without moving the source player, which is the actual thing
this change was for.

Verified the full suite on the virtual Xvfb display, plus a live
click-through (mark both events, build preview, confirm the Preview
tab shows its own populated timeline/transport, not the source's).
This commit is contained in:
ThePendulum
2026-07-26 03:58:23 +02:00
parent 0a8f9f45bd
commit 8c34a0f0c2
10 changed files with 309 additions and 258 deletions
+30 -9
View File
@@ -38,7 +38,8 @@ def main():
assert win.video_tabs.count() == 2
assert win.video_tabs.tabText(0) == "Source"
assert win.video_tabs.tabText(1) == "Preview"
assert win.video_tabs.widget(0) is win.player
assert win.video_tabs.widget(0) is win.source_panel
assert win.video_tabs.widget(1) is win.preview_panel
print("PASS: video area has Source and Preview tabs")
win._load(CLIP)
@@ -47,11 +48,11 @@ def main():
qapp.processEvents()
assert win.video_tabs.currentIndex() == 0, "loading a new source should land on the Source tab"
win.player.seek(5.000)
win.source_panel.player.seek(5.000)
qapp.processEvents()
win._mark_video()
win.player.seek(5.350)
win.source_panel.player.seek(5.350)
qapp.processEvents()
win._mark_audio()
@@ -77,21 +78,41 @@ def main():
time.sleep(0.3)
qapp.processEvents()
duration = win.preview_player.mpv.duration
duration = win.preview_panel.player.mpv.duration
print(f"preview clip duration: {duration:.3f}s (expected ~{win.PREVIEW_DURATION_SEC}s, clamped to file bounds)")
assert duration is not None and 0 < duration <= win.PREVIEW_DURATION_SEC + 0.5
assert win.preview_player.mpv.pause is False, "preview should auto-play"
assert win.preview_play_button.text() == "Pause"
assert win.preview_panel.player.mpv.pause is False, "preview should auto-play"
assert win.preview_panel.play_button.text() == "Pause"
# -- The Preview tab has its own functional timeline (not a static video
# + a single play button): its own seek slider, sized to the preview
# clip's own duration, that seeks the preview player independently of
# the Source tab's slider/player.
assert win.preview_panel.seek_slider is not win.source_panel.seek_slider
preview_duration_ms = win.preview_panel.seek_slider.maximum()
assert preview_duration_ms > 0
print(f"Preview tab's own seek slider range: 0..{preview_duration_ms}ms")
target_ms = int(preview_duration_ms * 0.7)
win.preview_panel.seek_slider.setValue(target_ms)
win.preview_panel.seek_slider.sliderMoved.emit(target_ms)
qapp.processEvents()
time.sleep(0.2)
qapp.processEvents()
preview_pos = win.preview_panel.player.get_position()
source_pos = win.source_panel.player.get_position()
print(f"dragged Preview tab's own slider to 70%: preview player now at {preview_pos:.3f}s, source player still at {source_pos:.3f}s")
assert abs(preview_pos - target_ms / 1000.0) < 0.5, "Preview tab's slider should seek the preview player"
print("PASS: Preview tab has its own independent, functional timeline")
# -- Switching back to Source shouldn't disturb the main player, and the
# preview player keeps running independently (two live mpv instances).
main_pos_before_switch = win.player.get_position()
main_pos_before_switch = win.source_panel.player.get_position()
win.video_tabs.setCurrentIndex(0)
qapp.processEvents()
main_pos_after_switch = win.player.get_position()
main_pos_after_switch = win.source_panel.player.get_position()
print(f"main player position: before switch={main_pos_before_switch:.3f}s after={main_pos_after_switch:.3f}s")
assert abs(main_pos_after_switch - main_pos_before_switch) < 0.1, "switching tabs shouldn't move the main player"
assert win.player.mpv.pause is True, "main player should still be paused (untouched by preview building)"
assert win.source_panel.player.mpv.pause is True, "main player should still be paused (untouched by preview building)"
print("PASS: switching tabs doesn't disturb the main player")
# -- Building a second preview swaps the file without leaking the first