feat: keyboard navigation for timeline
Arrow keys / J / L: step one frame; Shift = 1 second jump Space / P: toggle play/pause K: pause and return to cursor E: trigger export M: jump to next export marker (wraps) Keys are suppressed when a text field has focus. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@ from PyQt6.QtWidgets import (
|
|||||||
QComboBox, QDialog, QPlainTextEdit, QCheckBox,
|
QComboBox, QDialog, QPlainTextEdit, QCheckBox,
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal, QSettings
|
from PyQt6.QtCore import Qt, QThread, QTimer, pyqtSignal, QSettings
|
||||||
from PyQt6.QtGui import QPainter, QColor, QPen, QDragEnterEvent, QDropEvent, QCursor, QFont
|
from PyQt6.QtGui import QPainter, QColor, QPen, QDragEnterEvent, QDropEvent, QCursor, QFont, QKeyEvent
|
||||||
import mpv
|
import mpv
|
||||||
|
|
||||||
|
|
||||||
@@ -416,6 +416,14 @@ class MpvWidget(QFrame):
|
|||||||
return (self._player.width or 0, self._player.height or 0)
|
return (self._player.width or 0, self._player.height or 0)
|
||||||
return (0, 0)
|
return (0, 0)
|
||||||
|
|
||||||
|
def get_fps(self) -> float:
|
||||||
|
if self._player:
|
||||||
|
return self._player.container_fps or 25.0
|
||||||
|
return 25.0
|
||||||
|
|
||||||
|
def is_playing(self) -> bool:
|
||||||
|
return bool(self._player and not self._player.pause)
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
w = self.width()
|
w = self.width()
|
||||||
if w > 0:
|
if w > 0:
|
||||||
@@ -969,6 +977,53 @@ class MainWindow(QMainWindow):
|
|||||||
self._mpv.stop_loop()
|
self._mpv.stop_loop()
|
||||||
self._mpv.seek(self._cursor)
|
self._mpv.seek(self._cursor)
|
||||||
|
|
||||||
|
def _step_cursor(self, delta: float) -> None:
|
||||||
|
if not self._file_path:
|
||||||
|
return
|
||||||
|
dur = self._mpv.get_duration()
|
||||||
|
new_t = max(0.0, min(self._cursor + delta, max(0.0, dur - 8.0)))
|
||||||
|
self._timeline.set_cursor(new_t)
|
||||||
|
self._on_cursor_changed(new_t)
|
||||||
|
|
||||||
|
def _jump_to_next_marker(self) -> None:
|
||||||
|
markers = sorted(self._timeline._markers, key=lambda m: m[0])
|
||||||
|
if not markers:
|
||||||
|
return
|
||||||
|
for (t, _num, _path) in markers:
|
||||||
|
if t > self._cursor + 0.1:
|
||||||
|
self._step_cursor(t - self._cursor)
|
||||||
|
return
|
||||||
|
self._step_cursor(markers[0][0] - self._cursor) # wrap to first
|
||||||
|
|
||||||
|
def keyPressEvent(self, event: QKeyEvent) -> None:
|
||||||
|
focused = QApplication.focusWidget()
|
||||||
|
if isinstance(focused, (QLineEdit, QPlainTextEdit)):
|
||||||
|
super().keyPressEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
key = event.key()
|
||||||
|
shift = bool(event.modifiers() & Qt.KeyboardModifier.ShiftModifier)
|
||||||
|
frame = 1.0 / self._mpv.get_fps()
|
||||||
|
step = 1.0 if shift else frame
|
||||||
|
|
||||||
|
if key in (Qt.Key.Key_Left, Qt.Key.Key_J):
|
||||||
|
self._step_cursor(-step)
|
||||||
|
elif key in (Qt.Key.Key_Right, Qt.Key.Key_L):
|
||||||
|
self._step_cursor(step)
|
||||||
|
elif key in (Qt.Key.Key_Space, Qt.Key.Key_P):
|
||||||
|
if self._mpv.is_playing():
|
||||||
|
self._on_pause()
|
||||||
|
else:
|
||||||
|
self._on_play()
|
||||||
|
elif key == Qt.Key.Key_K:
|
||||||
|
self._on_pause()
|
||||||
|
elif key == Qt.Key.Key_E:
|
||||||
|
self._on_export()
|
||||||
|
elif key == Qt.Key.Key_M:
|
||||||
|
self._jump_to_next_marker()
|
||||||
|
else:
|
||||||
|
super().keyPressEvent(event)
|
||||||
|
|
||||||
# --- Export ---
|
# --- Export ---
|
||||||
|
|
||||||
def _pick_folder(self):
|
def _pick_folder(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user