feat: waveform drag-select + wheel zoom (selection_changed/view_changed)
This commit is contained in:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user