feat: persistent Library tab in ScanResultsPanel (survives per-file rebuilds)
This commit is contained in:
@@ -897,9 +897,10 @@ class ScanResultsPanel(QWidget):
|
|||||||
# col 0: UserRole+2 = disabled (bool)
|
# col 0: UserRole+2 = disabled (bool)
|
||||||
# col 1: UserRole = end_time (float)
|
# col 1: UserRole = end_time (float)
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
def __init__(self, db, settings=None, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._db = db
|
self._db = db
|
||||||
|
self._settings = settings or QSettings("8cut", "8cut")
|
||||||
self._filename = ""
|
self._filename = ""
|
||||||
self._profile = ""
|
self._profile = ""
|
||||||
self._neg_times: set[float] = set()
|
self._neg_times: set[float] = set()
|
||||||
@@ -916,6 +917,10 @@ class ScanResultsPanel(QWidget):
|
|||||||
self._tabs.currentChanged.connect(lambda: self.tab_changed.emit())
|
self._tabs.currentChanged.connect(lambda: self.tab_changed.emit())
|
||||||
layout.addWidget(self._tabs)
|
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()
|
btn_row = QHBoxLayout()
|
||||||
self._btn_neg = QPushButton("Add to Negatives")
|
self._btn_neg = QPushButton("Add to Negatives")
|
||||||
self._btn_neg.setToolTip("Mark selected rows as hard-negative training examples")
|
self._btn_neg.setToolTip("Mark selected rows as hard-negative training examples")
|
||||||
@@ -963,6 +968,12 @@ class ScanResultsPanel(QWidget):
|
|||||||
return table
|
return table
|
||||||
return None
|
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:
|
def load_for_file(self, filename: str, profile: str) -> None:
|
||||||
"""Load saved scan results for a file — DB reads run off the UI thread,
|
"""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."""
|
the table rebuild happens in _on_scan_bundle_loaded when they finish."""
|
||||||
@@ -970,7 +981,7 @@ class ScanResultsPanel(QWidget):
|
|||||||
self._profile = profile
|
self._profile = profile
|
||||||
# Show an empty panel immediately; the worker fills it in shortly.
|
# Show an empty panel immediately; the worker fills it in shortly.
|
||||||
self._tabs.blockSignals(True)
|
self._tabs.blockSignals(True)
|
||||||
self._tabs.clear()
|
self._clear_model_tabs()
|
||||||
self._tabs.blockSignals(False)
|
self._tabs.blockSignals(False)
|
||||||
self._neg_times = set()
|
self._neg_times = set()
|
||||||
self._exported_times = []
|
self._exported_times = []
|
||||||
@@ -996,10 +1007,14 @@ class ScanResultsPanel(QWidget):
|
|||||||
self._neg_times = neg
|
self._neg_times = neg
|
||||||
self._exported_times = exported
|
self._exported_times = exported
|
||||||
self._tabs.blockSignals(True)
|
self._tabs.blockSignals(True)
|
||||||
self._tabs.clear()
|
self._clear_model_tabs()
|
||||||
for model, rows in results.items():
|
for model, rows in results.items():
|
||||||
self._add_tab(model, rows)
|
self._add_tab(model, rows)
|
||||||
self._populate_version_combos()
|
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._tabs.blockSignals(False)
|
||||||
self.loaded.emit(filename)
|
self.loaded.emit(filename)
|
||||||
|
|
||||||
@@ -1215,6 +1230,9 @@ class ScanResultsPanel(QWidget):
|
|||||||
|
|
||||||
def current_model_name(self) -> str:
|
def current_model_name(self) -> str:
|
||||||
"""Return the model name of the currently active tab."""
|
"""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()
|
idx = self._tabs.currentIndex()
|
||||||
if idx >= 0:
|
if idx >= 0:
|
||||||
return self._tabs.tabText(idx).split(" (")[0]
|
return self._tabs.tabText(idx).split(" (")[0]
|
||||||
@@ -1642,7 +1660,9 @@ class ScanResultsPanel(QWidget):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
def has_results(self) -> bool:
|
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:
|
def undo(self) -> None:
|
||||||
"""Pop the last action from the undo stack and revert it."""
|
"""Pop the last action from the undo stack and revert it."""
|
||||||
@@ -5276,7 +5296,7 @@ class MainWindow(QMainWindow):
|
|||||||
left_layout.addWidget(self._list_stack)
|
left_layout.addWidget(self._list_stack)
|
||||||
|
|
||||||
# Scan results panel (right side)
|
# 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.seek_requested.connect(self._on_scan_seek)
|
||||||
self._scan_panel.active_region_changed.connect(
|
self._scan_panel.active_region_changed.connect(
|
||||||
self._timeline.set_active_scan_region)
|
self._timeline.set_active_scan_region)
|
||||||
|
|||||||
@@ -869,3 +869,33 @@ def test_audio_library_edit_emits(win, tmp_path):
|
|||||||
lib.edit_requested.connect(lambda p: got.append(p))
|
lib.edit_requested.connect(lambda p: got.append(p))
|
||||||
lib._on_edit()
|
lib._on_edit()
|
||||||
assert got == [str(a)]
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user