From 3c903c7188c40574b15037643585d884859a1aef Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Mon, 13 Apr 2026 17:11:32 +0200 Subject: [PATCH] feat: right-click keyframe diamond on timeline to delete it Co-Authored-By: Claude Opus 4.6 --- main.py | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/main.py b/main.py index 543ea29..7dc8d01 100755 --- a/main.py +++ b/main.py @@ -678,6 +678,7 @@ class TimelineWidget(QWidget): cursor_changed = pyqtSignal(float) # emits position in seconds seek_changed = pyqtSignal(float) # emits seek position (lock mode) marker_delete_requested = pyqtSignal(str) # emits output_path + keyframe_delete_requested = pyqtSignal(float) # emits keyframe time marker_clicked = pyqtSignal(float, str) # emits (start_time, output_path) marker_deselected = pyqtSignal() # double-click on empty space @@ -932,22 +933,38 @@ class TimelineWidget(QWidget): self._emit_seek() def contextMenuEvent(self, event): - if not self._hover_cache or self._duration <= 0: + if self._duration <= 0: return x = event.pos().x() w = self.width() - hit_path = None - for (frac, output_path) in self._hover_cache: - if abs(x - frac * w) <= 10: - hit_path = output_path + # Check keyframe diamonds first. + hit_kf_time = None + for (kt, _kc) in self._crop_keyframes: + kx = kt / self._duration * w + if abs(x - kx) <= 8: + hit_kf_time = kt break - if hit_path is None: + # Check export markers. + hit_path = None + if self._hover_cache: + for (frac, output_path) in self._hover_cache: + if abs(x - frac * w) <= 10: + hit_path = output_path + break + if hit_kf_time is None and hit_path is None: return from PyQt6.QtWidgets import QMenu menu = QMenu(self) - name = os.path.basename(hit_path) - action = menu.addAction(f"Delete marker: {name}") - if menu.exec(event.globalPos()) == action: + act_kf = None + act_marker = None + if hit_kf_time is not None: + act_kf = menu.addAction(f"Delete keyframe @ {format_time(hit_kf_time)}") + if hit_path is not None: + act_marker = menu.addAction(f"Delete marker: {os.path.basename(hit_path)}") + chosen = menu.exec(event.globalPos()) + if chosen and chosen == act_kf: + self.keyframe_delete_requested.emit(hit_kf_time) + elif chosen and chosen == act_marker: self.marker_delete_requested.emit(hit_path) def _seek(self, x: float): @@ -1713,6 +1730,7 @@ class MainWindow(QMainWindow): self._timeline.cursor_changed.connect(self._on_cursor_changed) self._timeline.seek_changed.connect(self._on_seek_changed) self._timeline.marker_delete_requested.connect(self._on_delete_marker) + self._timeline.keyframe_delete_requested.connect(self._on_delete_keyframe) self._mpv.time_pos_changed.connect(self._timeline.set_play_position) self._timeline.marker_clicked.connect(self._on_marker_clicked) self._timeline.marker_deselected.connect(self._on_marker_deselected) @@ -2267,6 +2285,15 @@ class MainWindow(QMainWindow): f"Deleted marker ({n} clip{'s' if n != 1 else ''})", 4000 ) + def _on_delete_keyframe(self, time: float) -> None: + self._crop_keyframes = [ + (t, c) for t, c in self._crop_keyframes + if abs(t - time) > 0.05 + ] + self._timeline.set_crop_keyframes(self._crop_keyframes) + _log(f"Deleted crop keyframe @ {format_time(time)} ({len(self._crop_keyframes)} remaining)") + self.statusBar().showMessage(f"Deleted keyframe @ {format_time(time)}", 3000) + def _on_marker_clicked(self, start_time: float, output_path: str) -> None: self._overwrite_path = output_path self._overwrite_group = self._db.get_group(output_path)