fix: anchor locked marker review at start
This commit is contained in:
@@ -2528,16 +2528,16 @@ class TimelineWidget(QWidget):
|
|||||||
p.drawText(mx + 1, rh + 2, 13, 12,
|
p.drawText(mx + 1, rh + 2, 13, 12,
|
||||||
Qt.AlignmentFlag.AlignCenter, str(num))
|
Qt.AlignmentFlag.AlignCenter, str(num))
|
||||||
|
|
||||||
# ── scan mode cursor + playback line ─────────────────────────
|
# ── scan/locked playback line ────────────────────────────────
|
||||||
if self._scan_mode:
|
if self._scan_mode:
|
||||||
# Export cursor (dim)
|
# Export cursor (dim)
|
||||||
p.setPen(QPen(QColor(255, 255, 255, 80), 1))
|
p.setPen(QPen(QColor(255, 255, 255, 80), 1))
|
||||||
p.drawLine(x_start, rh, x_start, h)
|
p.drawLine(x_start, rh, x_start, h)
|
||||||
# Playback position (bright green)
|
if ((self._scan_mode or self._locked)
|
||||||
if self._play_pos is not None and self._play_pos >= 0:
|
and self._play_pos is not None and self._play_pos >= 0):
|
||||||
px = int(self._time_to_x(self._play_pos))
|
px = int(self._time_to_x(self._play_pos))
|
||||||
p.setPen(QPen(QColor(80, 255, 80, 220), 2))
|
p.setPen(QPen(QColor(80, 255, 80, 220), 2))
|
||||||
p.drawLine(px, rh, px, h)
|
p.drawLine(px, rh, px, h)
|
||||||
|
|
||||||
# ── crop keyframe diamonds ────────────────────────────────────
|
# ── crop keyframe diamonds ────────────────────────────────────
|
||||||
if self._crop_keyframes and self._duration > 0:
|
if self._crop_keyframes and self._duration > 0:
|
||||||
@@ -5977,7 +5977,7 @@ class MainWindow(QMainWindow):
|
|||||||
"<tr><td><b>Ctrl+Z</b></td><td>Undo last scan panel action</td></tr>"
|
"<tr><td><b>Ctrl+Z</b></td><td>Undo last scan panel action</td></tr>"
|
||||||
"<tr><td><b>? / F1</b></td><td>This help</td></tr>"
|
"<tr><td><b>? / F1</b></td><td>This help</td></tr>"
|
||||||
"<tr><td colspan='2'><hr></td></tr>"
|
"<tr><td colspan='2'><hr></td></tr>"
|
||||||
"<tr><td><b>Double-click marker</b></td><td>Enter overwrite mode (locked: jump to end of clip span)</td></tr>"
|
"<tr><td><b>Double-click marker</b></td><td>Enter overwrite mode (locked: review from start to end)</td></tr>"
|
||||||
"<tr><td><b>Right-click marker</b></td><td>Delete clip group</td></tr>"
|
"<tr><td><b>Right-click marker</b></td><td>Delete clip group</td></tr>"
|
||||||
"<tr><td><b>Click video / crop bar</b></td><td>Reposition portrait crop</td></tr>"
|
"<tr><td><b>Click video / crop bar</b></td><td>Reposition portrait crop</td></tr>"
|
||||||
"<tr><td><b>Shift+drag scan region edge</b></td><td>Resize scan region</td></tr>"
|
"<tr><td><b>Shift+drag scan region edge</b></td><td>Resize scan region</td></tr>"
|
||||||
@@ -7013,22 +7013,36 @@ class MainWindow(QMainWindow):
|
|||||||
self._show_status(f"Deleted keyframe @ {format_time(time)}", 3000)
|
self._show_status(f"Deleted keyframe @ {format_time(time)}", 3000)
|
||||||
|
|
||||||
def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
|
def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
|
||||||
# In lock mode, move cursor to the end of this marker's span.
|
# In lock mode, keep the marker start anchored and scrub the playhead
|
||||||
|
# to the marker end so the highlighted zone matches what is reviewed.
|
||||||
if self._btn_lock.isChecked():
|
if self._btn_lock.isChecked():
|
||||||
meta = self._db.get_by_output_path(output_path)
|
meta = self._db.get_by_output_path(output_path)
|
||||||
clip_count = meta["clip_count"] or self._spn_clips.value() if meta else self._spn_clips.value()
|
clip_count = (
|
||||||
clip_dur = meta.get("clip_duration", self._clip_dur) if meta else self._clip_dur
|
meta.get("clip_count") or self._spn_clips.value()
|
||||||
spread = meta["spread"] or self._spn_spread.value() if meta else self._spn_spread.value()
|
) if meta else self._spn_clips.value()
|
||||||
next_pos = start_time + clip_dur + (clip_count - 1) * spread
|
clip_dur = (
|
||||||
self._cursor = next_pos
|
meta.get("clip_duration") or self._clip_dur
|
||||||
self._timeline.set_cursor(next_pos)
|
) if meta else self._clip_dur
|
||||||
self._mpv.seek(next_pos)
|
spread = (
|
||||||
self._lbl_time.setText(f"{format_time(next_pos)} / {format_time(self._mpv.get_duration())}")
|
meta.get("spread") or self._spn_spread.value()
|
||||||
|
) if meta else self._spn_spread.value()
|
||||||
|
self._spn_clips.setValue(int(clip_count))
|
||||||
|
self._spn_clip_dur.setValue(float(clip_dur))
|
||||||
|
self._spn_spread.setValue(float(spread))
|
||||||
|
clip_span = float(clip_dur) + (int(clip_count) - 1) * float(spread)
|
||||||
|
dur = self._mpv.get_duration()
|
||||||
|
end_pos = min(start_time + clip_span, dur) if dur else start_time + clip_span
|
||||||
|
self._cursor = start_time
|
||||||
|
self._timeline.set_cursor(start_time)
|
||||||
|
self._timeline.set_clip_span(clip_span, float(clip_dur), float(spread))
|
||||||
|
self._timeline.set_play_position(end_pos)
|
||||||
|
self._mpv.seek(end_pos)
|
||||||
|
self._lbl_time.setText(f"{format_time(end_pos)} / {format_time(dur)}")
|
||||||
self._update_next_label()
|
self._update_next_label()
|
||||||
self._preview_timer.start()
|
self._preview_timer.start()
|
||||||
stem = os.path.splitext(os.path.basename(output_path))[0]
|
stem = os.path.splitext(os.path.basename(output_path))[0]
|
||||||
group_label = stem.rsplit("_", 1)[0]
|
group_label = stem.rsplit("_", 1)[0]
|
||||||
self._show_status(f"Cursor → end of {group_label}", 3000)
|
self._show_status(f"Reviewing {group_label}: start locked, playhead at end", 3000)
|
||||||
return
|
return
|
||||||
self._cursor = start_time
|
self._cursor = start_time
|
||||||
self._timeline.set_cursor(start_time)
|
self._timeline.set_cursor(start_time)
|
||||||
|
|||||||
@@ -407,6 +407,56 @@ def test_timeline_audio_mode_lock_click_scrubs_playhead_not_region(win):
|
|||||||
assert got_seek and abs(got_seek[-1] - 12.0) < 0.1
|
assert got_seek and abs(got_seek[-1] - 12.0) < 0.1
|
||||||
|
|
||||||
|
|
||||||
|
def test_timeline_lock_mode_paints_playhead_line(win):
|
||||||
|
from PyQt6.QtGui import QColor, QImage
|
||||||
|
tl = win._timeline
|
||||||
|
tl._duration = 20.0
|
||||||
|
tl._view_start = 0.0
|
||||||
|
tl._view_span = 20.0
|
||||||
|
tl.resize(400, 160)
|
||||||
|
tl.set_audio_mode(False)
|
||||||
|
tl.set_clip_span(8.0, 8.0, 3.0)
|
||||||
|
tl.set_cursor(4.0)
|
||||||
|
tl._locked = True
|
||||||
|
tl.set_play_position(7.0)
|
||||||
|
|
||||||
|
img = QImage(tl.size(), QImage.Format.Format_ARGB32)
|
||||||
|
img.fill(QColor(0, 0, 0))
|
||||||
|
tl.render(img)
|
||||||
|
|
||||||
|
x = int(tl._time_to_x(7.0))
|
||||||
|
y = tl._SCROLLBAR_H + tl._RULER_H + 24
|
||||||
|
px = QColor(img.pixel(x, y))
|
||||||
|
assert px.green() > 200
|
||||||
|
assert px.green() > px.red() + 80
|
||||||
|
|
||||||
|
|
||||||
|
def test_locked_marker_click_anchors_start_and_shows_end_playhead(win, monkeypatch):
|
||||||
|
win._file_path = "/x/video.mp4"
|
||||||
|
win._timeline._duration = 60.0
|
||||||
|
win._spn_clip_dur.setValue(8.0)
|
||||||
|
win._spn_clips.setValue(1)
|
||||||
|
win._spn_spread.setValue(3.0)
|
||||||
|
win._timeline.set_clip_span(win._clip_span, win._clip_dur, win._spn_spread.value())
|
||||||
|
win._btn_lock.setChecked(True)
|
||||||
|
seeks = []
|
||||||
|
monkeypatch.setattr(win._mpv, "seek", lambda t: seeks.append(t))
|
||||||
|
monkeypatch.setattr(win._mpv, "get_duration", lambda: 60.0)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
win._db,
|
||||||
|
"get_by_output_path",
|
||||||
|
lambda _path: {"clip_count": 3, "clip_duration": 6.0, "spread": 2.0},
|
||||||
|
)
|
||||||
|
|
||||||
|
win._on_marker_clicked(10.0, "/tmp/clip_001.mp4")
|
||||||
|
|
||||||
|
assert abs(win._cursor - 10.0) < 0.01
|
||||||
|
assert abs(win._timeline._cursor - 10.0) < 0.01
|
||||||
|
assert abs(win._timeline._clip_span - 10.0) < 0.01
|
||||||
|
assert abs((win._timeline._play_pos or 0.0) - 20.0) < 0.01
|
||||||
|
assert seeks == [20.0]
|
||||||
|
|
||||||
|
|
||||||
def test_timeline_audio_mode_follows_deck(win):
|
def test_timeline_audio_mode_follows_deck(win):
|
||||||
win._control_deck.setCurrentWidget(win._tab_audio)
|
win._control_deck.setCurrentWidget(win._tab_audio)
|
||||||
assert win._timeline._audio_mode is True
|
assert win._timeline._audio_mode is True
|
||||||
|
|||||||
Reference in New Issue
Block a user