feat: right-click keyframe diamond on timeline to delete it

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 17:11:32 +02:00
parent bef08be091
commit 3c903c7188
+32 -5
View File
@@ -678,6 +678,7 @@ class TimelineWidget(QWidget):
cursor_changed = pyqtSignal(float) # emits position in seconds cursor_changed = pyqtSignal(float) # emits position in seconds
seek_changed = pyqtSignal(float) # emits seek position (lock mode) seek_changed = pyqtSignal(float) # emits seek position (lock mode)
marker_delete_requested = pyqtSignal(str) # emits output_path 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_clicked = pyqtSignal(float, str) # emits (start_time, output_path)
marker_deselected = pyqtSignal() # double-click on empty space marker_deselected = pyqtSignal() # double-click on empty space
@@ -932,22 +933,38 @@ class TimelineWidget(QWidget):
self._emit_seek() self._emit_seek()
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
if not self._hover_cache or self._duration <= 0: if self._duration <= 0:
return return
x = event.pos().x() x = event.pos().x()
w = self.width() w = self.width()
# 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
# Check export markers.
hit_path = None hit_path = None
if self._hover_cache:
for (frac, output_path) in self._hover_cache: for (frac, output_path) in self._hover_cache:
if abs(x - frac * w) <= 10: if abs(x - frac * w) <= 10:
hit_path = output_path hit_path = output_path
break break
if hit_path is None: if hit_kf_time is None and hit_path is None:
return return
from PyQt6.QtWidgets import QMenu from PyQt6.QtWidgets import QMenu
menu = QMenu(self) menu = QMenu(self)
name = os.path.basename(hit_path) act_kf = None
action = menu.addAction(f"Delete marker: {name}") act_marker = None
if menu.exec(event.globalPos()) == action: 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) self.marker_delete_requested.emit(hit_path)
def _seek(self, x: float): def _seek(self, x: float):
@@ -1713,6 +1730,7 @@ class MainWindow(QMainWindow):
self._timeline.cursor_changed.connect(self._on_cursor_changed) self._timeline.cursor_changed.connect(self._on_cursor_changed)
self._timeline.seek_changed.connect(self._on_seek_changed) self._timeline.seek_changed.connect(self._on_seek_changed)
self._timeline.marker_delete_requested.connect(self._on_delete_marker) 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._mpv.time_pos_changed.connect(self._timeline.set_play_position)
self._timeline.marker_clicked.connect(self._on_marker_clicked) self._timeline.marker_clicked.connect(self._on_marker_clicked)
self._timeline.marker_deselected.connect(self._on_marker_deselected) 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 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: def _on_marker_clicked(self, start_time: float, output_path: str) -> None:
self._overwrite_path = output_path self._overwrite_path = output_path
self._overwrite_group = self._db.get_group(output_path) self._overwrite_group = self._db.get_group(output_path)