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
+18 -17
View File
@@ -1,14 +1,14 @@
"""Verifies arrow-key skips preserve whichever play/pause state the user
was already in, instead of always forcing a pause.
Calls the real MainWindow._seek_relative() method directly rather than
Calls the real PlayerPanel.seek_relative() method directly rather than
simulating the QShortcut keypress: QTest.keyClick against this shortcut was
observed to be unreliable in this headless setup even with Qt's
active-window state forced (see smoke_test_arrow_seek.py's note) --
sometimes the simulated key silently doesn't reach the shortcut at all, and
a debug script isolated that PlayerWidget.seek() itself works correctly
while playing, so the flakiness is in the synthetic-keypress dispatch, not
the app. The QShortcut -> _seek_relative wiring itself (a single line) was
the app. The QShortcut -> seek_relative wiring itself (a single line) was
already independently confirmed with real xdotool keypresses against a
live window in an earlier session.
"""
@@ -37,37 +37,38 @@ def main():
qapp.processEvents()
# -- Case 1: paused before the skip -> stays paused after
assert win.player.mpv.pause is True
before_pos = win.player.get_position()
win._seek_relative(5.0)
assert win.source_panel.player.mpv.pause is True
before_pos = win.source_panel.player.get_position()
win.source_panel.seek_relative(5.0)
qapp.processEvents()
time.sleep(0.2)
qapp.processEvents()
after_pos = win.player.get_position()
after_pos = win.source_panel.player.get_position()
print(f"paused case: before={before_pos:.3f}s after={after_pos:.3f}s")
assert win.player.mpv.pause is True, "should still be paused after skip"
assert win.play_button.text() == "Play"
assert win.source_panel.player.mpv.pause is True, "should still be paused after skip"
assert win.source_panel.play_button.text() == "Play"
assert after_pos > before_pos + 4.0, "should have actually skipped forward"
print("PASS: stays paused across arrow-key skip when it started paused")
# -- Case 2: playing before the skip -> stays playing after
win.player.seek(0.0)
win.player.play()
win.play_button.setText("Pause")
win.source_panel.player.seek(0.0)
win.source_panel.player.play()
win.source_panel.play_button.setText("Pause")
qapp.processEvents()
time.sleep(0.2)
before_pos = win.player.get_position()
win._seek_relative(5.0)
before_pos = win.source_panel.player.get_position()
win.source_panel.seek_relative(5.0)
qapp.processEvents()
time.sleep(0.2)
qapp.processEvents()
assert win.player.mpv.pause is False, "should still be playing after skip"
assert win.play_button.text() == "Pause"
after_pos = win.player.get_position()
assert win.source_panel.player.mpv.pause is False, "should still be playing after skip"
assert win.source_panel.play_button.text() == "Pause"
after_pos = win.source_panel.player.get_position()
print(f"playing case: before={before_pos:.3f}s after={after_pos:.3f}s (should have jumped ~5s and kept playing)")
assert after_pos > before_pos + 4.0, "should have actually skipped forward"
win.player.shutdown()
win.source_panel.shutdown()
win.preview_panel.shutdown()
print("\nPRESERVE PLAY STATE SMOKE TEST PASSED")