feat: right-click "Duplicate tab" — clone files into a new tab with adapted name + own folder
New tab copies the source tab's video list + separators, gets a unique
"<name> copy" label and an adapted own export folder ("<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 <noreply@anthropic.com>
This commit is contained in:
@@ -3302,6 +3302,7 @@ class _PlaylistTabBar(QTabBar):
|
|||||||
tab_renamed = pyqtSignal(int, str)
|
tab_renamed = pyqtSignal(int, str)
|
||||||
pin_toggle_requested = pyqtSignal(int)
|
pin_toggle_requested = pyqtSignal(int)
|
||||||
tab_folder_toggle_requested = pyqtSignal(int)
|
tab_folder_toggle_requested = pyqtSignal(int)
|
||||||
|
duplicate_requested = pyqtSignal(int)
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, event):
|
def mouseDoubleClickEvent(self, event):
|
||||||
idx = self.tabAt(event.pos())
|
idx = self.tabAt(event.pos())
|
||||||
@@ -3327,6 +3328,7 @@ class _PlaylistTabBar(QTabBar):
|
|||||||
act_tabfolder.setCheckable(True)
|
act_tabfolder.setCheckable(True)
|
||||||
act_tabfolder.setChecked(bool(getattr(pw, "_tab_folder", False)))
|
act_tabfolder.setChecked(bool(getattr(pw, "_tab_folder", False)))
|
||||||
act_rename = menu.addAction("Rename…")
|
act_rename = menu.addAction("Rename…")
|
||||||
|
act_dup = menu.addAction("Duplicate tab")
|
||||||
chosen = menu.exec(event.globalPos())
|
chosen = menu.exec(event.globalPos())
|
||||||
if chosen == act_pin:
|
if chosen == act_pin:
|
||||||
self.pin_toggle_requested.emit(idx)
|
self.pin_toggle_requested.emit(idx)
|
||||||
@@ -3334,6 +3336,8 @@ class _PlaylistTabBar(QTabBar):
|
|||||||
self.tab_folder_toggle_requested.emit(idx)
|
self.tab_folder_toggle_requested.emit(idx)
|
||||||
elif chosen == act_rename:
|
elif chosen == act_rename:
|
||||||
self._start_edit(idx)
|
self._start_edit(idx)
|
||||||
|
elif chosen == act_dup:
|
||||||
|
self.duplicate_requested.emit(idx)
|
||||||
|
|
||||||
def _start_edit(self, idx: int) -> None:
|
def _start_edit(self, idx: int) -> None:
|
||||||
editor = QLineEdit(self)
|
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().pin_toggle_requested.connect(self._on_pin_toggle)
|
||||||
self._playlist_tabs.tabBar().tab_folder_toggle_requested.connect(
|
self._playlist_tabs.tabBar().tab_folder_toggle_requested.connect(
|
||||||
self._on_tab_folder_toggle)
|
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.tabCloseRequested.connect(self._on_close_tab)
|
||||||
self._playlist_tabs.currentChanged.connect(self._on_tab_changed)
|
self._playlist_tabs.currentChanged.connect(self._on_tab_changed)
|
||||||
self._btn_add_tab = QPushButton("+")
|
self._btn_add_tab = QPushButton("+")
|
||||||
@@ -4974,6 +4979,32 @@ class MainWindow(QMainWindow):
|
|||||||
self._refresh_playlist_checks()
|
self._refresh_playlist_checks()
|
||||||
self._update_next_label()
|
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 ───────────────────────────────────────────
|
# ── File-list tabs ───────────────────────────────────────────
|
||||||
def _wire_pw(self, pw: "PlaylistWidget") -> None:
|
def _wire_pw(self, pw: "PlaylistWidget") -> None:
|
||||||
pw.file_selected.connect(self._load_file)
|
pw.file_selected.connect(self._load_file)
|
||||||
|
|||||||
@@ -107,3 +107,22 @@ def test_side_by_side_menu_pins_third_panel(win):
|
|||||||
win._deck_loading = False
|
win._deck_loading = False
|
||||||
assert win._tab_crop._pinned is True
|
assert win._tab_crop._pinned is True
|
||||||
assert len(_split_columns(win)) == 3
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user