From f32d303d26d57eacd83891a120e48a461e978a30 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 4 Jul 2026 00:12:07 +0200 Subject: [PATCH] feat: persistent Library tab in ScanResultsPanel (survives per-file rebuilds) --- main.py | 30 +++++++++++++++++++++++++----- tests/test_ui_structure.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 626ff69..bc31684 100755 --- a/main.py +++ b/main.py @@ -897,9 +897,10 @@ class ScanResultsPanel(QWidget): # col 0: UserRole+2 = disabled (bool) # col 1: UserRole = end_time (float) - def __init__(self, db, parent=None): + def __init__(self, db, settings=None, parent=None): super().__init__(parent) self._db = db + self._settings = settings or QSettings("8cut", "8cut") self._filename = "" self._profile = "" self._neg_times: set[float] = set() @@ -916,6 +917,10 @@ class ScanResultsPanel(QWidget): self._tabs.currentChanged.connect(lambda: self.tab_changed.emit()) layout.addWidget(self._tabs) + # Persistent Library tab (index 0) — survives per-file model-tab rebuilds. + self._library = AudioLibraryTab(self._settings, key="audio_library") + self._tabs.addTab(self._library, "Library") + btn_row = QHBoxLayout() self._btn_neg = QPushButton("Add to Negatives") self._btn_neg.setToolTip("Mark selected rows as hard-negative training examples") @@ -963,6 +968,12 @@ class ScanResultsPanel(QWidget): return table return None + def _clear_model_tabs(self) -> None: + """Remove all per-model result tabs but keep the persistent Library tab.""" + for i in range(self._tabs.count() - 1, -1, -1): + if self._tabs.widget(i) is not self._library: + self._tabs.removeTab(i) + def load_for_file(self, filename: str, profile: str) -> None: """Load saved scan results for a file — DB reads run off the UI thread, the table rebuild happens in _on_scan_bundle_loaded when they finish.""" @@ -970,7 +981,7 @@ class ScanResultsPanel(QWidget): self._profile = profile # Show an empty panel immediately; the worker fills it in shortly. self._tabs.blockSignals(True) - self._tabs.clear() + self._clear_model_tabs() self._tabs.blockSignals(False) self._neg_times = set() self._exported_times = [] @@ -996,10 +1007,14 @@ class ScanResultsPanel(QWidget): self._neg_times = neg self._exported_times = exported self._tabs.blockSignals(True) - self._tabs.clear() + self._clear_model_tabs() for model, rows in results.items(): self._add_tab(model, rows) self._populate_version_combos() + # Select the first model tab (index 1), not the persistent Library at 0, + # so _current_table()/current_regions_with_orig() resolve before `loaded`. + if self._tabs.count() > 1: + self._tabs.setCurrentIndex(1) self._tabs.blockSignals(False) self.loaded.emit(filename) @@ -1215,6 +1230,9 @@ class ScanResultsPanel(QWidget): def current_model_name(self) -> str: """Return the model name of the currently active tab.""" + # A non-table tab (e.g. the persistent Library) has no model name. + if self._current_table() is None: + return "" idx = self._tabs.currentIndex() if idx >= 0: return self._tabs.tabText(idx).split(" (")[0] @@ -1642,7 +1660,9 @@ class ScanResultsPanel(QWidget): return out def has_results(self) -> bool: - return self._tabs.count() > 0 + # Count only per-model result tabs, not the persistent Library tab. + return any(self._tabs.widget(i) is not self._library + for i in range(self._tabs.count())) def undo(self) -> None: """Pop the last action from the undo stack and revert it.""" @@ -5276,7 +5296,7 @@ class MainWindow(QMainWindow): left_layout.addWidget(self._list_stack) # Scan results panel (right side) - self._scan_panel = ScanResultsPanel(self._db) + self._scan_panel = ScanResultsPanel(self._db, self._settings) self._scan_panel.seek_requested.connect(self._on_scan_seek) self._scan_panel.active_region_changed.connect( self._timeline.set_active_scan_region) diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index c7d4852..f6d1750 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -869,3 +869,33 @@ def test_audio_library_edit_emits(win, tmp_path): lib.edit_requested.connect(lambda p: got.append(p)) lib._on_edit() assert got == [str(a)] + + +def test_scan_panel_library_tab_persists(win): + import main as m + from PyQt6.QtWidgets import QTableWidget + sp = win._scan_panel + assert isinstance(sp._library, m.AudioLibraryTab) + titles = [sp._tabs.tabText(i) for i in range(sp._tabs.count())] + assert "Library" in titles + # add a fake model tab, then clear model tabs -> library survives + sp._tabs.addTab(QTableWidget(), "EAT_LARGE (3)") + sp._clear_model_tabs() + titles2 = [sp._tabs.tabText(i) for i in range(sp._tabs.count())] + assert titles2 == ["Library"] # only the library remains + assert sp._library is sp._tabs.widget(0) + + +def test_scan_reload_selects_model_tab_not_library(win): + # Reloading a scanned file must leave a MODEL tab current (index 1), not the + # Library at index 0 — otherwise _current_table() is None and the timeline + # shows no scan regions until the user manually clicks a model tab. + sp = win._scan_panel + sp._filename = "clip.mp4" + sp._profile = "prof" + results = {"EAT_LARGE": [(1, 0.0, 1.0, 0.9, False, 0.0, 1.0)]} + sp._on_scan_bundle_loaded("clip.mp4", "prof", set(), [], results) + assert sp._library is sp._tabs.widget(0) # Library stays at index 0 + assert sp._tabs.currentIndex() == 1 # a model tab is selected + assert sp._current_table() is not None # regions resolve immediately + assert sp.current_model_name() == "EAT_LARGE"