feat: highlight active scan region on timeline when row clicked

Draws a yellow outline around the scan region corresponding to the
selected/clicked row, so overlapping regions can be distinguished.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 11:34:23 +02:00
parent 1bdeb33a6f
commit a731fbfc32
+32
View File
@@ -709,6 +709,7 @@ class TrainWorker(QThread):
class ScanResultsPanel(QWidget):
"""Tabbed panel showing scan results per model, with disable/resize/negatives."""
seek_requested = pyqtSignal(float) # request main window to seek to time
active_region_changed = pyqtSignal(float, float) # (start, end) of focused row
export_requested = pyqtSignal(list) # emit list of (start, end, score) to export
negatives_requested = pyqtSignal(list) # emit list of start times to mark as hard negatives
negatives_removed = pyqtSignal(list) # emit list of start times to un-mark as negatives
@@ -974,6 +975,12 @@ class ScanResultsPanel(QWidget):
return self._tabs.tabText(idx).split(" (")[0]
return ""
def _emit_active_region(self, table: QTableWidget, row: int) -> None:
start = table.item(row, 0).data(Qt.ItemDataRole.UserRole + 1)
end = table.item(row, 1).data(Qt.ItemDataRole.UserRole)
if start is not None and end is not None:
self.active_region_changed.emit(float(start), float(end))
def _on_selection_changed(self, table: QTableWidget) -> None:
"""Handle keyboard navigation (arrows) — seek to start of current row."""
cur = table.currentItem()
@@ -985,6 +992,7 @@ class ScanResultsPanel(QWidget):
start = table.item(cur.row(), 0).data(Qt.ItemDataRole.UserRole + 1)
if start is not None:
self.seek_requested.emit(float(start))
self._emit_active_region(table, cur.row())
def _on_cell_clicked(self, table: QTableWidget, row: int, col: int) -> None:
"""Click Time → seek to start; click End → seek to last 3s of clip."""
@@ -996,6 +1004,7 @@ class ScanResultsPanel(QWidget):
start = table.item(row, 0).data(Qt.ItemDataRole.UserRole + 1)
if start is not None:
self.seek_requested.emit(float(start))
self._emit_active_region(table, row)
def _on_cell_changed(self, table: QTableWidget, row: int, col: int) -> None:
"""Handle user editing a Time or End cell — parse and update DB."""
@@ -1468,6 +1477,7 @@ class TimelineWidget(QWidget):
# (start, end, score, orig_start, orig_end)
self._scan_regions: list[tuple[float, float, float, float, float]] = []
self._scan_neg_times: set[float] = set()
self._active_scan_region: tuple[float, float] | None = None
# Waveform data (numpy array of 0-1 peak values, or None)
self._waveform = None
@@ -1538,14 +1548,25 @@ class TimelineWidget(QWidget):
normed.append((r[0], r[1], r[2], r[0], r[1]))
self._scan_regions = normed
self._scan_neg_times = neg_times or set()
self._active_scan_region = None
self._drag_idx = None
self.update()
def clear_scan_regions(self) -> None:
self._scan_regions = []
self._active_scan_region = None
self._drag_idx = None
self.update()
def set_active_scan_region(self, start: float, end: float) -> None:
self._active_scan_region = (start, end)
self.update()
def clear_active_scan_region(self) -> None:
if self._active_scan_region is not None:
self._active_scan_region = None
self.update()
def set_play_position(self, t: float | None) -> None:
# In lock mode, ignore mpv position updates while the user is dragging
# — the async seek hasn't caught up yet, so mpv reports stale values.
@@ -1715,6 +1736,15 @@ class TimelineWidget(QWidget):
p.drawLine(x1, rh, x1, h)
p.drawLine(x2, rh, x2, h)
# Active region highlight (bright yellow outline)
if self._active_scan_region is not None:
a_start, a_end = self._active_scan_region
ax1 = int(a_start / self._duration * w)
ax2 = int(a_end / self._duration * w)
p.setBrush(Qt.BrushStyle.NoBrush)
p.setPen(QPen(QColor(255, 210, 0), 2))
p.drawRect(ax1, rh + 1, max(ax2 - ax1, 1), h - rh - 2)
# ── export markers ────────────────────────────────────────────
if not self._scan_mode:
p.setFont(self._marker_font)
@@ -3199,6 +3229,8 @@ class MainWindow(QMainWindow):
# Scan results panel (right side)
self._scan_panel = ScanResultsPanel(self._db)
self._scan_panel.seek_requested.connect(self._on_scan_seek)
self._scan_panel.active_region_changed.connect(
self._timeline.set_active_scan_region)
self._scan_panel.export_requested.connect(self._on_scan_export)
self._scan_panel.negatives_requested.connect(self._on_scan_negatives)
self._scan_panel.negatives_removed.connect(self._on_scan_negatives_removed)