diff --git a/main.py b/main.py index 539cd6d..5441115 100755 --- a/main.py +++ b/main.py @@ -1030,7 +1030,8 @@ class TimelineWidget(QWidget): if abs(x - frac * w) <= 10: t = frac * self._duration self.marker_clicked.emit(t, output_path) - self._seek(x) + if not self._locked: + self._seek(x) return self.marker_deselected.emit() self._seek(x) @@ -1336,12 +1337,17 @@ class MpvWidget(QWidget): except SystemError: pass - def play_loop(self, a: float, b: float): + def play_loop(self, a: float, b: float, resume: bool = False): self._player["ab-loop-a"] = a self._player["ab-loop-b"] = min(b, self._player.duration or b) - self._player.seek(a, "absolute") + if not resume: + self._player.seek(a, "absolute") self._player.pause = False + def update_loop_end(self, b: float): + """Adjust the B point of the current loop without seeking.""" + self._player["ab-loop-b"] = min(b, self._player.duration or b) + def stop_loop(self): self._player["ab-loop-a"] = "no" self._player["ab-loop-b"] = "no" @@ -1797,12 +1803,20 @@ class PlaylistWidget(QListWidget): class _KeyFilter(QObject): - """Suppress global keyboard shortcuts when a text input widget has focus.""" + """Suppress global keyboard shortcuts when a text input widget has focus, + and release focus from input widgets on click-away.""" + _INPUT_TYPES = (QSpinBox, QDoubleSpinBox, QLineEdit, QComboBox) + def eventFilter(self, obj, event): from PyQt6.QtCore import QEvent if event.type() == QEvent.Type.ShortcutOverride and isinstance(obj, QLineEdit): event.accept() return True + if event.type() == QEvent.Type.MouseButtonPress: + if not isinstance(obj, self._INPUT_TYPES): + focused = QApplication.focusWidget() + if isinstance(focused, self._INPUT_TYPES): + focused.clearFocus() return super().eventFilter(obj, event) @@ -2293,9 +2307,14 @@ class MainWindow(QMainWindow): ) QShortcut(QKeySequence("K"), self, context=ctx).activated.connect(self._on_pause) QShortcut(QKeySequence("E"), self, context=ctx).activated.connect(self._on_export) + for i in range(1, 10): + QShortcut(QKeySequence(str(i)), self, context=ctx).activated.connect( + lambda _, idx=i - 1: self._export_subprofile(idx) + ) QShortcut(QKeySequence("M"), self, context=ctx).activated.connect(self._jump_to_next_marker) QShortcut(QKeySequence("N"), self, context=ctx).activated.connect(self._playlist.advance) QShortcut(QKeySequence("G"), self, context=ctx).activated.connect(self._btn_lock.toggle) + QShortcut(QKeySequence("A"), self, context=ctx).activated.connect(self._autoclip) for key in ("?", "F1"): QShortcut(QKeySequence(key), self, context=ctx).activated.connect(self._show_shortcuts) @@ -2320,12 +2339,14 @@ class MainWindow(QMainWindow): "Space / PPlay / Pause" "KPause and snap to cursor" "EExport" + "1–9Export to subprofile 1–9" "MJump to next marker" "NNext file in playlist" "GToggle cursor lock" + "AAutoclip — fit clip count to pause position" "? / F1This help" "
" - "Double-click markerEnter overwrite mode" + "Double-click markerEnter overwrite mode (locked: jump to end of clip span)" "Right-click markerDelete clip group" "Click video / crop barReposition portrait crop" "" @@ -2430,6 +2451,10 @@ class MainWindow(QMainWindow): self._settings.setValue("subprofiles", self._subprofiles) self._rebuild_subprofile_buttons() + def _export_subprofile(self, idx: int): + if idx < len(self._subprofiles): + self._on_export(folder_suffix=self._subprofiles[idx]) + def _remove_subprofile(self, name: str): if name in self._subprofiles: self._subprofiles.remove(name) @@ -2585,6 +2610,20 @@ class MainWindow(QMainWindow): self._show_status(f"Deleted keyframe @ {format_time(time)}", 3000) 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. + if self._btn_lock.isChecked(): + 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() + spread = meta["spread"] or self._spn_spread.value() if meta else self._spn_spread.value() + next_pos = start_time + 8.0 + (clip_count - 1) * spread + self._cursor = next_pos + self._timeline.set_cursor(next_pos) + self._mpv.seek(next_pos) + self._lbl_time.setText(f"{format_time(next_pos)} / {format_time(self._mpv.get_duration())}") + self._update_next_label() + self._preview_timer.start() + self._show_status(f"Cursor → end of {os.path.basename(os.path.dirname(output_path))}", 3000) + return self._overwrite_path = output_path self._overwrite_group = self._db.get_group(output_path) n = len(self._overwrite_group) @@ -2900,26 +2939,38 @@ class MainWindow(QMainWindow): if self._mpv.is_playing(): self._on_pause() else: - self._on_play() + self._on_play(resume=True) @property def _clip_span(self) -> float: """Total time covered by the overlapping clips.""" return 8.0 + (self._spn_clips.value() - 1) * self._spn_spread.value() - def _on_play(self): + def _on_play(self, resume: bool = False): if not self._file_path: return - self._mpv.play_loop(self._cursor, self._cursor + self._clip_span) + self._mpv.play_loop(self._cursor, self._cursor + self._clip_span, resume=resume) def _update_play_loop(self): if self._file_path and self._mpv.is_playing(): - self._mpv.play_loop(self._cursor, self._cursor + self._clip_span) + self._mpv.update_loop_end(self._cursor + self._clip_span) def _on_pause(self): self._mpv.stop_loop() - self._mpv.seek(self._cursor) - self._timeline.set_play_position(None) + + def _autoclip(self): + """Set clip count to fit the current pause position.""" + if not self._file_path: + return + play_t = self._timeline._play_pos + if play_t is None or play_t <= self._cursor: + return + elapsed = play_t - self._cursor + spread = self._spn_spread.value() + # n clips span 8 + (n-1)*spread seconds + n = int((elapsed - 8.0) / spread) + 1 + n = max(1, n) + self._spn_clips.setValue(n) def _step_cursor(self, delta: float) -> None: if not self._file_path: