""" WaveformWidget: GTK4 DrawingArea that renders waveform peaks with a playhead. Driven by Timeline "changed" signal — redraws when cursor or duration changes. Peak data is set externally via set_peaks() from GLib.idle_add. """ import logging import math import numpy as np import gi gi.require_version("Gtk", "4.0") from gi.repository import Gtk, GLib log = logging.getLogger(__name__) class WaveformWidget(Gtk.Box): """Waveform display synced to Timeline state.""" def __init__(self, timeline, **kwargs): super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs) self._timeline = timeline self._peaks = None self._bucket_duration = 0.05 label = Gtk.Label(label="Waveform") label.add_css_class("heading") label.set_margin_top(4) label.set_margin_bottom(4) self.append(label) self._area = Gtk.DrawingArea() self._area.set_content_height(250) self._area.set_hexpand(True) self._area.set_vexpand(True) self._area.set_draw_func(self._draw) self.append(self._area) timeline.connect("changed", self._on_timeline_changed) def set_peaks(self, peaks, bucket_duration): """Update peak data. Call from GLib.idle_add.""" self._peaks = peaks self._bucket_duration = bucket_duration self._area.queue_draw() def _on_timeline_changed(self, timeline): self._area.queue_draw() def _draw(self, area, cr, width, height): # Background cr.set_source_rgb(0.1, 0.1, 0.12) cr.rectangle(0, 0, width, height) cr.fill() state = self._timeline.state duration = state.duration mid_y = height / 2 # Center line cr.set_source_rgba(0.3, 0.3, 0.35, 1.0) cr.set_line_width(1) cr.move_to(0, mid_y) cr.line_to(width, mid_y) cr.stroke() # Draw peaks — always stretch to fill full width if self._peaks is not None and len(self._peaks) > 0: n_peaks = len(self._peaks) max_peak = np.max(self._peaks) if np.max(self._peaks) > 0 else 1.0 for x in range(width): idx = int((x / width) * n_peaks) if 0 <= idx < n_peaks: val = self._peaks[idx] / max_peak bar_h = val * (height * 0.45) cr.set_source_rgba(0.2, 0.6 + val * 0.4, 0.3, 0.85) cr.rectangle(x, mid_y - bar_h, 1, bar_h * 2) cr.fill() # Scene markers and playhead use timeline duration x_duration = max(duration, 0.1) cr.set_source_rgba(1.0, 1.0, 0.3, 0.3) cr.set_line_width(1) for marker in state.scene_markers: mx = (marker / x_duration) * width if 0 <= mx <= width: cr.move_to(mx, 0) cr.line_to(mx, height) cr.stroke() # Playhead px = (state.cursor / x_duration) * width cr.set_source_rgba(1.0, 0.3, 0.3, 0.9) cr.set_line_width(2) cr.move_to(px, 0) cr.line_to(px, height) cr.stroke()