diff --git a/main.py b/main.py index fde58ff..2794a8c 100755 --- a/main.py +++ b/main.py @@ -4405,17 +4405,23 @@ class MainWindow(QMainWindow): transport_row.addStretch() transport_row.addWidget(self._lbl_next) transport_row.addWidget(self._btn_export) - # Subprofile export buttons sit right after Export + transport_row.addWidget(self._btn_cancel) + transport_row.addWidget(self._btn_delete) + self._transport_row = transport_row + + # Row 1b — subcategory (subprofile) export buttons live on their own + # centered row so the (often many) "▸ name" buttons don't crowd the + # transport controls. Stretches on both ends keep the group centered. + subprofile_row = QHBoxLayout() + subprofile_row.addStretch() self._subprofile_btns: list[QPushButton] = [] - self._sub_insert_anchor = self._btn_cancel # buttons inserted before this self._btn_add_sub = QPushButton("+") self._btn_add_sub.setFixedWidth(28) self._btn_add_sub.setToolTip("Add a subprofile — exports to folder_suffix") self._btn_add_sub.clicked.connect(self._add_subprofile) - transport_row.addWidget(self._btn_add_sub) - transport_row.addWidget(self._btn_cancel) - transport_row.addWidget(self._btn_delete) - self._transport_row = transport_row + subprofile_row.addWidget(self._btn_add_sub) + subprofile_row.addStretch() + self._subprofile_row = subprofile_row self._rebuild_subprofile_buttons() # Row 2/3 — annotation, output path, crop and scan controls all live in @@ -4431,6 +4437,7 @@ class MainWindow(QMainWindow): right_layout.addWidget(self._timeline) right_layout.addWidget(self._crop_bar) right_layout.addLayout(transport_row) + right_layout.addLayout(self._subprofile_row) right_layout.addWidget(self._build_control_deck()) self._build_export_tab() self._build_crop_tab() @@ -5581,17 +5588,17 @@ class MainWindow(QMainWindow): # ── Subprofiles ────────────────────────────────────────── def _rebuild_subprofile_buttons(self): - """Recreate the per-subprofile export buttons in the transport row.""" + """Recreate the per-subprofile export buttons on the subprofile row.""" for btn in self._format_btns: - self._transport_row.removeWidget(btn) btn.setParent(None) self._format_btns.clear() for btn in self._subprofile_btns: - self._transport_row.removeWidget(btn) + self._subprofile_row.removeWidget(btn) btn.deleteLater() self._subprofile_btns.clear() - # Find where to insert: right after the main Export button. - anchor = self._transport_row.indexOf(self._btn_add_sub) + # Insert before the "+" add button (which sits before the trailing + # stretch), so the buttons stay centered on the row. + anchor = self._subprofile_row.indexOf(self._btn_add_sub) has_file = bool(self._file_path) for i, name in enumerate(self._subprofiles): btn = QPushButton(f"▸ {name}") @@ -5599,7 +5606,7 @@ class MainWindow(QMainWindow): btn.setToolTip(f"Export to folder_{name} (right-click to remove)") btn.setEnabled(has_file) btn.clicked.connect(lambda _, s=name: self._on_export(folder_suffix=s)) - self._transport_row.insertWidget(anchor + i, btn) + self._subprofile_row.insertWidget(anchor + i, btn) self._subprofile_btns.append(btn) self._rebuild_format_buttons() # Keep the Edit ▸ Subprofiles ▸ Remove submenu in sync. Guarded because @@ -6122,7 +6129,7 @@ class MainWindow(QMainWindow): if sub_btn.isHidden(): continue suffix = sub_btn.text().removeprefix("▸ ") - sub_idx = self._transport_row.indexOf(sub_btn) + 1 + sub_idx = self._subprofile_row.indexOf(sub_btn) + 1 for j, (label, ratio) in enumerate(formats): btn = QPushButton(label) btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) @@ -6132,7 +6139,7 @@ class MainWindow(QMainWindow): btn.clicked.connect( lambda _, s=suffix, r=ratio: self._on_export( folder_suffix=s, force_ratio=r)) - self._transport_row.insertWidget(sub_idx + j, btn) + self._subprofile_row.insertWidget(sub_idx + j, btn) self._format_btns.append(btn) def _on_rand_toggle(self, _checked: bool = False) -> None: @@ -6695,11 +6702,16 @@ class MainWindow(QMainWindow): def _apply_subcat_visibility(self) -> None: self._timeline._hidden_subcats = self._hidden_subcats self._timeline.update() + # Match the subcategory folder EXACTLY (same name the menu shows and + # _hidden_subcats stores: "_"). A fuzzy endswith() match + # let a ghost "_blowjob" (empty-base leftover) or an unrelated + # "mp4_no_clap" hide the wrong button, so enabling a subcategory never + # revealed its export button. + base = os.path.basename(self._txt_folder.text().rstrip("/" + os.sep)) for btn in self._subprofile_btns: suffix = btn.text().removeprefix("▸ ") - visible = not any(f.endswith("_" + suffix) or f == suffix - for f in self._hidden_subcats) - btn.setVisible(visible) + folder = f"{base}_{suffix}" if base else suffix + btn.setVisible(folder not in self._hidden_subcats) self._rebuild_format_buttons() self._refresh_playlist_checks() diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index 5128b0e..8f83e61 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -222,3 +222,24 @@ def test_export_base_name_handles_trailing_slash(win): assert win._export_base_name() == "mp4" win._txt_folder.setText("/x/AlexisCrystal/mp4") assert win._export_base_name() == "mp4" + + +def test_subprofile_button_visibility_exact_match(win): + # A subcategory's export button must track ITS folder exactly. A ghost + # "_blowjob" (empty-base leftover) or an unrelated "mp4_no_clap" must NOT + # hide the "blowjob"/"clap" buttons (the old fuzzy endswith() match did, + # so enabling a subcategory never revealed its export button). + win._txt_folder.setText("/x/AlexisCrystal/mp4") + win._subprofiles = ["blowjob", "clap"] + win._rebuild_subprofile_buttons() + btns = {b.text().removeprefix("▸ "): b for b in win._subprofile_btns} + + win._hidden_subcats = {"_blowjob", "mp4_no_clap"} + win._apply_subcat_visibility() + assert not btns["blowjob"].isHidden() # ghost "_blowjob" must not hide it + assert not btns["clap"].isHidden() # "mp4_no_clap" must not hide "clap" + + win._hidden_subcats = {"mp4_blowjob"} # exact folder -> hidden + win._apply_subcat_visibility() + assert btns["blowjob"].isHidden() + assert not btns["clap"].isHidden()