feat: adapt export button for selection; show markers in review mode

- Scan panel button now reads "Export Selected (N)" while rows are
  selected, mirroring the clip-count estimate used for full exports.
  Selection changes fire an explicit signal so the label refreshes.
- Export markers remain visible on the timeline in scan/review mode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 13:12:00 +02:00
parent def966a913
commit de8840e1eb
+40 -24
View File
@@ -716,6 +716,7 @@ class ScanResultsPanel(QWidget):
negatives_removed = pyqtSignal(list) # emit list of start times to un-mark as negatives negatives_removed = pyqtSignal(list) # emit list of start times to un-mark as negatives
tab_changed = pyqtSignal() # active tab changed tab_changed = pyqtSignal() # active tab changed
regions_edited = pyqtSignal() # a region was resized or toggled regions_edited = pyqtSignal() # a region was resized or toggled
selection_changed = pyqtSignal() # user's row selection changed
# UserRole slots per item: # UserRole slots per item:
# col 0: UserRole = row_id (int) # col 0: UserRole = row_id (int)
@@ -1028,6 +1029,7 @@ class ScanResultsPanel(QWidget):
def _on_selection_changed(self, table: QTableWidget) -> None: def _on_selection_changed(self, table: QTableWidget) -> None:
"""Handle keyboard navigation (arrows) — seek to start of current row.""" """Handle keyboard navigation (arrows) — seek to start of current row."""
self.selection_changed.emit()
cur = table.currentItem() cur = table.currentItem()
if cur is None or not cur.isSelected(): if cur is None or not cur.isSelected():
selected = table.selectedItems() selected = table.selectedItems()
@@ -1290,30 +1292,14 @@ class ScanResultsPanel(QWidget):
table = self._current_table() table = self._current_table()
if table is None: if table is None:
return return
selected_rows = sorted({idx.row() for idx in table.selectedIndexes()}) sel = self.selected_regions()
if selected_rows: if sel:
regions: list[tuple[float, float, float]] = [] self.export_requested.emit(sel, False)
for r in selected_rows:
item0 = table.item(r, 0)
if item0 is None:
continue
if item0.data(Qt.ItemDataRole.UserRole + 2):
continue # disabled
start = item0.data(Qt.ItemDataRole.UserRole + 1)
end = table.item(r, 1).data(Qt.ItemDataRole.UserRole)
if start is None or end is None:
continue
if float(start) in self._neg_times:
continue
score = float(table.item(r, 2).text())
regions.append((float(start), float(end), score))
replace_all = False
else: else:
regions = [r for r in self._get_tab_regions(table) regions = [r for r in self._get_tab_regions(table)
if r[0] not in self._neg_times] if r[0] not in self._neg_times]
replace_all = True
if regions: if regions:
self.export_requested.emit(regions, replace_all) self.export_requested.emit(regions, True)
def current_regions(self) -> list[tuple[float, float, float]]: def current_regions(self) -> list[tuple[float, float, float]]:
"""Return (start, end, score) for enabled rows in the active tab.""" """Return (start, end, score) for enabled rows in the active tab."""
@@ -1345,13 +1331,37 @@ class ScanResultsPanel(QWidget):
table.blockSignals(False) table.blockSignals(False)
return return
def set_export_count(self, n: int) -> None: def set_export_count(self, n: int, partial: bool = False) -> None:
"""Update the export button label with estimated clip count.""" """Update the export button label with estimated clip count."""
if n > 0: if partial and n > 0:
self._btn_export.setText(f"Export Selected ({n})")
elif n > 0:
self._btn_export.setText(f"Export Scan Results ({n})") self._btn_export.setText(f"Export Scan Results ({n})")
else: else:
self._btn_export.setText("Export Scan Results") self._btn_export.setText("Export Scan Results")
def selected_regions(self) -> list[tuple[float, float, float]]:
"""Return (start, end, score) for rows selected in the active tab,
excluding disabled and negative rows."""
table = self._current_table()
if table is None:
return []
rows = sorted({idx.row() for idx in table.selectedIndexes()})
out: list[tuple[float, float, float]] = []
for r in rows:
item0 = table.item(r, 0)
if item0 is None or item0.data(Qt.ItemDataRole.UserRole + 2):
continue
start = item0.data(Qt.ItemDataRole.UserRole + 1)
end = table.item(r, 1).data(Qt.ItemDataRole.UserRole)
if start is None or end is None:
continue
if float(start) in self._neg_times:
continue
score = float(table.item(r, 2).text())
out.append((float(start), float(end), score))
return out
def has_results(self) -> bool: def has_results(self) -> bool:
return self._tabs.count() > 0 return self._tabs.count() > 0
@@ -1817,7 +1827,6 @@ class TimelineWidget(QWidget):
p.drawRect(ax1, rh + 1, max(ax2 - ax1, 1), h - rh - 2) p.drawRect(ax1, rh + 1, max(ax2 - ax1, 1), h - rh - 2)
# ── export markers ──────────────────────────────────────────── # ── export markers ────────────────────────────────────────────
if not self._scan_mode:
p.setFont(self._marker_font) p.setFont(self._marker_font)
for (t, num, _path) in self._markers: for (t, num, _path) in self._markers:
mx = int(t / self._duration * w) mx = int(t / self._duration * w)
@@ -3308,6 +3317,7 @@ class MainWindow(QMainWindow):
self._scan_panel.negatives_removed.connect(self._on_scan_negatives_removed) self._scan_panel.negatives_removed.connect(self._on_scan_negatives_removed)
self._scan_panel.tab_changed.connect(self._on_scan_regions_edited) self._scan_panel.tab_changed.connect(self._on_scan_regions_edited)
self._scan_panel.regions_edited.connect(self._on_scan_regions_edited) self._scan_panel.regions_edited.connect(self._on_scan_regions_edited)
self._scan_panel.selection_changed.connect(self._update_scan_export_count)
self._sld_threshold.valueChanged.connect(self._on_threshold_changed) self._sld_threshold.valueChanged.connect(self._on_threshold_changed)
# Root: horizontal splitter # Root: horizontal splitter
@@ -4336,7 +4346,13 @@ class MainWindow(QMainWindow):
def _update_scan_export_count(self) -> None: def _update_scan_export_count(self) -> None:
"""Recalculate and display estimated clip count on the export button.""" """Recalculate and display estimated clip count on the export button."""
neg = self._scan_panel._neg_times neg = self._scan_panel._neg_times
sel = self._scan_panel.selected_regions()
if sel:
regions = sel
partial = True
else:
regions = [r for r in self._scan_panel.current_regions() if r[0] not in neg] regions = [r for r in self._scan_panel.current_regions() if r[0] not in neg]
partial = False
if not regions: if not regions:
self._scan_panel.set_export_count(0) self._scan_panel.set_export_count(0)
return return
@@ -4345,7 +4361,7 @@ class MainWindow(QMainWindow):
spread=self._spn_spread.value(), spread=self._spn_spread.value(),
) )
n = sum(len(g) for g in groups) n = sum(len(g) for g in groups)
self._scan_panel.set_export_count(n) self._scan_panel.set_export_count(n, partial=partial)
def _on_scan_export(self, regions: list, replace_all: bool = True) -> None: def _on_scan_export(self, regions: list, replace_all: bool = True) -> None:
"""Export clips from scan results panel. replace_all=False for partial.""" """Export clips from scan results panel. replace_all=False for partial."""