fix: subcategory export buttons hidden by ghost entries + give them their own centered row

Two issues with the per-subprofile (subcategory) export buttons:

1. Visibility was decided by a fuzzy `f.endswith("_" + suffix)` match against
   the hidden-subcats set. A ghost "_blowjob" (empty-base leftover from the
   trailing-slash folder bug) or an unrelated "mp4_no_clap" would match and
   hide the wrong button — so enabling a subcategory in the Sub menu never
   revealed its export button. Match the exact "<base>_<suffix>" folder name
   instead (same name the menu shows and _hidden_subcats stores).

2. The buttons were crammed into the transport row after Export. Move them to
   their own row with stretches on both ends so the (often many) "▸ name"
   buttons stay centered and out of the transport controls.

Also cleared the polluted hidden_subcats/POV_Front set in the user's QSettings
(ghost "_*" names + a hide-all'd set of real "mp4_*"), so every subcategory is
visible again. Regression test added for the exact-match predicate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 14:19:20 +02:00
parent 514607eddd
commit 7ae1720b9e
2 changed files with 50 additions and 17 deletions
+29 -17
View File
@@ -4405,17 +4405,23 @@ class MainWindow(QMainWindow):
transport_row.addStretch() transport_row.addStretch()
transport_row.addWidget(self._lbl_next) transport_row.addWidget(self._lbl_next)
transport_row.addWidget(self._btn_export) 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._subprofile_btns: list[QPushButton] = []
self._sub_insert_anchor = self._btn_cancel # buttons inserted before this
self._btn_add_sub = QPushButton("+") self._btn_add_sub = QPushButton("+")
self._btn_add_sub.setFixedWidth(28) self._btn_add_sub.setFixedWidth(28)
self._btn_add_sub.setToolTip("Add a subprofile — exports to folder_suffix") self._btn_add_sub.setToolTip("Add a subprofile — exports to folder_suffix")
self._btn_add_sub.clicked.connect(self._add_subprofile) self._btn_add_sub.clicked.connect(self._add_subprofile)
transport_row.addWidget(self._btn_add_sub) subprofile_row.addWidget(self._btn_add_sub)
transport_row.addWidget(self._btn_cancel) subprofile_row.addStretch()
transport_row.addWidget(self._btn_delete) self._subprofile_row = subprofile_row
self._transport_row = transport_row
self._rebuild_subprofile_buttons() self._rebuild_subprofile_buttons()
# Row 2/3 — annotation, output path, crop and scan controls all live in # 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._timeline)
right_layout.addWidget(self._crop_bar) right_layout.addWidget(self._crop_bar)
right_layout.addLayout(transport_row) right_layout.addLayout(transport_row)
right_layout.addLayout(self._subprofile_row)
right_layout.addWidget(self._build_control_deck()) right_layout.addWidget(self._build_control_deck())
self._build_export_tab() self._build_export_tab()
self._build_crop_tab() self._build_crop_tab()
@@ -5581,17 +5588,17 @@ class MainWindow(QMainWindow):
# ── Subprofiles ────────────────────────────────────────── # ── Subprofiles ──────────────────────────────────────────
def _rebuild_subprofile_buttons(self): 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: for btn in self._format_btns:
self._transport_row.removeWidget(btn)
btn.setParent(None) btn.setParent(None)
self._format_btns.clear() self._format_btns.clear()
for btn in self._subprofile_btns: for btn in self._subprofile_btns:
self._transport_row.removeWidget(btn) self._subprofile_row.removeWidget(btn)
btn.deleteLater() btn.deleteLater()
self._subprofile_btns.clear() self._subprofile_btns.clear()
# Find where to insert: right after the main Export button. # Insert before the "+" add button (which sits before the trailing
anchor = self._transport_row.indexOf(self._btn_add_sub) # stretch), so the buttons stay centered on the row.
anchor = self._subprofile_row.indexOf(self._btn_add_sub)
has_file = bool(self._file_path) has_file = bool(self._file_path)
for i, name in enumerate(self._subprofiles): for i, name in enumerate(self._subprofiles):
btn = QPushButton(f"{name}") btn = QPushButton(f"{name}")
@@ -5599,7 +5606,7 @@ class MainWindow(QMainWindow):
btn.setToolTip(f"Export to folder_{name} (right-click to remove)") btn.setToolTip(f"Export to folder_{name} (right-click to remove)")
btn.setEnabled(has_file) btn.setEnabled(has_file)
btn.clicked.connect(lambda _, s=name: self._on_export(folder_suffix=s)) 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._subprofile_btns.append(btn)
self._rebuild_format_buttons() self._rebuild_format_buttons()
# Keep the Edit ▸ Subprofiles ▸ Remove submenu in sync. Guarded because # Keep the Edit ▸ Subprofiles ▸ Remove submenu in sync. Guarded because
@@ -6122,7 +6129,7 @@ class MainWindow(QMainWindow):
if sub_btn.isHidden(): if sub_btn.isHidden():
continue continue
suffix = sub_btn.text().removeprefix("") 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): for j, (label, ratio) in enumerate(formats):
btn = QPushButton(label) btn = QPushButton(label)
btn.setFocusPolicy(Qt.FocusPolicy.NoFocus) btn.setFocusPolicy(Qt.FocusPolicy.NoFocus)
@@ -6132,7 +6139,7 @@ class MainWindow(QMainWindow):
btn.clicked.connect( btn.clicked.connect(
lambda _, s=suffix, r=ratio: self._on_export( lambda _, s=suffix, r=ratio: self._on_export(
folder_suffix=s, force_ratio=r)) 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) self._format_btns.append(btn)
def _on_rand_toggle(self, _checked: bool = False) -> None: def _on_rand_toggle(self, _checked: bool = False) -> None:
@@ -6695,11 +6702,16 @@ class MainWindow(QMainWindow):
def _apply_subcat_visibility(self) -> None: def _apply_subcat_visibility(self) -> None:
self._timeline._hidden_subcats = self._hidden_subcats self._timeline._hidden_subcats = self._hidden_subcats
self._timeline.update() self._timeline.update()
# Match the subcategory folder EXACTLY (same name the menu shows and
# _hidden_subcats stores: "<base>_<suffix>"). 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: for btn in self._subprofile_btns:
suffix = btn.text().removeprefix("") suffix = btn.text().removeprefix("")
visible = not any(f.endswith("_" + suffix) or f == suffix folder = f"{base}_{suffix}" if base else suffix
for f in self._hidden_subcats) btn.setVisible(folder not in self._hidden_subcats)
btn.setVisible(visible)
self._rebuild_format_buttons() self._rebuild_format_buttons()
self._refresh_playlist_checks() self._refresh_playlist_checks()
+21
View File
@@ -222,3 +222,24 @@ def test_export_base_name_handles_trailing_slash(win):
assert win._export_base_name() == "mp4" assert win._export_base_name() == "mp4"
win._txt_folder.setText("/x/AlexisCrystal/mp4") win._txt_folder.setText("/x/AlexisCrystal/mp4")
assert win._export_base_name() == "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()