From bf4b6dad2d7e900de07484579dbb51607ecf2888 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 18 Jun 2026 14:36:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20right-click=20"Duplicate=20tab"=20?= =?UTF-8?q?=E2=80=94=20clone=20files=20into=20a=20new=20tab=20with=20adapt?= =?UTF-8?q?ed=20name=20+=20own=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New tab copies the source tab's video list + separators, gets a unique " copy" label and an adapted own export folder ("_copy"), and inherits the tab-named-folder flag. No files are moved or copied — you export into the new tab's folder. Keeps Foley/variant datasets separate without the file-shuffling that a misexport used to require. Co-Authored-By: Claude Fable 5 --- main.py | 31 +++++++++++++++++++++++++++++++ tests/test_ui_structure.py | 19 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/main.py b/main.py index d12df91..b6643a3 100755 --- a/main.py +++ b/main.py @@ -3302,6 +3302,7 @@ class _PlaylistTabBar(QTabBar): tab_renamed = pyqtSignal(int, str) pin_toggle_requested = pyqtSignal(int) tab_folder_toggle_requested = pyqtSignal(int) + duplicate_requested = pyqtSignal(int) def mouseDoubleClickEvent(self, event): idx = self.tabAt(event.pos()) @@ -3327,6 +3328,7 @@ class _PlaylistTabBar(QTabBar): act_tabfolder.setCheckable(True) act_tabfolder.setChecked(bool(getattr(pw, "_tab_folder", False))) act_rename = menu.addAction("Rename…") + act_dup = menu.addAction("Duplicate tab") chosen = menu.exec(event.globalPos()) if chosen == act_pin: self.pin_toggle_requested.emit(idx) @@ -3334,6 +3336,8 @@ class _PlaylistTabBar(QTabBar): self.tab_folder_toggle_requested.emit(idx) elif chosen == act_rename: self._start_edit(idx) + elif chosen == act_dup: + self.duplicate_requested.emit(idx) def _start_edit(self, idx: int) -> None: editor = QLineEdit(self) @@ -3946,6 +3950,7 @@ class MainWindow(QMainWindow): self._playlist_tabs.tabBar().pin_toggle_requested.connect(self._on_pin_toggle) self._playlist_tabs.tabBar().tab_folder_toggle_requested.connect( self._on_tab_folder_toggle) + self._playlist_tabs.tabBar().duplicate_requested.connect(self._on_duplicate_tab) self._playlist_tabs.tabCloseRequested.connect(self._on_close_tab) self._playlist_tabs.currentChanged.connect(self._on_tab_changed) self._btn_add_tab = QPushButton("+") @@ -4974,6 +4979,32 @@ class MainWindow(QMainWindow): self._refresh_playlist_checks() self._update_next_label() + def _on_duplicate_tab(self, idx: int) -> None: + """Clone a tab's file list into a new tab with an adapted name and its + own (adapted) export folder. No files are moved or copied — the new tab + just targets a separate dataset folder you export into.""" + src = self._playlist_tabs.widget(idx) + if src is None: + return + base = f"{src._label} copy" + label, n = base, 2 + existing = {pw._label for pw in self._pws} + while label in existing: + label = f"{base} {n}" + n += 1 + pw = self._add_playlist_tab( + label=label, + files=list(src._paths), + separators=sorted(src._separators_before), + select=True, + ) + src_folder = getattr(src, "_dest_folder", "") + pw._dest_folder = (src_folder + "_copy") if src_folder else "" + pw._tab_folder = getattr(src, "_tab_folder", False) + self._sync_folder_field_to_tab() + self._save_playlist_tabs() + self._show_status(f"Duplicated tab → {label}", 4000) + # ── File-list tabs ─────────────────────────────────────────── def _wire_pw(self, pw: "PlaylistWidget") -> None: pw.file_selected.connect(self._load_file) diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index 08534d7..1845e11 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -107,3 +107,22 @@ def test_side_by_side_menu_pins_third_panel(win): win._deck_loading = False assert win._tab_crop._pinned is True assert len(_split_columns(win)) == 3 + + +def test_duplicate_tab(win): + # Right-click → Duplicate tab: clones files into a new tab with an adapted + # name + adapted own folder, no file moves. Suppress QSettings writes via + # _loading_tabs so the test can't touch the real session. + win._loading_tabs = True + try: + src = win._pws[0] + src._label = "AlexisCrystal" + src._dest_folder = "/data/alexis" + n_before = len(win._pws) + win._on_duplicate_tab(win._playlist_tabs.indexOf(src)) + finally: + win._loading_tabs = False + assert len(win._pws) == n_before + 1 + dup = win._pws[-1] + assert dup._label == "AlexisCrystal copy" + assert dup._dest_folder == "/data/alexis_copy"