frame fix
This commit is contained in:
201
cht/ui/transcript_panel.py
Normal file
201
cht/ui/transcript_panel.py
Normal file
@@ -0,0 +1,201 @@
|
||||
"""
|
||||
TranscriptPanel: vertical transcript list with single/multi-selection.
|
||||
|
||||
Uses Gtk.ListBox. New rows are always added; auto-scroll is suppressed
|
||||
while a selection is active.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "4.0")
|
||||
from gi.repository import Gtk, GLib, GObject
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TranscriptPanel(Gtk.Box):
|
||||
"""Vertical transcript list with single/multi-selection."""
|
||||
|
||||
__gsignals__ = {
|
||||
"selection-changed": (GObject.SignalFlags.RUN_FIRST, None, ()),
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=0, **kwargs)
|
||||
|
||||
self._rows: dict[str, Gtk.ListBoxRow] = {}
|
||||
self._texts: dict[str, str] = {}
|
||||
self._order: list[str] = []
|
||||
self._selected: list[str] = []
|
||||
|
||||
label = Gtk.Label(label="Transcript")
|
||||
label.add_css_class("heading")
|
||||
label.set_margin_top(4)
|
||||
label.set_margin_bottom(4)
|
||||
self.append(label)
|
||||
|
||||
self._list = Gtk.ListBox()
|
||||
self._list.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
self._list.connect("row-activated", self._on_row_activated)
|
||||
|
||||
self._scroll = Gtk.ScrolledWindow()
|
||||
self._scroll.set_vexpand(True)
|
||||
self._scroll.set_min_content_height(150)
|
||||
self._scroll.set_child(self._list)
|
||||
self.append(self._scroll)
|
||||
|
||||
# -- Properties --
|
||||
|
||||
@property
|
||||
def selected(self) -> list[str]:
|
||||
return list(self._selected)
|
||||
|
||||
@property
|
||||
def selected_texts(self) -> list[str]:
|
||||
return [self._texts[sid] for sid in self._selected if sid in self._texts]
|
||||
|
||||
@property
|
||||
def order(self) -> list[str]:
|
||||
return list(self._order)
|
||||
|
||||
@property
|
||||
def has_selection(self) -> bool:
|
||||
return len(self._selected) > 0
|
||||
|
||||
# -- Public API --
|
||||
|
||||
def add_items(self, segments):
|
||||
"""Append new segments. Always adds; only auto-scrolls if no selection."""
|
||||
for seg in segments:
|
||||
if seg.id in self._rows:
|
||||
continue
|
||||
self._make_row(seg)
|
||||
|
||||
if not self._selected:
|
||||
adj = self._scroll.get_vadjustment()
|
||||
GLib.idle_add(lambda: adj.set_value(adj.get_upper()) or False)
|
||||
|
||||
def select(self, seg_id: str, extend: bool = False):
|
||||
"""Select a segment. extend=True adds to selection."""
|
||||
if not extend:
|
||||
self._clear_visual()
|
||||
self._selected.clear()
|
||||
|
||||
if seg_id in self._selected:
|
||||
if not extend:
|
||||
self.emit("selection-changed")
|
||||
return
|
||||
self._selected.remove(seg_id)
|
||||
if seg_id in self._rows:
|
||||
self._rows[seg_id].remove_css_class("frame-selected")
|
||||
self.emit("selection-changed")
|
||||
return
|
||||
|
||||
self._selected.append(seg_id)
|
||||
if seg_id in self._rows:
|
||||
self._rows[seg_id].add_css_class("frame-selected")
|
||||
GLib.timeout_add(50, self._scroll_to_row, self._rows[seg_id])
|
||||
self.emit("selection-changed")
|
||||
|
||||
def select_adjacent(self, delta: int, extend: bool = False):
|
||||
"""Move selection by delta. Shift-extend grows/shrinks range."""
|
||||
if not self._order:
|
||||
return
|
||||
if not self._selected:
|
||||
idx = 0 if delta > 0 else len(self._order) - 1
|
||||
self.select(self._order[idx], extend=False)
|
||||
return
|
||||
|
||||
if extend and len(self._selected) > 1:
|
||||
last = self._selected[-1]
|
||||
prev = self._selected[-2]
|
||||
try:
|
||||
last_idx = self._order.index(last)
|
||||
prev_idx = self._order.index(prev)
|
||||
except ValueError:
|
||||
last_idx = prev_idx = 0
|
||||
if (delta < 0 and last_idx > prev_idx) or (delta > 0 and last_idx < prev_idx):
|
||||
self._selected.remove(last)
|
||||
if last in self._rows:
|
||||
self._rows[last].remove_css_class("frame-selected")
|
||||
if self._selected and self._selected[-1] in self._rows:
|
||||
GLib.timeout_add(50, self._scroll_to_row, self._rows[self._selected[-1]])
|
||||
self.emit("selection-changed")
|
||||
return
|
||||
|
||||
last = self._selected[-1]
|
||||
try:
|
||||
cur = self._order.index(last)
|
||||
except ValueError:
|
||||
cur = 0
|
||||
idx = cur + delta
|
||||
if idx < 0 or idx >= len(self._order):
|
||||
return
|
||||
self.select(self._order[idx], extend=extend)
|
||||
|
||||
def clear_selection(self):
|
||||
"""Deselect all, then flush any pending items."""
|
||||
if not self._selected:
|
||||
return
|
||||
self._clear_visual()
|
||||
self._selected.clear()
|
||||
self.emit("selection-changed")
|
||||
|
||||
def clear(self):
|
||||
"""Remove all items and reset state."""
|
||||
self._selected.clear()
|
||||
self._rows.clear()
|
||||
self._texts.clear()
|
||||
self._order.clear()
|
||||
while child := self._list.get_first_child():
|
||||
self._list.remove(child)
|
||||
|
||||
# -- Internal --
|
||||
|
||||
def _on_row_activated(self, listbox, row):
|
||||
seg_id = getattr(row, "_seg_id", None)
|
||||
if seg_id:
|
||||
self.select(seg_id)
|
||||
|
||||
def _make_row(self, seg):
|
||||
m1, s1 = divmod(int(seg.start), 60)
|
||||
m2, s2 = divmod(int(seg.end), 60)
|
||||
text = f"{seg.id} [{m1:02d}:{s1:02d}-{m2:02d}:{s2:02d}] {seg.text}"
|
||||
|
||||
row_label = Gtk.Label(label=text)
|
||||
row_label.set_xalign(0)
|
||||
row_label.set_wrap(True)
|
||||
row_label.set_margin_start(8)
|
||||
row_label.set_margin_end(8)
|
||||
row_label.set_margin_top(2)
|
||||
row_label.set_margin_bottom(2)
|
||||
|
||||
row = Gtk.ListBoxRow()
|
||||
row.set_child(row_label)
|
||||
row.set_activatable(True)
|
||||
row._seg_id = seg.id
|
||||
|
||||
self._list.append(row)
|
||||
self._rows[seg.id] = row
|
||||
self._texts[seg.id] = seg.text
|
||||
self._order.append(seg.id)
|
||||
|
||||
def _clear_visual(self):
|
||||
for sid in self._selected:
|
||||
if sid in self._rows:
|
||||
self._rows[sid].remove_css_class("frame-selected")
|
||||
|
||||
def _scroll_to_row(self, row):
|
||||
adj = self._scroll.get_vadjustment()
|
||||
alloc = row.get_allocation()
|
||||
y, h = alloc.y, alloc.height
|
||||
if h <= 0:
|
||||
return False
|
||||
page = adj.get_page_size()
|
||||
val = adj.get_value()
|
||||
if y < val:
|
||||
adj.set_value(y)
|
||||
elif y + h > val + page:
|
||||
adj.set_value(y + h - page)
|
||||
return False
|
||||
Reference in New Issue
Block a user