From a1e90f135fbea3abeba66e2037e4bd8e68afd274 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 4 Jul 2026 21:41:35 +0200 Subject: [PATCH] feat: move audio editor selection with ctrl drag --- main.py | 49 ++++++++++++++++++++++++++++++++++++-- tests/test_ui_structure.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 33a402b..07c7f11 100755 --- a/main.py +++ b/main.py @@ -4101,6 +4101,8 @@ class AudioWaveform(QWidget): self._playhead: float | None = None self._markers: list[float] = [] self._drag_anchor: float | None = None + self._drag_mode: str | None = None + self._drag_orig_sel: tuple[float, float] | None = None self._press_x: float | None = None self.setFixedHeight(96) self.setToolTip("Waveform of the current audio area (↻ to refresh)") @@ -4153,6 +4155,8 @@ class AudioWaveform(QWidget): self._playhead = None self._markers = [] self._drag_anchor = None + self._drag_mode = None + self._drag_orig_sel = None self._press_x = None self.update() @@ -4208,18 +4212,50 @@ class AudioWaveform(QWidget): return self._sel[0] # drag end; pivot on start return None + def _selection_contains_x(self, x: float) -> bool: + if self._sel is None or self._view_dur <= 0: + return False + xs = self._t_to_px(self._sel[0]) + xe = self._t_to_px(self._sel[1]) + return min(xs, xe) <= x <= max(xs, xe) + + def _begin_move_at_x(self, x: float) -> None: + if self._sel is None: + return + self._drag_mode = "move" + self._drag_anchor = self._px_to_t(x) + self._drag_orig_sel = self._sel + def _begin_drag_at_x(self, x: float) -> None: t = self._px_to_t(x) anchor = self._selection_handle_anchor(x) if anchor is None: anchor = t + self._drag_mode = "select" self._drag_anchor = anchor + self._drag_orig_sel = self._sel self._drag_to_x(x) def _drag_to_x(self, x: float) -> None: if self._drag_anchor is None: return t = self._px_to_t(x) + if self._drag_mode == "move" and self._drag_orig_sel is not None: + s0, e0 = self._drag_orig_sel + width = e0 - s0 + delta = t - self._drag_anchor + lo = s0 + delta + hi = e0 + delta + vs, ve = self._view_start, self._view_start + self._view_dur + if self._view_dur > 0: + if lo < vs: + lo = vs + hi = lo + width + if hi > ve: + hi = ve + lo = hi - width + self.set_selection(lo, hi) + return lo, hi = min(self._drag_anchor, t), max(self._drag_anchor, t) vs, ve = self._view_start, self._view_start + self._view_dur if self._view_dur > 0: @@ -4236,6 +4272,8 @@ class AudioWaveform(QWidget): if self._drag_anchor is None: return self._drag_anchor = None + self._drag_mode = None + self._drag_orig_sel = None if self._sel is not None: self.selection_changed.emit(self._sel[0], self._sel[1]) @@ -4261,7 +4299,10 @@ class AudioWaveform(QWidget): if e.button() == Qt.MouseButton.LeftButton: x = e.position().x() self._press_x = x - if self._selection_handle_anchor(x) is not None: + if (e.modifiers() & Qt.KeyboardModifier.ControlModifier + and self._selection_contains_x(x)): + self._begin_move_at_x(x) + elif self._selection_handle_anchor(x) is not None: self._begin_drag_at_x(x) def mouseMoveEvent(self, e): @@ -4271,7 +4312,11 @@ class AudioWaveform(QWidget): elif (e.buttons() & Qt.MouseButton.LeftButton and self._press_x is not None and abs(x - self._press_x) >= self.CLICK_DRAG_PX): - self._begin_drag_at_x(self._press_x) + if (e.modifiers() & Qt.KeyboardModifier.ControlModifier + and self._selection_contains_x(self._press_x)): + self._begin_move_at_x(self._press_x) + else: + self._begin_drag_at_x(self._press_x) self._drag_to_x(x) def mouseReleaseEvent(self, e): diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index 9af840c..0bada50 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -671,6 +671,43 @@ def test_waveform_click_sets_playhead_without_changing_selection(win): assert abs((w.playhead() or 0.0) - 7.0) < 0.05 +def test_waveform_ctrl_drag_moves_selection_without_moving_playhead(win): + from PyQt6.QtCore import QEvent, QPointF, Qt + from PyQt6.QtGui import QMouseEvent + w = win._wave + w.resize(400, 96) + w.set_view(0.0, 10.0) + w.set_selection(2.0, 4.0) + w.set_playhead(7.0) + got_sel = [] + got_play = [] + w.selection_changed.connect(lambda s, e: got_sel.append((s, e))) + w.playhead_changed.connect(lambda t: got_play.append(t)) + + press = QMouseEvent( + QEvent.Type.MouseButtonPress, QPointF(w._t_to_px(3.0), 20), + Qt.MouseButton.LeftButton, Qt.MouseButton.LeftButton, + Qt.KeyboardModifier.ControlModifier) + move = QMouseEvent( + QEvent.Type.MouseMove, QPointF(w._t_to_px(5.0), 20), + Qt.MouseButton.NoButton, Qt.MouseButton.LeftButton, + Qt.KeyboardModifier.ControlModifier) + release = QMouseEvent( + QEvent.Type.MouseButtonRelease, QPointF(w._t_to_px(5.0), 20), + Qt.MouseButton.LeftButton, Qt.MouseButton.NoButton, + Qt.KeyboardModifier.ControlModifier) + w.mousePressEvent(press) + w.mouseMoveEvent(move) + w.mouseReleaseEvent(release) + + s, e = w.selection() + assert abs(s - 4.0) < 0.05 + assert abs(e - 6.0) < 0.05 + assert got_sel and abs(got_sel[-1][0] - 4.0) < 0.05 + assert got_play == [] + assert abs((w.playhead() or 0.0) - 7.0) < 0.05 + + def test_waveform_drag_emits_selection(win): w = win._wave w.resize(400, 96)