not sure about this commit
This commit is contained in:
@@ -194,6 +194,11 @@ class FramesPanel(Gtk.Box):
|
||||
self._scroll_to(widget)
|
||||
GLib.timeout_add(400, lambda: widget.remove_css_class("frame-highlight") or False)
|
||||
|
||||
def scroll_to_end(self):
|
||||
"""Scroll to the last frame."""
|
||||
if self._order and self._order[-1] in self._widgets:
|
||||
self._scroll_to(self._widgets[self._order[-1]])
|
||||
|
||||
def clear(self):
|
||||
"""Remove all items and reset state."""
|
||||
self._selected = None
|
||||
|
||||
@@ -21,14 +21,9 @@ import cairo
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
BAR_HEIGHT = 50
|
||||
BLOCK_GAP = 2
|
||||
BLOCK_COLOR = (0.25, 0.25, 0.30)
|
||||
BLOCK_ACTIVE_COLOR = (0.35, 0.45, 0.60)
|
||||
BLOCK_HOVER_COLOR = (0.30, 0.35, 0.40)
|
||||
BLOCK_GENERATING_COLOR = (0.30, 0.40, 0.35)
|
||||
BAR_COLOR = (0.20, 0.20, 0.25)
|
||||
CURSOR_COLOR = (0.9, 0.2, 0.2)
|
||||
MARKER_COLOR = (0.9, 0.8, 0.2)
|
||||
TEXT_COLOR = (0.8, 0.8, 0.8)
|
||||
|
||||
|
||||
class ScrubBar(Gtk.DrawingArea):
|
||||
@@ -170,80 +165,36 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
# -- Drawing --
|
||||
|
||||
def _draw(self, area, cr, width, height):
|
||||
if not self._manifest or self._total_duration <= 0:
|
||||
cr.set_source_rgb(0.15, 0.15, 0.15)
|
||||
cr.rectangle(0, 0, width, height)
|
||||
cr.fill()
|
||||
# Solid background — always full width
|
||||
cr.set_source_rgb(*BAR_COLOR)
|
||||
cr.rectangle(0, 0, width, height)
|
||||
cr.fill()
|
||||
|
||||
if self._total_duration <= 0:
|
||||
return
|
||||
|
||||
# Draw segment blocks
|
||||
for seg in self._manifest:
|
||||
x, w = self._segment_rect(seg, width)
|
||||
idx = seg["index"]
|
||||
|
||||
# Block color based on state
|
||||
if idx == self._active_index:
|
||||
cr.set_source_rgb(*BLOCK_ACTIVE_COLOR)
|
||||
elif idx == self._hover_index:
|
||||
cr.set_source_rgb(*BLOCK_HOVER_COLOR)
|
||||
elif self._proxy_states.get(idx) == "generating":
|
||||
cr.set_source_rgb(*BLOCK_GENERATING_COLOR)
|
||||
else:
|
||||
cr.set_source_rgb(*BLOCK_COLOR)
|
||||
|
||||
cr.rectangle(x, 0, w, height)
|
||||
cr.fill()
|
||||
|
||||
# Segment label
|
||||
dur = seg["duration"]
|
||||
m, s = divmod(int(dur), 60)
|
||||
label = f"S{idx}" if w < 40 else f"S{idx} {m}:{s:02d}"
|
||||
cr.set_source_rgb(*TEXT_COLOR)
|
||||
cr.set_font_size(11)
|
||||
cr.move_to(x + 4, height - 6)
|
||||
cr.show_text(label)
|
||||
|
||||
# Proxy state indicator
|
||||
state = self._proxy_states.get(idx)
|
||||
if state == "ready":
|
||||
cr.set_source_rgb(0.3, 0.7, 0.3)
|
||||
cr.arc(x + w - 8, 8, 3, 0, 6.28)
|
||||
cr.fill()
|
||||
elif state == "generating":
|
||||
cr.set_source_rgb(0.7, 0.7, 0.3)
|
||||
cr.arc(x + w - 8, 8, 3, 0, 6.28)
|
||||
cr.fill()
|
||||
|
||||
# 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.set_source_rgba(0, 0, 0, 0.6)
|
||||
cr.set_line_width(1.5)
|
||||
cr.rectangle(x0 - 0.5, y0 - 0.5, tw + 1, th + 1)
|
||||
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)
|
||||
# Scene markers
|
||||
cr.set_source_rgb(*MARKER_COLOR)
|
||||
for ts in self._scene_markers:
|
||||
mx = self._global_to_x(ts, width)
|
||||
if 0 <= mx <= width:
|
||||
cr.rectangle(mx, 0, 1, 6)
|
||||
cr.rectangle(mx, 0, 1, 5)
|
||||
cr.fill()
|
||||
|
||||
# Cursor
|
||||
@@ -256,11 +207,11 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
# -- Geometry helpers --
|
||||
|
||||
def _segment_rect(self, seg, total_width):
|
||||
"""Return (x, width) for a segment block."""
|
||||
"""Return (x, width) for a segment's region."""
|
||||
if self._total_duration <= 0:
|
||||
return 0, 0
|
||||
x = (seg["global_offset"] / self._total_duration) * total_width + BLOCK_GAP / 2
|
||||
w = (seg["duration"] / self._total_duration) * total_width - BLOCK_GAP
|
||||
x = (seg["global_offset"] / self._total_duration) * total_width
|
||||
w = (seg["duration"] / self._total_duration) * total_width
|
||||
return max(0, x), max(1, w)
|
||||
|
||||
def _global_to_x(self, global_time, total_width):
|
||||
@@ -287,43 +238,36 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
|
||||
def _on_pressed(self, gesture, n_press, x, y):
|
||||
width = self.get_width()
|
||||
gt = self._x_to_global(x, width)
|
||||
# Activate segment if different
|
||||
idx = self._segment_at_x(x, width)
|
||||
if idx >= 0:
|
||||
if idx != self._active_index:
|
||||
# New segment — activate it (proxy will be requested)
|
||||
self._active_index = idx
|
||||
self.emit("segment-activated", idx)
|
||||
else:
|
||||
# Already active — seek to click position
|
||||
gt = self._x_to_global(x, width)
|
||||
self.emit("scrub-position", gt)
|
||||
self.queue_draw()
|
||||
if idx >= 0 and idx != self._active_index:
|
||||
self._active_index = idx
|
||||
self.emit("segment-activated", idx)
|
||||
# Always seek to click position
|
||||
self.emit("scrub-position", gt)
|
||||
self.queue_draw()
|
||||
|
||||
def _on_released(self, gesture, n_press, x, y):
|
||||
self._scrubbing = False
|
||||
|
||||
def _on_motion(self, controller, x, y):
|
||||
width = self.get_width()
|
||||
old_hover = self._hover_index
|
||||
self._hover_index = self._segment_at_x(x, width)
|
||||
if self._hover_index != old_hover:
|
||||
self.queue_draw()
|
||||
|
||||
# If scrubbing (dragging within active block), emit position
|
||||
if self._scrubbing and self._active_index >= 0:
|
||||
if self._scrubbing:
|
||||
width = self.get_width()
|
||||
gt = self._x_to_global(x, width)
|
||||
gt = max(0, min(gt, self._total_duration))
|
||||
# Switch segment if drag crosses boundary
|
||||
idx = self._segment_at_x(x, width)
|
||||
if idx >= 0 and idx != self._active_index:
|
||||
self._active_index = idx
|
||||
self.emit("segment-activated", idx)
|
||||
self.emit("scrub-position", gt)
|
||||
|
||||
def _on_leave(self, controller):
|
||||
if self._hover_index != -1:
|
||||
self._hover_index = -1
|
||||
self.queue_draw()
|
||||
pass
|
||||
|
||||
def _on_drag_begin(self, gesture, start_x, start_y):
|
||||
width = self.get_width()
|
||||
idx = self._segment_at_x(start_x, width)
|
||||
if idx >= 0 and idx == self._active_index:
|
||||
self._scrubbing = True
|
||||
self._scrubbing = True
|
||||
|
||||
def _on_drag_update(self, gesture, offset_x, offset_y):
|
||||
if self._scrubbing:
|
||||
@@ -333,6 +277,10 @@ class ScrubBar(Gtk.DrawingArea):
|
||||
width = self.get_width()
|
||||
gt = self._x_to_global(x, width)
|
||||
gt = max(0, min(gt, self._total_duration))
|
||||
idx = self._segment_at_x(x, width)
|
||||
if idx >= 0 and idx != self._active_index:
|
||||
self._active_index = idx
|
||||
self.emit("segment-activated", idx)
|
||||
self.emit("scrub-position", gt)
|
||||
|
||||
def _on_drag_end(self, gesture, offset_x, offset_y):
|
||||
|
||||
@@ -173,6 +173,11 @@ class TranscriptPanel(Gtk.Box):
|
||||
self._selected.clear()
|
||||
self.emit("selection-changed")
|
||||
|
||||
def scroll_to_end(self):
|
||||
"""Scroll to the bottom (latest transcript)."""
|
||||
adj = self._scroll.get_vadjustment()
|
||||
GLib.idle_add(lambda: adj.set_value(adj.get_upper()) or False)
|
||||
|
||||
def highlight_nearest(self, timestamp: float) -> None:
|
||||
"""Scroll to and briefly highlight the transcript segment closest to *timestamp*."""
|
||||
if not self._order:
|
||||
|
||||
Reference in New Issue
Block a user