diff --git a/main.py b/main.py index db3c0e8..49b869c 100755 --- a/main.py +++ b/main.py @@ -4318,30 +4318,9 @@ class MainWindow(QMainWindow): self._transport_row = transport_row self._rebuild_subprofile_buttons() - # Row 2 — annotation + output path widgets now live in the Export tab - # of the control deck (_build_export_tab); path_row is no longer mounted. - - # Row 3 — crop & scan controls (encode/clip controls moved to the - # Export tab via _build_export_tab; crop/scan move to their own tabs - # in a later stage and stay mounted here until then). - settings_row = QHBoxLayout() - settings_row.addWidget(QLabel("Portrait:")) - settings_row.addWidget(self._cmb_portrait) - settings_row.addWidget(self._chk_rand_portrait) - settings_row.addWidget(self._chk_rand_square) - settings_row.addWidget(self._chk_track) - settings_row.addWidget(self._cmb_scan_model) - settings_row.addWidget(self._btn_model_history) - settings_row.addWidget(self._btn_scan) - settings_row.addWidget(self._btn_speech) - settings_row.addWidget(self._btn_scan_mode) - settings_row.addWidget(self._btn_hide_subcats) - settings_row.addWidget(self._btn_auto_export) - settings_row.addWidget(self._spn_auto_fuse) - settings_row.addWidget(self._sld_threshold) - settings_row.addWidget(self._btn_train) - settings_row.addWidget(self._btn_scan_all) - settings_row.addStretch() + # Row 2/3 — annotation, output path, crop and scan controls all live in + # the control deck's tabs now (_build_export_tab / _build_crop_tab / + # _build_scan_tab); path_row and settings_row are no longer mounted. right = QWidget() right_layout = QVBoxLayout(right) @@ -4354,7 +4333,8 @@ class MainWindow(QMainWindow): right_layout.addLayout(transport_row) right_layout.addWidget(self._build_control_deck()) self._build_export_tab() - right_layout.addLayout(settings_row) + self._build_crop_tab() + self._build_scan_tab() # Left: queue header + playlist self._btn_open = QPushButton("+ Open Files") @@ -4405,6 +4385,21 @@ class MainWindow(QMainWindow): self._build_menubar() self._build_status_bar() + # Reverse-sync the Scan tab's Review toggle back to the View ▸ Review + # mode action (the forward sync was wired in _build_menubar). Done here + # because _act_review only exists after _build_menubar(). setChecked + # does not re-emit on an unchanged value, so this cannot loop. + self._btn_scan_mode.toggled.connect(self._act_review.setChecked) + # Menu-only buttons (Train, Scan All, Sub) are reached via the menu bar + # now, but other code still references them (enable/disable, text). Keep + # the objects, re-parent to the window, and hide so they are not stray + # top-level windows. + for _b in (self._btn_train, self._btn_scan_all, self._btn_hide_subcats): + _b.setParent(self); _b.hide() + # Pin the deck height (after all tabs are populated) so switching tabs + # doesn't resize the video. + self._control_deck.setFixedHeight(self._control_deck.sizeHint().height()) + # Root: horizontal splitter splitter = QSplitter(Qt.Orientation.Horizontal) splitter.addWidget(left) @@ -4511,6 +4506,28 @@ class MainWindow(QMainWindow): g.addWidget(QLabel("Workers:"), 4, 0); g.addWidget(self._spn_workers, 4, 1) g.addWidget(self._btn_reexport, 4, 5) + def _build_crop_tab(self) -> None: + from PyQt6.QtWidgets import QGridLayout + g = QGridLayout(self._tab_crop) + g.setContentsMargins(8, 6, 8, 6); g.setHorizontalSpacing(8); g.setVerticalSpacing(6) + g.addWidget(QLabel("Portrait:"), 0, 0); g.addWidget(self._cmb_portrait, 0, 1) + g.addWidget(self._chk_rand_portrait, 1, 0, 1, 2) + g.addWidget(self._chk_rand_square, 2, 0, 1, 2) + g.addWidget(self._chk_track, 3, 0, 1, 2) + g.setRowStretch(4, 1); g.setColumnStretch(2, 1) + + def _build_scan_tab(self) -> None: + from PyQt6.QtWidgets import QGridLayout, QHBoxLayout + g = QGridLayout(self._tab_scan) + g.setContentsMargins(8, 6, 8, 6); g.setHorizontalSpacing(8); g.setVerticalSpacing(6) + model_row = QHBoxLayout() + model_row.addWidget(self._cmb_scan_model, 1); model_row.addWidget(self._btn_model_history) + g.addWidget(QLabel("Model:"), 0, 0); g.addLayout(model_row, 0, 1, 1, 3) + g.addWidget(self._btn_scan, 1, 0); g.addWidget(self._btn_auto_export, 1, 1) + g.addWidget(self._btn_speech, 1, 2); g.addWidget(self._btn_scan_mode, 1, 3) + g.addWidget(self._spn_auto_fuse, 2, 0); g.addWidget(self._sld_threshold, 2, 1) + g.setColumnStretch(3, 1) + # ── Menu bar ───────────────────────────────────────────── def _build_menubar(self) -> None: @@ -6070,6 +6087,7 @@ class MainWindow(QMainWindow): def _show_subcat_menu(self) -> None: from PyQt6.QtWidgets import QMenu, QWidgetAction, QCheckBox, QWidget, QVBoxLayout, QPushButton, QHBoxLayout + from PyQt6.QtGui import QCursor menu = QMenu(self) menu.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) base = os.path.basename(self._txt_folder.text()) @@ -6090,8 +6108,7 @@ class MainWindow(QMainWindow): folders = sorted(folder_set) if not folders: menu.addAction("(no subcategories)").setEnabled(False) - menu.exec(self._btn_hide_subcats.mapToGlobal( - self._btn_hide_subcats.rect().bottomLeft())) + menu.exec(QCursor.pos()) return container = QWidget() @@ -6144,8 +6161,7 @@ class MainWindow(QMainWindow): wa = QWidgetAction(menu) wa.setDefaultWidget(container) menu.addAction(wa) - menu.exec(self._btn_hide_subcats.mapToGlobal( - self._btn_hide_subcats.rect().bottomLeft())) + menu.exec(QCursor.pos()) def _disable_all_subcats(self) -> None: """Disable every enabled subcategory at once (across all videos).""" diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index cfca82f..de8b856 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -27,3 +27,30 @@ def win(app): def test_window_constructs(win): assert win.windowTitle().startswith("8-cut") + + +def test_status_bar_exists(win): + assert win.statusBar() is not None + + +def test_workers_spinbox_in_export_tab(win): + from PyQt6.QtWidgets import QSpinBox + assert win._spn_workers in win._tab_export.findChildren(QSpinBox) + + +def test_scan_button_in_scan_tab(win): + from PyQt6.QtWidgets import QPushButton + assert win._btn_scan in win._tab_scan.findChildren(QPushButton) + + +def test_portrait_combo_in_crop_tab(win): + from PyQt6.QtWidgets import QComboBox + assert win._cmb_portrait in win._tab_crop.findChildren(QComboBox) + + +def test_menu_only_buttons_not_in_deck(win): + from PyQt6.QtWidgets import QPushButton + deck_btns = win._control_deck.findChildren(QPushButton) + assert win._btn_train not in deck_btns + assert win._btn_scan_all not in deck_btns + assert win._btn_hide_subcats not in deck_btns