From c03ee0665ba343adfb9a5db43320611dc5568925 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Thu, 12 Feb 2026 11:55:48 +0100 Subject: [PATCH] Fix session save/restore losing folders, add frame column and UI improvements Session restore fixes: - AUTO-typed folders now default to MAIN on restore instead of using position-based index%2, which silently flipped half the folders to TRANSITION when restoring legacy sessions - All restored folders get explicit type overrides so no folder relies on position-based typing after restore - TRANSITION folders with symlink data are auto-recovered as MAIN (catches incorrectly saved types from older export paths) - Export Sequence path now saves with save_effective_types=True, preventing folder type loss - Removed redundant trim-only save that used unresolved paths - Auto-save guards against overwriting sessions with empty file lists UI improvements: - Added 4th "Frame" column to Sequence Order tab showing overall frame number (1-based) - Last frame of each sequence is bold for visual clarity - Fixed column resizing (ResizeToContents + Stretch) to prevent column collapse bugs - Save Session dialog now reports main + transition folder counts - Default optical flow preset changed to Max Co-Authored-By: Claude Opus 4.6 --- ui/main_window.py | 132 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 30 deletions(-) diff --git a/ui/main_window.py b/ui/main_window.py index 297d909..e208843 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -495,12 +495,15 @@ class SequenceLinkerUI(QWidget): # File list (Sequence Order tab) self.file_list = QTreeWidget() - self.file_list.setHeaderLabels(["Sequence Name", "Original Filename", "Source Folder"]) + self.file_list.setHeaderLabels(["Sequence Name", "Original Filename", "Source Folder", "Frame"]) self.file_list.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.file_list.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.file_list.setRootIsDecorated(False) - self.file_list.header().setStretchLastSection(True) - self.file_list.header().setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive) + self.file_list.header().setStretchLastSection(False) + self.file_list.header().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) + self.file_list.header().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + self.file_list.header().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) + self.file_list.header().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) self.file_list.setToolTip("Drag to reorder within folder, Del to remove") # Action buttons @@ -766,7 +769,7 @@ class SequenceLinkerUI(QWidget): self.of_preset_combo.addItem("Quality", "quality") self.of_preset_combo.addItem("Max", "max") self.of_preset_combo.addItem("Custom", "custom") - self.of_preset_combo.setCurrentIndex(1) # Default to Balanced + self.of_preset_combo.setCurrentIndex(3) # Default to Max self.of_preset_combo.setToolTip( "Optical flow quality preset:\n" "- Fast: Quick processing, lower quality\n" @@ -2853,8 +2856,14 @@ class SequenceLinkerUI(QWidget): seen_resolved.add(resolved_str) if trim_start > 0 or trim_end > 0: self._folder_trim_settings[folder_path] = (trim_start, trim_end) + # Always set explicit type override so position-based index%2 + # fallback never silently flips a folder's type after reordering + # or transition insertion. AUTO defaults to MAIN (safe for + # legacy sessions that pre-date the type system). if folder_type != FolderType.AUTO: self._folder_type_overrides[folder_path] = folder_type + else: + self._folder_type_overrides[folder_path] = FolderType.MAIN pts_key = _resolve_lookup(folder_str, db_per_trans_settings) if pts_key is None: pts_key = _resolve_lookup(resolved_str, db_per_trans_settings) @@ -2866,10 +2875,21 @@ class SequenceLinkerUI(QWidget): if rm_key is not None: self._removed_files[folder_path] = db_removed_files[rm_key] # Assign folder_data index for MAIN folders used by _restore_files_from_session + # Remove TRANSITION folders from folder_data — their files aren't shown + # in the file list (they participate in blending only). + # AUTO defaults to MAIN so legacy/incomplete saves don't lose data. + effective_type = folder_type if folder_type != FolderType.AUTO else FolderType.MAIN fd_key = _resolve_lookup(folder_str, folder_data) if fd_key is None: fd_key = _resolve_lookup(resolved_str, folder_data) if fd_key is not None: + if effective_type == FolderType.TRANSITION: + # TRANSITION folders shouldn't have symlink data — if + # they do, the type was likely saved incorrectly (e.g. + # by an older export path). Recover by treating as + # MAIN so the files aren't lost. + effective_type = FolderType.MAIN + self._folder_type_overrides[folder_path] = FolderType.MAIN folder_data[fd_key] = (main_idx, folder_data[fd_key][1]) main_idx += 1 @@ -2922,6 +2942,8 @@ class SequenceLinkerUI(QWidget): self._folder_trim_settings[folder_path] = (ts, te) if ft != FolderType.AUTO: self._folder_type_overrides[folder_path] = ft + else: + self._folder_type_overrides[folder_path] = FolderType.MAIN # Apply per-transition settings pts_key = _resolve_lookup(folder, db_per_trans_settings) if pts_key is None: @@ -3130,25 +3152,31 @@ class SequenceLinkerUI(QWidget): session_id = self.db.create_session(dest) self._current_session_id = session_id + # Get current file list before clearing — don't clear if empty + # to avoid corrupting the session + files = self._get_files_in_order() + if not files and self.source_folders: + # file_list is empty but we have folders — don't overwrite + # the session, just save settings + return + # Clear all stale data for this session before re-saving self.db.clear_session_data(session_id) self._save_session_settings(session_id, save_effective_types=True) - # Also save the file list so the exact sequence can be restored - files = self._get_files_in_order() - if files: - for i, (source_dir, filename, folder_idx, file_idx) in enumerate(files): - source_path = source_dir / filename - ext = source_path.suffix - link_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" - self.db.record_symlink( - session_id=session_id, - source=str(source_path.resolve()), - link=str(Path(dest) / link_name), - filename=filename, - seq=i, - ) + # Save the file list so the exact sequence can be restored + for i, (source_dir, filename, folder_idx, file_idx) in enumerate(files): + source_path = source_dir / filename + ext = source_path.suffix + link_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" + self.db.record_symlink( + session_id=session_id, + source=str(source_path.resolve()), + link=str(Path(dest) / link_name), + filename=filename, + seq=i, + ) except Exception: pass # Best-effort save on close @@ -3186,9 +3214,17 @@ class SequenceLinkerUI(QWidget): seq=i, ) + main_count = sum( + 1 for i, f in enumerate(self.source_folders) + if self._get_effective_folder_type(i, f) == FolderType.MAIN + ) + trans_count = len(self.source_folders) - main_count + folder_info = f"{main_count} main" + if trans_count > 0: + folder_info += f" + {trans_count} transition" QMessageBox.information( self, "Session Saved", - f"Saved {len(files)} files from {len(self.source_folders)} folders." + f"Saved {len(files)} files from {folder_info} folders." ) except Exception as e: @@ -3567,15 +3603,24 @@ class SequenceLinkerUI(QWidget): self.file_list.addTopLevelItem(separator) is_first_folder = False - for filename in trimmed_files: + for file_i, filename in enumerate(trimmed_files): file_idx = folder_file_counts.get(folder, 0) folder_file_counts[folder] = file_idx + 1 ext = Path(filename).suffix seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" + overall_frame = sum(folder_file_counts.values()) - item = QTreeWidgetItem([seq_name, filename, str(folder)]) + item = QTreeWidgetItem([seq_name, filename, str(folder), str(overall_frame)]) item.setData(0, Qt.ItemDataRole.UserRole, (folder, filename, folder_idx, file_idx)) + + # Bold the last frame of each sequence + if file_i == len(trimmed_files) - 1: + font = item.font(0) + font.setBold(True) + for col in range(4): + item.setFont(col, font) + self.file_list.addTopLevelItem(item) total = self.file_list.topLevelItemCount() @@ -3609,6 +3654,7 @@ class SequenceLinkerUI(QWidget): self._folder_file_counts = {} is_first_folder = True + overall_frame = 0 # Batch UI updates for performance self.file_list.setUpdatesEnabled(False) @@ -3634,12 +3680,21 @@ class SequenceLinkerUI(QWidget): self.file_list.addTopLevelItem(separator) is_first_folder = False - for file_idx, filename in sorted_files: + for file_i, (file_idx, filename) in enumerate(sorted_files): ext = Path(filename).suffix seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" + overall_frame += 1 - item = QTreeWidgetItem([seq_name, filename, str(folder_path)]) + item = QTreeWidgetItem([seq_name, filename, str(folder_path), str(overall_frame)]) item.setData(0, Qt.ItemDataRole.UserRole, (folder_path, filename, folder_idx, file_idx)) + + # Bold the last frame of each sequence + if file_i == len(sorted_files) - 1: + font = item.font(0) + font.setBold(True) + for col in range(4): + item.setFont(col, font) + self.file_list.addTopLevelItem(item) self.file_list.setUpdatesEnabled(True) @@ -3655,11 +3710,11 @@ class SequenceLinkerUI(QWidget): def _create_folder_separator(self, next_folder_idx: int) -> QTreeWidgetItem: """Create a visual separator item between folders.""" - separator = QTreeWidgetItem(["", f"── Sequence {next_folder_idx + 1} ──", ""]) + separator = QTreeWidgetItem(["", f"── Sequence {next_folder_idx + 1} ──", "", ""]) separator.setData(0, Qt.ItemDataRole.UserRole, None) # No data = separator # Light grey background grey = QColor(220, 220, 220) - for col in range(3): + for col in range(4): separator.setBackground(col, grey) # Make it non-selectable and non-draggable separator.setFlags(Qt.ItemFlag.NoItemFlags) @@ -3688,6 +3743,17 @@ class SequenceLinkerUI(QWidget): folder_file_counts: dict[Path, int] = {} last_folder_idx = -1 + # Collect items per folder to detect last file + folder_items: dict[Path, list[int]] = {} + for i in range(self.file_list.topLevelItemCount()): + item = self.file_list.topLevelItem(i) + data = item.data(0, Qt.ItemDataRole.UserRole) + if data: + src = data[0] + if src not in folder_items: + folder_items[src] = [] + folder_items[src].append(i) + for i in range(self.file_list.topLevelItemCount()): item = self.file_list.topLevelItem(i) data = item.data(0, Qt.ItemDataRole.UserRole) @@ -3700,8 +3766,18 @@ class SequenceLinkerUI(QWidget): ext = Path(filename).suffix seq_name = f"seq{folder_idx + 1:02d}_{file_idx:04d}{ext}" + overall_frame = sum(folder_file_counts.values()) item.setText(0, seq_name) + item.setText(3, str(overall_frame)) item.setData(0, Qt.ItemDataRole.UserRole, (source_dir, filename, folder_idx, file_idx)) + + # Bold the last frame of each sequence + is_last = (folder_items.get(source_dir, [])[-1] == i) + font = item.font(0) + font.setBold(is_last) + for col in range(4): + item.setFont(col, font) + last_folder_idx = folder_idx elif self._is_separator_item(item): # Update separator label based on next file's folder @@ -4122,11 +4198,7 @@ class SequenceLinkerUI(QWidget): self._current_session_id = session_id if session_id: - # Save trim settings - for folder, (trim_start, trim_end) in self._folder_trim_settings.items(): - if trim_start > 0 or trim_end > 0: - self.db.save_trim_settings(session_id, str(folder), trim_start, trim_end) - self._save_session_settings(session_id) + self._save_session_settings(session_id, save_effective_types=True) total = len(files) link_type = "copies" if copy_files else "symlinks"