very nice
This commit is contained in:
@@ -9,9 +9,12 @@ Drag within a block to scrub frame-by-frame at mouse speed.
|
||||
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, GObject, Pango
|
||||
gi.require_version("GdkPixbuf", "2.0")
|
||||
from gi.repository import Gtk, Gdk, GLib, GObject, GdkPixbuf
|
||||
|
||||
import cairo
|
||||
|
||||
@@ -49,6 +52,7 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
self._proxy_states = {} # segment_index → "generating" | "ready"
|
||||
self._scene_markers = [] # global timestamps
|
||||
self._scrubbing = False
|
||||
self._frame_thumbs = [] # list of {timestamp, surface} — cairo surfaces
|
||||
|
||||
self.set_draw_func(self._draw)
|
||||
|
||||
@@ -77,6 +81,12 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
self._total_duration = sum(s["duration"] for s in manifest)
|
||||
self.queue_draw()
|
||||
|
||||
def set_duration(self, duration: float) -> None:
|
||||
"""Update total duration (from Timeline, overrides manifest sum if larger)."""
|
||||
if duration > self._total_duration:
|
||||
self._total_duration = duration
|
||||
self.queue_draw()
|
||||
|
||||
def set_cursor(self, global_time: float) -> None:
|
||||
"""Update the cursor position (from Timeline)."""
|
||||
self._cursor = global_time
|
||||
@@ -97,6 +107,66 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
self._proxy_states[segment_index] = state
|
||||
self.queue_draw()
|
||||
|
||||
def set_frames(self, frames: list[dict]) -> None:
|
||||
"""Set frame thumbnails. Each dict: {timestamp, path}.
|
||||
|
||||
Loads thumbnails scaled to fit the bar height and caches as cairo surfaces.
|
||||
"""
|
||||
self._frame_thumbs = []
|
||||
thumb_h = BAR_HEIGHT - 4 # 2px margin top/bottom
|
||||
thumb_w = int(thumb_h * 16 / 9) # assume 16:9 aspect
|
||||
for f in frames:
|
||||
path = f.get("path")
|
||||
ts = f.get("timestamp", 0)
|
||||
if not path or not Path(path).exists():
|
||||
continue
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
|
||||
str(path), thumb_w, thumb_h, True
|
||||
)
|
||||
surface = self._pixbuf_to_surface(pixbuf)
|
||||
self._frame_thumbs.append({
|
||||
"timestamp": ts,
|
||||
"surface": surface,
|
||||
"width": pixbuf.get_width(),
|
||||
"height": pixbuf.get_height(),
|
||||
})
|
||||
except Exception as e:
|
||||
log.debug("Thumb load failed for %s: %s", path, e)
|
||||
self.queue_draw()
|
||||
|
||||
def add_frame(self, timestamp: float, path: str) -> None:
|
||||
"""Add a single frame thumbnail incrementally."""
|
||||
if not Path(path).exists():
|
||||
return
|
||||
thumb_h = BAR_HEIGHT - 4
|
||||
thumb_w = int(thumb_h * 16 / 9)
|
||||
try:
|
||||
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, thumb_w, thumb_h, True)
|
||||
surface = self._pixbuf_to_surface(pixbuf)
|
||||
self._frame_thumbs.append({
|
||||
"timestamp": timestamp,
|
||||
"surface": surface,
|
||||
"width": pixbuf.get_width(),
|
||||
"height": pixbuf.get_height(),
|
||||
})
|
||||
self.queue_draw()
|
||||
except Exception as e:
|
||||
log.debug("Thumb load failed for %s: %s", path, e)
|
||||
|
||||
@staticmethod
|
||||
def _pixbuf_to_surface(pixbuf):
|
||||
"""Convert a GdkPixbuf to a cairo ImageSurface."""
|
||||
w, h = pixbuf.get_width(), pixbuf.get_height()
|
||||
has_alpha = pixbuf.get_has_alpha()
|
||||
fmt = cairo.FORMAT_ARGB32 if has_alpha else cairo.FORMAT_RGB24
|
||||
surface = cairo.ImageSurface(fmt, w, h)
|
||||
cr = cairo.Context(surface)
|
||||
Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
|
||||
cr.paint()
|
||||
surface.flush()
|
||||
return surface
|
||||
|
||||
# -- Drawing --
|
||||
|
||||
def _draw(self, area, cr, width, height):
|
||||
@@ -144,7 +214,31 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
cr.arc(x + w - 8, 8, 3, 0, 6.28)
|
||||
cr.fill()
|
||||
|
||||
# Scene markers
|
||||
# Frame thumbnails at their timestamp positions
|
||||
for thumb in self._frame_thumbs:
|
||||
tx = self._global_to_x(thumb["timestamp"], width)
|
||||
tw, th = thumb["width"], thumb["height"]
|
||||
# Center thumbnail on its timestamp, vertically centered in bar
|
||||
x0 = tx - tw / 2
|
||||
y0 = (height - th) / 2
|
||||
cr.save()
|
||||
# Dark outline for separation against any background
|
||||
cr.set_source_rgba(0, 0, 0, 0.7)
|
||||
cr.set_line_width(2)
|
||||
cr.rectangle(x0 - 1, y0 - 1, tw + 2, th + 2)
|
||||
cr.stroke()
|
||||
# Thumbnail
|
||||
cr.set_source_surface(thumb["surface"], x0, y0)
|
||||
cr.rectangle(x0, y0, tw, th)
|
||||
cr.fill()
|
||||
# Bright inner border
|
||||
cr.set_source_rgba(1, 1, 1, 0.5)
|
||||
cr.set_line_width(1)
|
||||
cr.rectangle(x0, y0, tw, th)
|
||||
cr.stroke()
|
||||
cr.restore()
|
||||
|
||||
# Scene markers (on top of thumbs)
|
||||
cr.set_source_rgb(*MARKER_COLOR)
|
||||
for ts in self._scene_markers:
|
||||
mx = self._global_to_x(ts, width)
|
||||
|
||||
Reference in New Issue
Block a user