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:
@@ -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()
|
||||||
hit_path = None
|
# Check keyframe diamonds first.
|
||||||
for (frac, output_path) in self._hover_cache:
|
hit_kf_time = None
|
||||||
if abs(x - frac * w) <= 10:
|
for (kt, _kc) in self._crop_keyframes:
|
||||||
hit_path = output_path
|
kx = kt / self._duration * w
|
||||||
|
if abs(x - kx) <= 8:
|
||||||
|
hit_kf_time = kt
|
||||||
break
|
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
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user