feat: waveform drag-select + wheel zoom (selection_changed/view_changed)

This commit is contained in:
2026-07-02 17:06:46 +02:00
parent 7ca894d585
commit 25fbf8142f
2 changed files with 110 additions and 0 deletions
+84
View File
@@ -3943,6 +3943,13 @@ class AudioWaveform(QWidget):
audio region. Populated on demand via manual refresh (no implicit UI-thread
decode)."""
selection_changed = pyqtSignal(float, float)
view_changed = pyqtSignal(float, float)
HANDLE_PX = 6
MIN_SPAN = 0.05
MIN_SEL = 0.01
def __init__(self, parent=None):
super().__init__(parent)
self._peaks: list[float] = []
@@ -3950,6 +3957,7 @@ class AudioWaveform(QWidget):
self._view_dur = 0.0
self._sel: tuple[float, float] | None = None
self._playhead: float | None = None
self._drag_anchor: float | None = None
self.setFixedHeight(96)
self.setToolTip("Waveform of the current audio area (↻ to refresh)")
@@ -4011,6 +4019,82 @@ class AudioWaveform(QWidget):
p.setPen(QPen(QColor(255, 255, 255, 160), 1))
p.drawLine(px, 0, px, h)
# --- pixel/time helpers -------------------------------------------------
def _px_to_t(self, x: float) -> float:
from core.waveform import x_to_t
return x_to_t(x, self.width(), self._view_start, self._view_dur)
def _t_to_px(self, t: float) -> int:
from core.waveform import t_to_x
return t_to_x(t, self.width(), self._view_start, self._view_dur)
# --- drag-select / zoom -------------------------------------------------
def _begin_drag_at_x(self, x: float) -> None:
t = self._px_to_t(x)
anchor = t
if self._sel is not None and self._view_dur > 0:
xs = self._t_to_px(self._sel[0])
xe = self._t_to_px(self._sel[1])
if abs(x - xs) <= self.HANDLE_PX:
anchor = self._sel[1] # drag start; pivot on end
elif abs(x - xe) <= self.HANDLE_PX:
anchor = self._sel[0] # drag end; pivot on start
self._drag_anchor = anchor
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)
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:
lo = max(vs, min(lo, ve))
hi = max(vs, min(hi, ve))
if hi - lo < self.MIN_SEL:
if hi + self.MIN_SEL <= ve or self._view_dur <= 0:
hi = lo + self.MIN_SEL # room above
else:
lo = hi - self.MIN_SEL # at the ceiling -> grow downward
self.set_selection(lo, hi)
def _end_drag(self) -> None:
if self._drag_anchor is None:
return
self._drag_anchor = None
if self._sel is not None:
self.selection_changed.emit(self._sel[0], self._sel[1])
def _zoom_at_x(self, x: float, factor: float) -> None:
if self._view_dur <= 0 or self.width() <= 0:
return
t = self._px_to_t(x)
new_dur = max(self.MIN_SPAN, self._view_dur * factor)
frac = x / self.width()
self._view_start = t - frac * new_dur
self._view_dur = new_dur
self.update()
self.view_changed.emit(self._view_start, self._view_dur)
# --- Qt events ----------------------------------------------------------
def mousePressEvent(self, e):
if e.button() == Qt.MouseButton.LeftButton:
self._begin_drag_at_x(e.position().x())
def mouseMoveEvent(self, e):
self._drag_to_x(e.position().x())
def mouseReleaseEvent(self, e):
if e.button() == Qt.MouseButton.LeftButton:
self._end_drag()
def wheelEvent(self, e):
dy = e.angleDelta().y()
if dy == 0:
return
factor = 0.8 if dy > 0 else 1.25
self._zoom_at_x(e.position().x(), factor)
class MainWindow(QMainWindow):
def __init__(self):