{
"sourceFile": "gui/main_window.py",
"activeCommit": 0,
"commits": [
{
"activePatchIndex": 43,
"patches": [
{
"date": 1745236085615,
"content": "Index: \n===================================================================\n--- \n+++ \n"
},
{
"date": 1745236299610,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -605,13 +605,19 @@\n \r\n \r\n # --- Preview Update Method ---\r\n def update_preview(self):\r\n- log.debug(f\"[{time.time():.4f}] update_preview triggered.\")\r\n+ # <<< ADDED LOG >>>\r\n+ log.info(f\"--> Entered update_preview. Checkbox exists: {hasattr(self, 'disable_preview_checkbox')}\")\r\n+ if hasattr(self, 'disable_preview_checkbox'):\r\n+ log.info(f\" Checkbox checked: {self.disable_preview_checkbox.isChecked()}\")\r\n+ # <<< END ADDED LOG >>>\r\n \r\n # --- Handle Preview Toggle ---\r\n if hasattr(self, 'disable_preview_checkbox') and self.disable_preview_checkbox.isChecked():\r\n- log.info(\"Detailed preview disabled. Showing simple input list.\")\r\n+ # <<< MODIFIED LOG >>>\r\n+ log.info(\" Preview Toggle Check: DETAILED PREVIEW DISABLED. Preparing simple list view.\")\r\n+ # <<< END MODIFIED LOG >>>\r\n self.preview_table.setSortingEnabled(False)\r\n self.preview_table.setRowCount(0) # Clear previous\r\n self.preview_table.setColumnCount(1)\r\n self.preview_table.setHorizontalHeaderLabels([\"Input Path\"])\r\n@@ -632,9 +638,11 @@\n return # Stop here, do not run PredictionHandler\r\n # --- End Preview Toggle Handling ---\r\n \r\n # --- Proceed with Detailed Preview ---\r\n- log.debug(\"Detailed preview enabled. Proceeding with prediction handler.\")\r\n+ # <<< ADDED LOG >>>\r\n+ log.info(\" Preview Toggle Check: DETAILED PREVIEW ENABLED. Preparing for prediction handler.\")\r\n+ # <<< END ADDED LOG >>>\r\n # Reset table headers for detailed view\r\n self.preview_table.setColumnCount(4)\r\n self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Original Path\", \"Predicted Name\", \"Details\"])\r\n self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Status\r\n"
},
{
"date": 1745236975603,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -10,12 +10,13 @@\n QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, # Added QSplitter\r\n QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,\r\n QProgressBar, QLabel, QFrame, QCheckBox, QSpinBox, QListWidget, QTextEdit, # Added QListWidget, QTextEdit\r\n QLineEdit, QMessageBox, QFileDialog, QInputDialog, QListWidgetItem, QTabWidget, # Added more widgets\r\n- QFormLayout, QGroupBox, QAbstractItemView, QSizePolicy # Added more layout/widget items\r\n+ QFormLayout, QGroupBox, QAbstractItemView, QSizePolicy, # Added more layout/widget items\r\n+ QMenuBar, QMenu # Added for menu\r\n )\r\n-from PySide6.QtCore import Qt, QThread, Slot, Signal # Added Signal\r\n-from PySide6.QtGui import QColor # Add QColor import\r\n+from PySide6.QtCore import Qt, QThread, Slot, Signal, QObject # Added Signal, QObject\r\n+from PySide6.QtGui import QColor, QAction # Add QColor import, QAction\r\n \r\n # --- Backend Imports ---\r\n script_dir = Path(__file__).parent\r\n project_root = script_dir.parent\r\n@@ -49,8 +50,31 @@\n # Set level back to INFO for normal operation\r\n logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') # Reverted level and format\r\n \r\n \r\n+# --- Custom Log Handler ---\r\n+class QtLogHandler(logging.Handler, QObject):\r\n+ \"\"\"\r\n+ Custom logging handler that emits a Qt signal for each log record.\r\n+ Inherits from QObject to support signals.\r\n+ \"\"\"\r\n+ log_record_received = Signal(str) # Signal emitting the formatted log string\r\n+\r\n+ def __init__(self, parent=None):\r\n+ logging.Handler.__init__(self)\r\n+ QObject.__init__(self, parent) # Initialize QObject part\r\n+\r\n+ def emit(self, record):\r\n+ \"\"\"\r\n+ Overrides the default emit method to format the record and emit a signal.\r\n+ \"\"\"\r\n+ try:\r\n+ msg = self.format(record)\r\n+ self.log_record_received.emit(msg)\r\n+ except Exception:\r\n+ self.handleError(record)\r\n+\r\n+\r\n # --- Helper Functions (from PresetEditorDialog) ---\r\n # NOTE: Consider moving these to a utils file if reused elsewhere\r\n \r\n def setup_list_widget_with_controls(parent_layout, label_text, attribute_name, instance):\r\n@@ -155,16 +179,18 @@\n \r\n # --- Setup UI Elements for each panel ---\r\n self.setup_editor_panel_ui()\r\n self.setup_main_panel_ui()\r\n+ self.setup_menu_bar() # Setup menu bar\r\n \r\n # --- Status Bar ---\r\n self.statusBar().showMessage(\"Ready\")\r\n \r\n # --- Initial State ---\r\n self._clear_editor() # Clear/disable editor fields initially\r\n self._set_editor_enabled(False) # Disable editor initially\r\n self.populate_presets() # Populate both preset list and combo box\r\n+ self.setup_logging_handler() # Setup the custom log handler\r\n \r\n # --- Connect Editor Signals ---\r\n self._connect_editor_change_signals()\r\n \r\n@@ -177,8 +203,22 @@\n \"\"\"Sets up the UI elements for the left preset editor panel.\"\"\"\r\n editor_layout = QVBoxLayout(self.editor_panel)\r\n editor_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins\r\n \r\n+ # --- Log Console Output (Initially Hidden) ---\r\n+ self.log_console_widget = QWidget()\r\n+ log_console_layout = QVBoxLayout(self.log_console_widget)\r\n+ log_console_layout.setContentsMargins(0, 0, 0, 5) # Add some bottom margin\r\n+ log_console_label = QLabel(\"Log Console:\")\r\n+ self.log_console_output = QTextEdit()\r\n+ self.log_console_output.setReadOnly(True)\r\n+ self.log_console_output.setMaximumHeight(150) # Limit initial height\r\n+ self.log_console_output.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum)\r\n+ log_console_layout.addWidget(log_console_label)\r\n+ log_console_layout.addWidget(self.log_console_output)\r\n+ self.log_console_widget.setVisible(False) # Start hidden\r\n+ editor_layout.addWidget(self.log_console_widget) # Add it at the top\r\n+\r\n # Preset List and Controls\r\n list_layout = QVBoxLayout()\r\n list_layout.addWidget(QLabel(\"Presets:\"))\r\n self.editor_preset_list = QListWidget()\r\n@@ -366,15 +406,15 @@\n self.overwrite_checkbox = QCheckBox(\"Overwrite Existing\")\r\n self.overwrite_checkbox.setToolTip(\"If checked, existing output folders for processed assets will be deleted and replaced.\")\r\n bottom_controls_layout.addWidget(self.overwrite_checkbox)\r\n \r\n- self.disable_preview_checkbox = QCheckBox(\"Disable Detailed Preview\")\r\n- self.disable_preview_checkbox.setToolTip(\"If checked, shows only the list of input assets instead of detailed file predictions.\")\r\n- self.disable_preview_checkbox.setChecked(False) # Default is detailed preview enabled\r\n- self.disable_preview_checkbox.toggled.connect(self.update_preview) # Update preview when toggled\r\n- bottom_controls_layout.addWidget(self.disable_preview_checkbox)\r\n+ # self.disable_preview_checkbox = QCheckBox(\"Disable Detailed Preview\") # REMOVED - Moved to View Menu\r\n+ # self.disable_preview_checkbox.setToolTip(\"If checked, shows only the list of input assets instead of detailed file predictions.\")\r\n+ # self.disable_preview_checkbox.setChecked(False) # Default is detailed preview enabled\r\n+ # self.disable_preview_checkbox.toggled.connect(self.update_preview) # Update preview when toggled\r\n+ # bottom_controls_layout.addWidget(self.disable_preview_checkbox)\r\n \r\n- bottom_controls_layout.addSpacing(20) # Add some space\r\n+ # bottom_controls_layout.addSpacing(20) # Add some space # REMOVED - No longer needed after checkbox removal\r\n \r\n self.workers_label = QLabel(\"Workers:\")\r\n self.workers_spinbox = QSpinBox()\r\n default_workers = 1\r\n@@ -606,17 +646,17 @@\n \r\n # --- Preview Update Method ---\r\n def update_preview(self):\r\n # <<< ADDED LOG >>>\r\n- log.info(f\"--> Entered update_preview. Checkbox exists: {hasattr(self, 'disable_preview_checkbox')}\")\r\n- if hasattr(self, 'disable_preview_checkbox'):\r\n- log.info(f\" Checkbox checked: {self.disable_preview_checkbox.isChecked()}\")\r\n+ log.info(f\"--> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}\")\r\n+ if hasattr(self, 'toggle_preview_action'):\r\n+ log.info(f\" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n # <<< END ADDED LOG >>>\r\n \r\n # --- Handle Preview Toggle ---\r\n- if hasattr(self, 'disable_preview_checkbox') and self.disable_preview_checkbox.isChecked():\r\n+ if hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked(): # Check menu action state\r\n # <<< MODIFIED LOG >>>\r\n- log.info(\" Preview Toggle Check: DETAILED PREVIEW DISABLED. Preparing simple list view.\")\r\n+ log.info(\" Preview Toggle Check: DETAILED PREVIEW DISABLED (via menu). Preparing simple list view.\")\r\n # <<< END MODIFIED LOG >>>\r\n self.preview_table.setSortingEnabled(False)\r\n self.preview_table.setRowCount(0) # Clear previous\r\n self.preview_table.setColumnCount(1)\r\n@@ -639,9 +679,9 @@\n # --- End Preview Toggle Handling ---\r\n \r\n # --- Proceed with Detailed Preview ---\r\n # <<< ADDED LOG >>>\r\n- log.info(\" Preview Toggle Check: DETAILED PREVIEW ENABLED. Preparing for prediction handler.\")\r\n+ log.info(\" Preview Toggle Check: DETAILED PREVIEW ENABLED (via menu). Preparing for prediction handler.\")\r\n # <<< END ADDED LOG >>>\r\n # Reset table headers for detailed view\r\n self.preview_table.setColumnCount(4)\r\n self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Original Path\", \"Predicted Name\", \"Details\"])\r\n@@ -1200,8 +1240,74 @@\n except Exception as e:\r\n log.exception(f\"Error deleting preset file {preset_path}: {e}\")\r\n QMessageBox.critical(self, \"Delete Error\", f\"Could not delete preset file:\\n{preset_path}\\n\\nError: {e}\")\r\n \r\n+ # --- Menu Bar Setup ---\r\n+ def setup_menu_bar(self):\r\n+ \"\"\"Creates the main menu bar and View menu.\"\"\"\r\n+ self.menu_bar = self.menuBar()\r\n+ view_menu = self.menu_bar.addMenu(\"&View\")\r\n+\r\n+ # Log Console Action\r\n+ self.toggle_log_action = QAction(\"Show Log Console\", self, checkable=True)\r\n+ self.toggle_log_action.setChecked(False) # Start hidden\r\n+ self.toggle_log_action.toggled.connect(self._toggle_log_console_visibility)\r\n+ view_menu.addAction(self.toggle_log_action)\r\n+\r\n+ # Detailed Preview Action\r\n+ self.toggle_preview_action = QAction(\"Disable Detailed Preview\", self, checkable=True)\r\n+ self.toggle_preview_action.setChecked(False) # Start enabled (detailed view)\r\n+ # Connect to update_preview, which now checks this action's state\r\n+ self.toggle_preview_action.toggled.connect(self.update_preview)\r\n+ view_menu.addAction(self.toggle_preview_action)\r\n+\r\n+ # --- Logging Handler Setup ---\r\n+ def setup_logging_handler(self):\r\n+ \"\"\"Creates and configures the custom QtLogHandler.\"\"\"\r\n+ self.log_handler = QtLogHandler(self)\r\n+ # Set the formatter to match the basicConfig format\r\n+ log_format = '%(levelname)s: %(message)s' # Simpler format for UI console\r\n+ formatter = logging.Formatter(log_format)\r\n+ self.log_handler.setFormatter(formatter)\r\n+ # Set level (e.g., INFO to capture standard messages)\r\n+ self.log_handler.setLevel(logging.INFO)\r\n+ # Add handler to the root logger to capture logs from all modules\r\n+ logging.getLogger().addHandler(self.log_handler)\r\n+ # Connect the signal to the slot\r\n+ self.log_handler.log_record_received.connect(self._append_log_message)\r\n+ log.info(\"UI Log Handler Initialized.\") # Log that the handler is ready\r\n+\r\n+ # --- Slots for Menu Actions and Logging ---\r\n+ @Slot(bool)\r\n+ def _toggle_log_console_visibility(self, checked):\r\n+ \"\"\"Shows or hides the log console widget based on menu action.\"\"\"\r\n+ if hasattr(self, 'log_console_widget'):\r\n+ self.log_console_widget.setVisible(checked)\r\n+ log.debug(f\"Log console visibility set to: {checked}\")\r\n+\r\n+ @Slot(str)\r\n+ def _append_log_message(self, message):\r\n+ \"\"\"Appends a log message to the QTextEdit console.\"\"\"\r\n+ if hasattr(self, 'log_console_output'):\r\n+ # Optional: Add basic coloring (can be expanded)\r\n+ # if message.startswith(\"ERROR\"):\r\n+ # message = f\"{message}\"\r\n+ # elif message.startswith(\"WARNING\"):\r\n+ # message = f\"{message}\"\r\n+\r\n+ self.log_console_output.append(message) # Use append for plain text\r\n+ # Optional: Limit history size\r\n+ # MAX_LINES = 500\r\n+ # if self.log_console_output.document().blockCount() > MAX_LINES:\r\n+ # cursor = self.log_console_output.textCursor()\r\n+ # cursor.movePosition(QTextCursor.MoveOperation.Start)\r\n+ # cursor.select(QTextCursor.SelectionType.BlockUnderCursor)\r\n+ # cursor.removeSelectedText()\r\n+ # cursor.deletePreviousChar() # Remove the newline potentially left behind\r\n+ # Ensure the view scrolls to the bottom\r\n+ self.log_console_output.verticalScrollBar().setValue(self.log_console_output.verticalScrollBar().maximum())\r\n+\r\n+\r\n # --- Overridden Close Event ---\r\n def closeEvent(self, event):\r\n \"\"\"Overrides close event to check for unsaved changes in the editor.\"\"\"\r\n if self._check_editor_unsaved_changes():\r\n"
},
{
"date": 1745262283650,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -400,8 +400,56 @@\n self.progress_bar.setValue(0)\r\n self.progress_bar.setTextVisible(True)\r\n main_layout.addWidget(self.progress_bar)\r\n \r\n+ # --- Blender Integration Controls ---\r\n+ blender_group = QGroupBox(\"Blender Post-Processing\")\r\n+ blender_layout = QVBoxLayout(blender_group)\r\n+\r\n+ self.blender_integration_checkbox = QCheckBox(\"Run Blender Scripts After Processing\")\r\n+ self.blender_integration_checkbox.setToolTip(\"If checked, attempts to run create_nodegroups.py and create_materials.py in Blender.\")\r\n+ blender_layout.addWidget(self.blender_integration_checkbox)\r\n+\r\n+ # Nodegroup Blend Path\r\n+ nodegroup_layout = QHBoxLayout()\r\n+ nodegroup_layout.addWidget(QLabel(\"Nodegroup .blend:\"))\r\n+ self.nodegroup_blend_path_input = QLineEdit()\r\n+ self.browse_nodegroup_blend_button = QPushButton(\"...\")\r\n+ self.browse_nodegroup_blend_button.setFixedWidth(30)\r\n+ self.browse_nodegroup_blend_button.clicked.connect(self._browse_for_nodegroup_blend)\r\n+ nodegroup_layout.addWidget(self.nodegroup_blend_path_input)\r\n+ nodegroup_layout.addWidget(self.browse_nodegroup_blend_button)\r\n+ blender_layout.addLayout(nodegroup_layout)\r\n+\r\n+ # Materials Blend Path\r\n+ materials_layout = QHBoxLayout()\r\n+ materials_layout.addWidget(QLabel(\"Materials .blend:\"))\r\n+ self.materials_blend_path_input = QLineEdit()\r\n+ self.browse_materials_blend_button = QPushButton(\"...\")\r\n+ self.browse_materials_blend_button.setFixedWidth(30)\r\n+ self.browse_materials_blend_button.clicked.connect(self._browse_for_materials_blend)\r\n+ materials_layout.addWidget(self.materials_blend_path_input)\r\n+ materials_layout.addWidget(self.browse_materials_blend_button)\r\n+ blender_layout.addLayout(materials_layout)\r\n+\r\n+ # Initialize paths from config\r\n+ try:\r\n+ default_ng_path = getattr(core_config, 'DEFAULT_NODEGROUP_BLEND_PATH', '')\r\n+ default_mat_path = getattr(core_config, 'DEFAULT_MATERIALS_BLEND_PATH', '')\r\n+ self.nodegroup_blend_path_input.setText(default_ng_path if default_ng_path else \"\")\r\n+ self.materials_blend_path_input.setText(default_mat_path if default_mat_path else \"\")\r\n+ except Exception as e:\r\n+ log.error(f\"Error reading default Blender paths from config: {e}\")\r\n+\r\n+ # Disable Blender controls initially if checkbox is unchecked\r\n+ self.nodegroup_blend_path_input.setEnabled(False)\r\n+ self.browse_nodegroup_blend_button.setEnabled(False)\r\n+ self.materials_blend_path_input.setEnabled(False)\r\n+ self.browse_materials_blend_button.setEnabled(False)\r\n+ self.blender_integration_checkbox.toggled.connect(self._toggle_blender_controls)\r\n+\r\n+ main_layout.addWidget(blender_group) # Add the group box to the main layout\r\n+\r\n # --- Bottom Controls ---\r\n bottom_controls_layout = QHBoxLayout()\r\n self.overwrite_checkbox = QCheckBox(\"Overwrite Existing\")\r\n self.overwrite_checkbox.setToolTip(\"If checked, existing output folders for processed assets will be deleted and replaced.\")\r\n@@ -604,9 +652,13 @@\n try: self.processing_thread.started.disconnect()\r\n except RuntimeError: pass\r\n self.processing_thread.started.connect(\r\n lambda: self.processing_handler.run_processing(\r\n- input_paths, selected_preset, output_dir_str, overwrite, num_workers\r\n+ input_paths, selected_preset, output_dir_str, overwrite, num_workers,\r\n+ # Pass Blender integration settings\r\n+ run_blender=self.blender_integration_checkbox.isChecked(),\r\n+ nodegroup_blend_path=self.nodegroup_blend_path_input.text(),\r\n+ materials_blend_path=self.materials_blend_path_input.text()\r\n )\r\n )\r\n self.processing_thread.start()\r\n log.info(\"Processing thread started.\")\r\n@@ -900,10 +952,47 @@\n self.drag_drop_area.setEnabled(enabled)\r\n self.preview_table.setEnabled(enabled)\r\n # Editor panel controls (should generally be enabled unless processing)\r\n self.editor_panel.setEnabled(enabled) # Enable/disable the whole panel\r\n+ # Blender controls\r\n+ self.blender_integration_checkbox.setEnabled(enabled)\r\n+ # Only enable path inputs if checkbox is checked AND main controls are enabled\r\n+ blender_paths_enabled = enabled and self.blender_integration_checkbox.isChecked()\r\n+ self.nodegroup_blend_path_input.setEnabled(blender_paths_enabled)\r\n+ self.browse_nodegroup_blend_button.setEnabled(blender_paths_enabled)\r\n+ self.materials_blend_path_input.setEnabled(blender_paths_enabled)\r\n+ self.browse_materials_blend_button.setEnabled(blender_paths_enabled)\r\n \r\n \r\n+ @Slot(bool)\r\n+ def _toggle_blender_controls(self, checked):\r\n+ \"\"\"Enable/disable Blender path inputs based on the checkbox state.\"\"\"\r\n+ self.nodegroup_blend_path_input.setEnabled(checked)\r\n+ self.browse_nodegroup_blend_button.setEnabled(checked)\r\n+ self.materials_blend_path_input.setEnabled(checked)\r\n+ self.browse_materials_blend_button.setEnabled(checked)\r\n+\r\n+ def _browse_for_blend_file(self, line_edit_widget: QLineEdit):\r\n+ \"\"\"Opens a dialog to select a .blend file and updates the line edit.\"\"\"\r\n+ current_path = line_edit_widget.text()\r\n+ start_dir = str(Path(current_path).parent) if current_path and Path(current_path).exists() else str(project_root)\r\n+\r\n+ file_path, _ = QFileDialog.getOpenFileName(\r\n+ self,\r\n+ \"Select Blender File\",\r\n+ start_dir,\r\n+ \"Blender Files (*.blend);;All Files (*)\"\r\n+ )\r\n+ if file_path:\r\n+ line_edit_widget.setText(file_path)\r\n+ log.info(f\"User selected blend file: {file_path}\")\r\n+\r\n+ def _browse_for_nodegroup_blend(self):\r\n+ self._browse_for_blend_file(self.nodegroup_blend_path_input)\r\n+\r\n+ def _browse_for_materials_blend(self):\r\n+ self._browse_for_blend_file(self.materials_blend_path_input)\r\n+\r\n # --- Preset Editor Methods (Adapted from PresetEditorDialog) ---\r\n \r\n def _editor_add_list_item(self, list_widget: QListWidget):\r\n \"\"\"Adds an editable item to the specified list widget in the editor.\"\"\"\r\n"
},
{
"date": 1745269724128,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -375,8 +375,9 @@\n drag_drop_layout.addWidget(drag_drop_label)\r\n self.drag_drop_area.setMinimumHeight(100)\r\n self.setAcceptDrops(True) # Main window handles drops initially\r\n main_layout.addWidget(self.drag_drop_area)\r\n+ self.drag_drop_area.setVisible(False) # Hide the specific visual drag/drop area\r\n \r\n # --- Preview Area (Table) ---\r\n self.preview_label = QLabel(\"File Preview (using selected processing preset):\")\r\n self.preview_table = QTableWidget()\r\n@@ -568,10 +569,21 @@\n else: self.statusBar().showMessage(f\"Input path not found: {p.name}\", 5000); print(f\"Input path not found: {p_str}\")\r\n if added_count > 0:\r\n log.info(f\"Added {added_count} new asset paths: {newly_added_paths}\")\r\n self.statusBar().showMessage(f\"Added {added_count} asset(s). Updating preview...\", 3000)\r\n- self.update_preview()\r\n \r\n+ # --- Auto-disable detailed preview if > 10 assets ---\r\n+ preview_toggled = False\r\n+ if hasattr(self, 'toggle_preview_action') and len(self.current_asset_paths) > 10:\r\n+ if not self.toggle_preview_action.isChecked(): # Only check it if it's not already checked\r\n+ log.info(f\"Asset count ({len(self.current_asset_paths)}) > 10. Forcing simple preview.\")\r\n+ self.toggle_preview_action.setChecked(True) # This will trigger update_preview via its signal\r\n+ preview_toggled = True\r\n+\r\n+ # Only call update_preview directly if the toggle wasn't triggered\r\n+ if not preview_toggled:\r\n+ self.update_preview()\r\n+\r\n def _browse_for_output_directory(self):\r\n \"\"\"Opens a dialog to select the output directory.\"\"\"\r\n current_path = self.output_path_edit.text()\r\n if not current_path or not Path(current_path).is_dir():\r\n"
},
{
"date": 1745314361728,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -668,9 +668,11 @@\n input_paths, selected_preset, output_dir_str, overwrite, num_workers,\r\n # Pass Blender integration settings\r\n run_blender=self.blender_integration_checkbox.isChecked(),\r\n nodegroup_blend_path=self.nodegroup_blend_path_input.text(),\r\n- materials_blend_path=self.materials_blend_path_input.text()\r\n+ materials_blend_path=self.materials_blend_path_input.text(),\r\n+ # Pass verbose setting\r\n+ verbose=self.toggle_verbose_action.isChecked()\r\n )\r\n )\r\n self.processing_thread.start()\r\n log.info(\"Processing thread started.\")\r\n@@ -1360,8 +1362,14 @@\n # Connect to update_preview, which now checks this action's state\r\n self.toggle_preview_action.toggled.connect(self.update_preview)\r\n view_menu.addAction(self.toggle_preview_action)\r\n \r\n+ # Verbose Logging Action\r\n+ self.toggle_verbose_action = QAction(\"Verbose Logging (DEBUG)\", self, checkable=True)\r\n+ self.toggle_verbose_action.setChecked(False) # Start disabled (INFO level)\r\n+ self.toggle_verbose_action.toggled.connect(self._toggle_verbose_logging)\r\n+ view_menu.addAction(self.toggle_verbose_action)\r\n+\r\n # --- Logging Handler Setup ---\r\n def setup_logging_handler(self):\r\n \"\"\"Creates and configures the custom QtLogHandler.\"\"\"\r\n self.log_handler = QtLogHandler(self)\r\n@@ -1384,8 +1392,23 @@\n if hasattr(self, 'log_console_widget'):\r\n self.log_console_widget.setVisible(checked)\r\n log.debug(f\"Log console visibility set to: {checked}\")\r\n \r\n+ @Slot(bool)\r\n+ def _toggle_verbose_logging(self, checked):\r\n+ \"\"\"Sets the logging level for the root logger and the GUI handler.\"\"\"\r\n+ if not hasattr(self, 'log_handler'):\r\n+ log.error(\"Log handler not initialized, cannot change level.\")\r\n+ return\r\n+\r\n+ new_level = logging.DEBUG if checked else logging.INFO\r\n+ root_logger = logging.getLogger() # Get the root logger\r\n+ root_logger.setLevel(new_level)\r\n+ self.log_handler.setLevel(new_level)\r\n+ log.info(f\"Root and GUI logging level set to: {logging.getLevelName(new_level)}\")\r\n+ # Update status bar or log console to indicate change\r\n+ self.statusBar().showMessage(f\"Logging level set to {logging.getLevelName(new_level)}\", 3000)\r\n+\r\n @Slot(str)\r\n def _append_log_message(self, message):\r\n \"\"\"Appends a log message to the QTextEdit console.\"\"\"\r\n if hasattr(self, 'log_console_output'):\r\n"
},
{
"date": 1745317826770,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -860,76 +860,105 @@\n \r\n # Slot for prediction results (Updated for new format and coloring)\r\n @Slot(list)\r\n def on_prediction_results_ready(self, results: list):\r\n- \"\"\"Populates the preview table with detailed prediction results.\"\"\"\r\n+ \"\"\"Populates the preview table with detailed prediction results, handling multi-asset display.\"\"\"\r\n log.debug(f\"[{time.time():.4f}] on_prediction_results_ready received {len(results)} file details.\")\r\n self.preview_table.setSortingEnabled(False) # Disable sorting during population\r\n self.preview_table.setRowCount(0) # Clear previous results\r\n \r\n+ # Update column headers to include Predicted Asset Name\r\n+ self.preview_table.setColumnCount(5)\r\n+ self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Predicted Asset\", \"Original Path\", \"Predicted Output\", \"Details\"])\r\n+ # Adjust resize modes\r\n+ self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Status\r\n+ self.preview_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Predicted Asset\r\n+ self.preview_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Original Path\r\n+ self.preview_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Predicted Output\r\n+ self.preview_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents) # Details\r\n+\r\n self.preview_table.setRowCount(len(results))\r\n+ last_source_asset = None\r\n+ asset_group_color_toggle = False # To alternate background slightly for different assets\r\n+\r\n+ # Sort results primarily by source_asset, then by original_path for grouping\r\n+ results.sort(key=lambda x: (x.get(\"source_asset\", \"\"), x.get(\"original_path\", \"\")))\r\n+\r\n for row, file_details in enumerate(results):\r\n- # Ensure file_details is a dictionary before accessing keys\r\n if not isinstance(file_details, dict):\r\n log.warning(f\"Received non-dict item in prediction results: {file_details}\")\r\n- # Add a row indicating an error (adjust column order)\r\n+ # Add error row (adjust column count)\r\n item_status = QTableWidgetItem(\"Error\")\r\n+ item_pred_asset = QTableWidgetItem(\"\")\r\n item_orig = QTableWidgetItem(\"[Invalid Data Received]\")\r\n- item_pred_name = QTableWidgetItem(\"\")\r\n- item_details_display = QTableWidgetItem(\"Invalid data format from handler\")\r\n+ item_pred_out = QTableWidgetItem(\"\")\r\n+ item_details_display = QTableWidgetItem(\"Invalid data format\")\r\n self.preview_table.setItem(row, 0, item_status)\r\n- self.preview_table.setItem(row, 1, item_orig)\r\n- self.preview_table.setItem(row, 2, item_pred_name)\r\n- self.preview_table.setItem(row, 3, item_details_display)\r\n+ self.preview_table.setItem(row, 1, item_pred_asset)\r\n+ self.preview_table.setItem(row, 2, item_orig)\r\n+ self.preview_table.setItem(row, 3, item_pred_out)\r\n+ self.preview_table.setItem(row, 4, item_details_display)\r\n continue\r\n \r\n # Extract data using the new keys\r\n original_path = file_details.get('original_path', '[Missing Path]')\r\n- predicted_name = file_details.get('predicted_name', '') # Can be None or empty\r\n+ predicted_asset_name = file_details.get('predicted_asset_name') # Can be None\r\n+ predicted_output_name = file_details.get('predicted_output_name') # Can be None\r\n status = file_details.get('status', '[No Status]')\r\n- details = file_details.get('details', '') # Tooltip info\r\n- source_asset = file_details.get('source_asset', 'N/A') # For tooltip\r\n+ details = file_details.get('details', '')\r\n+ source_asset = file_details.get('source_asset', 'N/A')\r\n \r\n+ # Check if source asset changed for visual grouping\r\n+ if source_asset != last_source_asset:\r\n+ asset_group_color_toggle = not asset_group_color_toggle\r\n+ last_source_asset = source_asset\r\n+\r\n # Create items\r\n+ item_status = QTableWidgetItem(status)\r\n+ item_pred_asset = QTableWidgetItem(predicted_asset_name if predicted_asset_name else \"N/A\")\r\n item_orig = QTableWidgetItem(original_path)\r\n- item_pred_name = QTableWidgetItem(predicted_name if predicted_name else \"\") # Display empty if None\r\n- item_status = QTableWidgetItem(status)\r\n- item_details_display = QTableWidgetItem(details if details else \"\") # Show details directly for now\r\n+ item_pred_out = QTableWidgetItem(predicted_output_name if predicted_output_name else \"\")\r\n+ item_details_display = QTableWidgetItem(details if details else \"\")\r\n \r\n # Set tooltips\r\n item_orig.setToolTip(f\"Source Asset: {source_asset}\\nFull Path: {original_path}\")\r\n- item_status.setToolTip(details if details else status) # Use details for status tooltip\r\n+ item_status.setToolTip(details if details else status)\r\n+ item_pred_asset.setToolTip(f\"Predicted Asset Name: {predicted_asset_name if predicted_asset_name else 'None'}\")\r\n+ item_pred_out.setToolTip(f\"Predicted Output Name: {predicted_output_name if predicted_output_name else 'None'}\")\r\n item_details_display.setToolTip(details if details else \"\")\r\n \r\n # --- Apply Text Coloring based on Status ---\r\n color = None\r\n- if status == \"Mapped\":\r\n- color = QColor(\"#9dd9db\") # Light blue/teal\r\n- elif status == \"Ignored\":\r\n- color = QColor(\"#c1753d\") # Orange/brown\r\n- elif status == \"Extra\":\r\n- color = QColor(\"#cfdca4\") # Light green/yellow\r\n- elif status == \"Unrecognised\":\r\n- color = QColor(\"#92371f\") # User specified dark red/brown\r\n- elif status == \"Model\":\r\n- color = QColor(\"#a4b8dc\") # Light blue/grey (Example)\r\n- elif status == \"Error\":\r\n- color = QColor(Qt.GlobalColor.red) # Example for Error\r\n+ if status == \"Mapped\": color = QColor(\"#9dd9db\")\r\n+ elif status == \"Ignored\": color = QColor(\"#c1753d\")\r\n+ elif status == \"Extra\": color = QColor(\"#cfdca4\")\r\n+ elif status == \"Unrecognised\": color = QColor(\"#92371f\")\r\n+ elif status == \"Model\": color = QColor(\"#a4b8dc\")\r\n+ elif status == \"Unmatched Extra\": color = QColor(\"#777777\") # Grey for unmatched\r\n+ elif status == \"Error\": color = QColor(Qt.GlobalColor.red)\r\n \r\n+ # Apply color to all items in the row\r\n+ items_in_row = [item_status, item_pred_asset, item_orig, item_pred_out, item_details_display]\r\n if color:\r\n- item_status.setForeground(color)\r\n- item_orig.setForeground(color)\r\n- item_pred_name.setForeground(color)\r\n- item_details_display.setForeground(color)\r\n- # --- End Coloring ---\r\n+ for item in items_in_row:\r\n+ if item: item.setForeground(color)\r\n \r\n+ # --- Apply Subtle Background Shading for Asset Grouping ---\r\n+ # bg_color = QColor(\"#FFFFFF\") # Default white\r\n+ # if asset_group_color_toggle:\r\n+ # bg_color = QColor(\"#F0F0F0\") # Slightly grey for alternate groups\r\n+ # for item in items_in_row:\r\n+ # if item: item.setBackground(bg_color)\r\n+ # Note: Background coloring might interfere with text color, disable for now\r\n+\r\n # Set items in table (new column order)\r\n self.preview_table.setItem(row, 0, item_status)\r\n- self.preview_table.setItem(row, 1, item_orig)\r\n- self.preview_table.setItem(row, 2, item_pred_name)\r\n- self.preview_table.setItem(row, 3, item_details_display)\r\n+ self.preview_table.setItem(row, 1, item_pred_asset)\r\n+ self.preview_table.setItem(row, 2, item_orig)\r\n+ self.preview_table.setItem(row, 3, item_pred_out)\r\n+ self.preview_table.setItem(row, 4, item_details_display)\r\n \r\n- self.preview_table.sortItems(1, Qt.SortOrder.AscendingOrder) # Sort by original path (now column 1)\r\n+ # self.preview_table.sortItems(1, Qt.SortOrder.AscendingOrder) # Sorting is done before loop now\r\n self.preview_table.setSortingEnabled(True) # Re-enable sorting\r\n log.debug(f\"[{time.time():.4f}] Preview table populated with detailed results and coloring.\")\r\n \r\n \r\n"
},
{
"date": 1745323149423,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -16,8 +16,11 @@\n )\r\n from PySide6.QtCore import Qt, QThread, Slot, Signal, QObject # Added Signal, QObject\r\n from PySide6.QtGui import QColor, QAction # Add QColor import, QAction\r\n \r\n+# --- GUI Model Imports ---\r\n+from gui.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel\r\n+\r\n # --- Backend Imports ---\r\n script_dir = Path(__file__).parent\r\n project_root = script_dir.parent\r\n if str(project_root) not in sys.path:\r\n@@ -379,23 +382,51 @@\n self.drag_drop_area.setVisible(False) # Hide the specific visual drag/drop area\r\n \r\n # --- Preview Area (Table) ---\r\n self.preview_label = QLabel(\"File Preview (using selected processing preset):\")\r\n- self.preview_table = QTableWidget()\r\n- self.preview_table.setColumnCount(4)\r\n- # New column order: Status, Original Path, Predicted Name, Details\r\n- self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Original Path\", \"Predicted Name\", \"Details\"])\r\n- # Adjust resize modes\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Status\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Original Path\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Predicted Name\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Details (Slimmer)\r\n- self.preview_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)\r\n- self.preview_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)\r\n- self.preview_table.setAlternatingRowColors(True)\r\n- self.preview_table.setSortingEnabled(True)\r\n+ self.preview_table = QTableWidget() # Keep QTableWidget for now, will replace with QTableView later\r\n+ # Initialize models\r\n+ self.preview_model = PreviewTableModel()\r\n+ self.preview_proxy_model = PreviewSortFilterProxyModel()\r\n+ self.preview_proxy_model.setSourceModel(self.preview_model)\r\n+\r\n+ # Use the proxy model for the table view\r\n+ # NOTE: QTableWidget is simpler but less flexible with models.\r\n+ # For full model/view benefits (like multi-column sorting via proxy),\r\n+ # we should ideally switch to QTableView. Sticking with QTableWidget for minimal change first.\r\n+ # However, QTableWidget doesn't fully support QSortFilterProxyModel for sorting.\r\n+ # Let's switch to QTableView now for proper model/proxy integration.\r\n+ from PySide6.QtWidgets import QTableView # Import QTableView\r\n+\r\n+ self.preview_table_view = QTableView() # Use QTableView instead of QTableWidget\r\n+ self.preview_table_view.setModel(self.preview_proxy_model) # Set the proxy model\r\n+\r\n+ # Set headers and resize modes using the model's headerData\r\n+ # The model defines the columns and headers\r\n+ header = self.preview_table_view.horizontalHeader()\r\n+ header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)\r\n+ header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents)\r\n+ header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.Stretch)\r\n+ # Remove Predicted Output column as requested\r\n+ # header.setSectionResizeMode(self.preview_model.COL_PREDICTED_OUTPUT, QHeaderView.ResizeMode.ResizeToContents)\r\n+ header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents)\r\n+\r\n+ # Hide the Predicted Output column\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True)\r\n+\r\n+ # Set selection behavior and alternating colors\r\n+ self.preview_table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)\r\n+ self.preview_table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)\r\n+ self.preview_table_view.setAlternatingRowColors(True)\r\n+\r\n+ # Enable sorting via header clicks\r\n+ self.preview_table_view.setSortingEnabled(True)\r\n+ # Set default sort column (Status) - the proxy model's lessThan handles the custom order\r\n+ self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)\r\n+\r\n+\r\n main_layout.addWidget(self.preview_label)\r\n- main_layout.addWidget(self.preview_table, 1) # Allow table to stretch\r\n+ main_layout.addWidget(self.preview_table_view, 1) # Add the table view to layout\r\n \r\n # --- Progress Bar ---\r\n self.progress_bar = QProgressBar()\r\n self.progress_bar.setValue(0)\r\n@@ -703,9 +734,9 @@\n \r\n if hasattr(self, 'current_asset_paths') and self.current_asset_paths:\r\n log.info(f\"Clearing asset queue ({len(self.current_asset_paths)} items).\")\r\n self.current_asset_paths.clear()\r\n- self.preview_table.setRowCount(0)\r\n+ self.preview_model.clear_data() # Clear the model data\r\n self.statusBar().showMessage(\"Asset queue cleared.\", 3000)\r\n else:\r\n self.statusBar().showMessage(\"Asset queue is already empty.\", 3000)\r\n \r\n"
},
{
"date": 1745324014707,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -891,109 +891,14 @@\n \r\n # Slot for prediction results (Updated for new format and coloring)\r\n @Slot(list)\r\n def on_prediction_results_ready(self, results: list):\r\n- \"\"\"Populates the preview table with detailed prediction results, handling multi-asset display.\"\"\"\r\n+ \"\"\"Populates the preview table model with detailed prediction results.\"\"\"\r\n log.debug(f\"[{time.time():.4f}] on_prediction_results_ready received {len(results)} file details.\")\r\n- self.preview_table.setSortingEnabled(False) # Disable sorting during population\r\n- self.preview_table.setRowCount(0) # Clear previous results\r\n+ # Update the model with the new data\r\n+ self.preview_model.set_data(results)\r\n+ log.debug(f\"[{time.time():.4f}] Preview model updated with detailed results.\")\r\n \r\n- # Update column headers to include Predicted Asset Name\r\n- self.preview_table.setColumnCount(5)\r\n- self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Predicted Asset\", \"Original Path\", \"Predicted Output\", \"Details\"])\r\n- # Adjust resize modes\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Status\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Predicted Asset\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Original Path\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Predicted Output\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents) # Details\r\n-\r\n- self.preview_table.setRowCount(len(results))\r\n- last_source_asset = None\r\n- asset_group_color_toggle = False # To alternate background slightly for different assets\r\n-\r\n- # Sort results primarily by source_asset, then by original_path for grouping\r\n- results.sort(key=lambda x: (x.get(\"source_asset\", \"\"), x.get(\"original_path\", \"\")))\r\n-\r\n- for row, file_details in enumerate(results):\r\n- if not isinstance(file_details, dict):\r\n- log.warning(f\"Received non-dict item in prediction results: {file_details}\")\r\n- # Add error row (adjust column count)\r\n- item_status = QTableWidgetItem(\"Error\")\r\n- item_pred_asset = QTableWidgetItem(\"\")\r\n- item_orig = QTableWidgetItem(\"[Invalid Data Received]\")\r\n- item_pred_out = QTableWidgetItem(\"\")\r\n- item_details_display = QTableWidgetItem(\"Invalid data format\")\r\n- self.preview_table.setItem(row, 0, item_status)\r\n- self.preview_table.setItem(row, 1, item_pred_asset)\r\n- self.preview_table.setItem(row, 2, item_orig)\r\n- self.preview_table.setItem(row, 3, item_pred_out)\r\n- self.preview_table.setItem(row, 4, item_details_display)\r\n- continue\r\n-\r\n- # Extract data using the new keys\r\n- original_path = file_details.get('original_path', '[Missing Path]')\r\n- predicted_asset_name = file_details.get('predicted_asset_name') # Can be None\r\n- predicted_output_name = file_details.get('predicted_output_name') # Can be None\r\n- status = file_details.get('status', '[No Status]')\r\n- details = file_details.get('details', '')\r\n- source_asset = file_details.get('source_asset', 'N/A')\r\n-\r\n- # Check if source asset changed for visual grouping\r\n- if source_asset != last_source_asset:\r\n- asset_group_color_toggle = not asset_group_color_toggle\r\n- last_source_asset = source_asset\r\n-\r\n- # Create items\r\n- item_status = QTableWidgetItem(status)\r\n- item_pred_asset = QTableWidgetItem(predicted_asset_name if predicted_asset_name else \"N/A\")\r\n- item_orig = QTableWidgetItem(original_path)\r\n- item_pred_out = QTableWidgetItem(predicted_output_name if predicted_output_name else \"\")\r\n- item_details_display = QTableWidgetItem(details if details else \"\")\r\n-\r\n- # Set tooltips\r\n- item_orig.setToolTip(f\"Source Asset: {source_asset}\\nFull Path: {original_path}\")\r\n- item_status.setToolTip(details if details else status)\r\n- item_pred_asset.setToolTip(f\"Predicted Asset Name: {predicted_asset_name if predicted_asset_name else 'None'}\")\r\n- item_pred_out.setToolTip(f\"Predicted Output Name: {predicted_output_name if predicted_output_name else 'None'}\")\r\n- item_details_display.setToolTip(details if details else \"\")\r\n-\r\n- # --- Apply Text Coloring based on Status ---\r\n- color = None\r\n- if status == \"Mapped\": color = QColor(\"#9dd9db\")\r\n- elif status == \"Ignored\": color = QColor(\"#c1753d\")\r\n- elif status == \"Extra\": color = QColor(\"#cfdca4\")\r\n- elif status == \"Unrecognised\": color = QColor(\"#92371f\")\r\n- elif status == \"Model\": color = QColor(\"#a4b8dc\")\r\n- elif status == \"Unmatched Extra\": color = QColor(\"#777777\") # Grey for unmatched\r\n- elif status == \"Error\": color = QColor(Qt.GlobalColor.red)\r\n-\r\n- # Apply color to all items in the row\r\n- items_in_row = [item_status, item_pred_asset, item_orig, item_pred_out, item_details_display]\r\n- if color:\r\n- for item in items_in_row:\r\n- if item: item.setForeground(color)\r\n-\r\n- # --- Apply Subtle Background Shading for Asset Grouping ---\r\n- # bg_color = QColor(\"#FFFFFF\") # Default white\r\n- # if asset_group_color_toggle:\r\n- # bg_color = QColor(\"#F0F0F0\") # Slightly grey for alternate groups\r\n- # for item in items_in_row:\r\n- # if item: item.setBackground(bg_color)\r\n- # Note: Background coloring might interfere with text color, disable for now\r\n-\r\n- # Set items in table (new column order)\r\n- self.preview_table.setItem(row, 0, item_status)\r\n- self.preview_table.setItem(row, 1, item_pred_asset)\r\n- self.preview_table.setItem(row, 2, item_orig)\r\n- self.preview_table.setItem(row, 3, item_pred_out)\r\n- self.preview_table.setItem(row, 4, item_details_display)\r\n-\r\n- # self.preview_table.sortItems(1, Qt.SortOrder.AscendingOrder) # Sorting is done before loop now\r\n- self.preview_table.setSortingEnabled(True) # Re-enable sorting\r\n- log.debug(f\"[{time.time():.4f}] Preview table populated with detailed results and coloring.\")\r\n-\r\n-\r\n @Slot()\r\n def on_prediction_finished(self):\r\n log.debug(f\"[{time.time():.4f}] Prediction finished signal received.\")\r\n \r\n@@ -1023,9 +928,10 @@\n self.preset_combo.setEnabled(enabled)\r\n self.start_button.setEnabled(enabled)\r\n self.setAcceptDrops(enabled)\r\n self.drag_drop_area.setEnabled(enabled)\r\n- self.preview_table.setEnabled(enabled)\r\n+ # self.preview_table.setEnabled(enabled) # This was the old QTableWidget\r\n+ self.preview_table_view.setEnabled(enabled) # Enable/disable the QTableView instead\r\n # Editor panel controls (should generally be enabled unless processing)\r\n self.editor_panel.setEnabled(enabled) # Enable/disable the whole panel\r\n # Blender controls\r\n self.blender_integration_checkbox.setEnabled(enabled)\r\n"
},
{
"date": 1745324072991,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -422,8 +422,13 @@\n self.preview_table_view.setSortingEnabled(True)\r\n # Set default sort column (Status) - the proxy model's lessThan handles the custom order\r\n self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)\r\n \r\n+ # Move \"Original Path\" column to the far right\r\n+ original_path_logical_index = self.preview_model.COL_ORIGINAL_PATH\r\n+ original_path_visual_index = header.visualIndex(original_path_logical_index)\r\n+ if original_path_visual_index != -1: # Check if the column is visible\r\n+ header.moveSection(original_path_visual_index, header.count() - 1)\r\n \r\n main_layout.addWidget(self.preview_label)\r\n main_layout.addWidget(self.preview_table_view, 1) # Add the table view to layout\r\n \r\n"
},
{
"date": 1745324180112,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -5,8 +5,11 @@\n import time\r\n from pathlib import Path\r\n from functools import partial # For connecting signals with arguments\r\n \r\n+log = logging.getLogger(__name__)\r\n+log.info(f\"sys.path: {sys.path}\")\r\n+\r\n from PySide6.QtWidgets import (\r\n QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, # Added QSplitter\r\n QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,\r\n QProgressBar, QLabel, QFrame, QCheckBox, QSpinBox, QListWidget, QTextEdit, # Added QListWidget, QTextEdit\r\n"
},
{
"date": 1745324454266,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -19,8 +19,15 @@\n )\r\n from PySide6.QtCore import Qt, QThread, Slot, Signal, QObject # Added Signal, QObject\r\n from PySide6.QtGui import QColor, QAction # Add QColor import, QAction\r\n \r\n+# --- Debugging sys.path ---\r\n+import sys\r\n+import logging # Ensure logging is imported early\r\n+log = logging.getLogger(__name__) # Get logger early\r\n+log.info(f\"Initial sys.path: {sys.path}\")\r\n+# --- End Debugging ---\r\n+\r\n # --- GUI Model Imports ---\r\n from gui.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel\r\n \r\n # --- Backend Imports ---\r\n"
},
{
"date": 1745324581046,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -19,15 +19,8 @@\n )\r\n from PySide6.QtCore import Qt, QThread, Slot, Signal, QObject # Added Signal, QObject\r\n from PySide6.QtGui import QColor, QAction # Add QColor import, QAction\r\n \r\n-# --- Debugging sys.path ---\r\n-import sys\r\n-import logging # Ensure logging is imported early\r\n-log = logging.getLogger(__name__) # Get logger early\r\n-log.info(f\"Initial sys.path: {sys.path}\")\r\n-# --- End Debugging ---\r\n-\r\n # --- GUI Model Imports ---\r\n from gui.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel\r\n \r\n # --- Backend Imports ---\r\n"
},
{
"date": 1745326087649,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -335,17 +335,8 @@\n \"\"\"Sets up the UI elements for the right main processing panel.\"\"\"\r\n main_layout = QVBoxLayout(self.main_panel)\r\n main_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins\r\n \r\n- # --- Top Section: Preset Selection (for processing) ---\r\n- top_layout = QHBoxLayout()\r\n- self.preset_label = QLabel(\"Preset (for processing):\")\r\n- self.preset_combo = QComboBox() # This is for selecting the preset to USE for processing\r\n- self.preset_combo.currentIndexChanged.connect(self.update_preview) # Update preview when processing preset changes\r\n- top_layout.addWidget(self.preset_label)\r\n- top_layout.addWidget(self.preset_combo, 1)\r\n- main_layout.addLayout(top_layout)\r\n-\r\n # --- Output Directory Selection ---\r\n output_layout = QHBoxLayout()\r\n self.output_dir_label = QLabel(\"Output Directory:\")\r\n self.output_path_edit = QLineEdit()\r\n@@ -384,9 +375,9 @@\n main_layout.addWidget(self.drag_drop_area)\r\n self.drag_drop_area.setVisible(False) # Hide the specific visual drag/drop area\r\n \r\n # --- Preview Area (Table) ---\r\n- self.preview_label = QLabel(\"File Preview (using selected processing preset):\")\r\n+ self.preview_label = QLabel(\"File Preview:\") # Updated Label\r\n self.preview_table = QTableWidget() # Keep QTableWidget for now, will replace with QTableView later\r\n # Initialize models\r\n self.preview_model = PreviewTableModel()\r\n self.preview_proxy_model = PreviewSortFilterProxyModel()\r\n@@ -655,11 +646,14 @@\n input_paths = list(self.current_asset_paths)\r\n if not input_paths:\r\n self.statusBar().showMessage(\"No assets added to process.\", 3000)\r\n return\r\n- selected_preset = self.preset_combo.currentText() # Use the processing combo box\r\n+ # --- Get preset from editor list ---\r\n+ current_editor_item = self.editor_preset_list.currentItem()\r\n+ selected_preset = current_editor_item.text() if current_editor_item else None\r\n+ # --- End Get preset ---\r\n if not selected_preset:\r\n- self.statusBar().showMessage(\"Please select a preset for processing.\", 3000)\r\n+ self.statusBar().showMessage(\"Please select a preset from the list on the left.\", 3000) # Updated message\r\n return\r\n overwrite = self.overwrite_checkbox.isChecked()\r\n num_workers = self.workers_spinbox.value()\r\n \r\n@@ -802,13 +796,17 @@\n return\r\n if PredictionHandler is None:\r\n self.statusBar().showMessage(\"Error: Prediction components not loaded.\", 5000)\r\n return\r\n- selected_preset = self.preset_combo.currentText() # Use the processing combo box\r\n+ # --- Get preset from editor list ---\r\n+ current_editor_item = self.editor_preset_list.currentItem()\r\n+ selected_preset = current_editor_item.text() if current_editor_item else None\r\n+ # --- End Get preset ---\r\n if not selected_preset:\r\n- log.debug(\"Update preview called with no processing preset selected.\")\r\n- self.preview_table.setRowCount(0)\r\n- self.statusBar().showMessage(\"Select a processing preset to update preview.\", 3000)\r\n+ log.debug(\"Update preview called with no preset selected in the editor list.\")\r\n+ # self.preview_table.setRowCount(0) # Clearing is handled by model now\r\n+ self.preview_model.clear_data() # Clear model if no preset selected\r\n+ self.statusBar().showMessage(\"Select a preset from the list on the left to update preview.\", 3000) # Updated message\r\n return\r\n if not hasattr(self, 'current_asset_paths') or not self.current_asset_paths:\r\n log.debug(\"Update preview called with no assets tracked.\")\r\n self.preview_table.setRowCount(0)\r\n@@ -1162,9 +1160,13 @@\n self.editor_preset_list.setCurrentItem(previous_item)\r\n self.editor_preset_list.blockSignals(False)\r\n return\r\n if current_item:\r\n- self._load_preset_for_editing(current_item.data(Qt.ItemDataRole.UserRole))\r\n+ preset_path = current_item.data(Qt.ItemDataRole.UserRole)\r\n+ self._load_preset_for_editing(preset_path)\r\n+ # --- Trigger preview update after loading editor ---\r\n+ self.update_preview()\r\n+ # --- End Trigger ---\r\n else:\r\n self._clear_editor() # Clear editor if selection is cleared\r\n \r\n def _gather_editor_data(self) -> dict:\r\n"
},
{
"date": 1745326177903,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -523,16 +523,16 @@\n # --- Preset Population and Handling ---\r\n \r\n def populate_presets(self):\r\n \"\"\"Scans presets dir and populates BOTH the editor list and processing combo.\"\"\"\r\n- log.debug(\"Populating preset list and combo box...\")\r\n- # Store current selections\r\n- current_combo_selection = self.preset_combo.currentText()\r\n+ log.debug(\"Populating preset list...\") # Updated log message\r\n+ # Store current list selection\r\n+ # current_combo_selection = self.preset_combo.currentText() # REMOVED\r\n current_list_item = self.editor_preset_list.currentItem()\r\n current_list_selection_text = current_list_item.text() if current_list_item else None\r\n \r\n- # Clear both\r\n- self.preset_combo.clear()\r\n+ # Clear list\r\n+ # self.preset_combo.clear() # REMOVED\r\n self.editor_preset_list.clear()\r\n \r\n if not PRESETS_DIR.is_dir():\r\n msg = f\"Error: Presets directory not found at {PRESETS_DIR}\"\r\n@@ -548,22 +548,20 @@\n msg = \"Warning: No presets found in presets directory.\"\r\n self.statusBar().showMessage(msg)\r\n log.warning(msg)\r\n else:\r\n- # Populate Combo Box (for processing)\r\n- self.preset_combo.addItems(preset_names)\r\n # Populate List Widget (for editing)\r\n for preset_path in presets:\r\n item = QListWidgetItem(preset_path.stem)\r\n item.setData(Qt.ItemDataRole.UserRole, preset_path) # Store full path\r\n self.editor_preset_list.addItem(item)\r\n \r\n self.statusBar().showMessage(f\"Loaded {len(presets)} presets.\")\r\n \r\n- # Try to restore selections\r\n- combo_index = self.preset_combo.findText(current_combo_selection)\r\n- if combo_index != -1:\r\n- self.preset_combo.setCurrentIndex(combo_index)\r\n+ # Try to restore list selection\r\n+ # combo_index = self.preset_combo.findText(current_combo_selection) # REMOVED\r\n+ # if combo_index != -1: # REMOVED\r\n+ # self.preset_combo.setCurrentIndex(combo_index) # REMOVED\r\n \r\n if current_list_selection_text:\r\n items = self.editor_preset_list.findItems(current_list_selection_text, Qt.MatchFlag.MatchExactly)\r\n if items:\r\n"
},
{
"date": 1745326406869,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -928,9 +928,8 @@\n \r\n def set_controls_enabled(self, enabled: bool):\r\n \"\"\"Enables/disables input controls during processing.\"\"\"\r\n # Main panel controls\r\n- self.preset_combo.setEnabled(enabled)\r\n self.start_button.setEnabled(enabled)\r\n self.setAcceptDrops(enabled)\r\n self.drag_drop_area.setEnabled(enabled)\r\n # self.preview_table.setEnabled(enabled) # This was the old QTableWidget\r\n"
},
{
"date": 1745332291002,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -742,97 +742,108 @@\n \r\n \r\n # --- Preview Update Method ---\r\n def update_preview(self):\r\n- # <<< ADDED LOG >>>\r\n log.info(f\"--> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}\")\r\n if hasattr(self, 'toggle_preview_action'):\r\n log.info(f\" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n- # <<< END ADDED LOG >>>\r\n \r\n- # --- Handle Preview Toggle ---\r\n- if hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked(): # Check menu action state\r\n- # <<< MODIFIED LOG >>>\r\n- log.info(\" Preview Toggle Check: DETAILED PREVIEW DISABLED (via menu). Preparing simple list view.\")\r\n- # <<< END MODIFIED LOG >>>\r\n- self.preview_table.setSortingEnabled(False)\r\n- self.preview_table.setRowCount(0) # Clear previous\r\n- self.preview_table.setColumnCount(1)\r\n- self.preview_table.setHorizontalHeaderLabels([\"Input Path\"])\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)\r\n+ # Determine mode based on menu action\r\n+ simple_mode_enabled = hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked()\r\n \r\n+ # Set the model's mode\r\n+ self.preview_model.set_simple_mode(simple_mode_enabled)\r\n+\r\n+ # Configure the QTableView based on the mode\r\n+ header = self.preview_table_view.horizontalHeader()\r\n+ if simple_mode_enabled:\r\n+ log.info(\" Configuring QTableView for SIMPLE mode.\")\r\n+ # Hide detailed columns, show simple column\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_STATUS, True)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_ASSET, True)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_ORIGINAL_PATH, True)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True) # Already hidden, but good practice\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_DETAILS, True)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, False) # Show the simple path column\r\n+\r\n+ # Set resize mode for the single visible column\r\n+ header.setSectionResizeMode(self.preview_model.COL_SIMPLE_PATH, QHeaderView.ResizeMode.Stretch)\r\n+\r\n+ # Disable sorting in simple mode (optional, but makes sense)\r\n+ self.preview_table_view.setSortingEnabled(False)\r\n+\r\n+ # Update status bar\r\n if hasattr(self, 'current_asset_paths') and self.current_asset_paths:\r\n- input_paths_list = sorted(list(self.current_asset_paths))\r\n- self.preview_table.setRowCount(len(input_paths_list))\r\n- for row, path_str in enumerate(input_paths_list):\r\n- item = QTableWidgetItem(path_str)\r\n- item.setToolTip(path_str) # Simple tooltip\r\n- self.preview_table.setItem(row, 0, item)\r\n- self.statusBar().showMessage(f\"Preview disabled. Showing {len(input_paths_list)} input assets.\", 3000)\r\n+ self.statusBar().showMessage(f\"Preview disabled. Showing {len(self.current_asset_paths)} input assets.\", 3000)\r\n else:\r\n- self.statusBar().showMessage(\"Preview disabled. No assets added.\", 3000)\r\n+ self.statusBar().showMessage(\"Preview disabled. No assets added.\", 3000)\r\n \r\n- self.preview_table.setSortingEnabled(True)\r\n- return # Stop here, do not run PredictionHandler\r\n- # --- End Preview Toggle Handling ---\r\n+ # Stop here, do not run PredictionHandler in simple mode\r\n+ return\r\n+ else:\r\n+ # --- Proceed with Detailed Preview ---\r\n+ log.info(\" Configuring QTableView for DETAILED mode.\")\r\n+ # Show detailed columns, hide simple column\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_STATUS, False)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_ASSET, False)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_ORIGINAL_PATH, False)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True) # Keep this hidden\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_DETAILS, False)\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, True) # Hide the simple path column\r\n \r\n- # --- Proceed with Detailed Preview ---\r\n- # <<< ADDED LOG >>>\r\n- log.info(\" Preview Toggle Check: DETAILED PREVIEW ENABLED (via menu). Preparing for prediction handler.\")\r\n- # <<< END ADDED LOG >>>\r\n- # Reset table headers for detailed view\r\n- self.preview_table.setColumnCount(4)\r\n- self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Original Path\", \"Predicted Name\", \"Details\"])\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Status\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Original Path\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Predicted Name\r\n- self.preview_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Details (Slimmer)\r\n+ # Set resize modes for detailed columns\r\n+ header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)\r\n+ header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents)\r\n+ header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.Stretch)\r\n+ header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents)\r\n \r\n+ # Re-enable sorting for detailed mode\r\n+ self.preview_table_view.setSortingEnabled(True)\r\n+ # Reset sort order if needed (optional, proxy model handles default)\r\n+ # self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)\r\n \r\n- if self.prediction_handler and self.prediction_handler.is_running:\r\n- log.warning(f\"[{time.time():.4f}] Preview update requested, but already running.\")\r\n- # Optionally: Request cancel previous prediction? For now, just return.\r\n- return\r\n- if PredictionHandler is None:\r\n- self.statusBar().showMessage(\"Error: Prediction components not loaded.\", 5000)\r\n- return\r\n- # --- Get preset from editor list ---\r\n- current_editor_item = self.editor_preset_list.currentItem()\r\n- selected_preset = current_editor_item.text() if current_editor_item else None\r\n- # --- End Get preset ---\r\n- if not selected_preset:\r\n- log.debug(\"Update preview called with no preset selected in the editor list.\")\r\n- # self.preview_table.setRowCount(0) # Clearing is handled by model now\r\n- self.preview_model.clear_data() # Clear model if no preset selected\r\n- self.statusBar().showMessage(\"Select a preset from the list on the left to update preview.\", 3000) # Updated message\r\n- return\r\n- if not hasattr(self, 'current_asset_paths') or not self.current_asset_paths:\r\n- log.debug(\"Update preview called with no assets tracked.\")\r\n- self.preview_table.setRowCount(0)\r\n- return\r\n- input_paths = list(self.current_asset_paths)\r\n- if not input_paths:\r\n- log.debug(\"Update preview called but no input paths derived.\")\r\n- self.preview_table.setRowCount(0)\r\n- return\r\n+ # --- Trigger Prediction Handler ---\r\n+ if self.prediction_handler and self.prediction_handler.is_running:\r\n+ log.warning(f\"[{time.time():.4f}] Preview update requested, but already running.\")\r\n+ return\r\n+ if PredictionHandler is None:\r\n+ self.statusBar().showMessage(\"Error: Prediction components not loaded.\", 5000)\r\n+ return\r\n+ # Get preset from editor list\r\n+ current_editor_item = self.editor_preset_list.currentItem()\r\n+ selected_preset = current_editor_item.text() if current_editor_item else None\r\n+ if not selected_preset:\r\n+ log.debug(\"Update preview called with no preset selected in the editor list.\")\r\n+ self.preview_model.clear_data() # Clear model if no preset selected\r\n+ self.statusBar().showMessage(\"Select a preset from the list on the left to update preview.\", 3000)\r\n+ return\r\n+ if not hasattr(self, 'current_asset_paths') or not self.current_asset_paths:\r\n+ log.debug(\"Update preview called with no assets tracked.\")\r\n+ self.preview_model.clear_data() # Clear model if no assets\r\n+ return\r\n+ input_paths = list(self.current_asset_paths)\r\n+ if not input_paths:\r\n+ log.debug(\"Update preview called but no input paths derived.\")\r\n+ self.preview_model.clear_data() # Clear model if no paths\r\n+ return\r\n \r\n- log.info(f\"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items, Preset='{selected_preset}'\")\r\n- self.statusBar().showMessage(f\"Updating preview for '{selected_preset}'...\", 0)\r\n- self.preview_table.setRowCount(0) # Clear before starting prediction\r\n- self.setup_threads() # Ensure threads are ready\r\n- if self.prediction_thread and self.prediction_handler:\r\n- try: self.prediction_thread.started.disconnect() # Disconnect previous lambda if any\r\n- except RuntimeError: pass\r\n- # Connect the lambda to start the prediction\r\n- self.prediction_thread.started.connect(\r\n- lambda: self.prediction_handler.run_prediction(input_paths, selected_preset)\r\n- )\r\n- log.debug(f\"[{time.time():.4f}] Starting prediction thread...\")\r\n- self.prediction_thread.start()\r\n- log.debug(f\"[{time.time():.4f}] Prediction thread start requested.\")\r\n- else:\r\n- log.error(f\"[{time.time():.4f}] Failed to start prediction: Thread or handler not initialized.\")\r\n- self.statusBar().showMessage(\"Error: Failed to initialize prediction thread.\", 5000)\r\n+ log.info(f\"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items, Preset='{selected_preset}'\")\r\n+ self.statusBar().showMessage(f\"Updating preview for '{selected_preset}'...\", 0)\r\n+ # Clearing is handled by model's set_data now, no need to clear table view directly\r\n+ self.setup_threads() # Ensure threads are ready\r\n+ if self.prediction_thread and self.prediction_handler:\r\n+ try: self.prediction_thread.started.disconnect() # Disconnect previous lambda if any\r\n+ except RuntimeError: pass\r\n+ # Connect the lambda to start the prediction\r\n+ self.prediction_thread.started.connect(\r\n+ lambda: self.prediction_handler.run_prediction(input_paths, selected_preset)\r\n+ )\r\n+ log.debug(f\"[{time.time():.4f}] Starting prediction thread...\")\r\n+ self.prediction_thread.start()\r\n+ log.debug(f\"[{time.time():.4f}] Prediction thread start requested.\")\r\n+ else:\r\n+ log.error(f\"[{time.time():.4f}] Failed to start prediction: Thread or handler not initialized.\")\r\n+ self.statusBar().showMessage(\"Error: Failed to initialize prediction thread.\", 5000)\r\n \r\n # --- Threading and Processing Control ---\r\n def setup_threads(self):\r\n # Setup Processing Thread\r\n"
},
{
"date": 1745332400975,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -750,8 +750,9 @@\n # Determine mode based on menu action\r\n simple_mode_enabled = hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked()\r\n \r\n # Set the model's mode\r\n+ log.debug(f\" Setting model simple mode to: {simple_mode_enabled}\")\r\n self.preview_model.set_simple_mode(simple_mode_enabled)\r\n \r\n # Configure the QTableView based on the mode\r\n header = self.preview_table_view.horizontalHeader()\r\n@@ -762,12 +763,16 @@\n self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_ASSET, True)\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_ORIGINAL_PATH, True)\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True) # Already hidden, but good practice\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_DETAILS, True)\r\n- self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, False) # Show the simple path column\r\n+ # Ensure the simple path column exists and is visible\r\n+ if self.preview_model.columnCount() > self.preview_model.COL_SIMPLE_PATH:\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, False) # Show the simple path column\r\n+ # Set resize mode for the single visible column\r\n+ header.setSectionResizeMode(self.preview_model.COL_SIMPLE_PATH, QHeaderView.ResizeMode.Stretch)\r\n+ else:\r\n+ log.error(\" Simple path column index out of bounds for model.\")\r\n \r\n- # Set resize mode for the single visible column\r\n- header.setSectionResizeMode(self.preview_model.COL_SIMPLE_PATH, QHeaderView.ResizeMode.Stretch)\r\n \r\n # Disable sorting in simple mode (optional, but makes sense)\r\n self.preview_table_view.setSortingEnabled(False)\r\n \r\n@@ -776,8 +781,18 @@\n self.statusBar().showMessage(f\"Preview disabled. Showing {len(self.current_asset_paths)} input assets.\", 3000)\r\n else:\r\n self.statusBar().showMessage(\"Preview disabled. No assets added.\", 3000)\r\n \r\n+ # In simple mode, the model's data is derived from current_asset_paths.\r\n+ # We need to ensure the model's simple data is up-to-date.\r\n+ # The simplest way is to re-set the data, which will re-extract simple data.\r\n+ # This might be slightly inefficient if only the mode changed, but safe.\r\n+ # A more optimized approach would be to have a separate method in the model\r\n+ # to just update the simple data from a list of paths.\r\n+ # For now, let's re-set the data.\r\n+ log.debug(\" Simple mode enabled. Re-setting model data to trigger simple data update.\")\r\n+ self.preview_model.set_data(list(self.current_asset_paths)) # Pass the list of paths\r\n+\r\n # Stop here, do not run PredictionHandler in simple mode\r\n return\r\n else:\r\n # --- Proceed with Detailed Preview ---\r\n@@ -787,10 +802,15 @@\n self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_ASSET, False)\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_ORIGINAL_PATH, False)\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True) # Keep this hidden\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_DETAILS, False)\r\n- self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, True) # Hide the simple path column\r\n+ # Ensure the simple path column exists and is hidden\r\n+ if self.preview_model.columnCount() > self.preview_model.COL_SIMPLE_PATH:\r\n+ self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, True) # Hide the simple path column\r\n+ else:\r\n+ log.warning(\" Simple path column index out of bounds for model when hiding.\")\r\n \r\n+\r\n # Set resize modes for detailed columns\r\n header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)\r\n header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents)\r\n header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.Stretch)\r\n"
},
{
"date": 1745332587088,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -927,16 +927,19 @@\n # Slot for prediction results (Updated for new format and coloring)\r\n @Slot(list)\r\n def on_prediction_results_ready(self, results: list):\r\n \"\"\"Populates the preview table model with detailed prediction results.\"\"\"\r\n- log.debug(f\"[{time.time():.4f}] on_prediction_results_ready received {len(results)} file details.\")\r\n+ log.info(f\"[{time.time():.4f}] --> on_prediction_results_ready received {len(results)} file details.\")\r\n # Update the model with the new data\r\n self.preview_model.set_data(results)\r\n- log.debug(f\"[{time.time():.4f}] Preview model updated with detailed results.\")\r\n+ log.info(f\"[{time.time():.4f}] Preview model updated with detailed results.\")\r\n \r\n @Slot()\r\n def on_prediction_finished(self):\r\n- log.debug(f\"[{time.time():.4f}] Prediction finished signal received.\")\r\n+ log.info(f\"[{time.time():.4f}] --> Prediction finished signal received.\")\r\n+ # Optionally update status bar or re-enable controls if needed after prediction finishes\r\n+ # (Controls are primarily managed by processing_finished, but prediction is a separate background task)\r\n+ self.statusBar().showMessage(\"Preview updated.\", 3000)\r\n \r\n @Slot(str, str, str)\r\n def update_file_status(self, input_path_str, status, message):\r\n # TODO: Update status bar or potentially find rows in table later\r\n"
},
{
"date": 1745332710872,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -746,11 +746,25 @@\n log.info(f\"--> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}\")\r\n if hasattr(self, 'toggle_preview_action'):\r\n log.info(f\" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n \r\n+ # --- Preview Update Method ---\r\n+ def update_preview(self):\r\n+ log.info(f\"--> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}\")\r\n+ if hasattr(self, 'toggle_preview_action'):\r\n+ log.info(f\" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n+\r\n # Determine mode based on menu action\r\n simple_mode_enabled = hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked()\r\n \r\n+ # --- Cancel Prediction if Running ---\r\n+ if self.prediction_handler and self.prediction_handler.is_running:\r\n+ log.warning(f\"[{time.time():.4f}] Prediction is running. Requesting cancellation before changing preview mode.\")\r\n+ self.prediction_handler.request_cancel()\r\n+ # Note: Cancellation is not immediate. The thread will stop when it next checks the flag.\r\n+ # We proceed with updating the UI immediately, which might briefly show inconsistent state\r\n+ # if the thread hasn't fully stopped, but it prevents the crash.\r\n+\r\n # Set the model's mode\r\n log.debug(f\" Setting model simple mode to: {simple_mode_enabled}\")\r\n self.preview_model.set_simple_mode(simple_mode_enabled)\r\n \r\n"
},
{
"date": 1745332971203,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -748,26 +748,35 @@\n log.info(f\" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n \r\n # --- Preview Update Method ---\r\n def update_preview(self):\r\n- log.info(f\"--> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}\")\r\n+ thread_id = QThread.currentThreadId()\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] --> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}\")\r\n if hasattr(self, 'toggle_preview_action'):\r\n- log.info(f\" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n \r\n # Determine mode based on menu action\r\n simple_mode_enabled = hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked()\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Determined simple_mode_enabled: {simple_mode_enabled}\")\r\n \r\n # --- Cancel Prediction if Running ---\r\n if self.prediction_handler and self.prediction_handler.is_running:\r\n- log.warning(f\"[{time.time():.4f}] Prediction is running. Requesting cancellation before changing preview mode.\")\r\n- self.prediction_handler.request_cancel()\r\n- # Note: Cancellation is not immediate. The thread will stop when it next checks the flag.\r\n- # We proceed with updating the UI immediately, which might briefly show inconsistent state\r\n- # if the thread hasn't fully stopped, but it prevents the crash.\r\n+ log.warning(f\"[{time.time():.4f}][T:{thread_id}] Prediction is running. Attempting to call prediction_handler.request_cancel()...\")\r\n+ try:\r\n+ # --- THIS METHOD DOES NOT EXIST IN PredictionHandler ---\r\n+ self.prediction_handler.request_cancel()\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Called prediction_handler.request_cancel() (Method might be missing!).\")\r\n+ except AttributeError as e:\r\n+ log.error(f\"[{time.time():.4f}][T:{thread_id}] AttributeError calling prediction_handler.request_cancel(): {e}. Prediction cannot be cancelled.\")\r\n+ except Exception as e:\r\n+ log.exception(f\"[{time.time():.4f}][T:{thread_id}] Unexpected error calling prediction_handler.request_cancel(): {e}\")\r\n+ # Note: Cancellation is not immediate even if it existed. The thread would stop when it next checks the flag.\r\n+ # We proceed with updating the UI immediately.\r\n \r\n # Set the model's mode\r\n- log.debug(f\" Setting model simple mode to: {simple_mode_enabled}\")\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Calling preview_model.set_simple_mode({simple_mode_enabled})...\")\r\n self.preview_model.set_simple_mode(simple_mode_enabled)\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Returned from preview_model.set_simple_mode({simple_mode_enabled}).\")\r\n \r\n # Configure the QTableView based on the mode\r\n header = self.preview_table_view.horizontalHeader()\r\n if simple_mode_enabled:\r\n@@ -806,12 +815,13 @@\n log.debug(\" Simple mode enabled. Re-setting model data to trigger simple data update.\")\r\n self.preview_model.set_data(list(self.current_asset_paths)) # Pass the list of paths\r\n \r\n # Stop here, do not run PredictionHandler in simple mode\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] <-- Exiting update_preview (Simple Mode).\")\r\n return\r\n else:\r\n # --- Proceed with Detailed Preview ---\r\n- log.info(\" Configuring QTableView for DETAILED mode.\")\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Configuring QTableView for DETAILED mode.\")\r\n # Show detailed columns, hide simple column\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_STATUS, False)\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_ASSET, False)\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_ORIGINAL_PATH, False)\r\n@@ -874,11 +884,13 @@\n log.debug(f\"[{time.time():.4f}] Starting prediction thread...\")\r\n self.prediction_thread.start()\r\n log.debug(f\"[{time.time():.4f}] Prediction thread start requested.\")\r\n else:\r\n- log.error(f\"[{time.time():.4f}] Failed to start prediction: Thread or handler not initialized.\")\r\n+ log.error(f\"[{time.time():.4f}][T:{thread_id}] Failed to start prediction: Thread or handler not initialized.\")\r\n self.statusBar().showMessage(\"Error: Failed to initialize prediction thread.\", 5000)\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] <-- Exiting update_preview (Detailed Mode).\")\r\n \r\n+\r\n # --- Threading and Processing Control ---\r\n def setup_threads(self):\r\n # Setup Processing Thread\r\n if ProcessingHandler and self.processing_thread is None:\r\n@@ -941,12 +953,15 @@\n # Slot for prediction results (Updated for new format and coloring)\r\n @Slot(list)\r\n def on_prediction_results_ready(self, results: list):\r\n \"\"\"Populates the preview table model with detailed prediction results.\"\"\"\r\n- log.info(f\"[{time.time():.4f}] --> on_prediction_results_ready received {len(results)} file details.\")\r\n+ thread_id = QThread.currentThreadId()\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] --> Entered on_prediction_results_ready. Received {len(results)} file details.\")\r\n # Update the model with the new data\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Calling preview_model.set_data()...\")\r\n self.preview_model.set_data(results)\r\n- log.info(f\"[{time.time():.4f}] Preview model updated with detailed results.\")\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] Returned from preview_model.set_data().\")\r\n+ log.info(f\"[{time.time():.4f}][T:{thread_id}] <-- Exiting on_prediction_results_ready.\")\r\n \r\n @Slot()\r\n def on_prediction_finished(self):\r\n log.info(f\"[{time.time():.4f}] --> Prediction finished signal received.\")\r\n"
},
{
"date": 1745333378823,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -748,9 +748,9 @@\n log.info(f\" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n \r\n # --- Preview Update Method ---\r\n def update_preview(self):\r\n- thread_id = QThread.currentThreadId()\r\n+ thread_id = QThread.currentThread() # Get current thread object\r\n log.info(f\"[{time.time():.4f}][T:{thread_id}] --> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}\")\r\n if hasattr(self, 'toggle_preview_action'):\r\n log.info(f\"[{time.time():.4f}][T:{thread_id}] Disable Preview Action checked: {self.toggle_preview_action.isChecked()}\")\r\n \r\n@@ -953,9 +953,9 @@\n # Slot for prediction results (Updated for new format and coloring)\r\n @Slot(list)\r\n def on_prediction_results_ready(self, results: list):\r\n \"\"\"Populates the preview table model with detailed prediction results.\"\"\"\r\n- thread_id = QThread.currentThreadId()\r\n+ thread_id = QThread.currentThread() # Get current thread object\r\n log.info(f\"[{time.time():.4f}][T:{thread_id}] --> Entered on_prediction_results_ready. Received {len(results)} file details.\")\r\n # Update the model with the new data\r\n log.info(f\"[{time.time():.4f}][T:{thread_id}] Calling preview_model.set_data()...\")\r\n self.preview_model.set_data(results)\r\n"
},
{
"date": 1745333556600,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -811,10 +811,14 @@\n # This might be slightly inefficient if only the mode changed, but safe.\r\n # A more optimized approach would be to have a separate method in the model\r\n # to just update the simple data from a list of paths.\r\n # For now, let's re-set the data.\r\n- log.debug(\" Simple mode enabled. Re-setting model data to trigger simple data update.\")\r\n- self.preview_model.set_data(list(self.current_asset_paths)) # Pass the list of paths\r\n+ # --- REMOVED REDUNDANT set_data CALL ---\r\n+ # The set_simple_mode(True) call above should be sufficient as the model\r\n+ # already holds the simple data internally. This extra reset seems to cause instability.\r\n+ # log.debug(\" Simple mode enabled. Re-setting model data to trigger simple data update.\")\r\n+ # self.preview_model.set_data(list(self.current_asset_paths)) # Pass the list of paths\r\n+ # --- END REMOVAL ---\r\n \r\n # Stop here, do not run PredictionHandler in simple mode\r\n log.info(f\"[{time.time():.4f}][T:{thread_id}] <-- Exiting update_preview (Simple Mode).\")\r\n return\r\n"
},
{
"date": 1745333721426,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -607,11 +607,17 @@\n self.toggle_preview_action.setChecked(True) # This will trigger update_preview via its signal\r\n preview_toggled = True\r\n \r\n # Only call update_preview directly if the toggle wasn't triggered\r\n- if not preview_toggled:\r\n+ # If in simple mode, we need to explicitly update the model with the simple list of paths\r\n+ if hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked():\r\n+ log.debug(\"Currently in simple preview mode. Updating model with simple paths.\")\r\n+ self.preview_model.set_data(list(self.current_asset_paths)) # Update model with simple list\r\n+ self.statusBar().showMessage(f\"Added {added_count} asset(s). Preview updated.\", 3000)\r\n+ elif not preview_toggled:\r\n+ # If not in simple mode and toggle wasn't triggered, run detailed preview update\r\n self.update_preview()\r\n-\r\n+ \r\n def _browse_for_output_directory(self):\r\n \"\"\"Opens a dialog to select the output directory.\"\"\"\r\n current_path = self.output_path_edit.text()\r\n if not current_path or not Path(current_path).is_dir():\r\n"
},
{
"date": 1745498820388,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -409,9 +409,9 @@\n \r\n # Set selection behavior and alternating colors\r\n self.preview_table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)\r\n self.preview_table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)\r\n- self.preview_table_view.setAlternatingRowColors(True)\r\n+ self.preview_table_view.setAlternatingRowColors(False)\r\n \r\n # Enable sorting via header clicks\r\n self.preview_table_view.setSortingEnabled(True)\r\n # Set default sort column (Status) - the proxy model's lessThan handles the custom order\r\n"
},
{
"date": 1745501490485,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -425,8 +425,11 @@\n \r\n main_layout.addWidget(self.preview_label)\r\n main_layout.addWidget(self.preview_table_view, 1) # Add the table view to layout\r\n \r\n+ # Apply style sheet to remove borders and rounded corners\r\n+ self.preview_table_view.setStyleSheet(\"QTableView { border: none; }\")\r\n+\r\n # --- Progress Bar ---\r\n self.progress_bar = QProgressBar()\r\n self.progress_bar.setValue(0)\r\n self.progress_bar.setTextVisible(True)\r\n"
},
{
"date": 1745501886976,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1491,8 +1491,9 @@\n # --- Main Execution ---\r\n def run_gui():\r\n \"\"\"Initializes and runs the Qt application.\"\"\"\r\n app = QApplication(sys.argv)\r\n+ app.setStyle('Fusion')\r\n window = MainWindow()\r\n window.show()\r\n sys.exit(app.exec())\r\n \r\n"
},
{
"date": 1745502118371,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -17,9 +17,9 @@\n QFormLayout, QGroupBox, QAbstractItemView, QSizePolicy, # Added more layout/widget items\r\n QMenuBar, QMenu # Added for menu\r\n )\r\n from PySide6.QtCore import Qt, QThread, Slot, Signal, QObject # Added Signal, QObject\r\n-from PySide6.QtGui import QColor, QAction # Add QColor import, QAction\r\n+from PySide6.QtGui import QColor, QAction, QPalette # Add QColor import, QAction, QPalette\r\n \r\n # --- GUI Model Imports ---\r\n from gui.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel\r\n \r\n@@ -1492,8 +1492,20 @@\n def run_gui():\r\n \"\"\"Initializes and runs the Qt application.\"\"\"\r\n app = QApplication(sys.argv)\r\n app.setStyle('Fusion')\r\n+\r\n+ # Set a custom palette to override default Fusion colors\r\n+ palette = app.palette()\r\n+ grey_color = QColor(\"#535353\")\r\n+ palette.setColor(QPalette.ColorRole.Base, grey_color)\r\n+ palette.setColor(QPalette.ColorRole.AlternateBase, grey_color.lighter(110)) # Use a slightly lighter shade for alternate rows if needed\r\n+ # You might need to experiment with other roles depending on which widgets are affected\r\n+ # palette.setColor(QPalette.ColorRole.Window, grey_color)\r\n+ # palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white) # Example: set text color to white\r\n+\r\n+ app.setPalette(palette)\r\n+\r\n window = MainWindow()\r\n window.show()\r\n sys.exit(app.exec())\r\n \r\n"
},
{
"date": 1745502783700,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1495,9 +1495,9 @@\n app.setStyle('Fusion')\r\n \r\n # Set a custom palette to override default Fusion colors\r\n palette = app.palette()\r\n- grey_color = QColor(\"#535353\")\r\n+ grey_color = QColor(\"#3a3a3a\")\r\n palette.setColor(QPalette.ColorRole.Base, grey_color)\r\n palette.setColor(QPalette.ColorRole.AlternateBase, grey_color.lighter(110)) # Use a slightly lighter shade for alternate rows if needed\r\n # You might need to experiment with other roles depending on which widgets are affected\r\n # palette.setColor(QPalette.ColorRole.Window, grey_color)\r\n"
},
{
"date": 1745504039295,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -398,9 +398,10 @@\n # The model defines the columns and headers\r\n header = self.preview_table_view.horizontalHeader()\r\n header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)\r\n header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents)\r\n- header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.Stretch)\r\n+ header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Changed to ResizeToContents\r\n+ header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Added for Additional Files\r\n # Remove Predicted Output column as requested\r\n # header.setSectionResizeMode(self.preview_model.COL_PREDICTED_OUTPUT, QHeaderView.ResizeMode.ResizeToContents)\r\n header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents)\r\n \r\n@@ -416,13 +417,13 @@\n self.preview_table_view.setSortingEnabled(True)\r\n # Set default sort column (Status) - the proxy model's lessThan handles the custom order\r\n self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)\r\n \r\n- # Move \"Original Path\" column to the far right\r\n- original_path_logical_index = self.preview_model.COL_ORIGINAL_PATH\r\n- original_path_visual_index = header.visualIndex(original_path_logical_index)\r\n- if original_path_visual_index != -1: # Check if the column is visible\r\n- header.moveSection(original_path_visual_index, header.count() - 1)\r\n+ # Move \"Additional Files\" column to the far right\r\n+ additional_files_logical_index = self.preview_model.COL_ADDITIONAL_FILES\r\n+ additional_files_visual_index = header.visualIndex(additional_files_logical_index)\r\n+ if additional_files_visual_index != -1: # Check if the column is visible\r\n+ header.moveSection(additional_files_visual_index, header.count() - 1)\r\n \r\n main_layout.addWidget(self.preview_label)\r\n main_layout.addWidget(self.preview_table_view, 1) # Add the table view to layout\r\n \r\n@@ -849,11 +850,18 @@\n \r\n # Set resize modes for detailed columns\r\n header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)\r\n header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents)\r\n- header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.Stretch)\r\n+ header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Changed to ResizeToContents\r\n+ header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Added for Additional Files\r\n header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents)\r\n \r\n+ # Move \"Additional Files\" column to the far right\r\n+ additional_files_logical_index = self.preview_model.COL_ADDITIONAL_FILES\r\n+ additional_files_visual_index = header.visualIndex(additional_files_logical_index)\r\n+ if additional_files_visual_index != -1: # Check if the column is visible\r\n+ header.moveSection(additional_files_visual_index, header.count() - 1)\r\n+\r\n # Re-enable sorting for detailed mode\r\n self.preview_table_view.setSortingEnabled(True)\r\n # Reset sort order if needed (optional, proxy model handles default)\r\n # self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)\r\n"
},
{
"date": 1745504410196,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -849,19 +849,32 @@\n \r\n \r\n # Set resize modes for detailed columns\r\n header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)\r\n- header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents)\r\n- header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Changed to ResizeToContents\r\n- header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Added for Additional Files\r\n- header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents)\r\n+ header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents) # Fit\r\n+ header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents) # Fit\r\n+ header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Fixed width (using ResizeToContents as closest)\r\n+ header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Stretch (Fit-If-Possible)\r\n \r\n- # Move \"Additional Files\" column to the far right\r\n- additional_files_logical_index = self.preview_model.COL_ADDITIONAL_FILES\r\n- additional_files_visual_index = header.visualIndex(additional_files_logical_index)\r\n- if additional_files_visual_index != -1: # Check if the column is visible\r\n- header.moveSection(additional_files_visual_index, header.count() - 1)\r\n+ # Move columns to the desired order: Status, Predicted Asset, Details, Original Path, Additional Files\r\n+ # Initial logical order: [0, 1, 2, 3(hidden), 4, 5]\r\n+ # Initial visual order: [0, 1, 2, 3, 4, 5] (assuming no initial moves)\r\n+ # Desired visual order: [0, 1, 4, 2, 5, 3(hidden)]\r\n \r\n+ # Move Details (logical 4) after Predicted Asset (logical 1)\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), header.visualIndex(self.preview_model.COL_PREDICTED_ASSET) + 1)\r\n+ # Current visual: [0, 1, 4, 2, 3, 5]\r\n+\r\n+ # Move Original Path (logical 2) after Details (logical 4)\r\n+ # Original Path is now at visual index 3. Details is at visual index 2.\r\n+ # Target visual index is 3. It's already there. No move needed.\r\n+\r\n+ # Move Additional Files (logical 5) after Original Path (logical 2)\r\n+ # Additional Files is at visual index 5. Original Path is at visual index 3.\r\n+ # Target visual index is 4.\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), header.visualIndex(self.preview_model.COL_ORIGINAL_PATH) + 1)\r\n+ # Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.\r\n+\r\n # Re-enable sorting for detailed mode\r\n self.preview_table_view.setSortingEnabled(True)\r\n # Reset sort order if needed (optional, proxy model handles default)\r\n # self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)\r\n"
},
{
"date": 1745504427537,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -396,15 +396,14 @@\n \r\n # Set headers and resize modes using the model's headerData\r\n # The model defines the columns and headers\r\n header = self.preview_table_view.horizontalHeader()\r\n+ # Set resize modes for detailed columns\r\n header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)\r\n- header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents)\r\n- header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Changed to ResizeToContents\r\n- header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Added for Additional Files\r\n- # Remove Predicted Output column as requested\r\n- # header.setSectionResizeMode(self.preview_model.COL_PREDICTED_OUTPUT, QHeaderView.ResizeMode.ResizeToContents)\r\n- header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents)\r\n+ header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents) # Fit\r\n+ header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents) # Fit\r\n+ header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Fixed width (using ResizeToContents as closest)\r\n+ header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Stretch (Fit-If-Possible)\r\n \r\n # Hide the Predicted Output column\r\n self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True)\r\n \r\n@@ -417,14 +416,27 @@\n self.preview_table_view.setSortingEnabled(True)\r\n # Set default sort column (Status) - the proxy model's lessThan handles the custom order\r\n self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)\r\n \r\n- # Move \"Additional Files\" column to the far right\r\n- additional_files_logical_index = self.preview_model.COL_ADDITIONAL_FILES\r\n- additional_files_visual_index = header.visualIndex(additional_files_logical_index)\r\n- if additional_files_visual_index != -1: # Check if the column is visible\r\n- header.moveSection(additional_files_visual_index, header.count() - 1)\r\n+ # Move columns to the desired order: Status, Predicted Asset, Details, Original Path, Additional Files\r\n+ # Initial logical order: [0, 1, 2, 3(hidden), 4, 5]\r\n+ # Initial visual order: [0, 1, 2, 3, 4, 5] (assuming no initial moves)\r\n+ # Desired visual order: [0, 1, 4, 2, 5, 3(hidden)]\r\n \r\n+ # Move Details (logical 4) after Predicted Asset (logical 1)\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), header.visualIndex(self.preview_model.COL_PREDICTED_ASSET) + 1)\r\n+ # Current visual: [0, 1, 4, 2, 3, 5]\r\n+\r\n+ # Move Original Path (logical 2) after Details (logical 4)\r\n+ # Original Path is now at visual index 3. Details is at visual index 2.\r\n+ # Target visual index is 3. It's already there. No move needed.\r\n+\r\n+ # Move Additional Files (logical 5) after Original Path (logical 2)\r\n+ # Additional Files is at visual index 5. Original Path is at visual index 3.\r\n+ # Target visual index is 4.\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), header.visualIndex(self.preview_model.COL_ORIGINAL_PATH) + 1)\r\n+ # Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.\r\n+\r\n main_layout.addWidget(self.preview_label)\r\n main_layout.addWidget(self.preview_table_view, 1) # Add the table view to layout\r\n \r\n # Apply style sheet to remove borders and rounded corners\r\n"
},
{
"date": 1745504444836,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -421,20 +421,20 @@\n # Initial logical order: [0, 1, 2, 3(hidden), 4, 5]\r\n # Initial visual order: [0, 1, 2, 3, 4, 5] (assuming no initial moves)\r\n # Desired visual order: [0, 1, 4, 2, 5, 3(hidden)]\r\n \r\n- # Move Details (logical 4) after Predicted Asset (logical 1)\r\n- header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), header.visualIndex(self.preview_model.COL_PREDICTED_ASSET) + 1)\r\n+ # Move Predicted Asset (logical 1) to visual index 1 (already there)\r\n+\r\n+ # Move Details (logical 4) to visual index 2\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), 2)\r\n # Current visual: [0, 1, 4, 2, 3, 5]\r\n \r\n- # Move Original Path (logical 2) after Details (logical 4)\r\n- # Original Path is now at visual index 3. Details is at visual index 2.\r\n- # Target visual index is 3. It's already there. No move needed.\r\n+ # Move Original Path (logical 2) to visual index 3\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_ORIGINAL_PATH), 3)\r\n+ # Current visual: [0, 1, 4, 2, 3, 5] - Original Path is already at visual index 3 after moving Details\r\n \r\n- # Move Additional Files (logical 5) after Original Path (logical 2)\r\n- # Additional Files is at visual index 5. Original Path is at visual index 3.\r\n- # Target visual index is 4.\r\n- header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), header.visualIndex(self.preview_model.COL_ORIGINAL_PATH) + 1)\r\n+ # Move Additional Files (logical 5) to visual index 4\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), 4)\r\n # Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.\r\n \r\n main_layout.addWidget(self.preview_label)\r\n main_layout.addWidget(self.preview_table_view, 1) # Add the table view to layout\r\n@@ -871,20 +871,20 @@\n # Initial logical order: [0, 1, 2, 3(hidden), 4, 5]\r\n # Initial visual order: [0, 1, 2, 3, 4, 5] (assuming no initial moves)\r\n # Desired visual order: [0, 1, 4, 2, 5, 3(hidden)]\r\n \r\n- # Move Details (logical 4) after Predicted Asset (logical 1)\r\n- header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), header.visualIndex(self.preview_model.COL_PREDICTED_ASSET) + 1)\r\n+ # Move Predicted Asset (logical 1) to visual index 1 (already there)\r\n+\r\n+ # Move Details (logical 4) to visual index 2\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), 2)\r\n # Current visual: [0, 1, 4, 2, 3, 5]\r\n \r\n- # Move Original Path (logical 2) after Details (logical 4)\r\n- # Original Path is now at visual index 3. Details is at visual index 2.\r\n- # Target visual index is 3. It's already there. No move needed.\r\n+ # Move Original Path (logical 2) to visual index 3\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_ORIGINAL_PATH), 3)\r\n+ # Current visual: [0, 1, 4, 2, 3, 5] - Original Path is already at visual index 3 after moving Details\r\n \r\n- # Move Additional Files (logical 5) after Original Path (logical 2)\r\n- # Additional Files is at visual index 5. Original Path is at visual index 3.\r\n- # Target visual index is 4.\r\n- header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), header.visualIndex(self.preview_model.COL_ORIGINAL_PATH) + 1)\r\n+ # Move Additional Files (logical 5) to visual index 4\r\n+ header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), 4)\r\n # Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.\r\n \r\n # Re-enable sorting for detailed mode\r\n self.preview_table_view.setSortingEnabled(True)\r\n"
},
{
"date": 1745507368001,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -602,15 +602,16 @@\n newly_added_paths = []\r\n for p_str in paths:\r\n p = Path(p_str)\r\n if p.exists():\r\n- if p.is_dir() or (p.is_file() and p.suffix.lower() == '.zip'):\r\n+ supported_suffixes = ['.zip', '.rar', '.7z']\r\n+ if p.is_dir() or (p.is_file() and p.suffix.lower() in supported_suffixes):\r\n if p_str not in self.current_asset_paths:\r\n self.current_asset_paths.add(p_str)\r\n newly_added_paths.append(p_str)\r\n added_count += 1\r\n- else: print(f\"Skipping duplicate asset path: {p_str}\")\r\n- else: self.statusBar().showMessage(f\"Invalid input (not dir or .zip): {p.name}\", 5000); print(f\"Invalid input: {p_str}\")\r\n+ else: log.debug(f\"Skipping duplicate asset path: {p_str}\") # Changed print to log.debug\r\n+ else: self.statusBar().showMessage(f\"Invalid input (not dir or supported archive): {p.name}\", 5000); log.warning(f\"Invalid input: {p_str}\") # Changed print to log.warning and updated message\r\n else: self.statusBar().showMessage(f\"Input path not found: {p.name}\", 5000); print(f\"Input path not found: {p_str}\")\r\n if added_count > 0:\r\n log.info(f\"Added {added_count} new asset paths: {newly_added_paths}\")\r\n self.statusBar().showMessage(f\"Added {added_count} asset(s). Updating preview...\", 3000)\r\n"
},
{
"date": 1745508942440,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -193,11 +193,14 @@\n \r\n # --- Initial State ---\r\n self._clear_editor() # Clear/disable editor fields initially\r\n self._set_editor_enabled(False) # Disable editor initially\r\n- self.populate_presets() # Populate both preset list and combo box\r\n+ self.populate_presets() # Populate preset list\r\n self.setup_logging_handler() # Setup the custom log handler\r\n \r\n+ # Set placeholder text for the preview table\r\n+ self.preview_table_view.setPlaceholderText(\"Please select a preset to view file predictions\")\r\n+\r\n # --- Connect Editor Signals ---\r\n self._connect_editor_change_signals()\r\n \r\n # --- Adjust Splitter ---\r\n@@ -577,12 +580,11 @@\n # combo_index = self.preset_combo.findText(current_combo_selection) # REMOVED\r\n # if combo_index != -1: # REMOVED\r\n # self.preset_combo.setCurrentIndex(combo_index) # REMOVED\r\n \r\n- if current_list_selection_text:\r\n- items = self.editor_preset_list.findItems(current_list_selection_text, Qt.MatchFlag.MatchExactly)\r\n- if items:\r\n- self.editor_preset_list.setCurrentItem(items[0]) # This might trigger _load_selected_preset_for_editing\r\n+ # Do NOT attempt to restore list selection by default\r\n+ # The user must explicitly select a preset to trigger preview\r\n+ pass\r\n \r\n # --- Drag and Drop Event Handlers (Unchanged) ---\r\n def dragEnterEvent(self, event):\r\n if event.mimeData().hasUrls(): event.acceptProposedAction()\r\n"
},
{
"date": 1745508953551,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -631,11 +631,16 @@\n if hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked():\r\n log.debug(\"Currently in simple preview mode. Updating model with simple paths.\")\r\n self.preview_model.set_data(list(self.current_asset_paths)) # Update model with simple list\r\n self.statusBar().showMessage(f\"Added {added_count} asset(s). Preview updated.\", 3000)\r\n- elif not preview_toggled:\r\n- # If not in simple mode and toggle wasn't triggered, run detailed preview update\r\n+ # Only call update_preview if a preset is currently selected in the editor list\r\n+ current_editor_item = self.editor_preset_list.currentItem()\r\n+ if not preview_toggled and current_editor_item:\r\n+ log.debug(\"Preset selected and not in simple mode. Triggering detailed preview update.\")\r\n self.update_preview()\r\n+ elif not current_editor_item:\r\n+ log.debug(\"No preset selected. Not triggering detailed preview update.\")\r\n+ self.statusBar().showMessage(f\"Added {added_count} asset(s). Select a preset to update preview.\", 3000)\r\n \r\n def _browse_for_output_directory(self):\r\n \"\"\"Opens a dialog to select the output directory.\"\"\"\r\n current_path = self.output_path_edit.text()\r\n"
},
{
"date": 1745509017091,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -196,11 +196,8 @@\n self._set_editor_enabled(False) # Disable editor initially\r\n self.populate_presets() # Populate preset list\r\n self.setup_logging_handler() # Setup the custom log handler\r\n \r\n- # Set placeholder text for the preview table\r\n- self.preview_table_view.setPlaceholderText(\"Please select a preset to view file predictions\")\r\n-\r\n # --- Connect Editor Signals ---\r\n self._connect_editor_change_signals()\r\n \r\n # --- Adjust Splitter ---\r\n"
},
{
"date": 1745509032066,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -436,10 +436,23 @@\n header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), 4)\r\n # Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.\r\n \r\n main_layout.addWidget(self.preview_label)\r\n- main_layout.addWidget(self.preview_table_view, 1) # Add the table view to layout\r\n \r\n+ # Add placeholder label for the preview area\r\n+ self.preview_placeholder_label = QLabel(\"Please select a preset to view file predictions\")\r\n+ self.preview_placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\r\n+ self.preview_placeholder_label.setStyleSheet(\"QLabel { font-size: 16px; color: grey; }\") # Optional styling\r\n+\r\n+ # Add both the table view and the placeholder label to the layout\r\n+ # We will manage their visibility later\r\n+ main_layout.addWidget(self.preview_placeholder_label, 1) # Give it stretch factor\r\n+ main_layout.addWidget(self.preview_table_view, 1) # Give it stretch factor\r\n+\r\n+ # Initially hide the table view and show the placeholder\r\n+ self.preview_table_view.setVisible(False)\r\n+ self.preview_placeholder_label.setVisible(True)\r\n+\r\n # Apply style sheet to remove borders and rounded corners\r\n self.preview_table_view.setStyleSheet(\"QTableView { border: none; }\")\r\n \r\n # --- Progress Bar ---\r\n"
},
{
"date": 1745509042212,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1289,10 +1289,17 @@\n self._load_preset_for_editing(preset_path)\r\n # --- Trigger preview update after loading editor ---\r\n self.update_preview()\r\n # --- End Trigger ---\r\n+\r\n+ # Hide placeholder and show table view\r\n+ self.preview_placeholder_label.setVisible(False)\r\n+ self.preview_table_view.setVisible(True)\r\n else:\r\n self._clear_editor() # Clear editor if selection is cleared\r\n+ # Ensure placeholder is visible if no preset is selected\r\n+ self.preview_placeholder_label.setVisible(True)\r\n+ self.preview_table_view.setVisible(False)\r\n \r\n def _gather_editor_data(self) -> dict:\r\n \"\"\"Gathers data from all editor UI widgets and returns a dictionary.\"\"\"\r\n preset_data = {}\r\n"
},
{
"date": 1745509439389,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -552,17 +552,16 @@\n # --- Preset Population and Handling ---\r\n \r\n def populate_presets(self):\r\n \"\"\"Scans presets dir and populates BOTH the editor list and processing combo.\"\"\"\r\n- log.debug(\"Populating preset list...\") # Updated log message\r\n+ log.debug(\"Populating preset list...\")\r\n # Store current list selection\r\n- # current_combo_selection = self.preset_combo.currentText() # REMOVED\r\n current_list_item = self.editor_preset_list.currentItem()\r\n current_list_selection_text = current_list_item.text() if current_list_item else None\r\n \r\n # Clear list\r\n- # self.preset_combo.clear() # REMOVED\r\n self.editor_preset_list.clear()\r\n+ log.debug(\"Preset list cleared.\")\r\n \r\n if not PRESETS_DIR.is_dir():\r\n msg = f\"Error: Presets directory not found at {PRESETS_DIR}\"\r\n self.statusBar().showMessage(msg)\r\n@@ -591,10 +590,13 @@\n # if combo_index != -1: # REMOVED\r\n # self.preset_combo.setCurrentIndex(combo_index) # REMOVED\r\n \r\n # Do NOT attempt to restore list selection by default\r\n+ # Do NOT attempt to restore list selection by default\r\n # The user must explicitly select a preset to trigger preview\r\n- pass\r\n+ log.debug(\"Preset list populated. Clearing selection.\")\r\n+ self.editor_preset_list.setCurrentItem(None) # Explicitly clear selection\r\n+ self.editor_preset_list.clearSelection() # Also clear selection state\r\n \r\n # --- Drag and Drop Event Handlers (Unchanged) ---\r\n def dragEnterEvent(self, event):\r\n if event.mimeData().hasUrls(): event.acceptProposedAction()\r\n@@ -1256,8 +1258,9 @@\n if not file_path or not file_path.is_file():\r\n self._clear_editor()\r\n return\r\n log.info(f\"Loading preset into editor: {file_path.name}\")\r\n+ log.info(f\"Loading preset into editor: {file_path.name}\")\r\n try:\r\n with open(file_path, 'r', encoding='utf-8') as f: preset_data = json.load(f)\r\n self._populate_editor_from_data(preset_data)\r\n self._set_editor_enabled(True)\r\n@@ -1265,8 +1268,11 @@\n self.editor_unsaved_changes = False\r\n self.editor_save_button.setEnabled(False)\r\n self.setWindowTitle(f\"Asset Processor Tool - {file_path.name}\")\r\n log.info(f\"Preset '{file_path.name}' loaded into editor.\")\r\n+ log.debug(\"Preset loaded. Checking visibility states.\")\r\n+ log.debug(f\"preview_placeholder_label visible: {self.preview_placeholder_label.isVisible()}\")\r\n+ log.debug(f\"preview_table_view visible: {self.preview_table_view.isVisible()}\")\r\n except json.JSONDecodeError as json_err:\r\n log.error(f\"Invalid JSON in {file_path.name}: {json_err}\")\r\n QMessageBox.warning(self, \"Load Error\", f\"Failed to load preset '{file_path.name}'.\\nInvalid JSON structure:\\n{json_err}\")\r\n self._clear_editor()\r\n@@ -1276,28 +1282,34 @@\n self._clear_editor()\r\n \r\n def _load_selected_preset_for_editing(self, current_item: QListWidgetItem, previous_item: QListWidgetItem):\r\n \"\"\"Loads the preset currently selected in the editor list.\"\"\"\r\n+ log.debug(f\"currentItemChanged signal triggered. current_item: {current_item.text() if current_item else 'None'}, previous_item: {previous_item.text() if previous_item else 'None'}\")\r\n if self._check_editor_unsaved_changes():\r\n # If user cancels, revert selection\r\n if previous_item:\r\n+ log.debug(\"Unsaved changes check cancelled. Reverting selection.\")\r\n self.editor_preset_list.blockSignals(True)\r\n self.editor_preset_list.setCurrentItem(previous_item)\r\n self.editor_preset_list.blockSignals(False)\r\n return\r\n if current_item:\r\n+ log.debug(f\"Loading preset for editing: {current_item.text()}\")\r\n preset_path = current_item.data(Qt.ItemDataRole.UserRole)\r\n self._load_preset_for_editing(preset_path)\r\n # --- Trigger preview update after loading editor ---\r\n self.update_preview()\r\n # --- End Trigger ---\r\n \r\n # Hide placeholder and show table view\r\n+ log.debug(\"Preset selected. Hiding placeholder, showing table view.\")\r\n self.preview_placeholder_label.setVisible(False)\r\n self.preview_table_view.setVisible(True)\r\n else:\r\n+ log.debug(\"No preset selected. Clearing editor.\")\r\n self._clear_editor() # Clear editor if selection is cleared\r\n # Ensure placeholder is visible if no preset is selected\r\n+ log.debug(\"No preset selected. Showing placeholder, hiding table view.\")\r\n self.preview_placeholder_label.setVisible(True)\r\n self.preview_table_view.setVisible(False)\r\n \r\n def _gather_editor_data(self) -> dict:\r\n"
},
{
"date": 1745509450464,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1194,8 +1194,15 @@\n self.editor_unsaved_changes = False\r\n self.editor_save_button.setEnabled(False)\r\n self.setWindowTitle(\"Asset Processor Tool\") # Reset window title\r\n self._set_editor_enabled(False)\r\n+\r\n+ # Ensure placeholder is visible and table is hidden when editor is cleared\r\n+ if hasattr(self, 'preview_placeholder_label') and hasattr(self, 'preview_table_view'):\r\n+ log.debug(\"Clearing editor. Showing placeholder, hiding table view.\")\r\n+ self.preview_placeholder_label.setVisible(True)\r\n+ self.preview_table_view.setVisible(False)\r\n+\r\n self._is_loading_editor = False\r\n \r\n def _populate_editor_from_data(self, preset_data: dict):\r\n \"\"\"Helper method to populate editor UI widgets from a preset data dictionary.\"\"\"\r\n"
},
{
"date": 1745509617164,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -561,8 +561,15 @@\n # Clear list\r\n self.editor_preset_list.clear()\r\n log.debug(\"Preset list cleared.\")\r\n \r\n+ # Add the \"Select a Preset\" placeholder item\r\n+ placeholder_item = QListWidgetItem(\"--- Select a Preset ---\")\r\n+ # Make it non-selectable and non-editable\r\n+ placeholder_item.setFlags(placeholder_item.flags() & ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEditable)\r\n+ self.editor_preset_list.addItem(placeholder_item)\r\n+ log.debug(\"Added '--- Select a Preset ---' placeholder item.\")\r\n+\r\n if not PRESETS_DIR.is_dir():\r\n msg = f\"Error: Presets directory not found at {PRESETS_DIR}\"\r\n self.statusBar().showMessage(msg)\r\n log.error(msg)\r\n@@ -590,14 +597,14 @@\n # if combo_index != -1: # REMOVED\r\n # self.preset_combo.setCurrentIndex(combo_index) # REMOVED\r\n \r\n # Do NOT attempt to restore list selection by default\r\n- # Do NOT attempt to restore list selection by default\r\n- # The user must explicitly select a preset to trigger preview\r\n- log.debug(\"Preset list populated. Clearing selection.\")\r\n- self.editor_preset_list.setCurrentItem(None) # Explicitly clear selection\r\n- self.editor_preset_list.clearSelection() # Also clear selection state\r\n+ self.statusBar().showMessage(f\"Loaded {len(presets)} presets.\")\r\n \r\n+ # Select the \"Select a Preset\" item by default\r\n+ log.debug(\"Preset list populated. Selecting '--- Select a Preset ---' item.\")\r\n+ self.editor_preset_list.setCurrentItem(placeholder_item) # Select the placeholder item\r\n+\r\n # --- Drag and Drop Event Handlers (Unchanged) ---\r\n def dragEnterEvent(self, event):\r\n if event.mimeData().hasUrls(): event.acceptProposedAction()\r\n else: event.ignore()\r\n"
},
{
"date": 1745514156207,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -565,8 +565,10 @@\n # Add the \"Select a Preset\" placeholder item\r\n placeholder_item = QListWidgetItem(\"--- Select a Preset ---\")\r\n # Make it non-selectable and non-editable\r\n placeholder_item.setFlags(placeholder_item.flags() & ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEditable)\r\n+ # Set unique data to identify the placeholder\r\n+ placeholder_item.setData(Qt.ItemDataRole.UserRole, \"__PLACEHOLDER__\")\r\n self.editor_preset_list.addItem(placeholder_item)\r\n log.debug(\"Added '--- Select a Preset ---' placeholder item.\")\r\n \r\n if not PRESETS_DIR.is_dir():\r\n@@ -695,13 +697,19 @@\n self.statusBar().showMessage(\"No assets added to process.\", 3000)\r\n return\r\n # --- Get preset from editor list ---\r\n current_editor_item = self.editor_preset_list.currentItem()\r\n+\r\n+ # Check if the selected item is the placeholder\r\n+ is_placeholder = current_editor_item and current_editor_item.data(Qt.ItemDataRole.UserRole) == \"__PLACEHOLDER__\"\r\n+\r\n+ if is_placeholder:\r\n+ self.statusBar().showMessage(\"Please select a valid preset from the list on the left.\", 3000)\r\n+ log.warning(\"Start processing failed: Placeholder preset selected.\")\r\n+ return\r\n+\r\n+ # Existing logic to get selected_preset text and proceed\r\n selected_preset = current_editor_item.text() if current_editor_item else None\r\n- # --- End Get preset ---\r\n- if not selected_preset:\r\n- self.statusBar().showMessage(\"Please select a preset from the list on the left.\", 3000) # Updated message\r\n- return\r\n overwrite = self.overwrite_checkbox.isChecked()\r\n num_workers = self.workers_spinbox.value()\r\n \r\n # --- Get Output Directory from UI and Validate ---\r\n@@ -926,8 +934,23 @@\n self.statusBar().showMessage(\"Error: Prediction components not loaded.\", 5000)\r\n return\r\n # Get preset from editor list\r\n current_editor_item = self.editor_preset_list.currentItem()\r\n+\r\n+ # Check if the selected item is the placeholder\r\n+ is_placeholder = current_editor_item and current_editor_item.data(Qt.ItemDataRole.UserRole) == \"__PLACEHOLDER__\"\r\n+\r\n+ if is_placeholder:\r\n+ log.debug(\"Update preview called with placeholder preset selected. Clearing preview.\")\r\n+ self.preview_model.clear_data() # Clear model if placeholder selected\r\n+ self.statusBar().showMessage(\"Select a preset from the list on the left to update preview.\", 3000)\r\n+ # Ensure placeholder is visible and table is hidden\r\n+ if hasattr(self, 'preview_placeholder_label') and hasattr(self, 'preview_table_view'):\r\n+ self.preview_placeholder_label.setVisible(True)\r\n+ self.preview_table_view.setVisible(False)\r\n+ return # Stop prediction as no valid preset is selected\r\n+\r\n+ # Existing logic to get selected_preset text and proceed\r\n selected_preset = current_editor_item.text() if current_editor_item else None\r\n if not selected_preset:\r\n log.debug(\"Update preview called with no preset selected in the editor list.\")\r\n self.preview_model.clear_data() # Clear model if no preset selected\r\n@@ -1297,16 +1320,31 @@\n \r\n def _load_selected_preset_for_editing(self, current_item: QListWidgetItem, previous_item: QListWidgetItem):\r\n \"\"\"Loads the preset currently selected in the editor list.\"\"\"\r\n log.debug(f\"currentItemChanged signal triggered. current_item: {current_item.text() if current_item else 'None'}, previous_item: {previous_item.text() if previous_item else 'None'}\")\r\n+\r\n+ # Check if the selected item is the placeholder\r\n+ is_placeholder = current_item and current_item.data(Qt.ItemDataRole.UserRole) == \"__PLACEHOLDER__\"\r\n+\r\n if self._check_editor_unsaved_changes():\r\n # If user cancels, revert selection\r\n if previous_item:\r\n log.debug(\"Unsaved changes check cancelled. Reverting selection.\")\r\n self.editor_preset_list.blockSignals(True)\r\n self.editor_preset_list.setCurrentItem(previous_item)\r\n self.editor_preset_list.blockSignals(False)\r\n return\r\n+\r\n+ if is_placeholder:\r\n+ log.debug(\"Placeholder item selected. Clearing editor and preview.\")\r\n+ self._clear_editor() # This also hides the table and shows the placeholder label\r\n+ self.preview_model.clear_data() # Ensure the model is empty\r\n+ # Visibility is handled by _clear_editor, but explicitly set here for clarity\r\n+ self.preview_placeholder_label.setVisible(True)\r\n+ self.preview_table_view.setVisible(False)\r\n+ return # Stop processing as no real preset is selected\r\n+\r\n+ # Existing logic for handling real preset items starts here\r\n if current_item:\r\n log.debug(f\"Loading preset for editing: {current_item.text()}\")\r\n preset_path = current_item.data(Qt.ItemDataRole.UserRole)\r\n self._load_preset_for_editing(preset_path)\r\n@@ -1314,13 +1352,14 @@\n self.update_preview()\r\n # --- End Trigger ---\r\n \r\n # Hide placeholder and show table view\r\n- log.debug(\"Preset selected. Hiding placeholder, showing table view.\")\r\n+ log.debug(\"Real preset selected. Hiding placeholder, showing table view.\")\r\n self.preview_placeholder_label.setVisible(False)\r\n self.preview_table_view.setVisible(True)\r\n else:\r\n- log.debug(\"No preset selected. Clearing editor.\")\r\n+ # This case should ideally not be reached if the placeholder is always present\r\n+ log.debug(\"No preset selected (unexpected state if placeholder is present). Clearing editor.\")\r\n self._clear_editor() # Clear editor if selection is cleared\r\n # Ensure placeholder is visible if no preset is selected\r\n log.debug(\"No preset selected. Showing placeholder, hiding table view.\")\r\n self.preview_placeholder_label.setVisible(True)\r\n"
},
{
"date": 1745514211949,
"content": "Index: \n===================================================================\n--- \n+++ \n@@ -1340,15 +1340,17 @@\n self.preview_model.clear_data() # Ensure the model is empty\r\n # Visibility is handled by _clear_editor, but explicitly set here for clarity\r\n self.preview_placeholder_label.setVisible(True)\r\n self.preview_table_view.setVisible(False)\r\n+ self.start_button.setEnabled(False) # Disable start button\r\n return # Stop processing as no real preset is selected\r\n \r\n # Existing logic for handling real preset items starts here\r\n if current_item:\r\n log.debug(f\"Loading preset for editing: {current_item.text()}\")\r\n preset_path = current_item.data(Qt.ItemDataRole.UserRole)\r\n self._load_preset_for_editing(preset_path)\r\n+ self.start_button.setEnabled(True) # Enable start button\r\n # --- Trigger preview update after loading editor ---\r\n self.update_preview()\r\n # --- End Trigger ---\r\n \r\n"
}
],
"date": 1745236085615,
"name": "Commit-0",
"content": "import sys\r\nimport os\r\nimport json\r\nimport logging\r\nimport time\r\nfrom pathlib import Path\r\nfrom functools import partial # For connecting signals with arguments\r\n\r\nfrom PySide6.QtWidgets import (\r\n QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, # Added QSplitter\r\n QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,\r\n QProgressBar, QLabel, QFrame, QCheckBox, QSpinBox, QListWidget, QTextEdit, # Added QListWidget, QTextEdit\r\n QLineEdit, QMessageBox, QFileDialog, QInputDialog, QListWidgetItem, QTabWidget, # Added more widgets\r\n QFormLayout, QGroupBox, QAbstractItemView, QSizePolicy # Added more layout/widget items\r\n)\r\nfrom PySide6.QtCore import Qt, QThread, Slot, Signal # Added Signal\r\nfrom PySide6.QtGui import QColor # Add QColor import\r\n\r\n# --- Backend Imports ---\r\nscript_dir = Path(__file__).parent\r\nproject_root = script_dir.parent\r\nif str(project_root) not in sys.path:\r\n sys.path.insert(0, str(project_root))\r\n\r\ntry:\r\n from configuration import Configuration, ConfigurationError\r\n from asset_processor import AssetProcessor, AssetProcessingError\r\n from gui.processing_handler import ProcessingHandler\r\n from gui.prediction_handler import PredictionHandler\r\n import config as core_config # Import the config module\r\n # PresetEditorDialog is no longer needed\r\nexcept ImportError as e:\r\n print(f\"ERROR: Failed to import backend modules: {e}\")\r\n print(f\"Ensure GUI is run from project root or backend modules are in PYTHONPATH.\")\r\n Configuration = None\r\n AssetProcessor = None\r\n ProcessingHandler = None\r\n PredictionHandler = None\r\n ConfigurationError = Exception\r\n AssetProcessingError = Exception\r\n\r\n# --- Constants ---\r\nPRESETS_DIR = project_root / \"presets\"\r\nTEMPLATE_PATH = PRESETS_DIR / \"_template.json\"\r\n\r\n# Setup basic logging\r\nlog = logging.getLogger(__name__)\r\nif not log.hasHandlers():\r\n # Set level back to INFO for normal operation\r\n logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') # Reverted level and format\r\n\r\n\r\n# --- Helper Functions (from PresetEditorDialog) ---\r\n# NOTE: Consider moving these to a utils file if reused elsewhere\r\n\r\ndef setup_list_widget_with_controls(parent_layout, label_text, attribute_name, instance):\r\n \"\"\"Adds a QListWidget with Add/Remove buttons to a layout.\"\"\"\r\n list_widget = QListWidget()\r\n list_widget.setAlternatingRowColors(True)\r\n # Make items editable by default in the editor context\r\n list_widget.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed)\r\n instance.__setattr__(attribute_name, list_widget) # Store list widget on the instance\r\n\r\n add_button = QPushButton(\"+\")\r\n remove_button = QPushButton(\"-\")\r\n add_button.setFixedWidth(30)\r\n remove_button.setFixedWidth(30)\r\n\r\n button_layout = QVBoxLayout()\r\n button_layout.addWidget(add_button)\r\n button_layout.addWidget(remove_button)\r\n button_layout.addStretch()\r\n\r\n list_layout = QHBoxLayout()\r\n list_layout.addWidget(list_widget)\r\n list_layout.addLayout(button_layout)\r\n\r\n group_box = QGroupBox(label_text)\r\n group_box_layout = QVBoxLayout(group_box)\r\n group_box_layout.addLayout(list_layout)\r\n\r\n parent_layout.addWidget(group_box)\r\n\r\n # Connections (use the instance's methods)\r\n add_button.clicked.connect(partial(instance._editor_add_list_item, list_widget))\r\n remove_button.clicked.connect(partial(instance._editor_remove_list_item, list_widget))\r\n list_widget.itemChanged.connect(instance._mark_editor_unsaved) # Mark unsaved on item edit\r\n\r\ndef setup_table_widget_with_controls(parent_layout, label_text, attribute_name, columns, instance):\r\n \"\"\"Adds a QTableWidget with Add/Remove buttons to a layout.\"\"\"\r\n table_widget = QTableWidget()\r\n table_widget.setColumnCount(len(columns))\r\n table_widget.setHorizontalHeaderLabels(columns)\r\n table_widget.setAlternatingRowColors(True)\r\n instance.__setattr__(attribute_name, table_widget) # Store table widget\r\n\r\n add_button = QPushButton(\"+ Row\")\r\n remove_button = QPushButton(\"- Row\")\r\n\r\n button_layout = QHBoxLayout()\r\n button_layout.addStretch()\r\n button_layout.addWidget(add_button)\r\n button_layout.addWidget(remove_button)\r\n\r\n group_box = QGroupBox(label_text)\r\n group_box_layout = QVBoxLayout(group_box)\r\n group_box_layout.addWidget(table_widget)\r\n group_box_layout.addLayout(button_layout)\r\n\r\n parent_layout.addWidget(group_box)\r\n\r\n # Connections (use the instance's methods)\r\n add_button.clicked.connect(partial(instance._editor_add_table_row, table_widget))\r\n remove_button.clicked.connect(partial(instance._editor_remove_table_row, table_widget))\r\n table_widget.itemChanged.connect(instance._mark_editor_unsaved) # Mark unsaved on item edit\r\n\r\n\r\n# --- Main Window Class ---\r\n\r\nclass MainWindow(QMainWindow):\r\n # Signal emitted when presets change in the editor panel\r\n presets_changed_signal = Signal()\r\n\r\n def __init__(self):\r\n super().__init__()\r\n\r\n self.setWindowTitle(\"Asset Processor Tool\")\r\n self.resize(1200, 700) # Increased default size\r\n\r\n # --- Internal State ---\r\n self.current_asset_paths = set() # Store unique paths of assets added\r\n\r\n # --- Editor State ---\r\n self.current_editing_preset_path = None\r\n self.editor_unsaved_changes = False\r\n self._is_loading_editor = False # Flag to prevent signals during load\r\n\r\n # --- Threading Setup ---\r\n self.processing_thread = None\r\n self.processing_handler = None\r\n self.prediction_thread = None\r\n self.prediction_handler = None\r\n self.setup_threads()\r\n\r\n # --- Main Layout with Splitter ---\r\n self.splitter = QSplitter(Qt.Orientation.Horizontal)\r\n self.setCentralWidget(self.splitter)\r\n\r\n # --- Create Panels ---\r\n self.editor_panel = QWidget()\r\n self.main_panel = QWidget()\r\n\r\n self.splitter.addWidget(self.editor_panel)\r\n self.splitter.addWidget(self.main_panel)\r\n\r\n # --- Setup UI Elements for each panel ---\r\n self.setup_editor_panel_ui()\r\n self.setup_main_panel_ui()\r\n\r\n # --- Status Bar ---\r\n self.statusBar().showMessage(\"Ready\")\r\n\r\n # --- Initial State ---\r\n self._clear_editor() # Clear/disable editor fields initially\r\n self._set_editor_enabled(False) # Disable editor initially\r\n self.populate_presets() # Populate both preset list and combo box\r\n\r\n # --- Connect Editor Signals ---\r\n self._connect_editor_change_signals()\r\n\r\n # --- Adjust Splitter ---\r\n self.splitter.setSizes([400, 800]) # Initial size ratio\r\n\r\n # --- UI Setup Methods ---\r\n\r\n def setup_editor_panel_ui(self):\r\n \"\"\"Sets up the UI elements for the left preset editor panel.\"\"\"\r\n editor_layout = QVBoxLayout(self.editor_panel)\r\n editor_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins\r\n\r\n # Preset List and Controls\r\n list_layout = QVBoxLayout()\r\n list_layout.addWidget(QLabel(\"Presets:\"))\r\n self.editor_preset_list = QListWidget()\r\n self.editor_preset_list.currentItemChanged.connect(self._load_selected_preset_for_editing)\r\n list_layout.addWidget(self.editor_preset_list)\r\n\r\n list_button_layout = QHBoxLayout()\r\n self.editor_new_button = QPushButton(\"New\")\r\n self.editor_delete_button = QPushButton(\"Delete\")\r\n self.editor_new_button.clicked.connect(self._new_preset)\r\n self.editor_delete_button.clicked.connect(self._delete_selected_preset)\r\n list_button_layout.addWidget(self.editor_new_button)\r\n list_button_layout.addWidget(self.editor_delete_button)\r\n list_layout.addLayout(list_button_layout)\r\n editor_layout.addLayout(list_layout, 1) # Allow list to stretch\r\n\r\n # Editor Tabs\r\n self.editor_tab_widget = QTabWidget()\r\n self.editor_tab_general_naming = QWidget()\r\n self.editor_tab_mapping_rules = QWidget()\r\n self.editor_tab_widget.addTab(self.editor_tab_general_naming, \"General & Naming\")\r\n self.editor_tab_widget.addTab(self.editor_tab_mapping_rules, \"Mapping & Rules\")\r\n self._create_editor_general_tab()\r\n self._create_editor_mapping_tab()\r\n editor_layout.addWidget(self.editor_tab_widget, 3) # Allow tabs to stretch more\r\n\r\n # Save Buttons\r\n save_button_layout = QHBoxLayout()\r\n self.editor_save_button = QPushButton(\"Save\")\r\n self.editor_save_as_button = QPushButton(\"Save As...\")\r\n self.editor_save_button.setEnabled(False) # Disabled initially\r\n self.editor_save_button.clicked.connect(self._save_current_preset)\r\n self.editor_save_as_button.clicked.connect(self._save_preset_as)\r\n save_button_layout.addStretch()\r\n save_button_layout.addWidget(self.editor_save_button)\r\n save_button_layout.addWidget(self.editor_save_as_button)\r\n editor_layout.addLayout(save_button_layout)\r\n\r\n def _create_editor_general_tab(self):\r\n \"\"\"Creates the widgets and layout for the 'General & Naming' editor tab.\"\"\"\r\n layout = QVBoxLayout(self.editor_tab_general_naming)\r\n form_layout = QFormLayout()\r\n form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)\r\n\r\n # Basic Info\r\n self.editor_preset_name = QLineEdit()\r\n self.editor_supplier_name = QLineEdit()\r\n self.editor_notes = QTextEdit()\r\n self.editor_notes.setAcceptRichText(False)\r\n self.editor_notes.setFixedHeight(60)\r\n form_layout.addRow(\"Preset Name:\", self.editor_preset_name)\r\n form_layout.addRow(\"Supplier Name:\", self.editor_supplier_name)\r\n form_layout.addRow(\"Notes:\", self.editor_notes)\r\n layout.addLayout(form_layout)\r\n\r\n # Source Naming Group\r\n naming_group = QGroupBox(\"Source File Naming Rules\")\r\n naming_layout_outer = QVBoxLayout(naming_group)\r\n naming_layout_form = QFormLayout()\r\n self.editor_separator = QLineEdit()\r\n self.editor_separator.setMaxLength(1)\r\n self.editor_spin_base_name_idx = QSpinBox()\r\n self.editor_spin_base_name_idx.setMinimum(-1)\r\n self.editor_spin_map_type_idx = QSpinBox()\r\n self.editor_spin_map_type_idx.setMinimum(-1)\r\n naming_layout_form.addRow(\"Separator:\", self.editor_separator)\r\n naming_layout_form.addRow(\"Base Name Index:\", self.editor_spin_base_name_idx)\r\n naming_layout_form.addRow(\"Map Type Index:\", self.editor_spin_map_type_idx)\r\n naming_layout_outer.addLayout(naming_layout_form)\r\n # Gloss Keywords List\r\n setup_list_widget_with_controls(naming_layout_outer, \"Glossiness Keywords\", \"editor_list_gloss_keywords\", self)\r\n # Bit Depth Variants Table\r\n setup_table_widget_with_controls(naming_layout_outer, \"16-bit Variant Patterns\", \"editor_table_bit_depth_variants\", [\"Map Type\", \"Pattern\"], self)\r\n self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)\r\n self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)\r\n layout.addWidget(naming_group)\r\n\r\n # Extra Files Group\r\n setup_list_widget_with_controls(layout, \"Move to 'Extra' Folder Patterns\", \"editor_list_extra_patterns\", self)\r\n\r\n layout.addStretch(1)\r\n\r\n def _create_editor_mapping_tab(self):\r\n \"\"\"Creates the widgets and layout for the 'Mapping & Rules' editor tab.\"\"\"\r\n layout = QVBoxLayout(self.editor_tab_mapping_rules)\r\n\r\n # Map Type Mapping Group\r\n setup_table_widget_with_controls(layout, \"Map Type Mapping (Standard Type <- Input Keywords)\", \"editor_table_map_type_mapping\", [\"Standard Type\", \"Input Keywords (comma-sep)\"], self)\r\n self.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)\r\n self.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)\r\n\r\n # Category Rules Group\r\n category_group = QGroupBox(\"Asset Category Rules\")\r\n category_layout = QVBoxLayout(category_group)\r\n setup_list_widget_with_controls(category_layout, \"Model File Patterns\", \"editor_list_model_patterns\", self)\r\n setup_list_widget_with_controls(category_layout, \"Decal Keywords\", \"editor_list_decal_keywords\", self)\r\n layout.addWidget(category_group)\r\n\r\n # Archetype Rules Group\r\n setup_table_widget_with_controls(layout, \"Archetype Rules\", \"editor_table_archetype_rules\", [\"Archetype Name\", \"Match Any (comma-sep)\", \"Match All (comma-sep)\"], self)\r\n self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)\r\n self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)\r\n self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)\r\n\r\n layout.addStretch(1)\r\n\r\n def setup_main_panel_ui(self):\r\n \"\"\"Sets up the UI elements for the right main processing panel.\"\"\"\r\n main_layout = QVBoxLayout(self.main_panel)\r\n main_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins\r\n\r\n # --- Top Section: Preset Selection (for processing) ---\r\n top_layout = QHBoxLayout()\r\n self.preset_label = QLabel(\"Preset (for processing):\")\r\n self.preset_combo = QComboBox() # This is for selecting the preset to USE for processing\r\n self.preset_combo.currentIndexChanged.connect(self.update_preview) # Update preview when processing preset changes\r\n top_layout.addWidget(self.preset_label)\r\n top_layout.addWidget(self.preset_combo, 1)\r\n main_layout.addLayout(top_layout)\r\n\r\n # --- Output Directory Selection ---\r\n output_layout = QHBoxLayout()\r\n self.output_dir_label = QLabel(\"Output Directory:\")\r\n self.output_path_edit = QLineEdit()\r\n # Make read-only for now, user must use Browse\r\n # self.output_path_edit.setReadOnly(True)\r\n self.browse_output_button = QPushButton(\"Browse...\")\r\n self.browse_output_button.clicked.connect(self._browse_for_output_directory)\r\n output_layout.addWidget(self.output_dir_label)\r\n output_layout.addWidget(self.output_path_edit, 1)\r\n output_layout.addWidget(self.browse_output_button)\r\n main_layout.addLayout(output_layout)\r\n\r\n # --- Set Initial Output Path ---\r\n try:\r\n output_base_dir_config = getattr(core_config, 'OUTPUT_BASE_DIR', '../Asset_Processor_Output') # Default if not found\r\n # Resolve the path relative to the project root\r\n default_output_dir = (project_root / output_base_dir_config).resolve()\r\n self.output_path_edit.setText(str(default_output_dir))\r\n log.info(f\"Default output directory set to: {default_output_dir}\")\r\n except Exception as e:\r\n log.error(f\"Error setting default output directory: {e}\")\r\n self.output_path_edit.setText(\"\") # Clear on error\r\n self.statusBar().showMessage(f\"Error setting default output path: {e}\", 5000)\r\n\r\n\r\n # --- Drag and Drop Area ---\r\n self.drag_drop_area = QFrame()\r\n self.drag_drop_area.setFrameShape(QFrame.Shape.StyledPanel)\r\n self.drag_drop_area.setFrameShadow(QFrame.Shadow.Sunken)\r\n drag_drop_layout = QVBoxLayout(self.drag_drop_area)\r\n drag_drop_label = QLabel(\"Drag and Drop Asset Files/Folders Here\")\r\n drag_drop_label.setAlignment(Qt.AlignmentFlag.AlignCenter)\r\n drag_drop_layout.addWidget(drag_drop_label)\r\n self.drag_drop_area.setMinimumHeight(100)\r\n self.setAcceptDrops(True) # Main window handles drops initially\r\n main_layout.addWidget(self.drag_drop_area)\r\n\r\n # --- Preview Area (Table) ---\r\n self.preview_label = QLabel(\"File Preview (using selected processing preset):\")\r\n self.preview_table = QTableWidget()\r\n self.preview_table.setColumnCount(4)\r\n # New column order: Status, Original Path, Predicted Name, Details\r\n self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Original Path\", \"Predicted Name\", \"Details\"])\r\n # Adjust resize modes\r\n self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Status\r\n self.preview_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Original Path\r\n self.preview_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Predicted Name\r\n self.preview_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Details (Slimmer)\r\n self.preview_table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)\r\n self.preview_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)\r\n self.preview_table.setAlternatingRowColors(True)\r\n self.preview_table.setSortingEnabled(True)\r\n main_layout.addWidget(self.preview_label)\r\n main_layout.addWidget(self.preview_table, 1) # Allow table to stretch\r\n\r\n # --- Progress Bar ---\r\n self.progress_bar = QProgressBar()\r\n self.progress_bar.setValue(0)\r\n self.progress_bar.setTextVisible(True)\r\n main_layout.addWidget(self.progress_bar)\r\n\r\n # --- Bottom Controls ---\r\n bottom_controls_layout = QHBoxLayout()\r\n self.overwrite_checkbox = QCheckBox(\"Overwrite Existing\")\r\n self.overwrite_checkbox.setToolTip(\"If checked, existing output folders for processed assets will be deleted and replaced.\")\r\n bottom_controls_layout.addWidget(self.overwrite_checkbox)\r\n\r\n self.disable_preview_checkbox = QCheckBox(\"Disable Detailed Preview\")\r\n self.disable_preview_checkbox.setToolTip(\"If checked, shows only the list of input assets instead of detailed file predictions.\")\r\n self.disable_preview_checkbox.setChecked(False) # Default is detailed preview enabled\r\n self.disable_preview_checkbox.toggled.connect(self.update_preview) # Update preview when toggled\r\n bottom_controls_layout.addWidget(self.disable_preview_checkbox)\r\n\r\n bottom_controls_layout.addSpacing(20) # Add some space\r\n\r\n self.workers_label = QLabel(\"Workers:\")\r\n self.workers_spinbox = QSpinBox()\r\n default_workers = 1\r\n try:\r\n cores = os.cpu_count()\r\n if cores: default_workers = max(1, cores // 2)\r\n except NotImplementedError: pass\r\n self.workers_spinbox.setMinimum(1)\r\n self.workers_spinbox.setMaximum(os.cpu_count() or 32)\r\n self.workers_spinbox.setValue(default_workers)\r\n self.workers_spinbox.setToolTip(\"Number of assets to process concurrently.\")\r\n bottom_controls_layout.addWidget(self.workers_label)\r\n bottom_controls_layout.addWidget(self.workers_spinbox)\r\n bottom_controls_layout.addStretch(1)\r\n self.clear_queue_button = QPushButton(\"Clear Queue\") # Added Clear button\r\n self.start_button = QPushButton(\"Start Processing\")\r\n self.cancel_button = QPushButton(\"Cancel\")\r\n self.cancel_button.setEnabled(False)\r\n self.clear_queue_button.clicked.connect(self.clear_queue) # Connect signal\r\n self.start_button.clicked.connect(self.start_processing)\r\n self.cancel_button.clicked.connect(self.cancel_processing)\r\n bottom_controls_layout.addWidget(self.clear_queue_button) # Add button to layout\r\n bottom_controls_layout.addWidget(self.start_button)\r\n bottom_controls_layout.addWidget(self.cancel_button)\r\n main_layout.addLayout(bottom_controls_layout)\r\n\r\n # --- Preset Population and Handling ---\r\n\r\n def populate_presets(self):\r\n \"\"\"Scans presets dir and populates BOTH the editor list and processing combo.\"\"\"\r\n log.debug(\"Populating preset list and combo box...\")\r\n # Store current selections\r\n current_combo_selection = self.preset_combo.currentText()\r\n current_list_item = self.editor_preset_list.currentItem()\r\n current_list_selection_text = current_list_item.text() if current_list_item else None\r\n\r\n # Clear both\r\n self.preset_combo.clear()\r\n self.editor_preset_list.clear()\r\n\r\n if not PRESETS_DIR.is_dir():\r\n msg = f\"Error: Presets directory not found at {PRESETS_DIR}\"\r\n self.statusBar().showMessage(msg)\r\n log.error(msg)\r\n return\r\n\r\n # Exclude template file starting with _\r\n presets = sorted([f for f in PRESETS_DIR.glob(\"*.json\") if f.is_file() and not f.name.startswith('_')])\r\n preset_names = [p.stem for p in presets]\r\n\r\n if not presets:\r\n msg = \"Warning: No presets found in presets directory.\"\r\n self.statusBar().showMessage(msg)\r\n log.warning(msg)\r\n else:\r\n # Populate Combo Box (for processing)\r\n self.preset_combo.addItems(preset_names)\r\n # Populate List Widget (for editing)\r\n for preset_path in presets:\r\n item = QListWidgetItem(preset_path.stem)\r\n item.setData(Qt.ItemDataRole.UserRole, preset_path) # Store full path\r\n self.editor_preset_list.addItem(item)\r\n\r\n self.statusBar().showMessage(f\"Loaded {len(presets)} presets.\")\r\n\r\n # Try to restore selections\r\n combo_index = self.preset_combo.findText(current_combo_selection)\r\n if combo_index != -1:\r\n self.preset_combo.setCurrentIndex(combo_index)\r\n\r\n if current_list_selection_text:\r\n items = self.editor_preset_list.findItems(current_list_selection_text, Qt.MatchFlag.MatchExactly)\r\n if items:\r\n self.editor_preset_list.setCurrentItem(items[0]) # This might trigger _load_selected_preset_for_editing\r\n\r\n # --- Drag and Drop Event Handlers (Unchanged) ---\r\n def dragEnterEvent(self, event):\r\n if event.mimeData().hasUrls(): event.acceptProposedAction()\r\n else: event.ignore()\r\n\r\n def dropEvent(self, event):\r\n if event.mimeData().hasUrls():\r\n event.acceptProposedAction()\r\n urls = event.mimeData().urls()\r\n paths = [url.toLocalFile() for url in urls]\r\n self.add_input_paths(paths)\r\n else: event.ignore()\r\n\r\n def add_input_paths(self, paths):\r\n if not hasattr(self, 'current_asset_paths'): self.current_asset_paths = set()\r\n added_count = 0\r\n newly_added_paths = []\r\n for p_str in paths:\r\n p = Path(p_str)\r\n if p.exists():\r\n if p.is_dir() or (p.is_file() and p.suffix.lower() == '.zip'):\r\n if p_str not in self.current_asset_paths:\r\n self.current_asset_paths.add(p_str)\r\n newly_added_paths.append(p_str)\r\n added_count += 1\r\n else: print(f\"Skipping duplicate asset path: {p_str}\")\r\n else: self.statusBar().showMessage(f\"Invalid input (not dir or .zip): {p.name}\", 5000); print(f\"Invalid input: {p_str}\")\r\n else: self.statusBar().showMessage(f\"Input path not found: {p.name}\", 5000); print(f\"Input path not found: {p_str}\")\r\n if added_count > 0:\r\n log.info(f\"Added {added_count} new asset paths: {newly_added_paths}\")\r\n self.statusBar().showMessage(f\"Added {added_count} asset(s). Updating preview...\", 3000)\r\n self.update_preview()\r\n\r\n def _browse_for_output_directory(self):\r\n \"\"\"Opens a dialog to select the output directory.\"\"\"\r\n current_path = self.output_path_edit.text()\r\n if not current_path or not Path(current_path).is_dir():\r\n # Fallback to project root or home directory if current path is invalid\r\n current_path = str(project_root)\r\n\r\n directory = QFileDialog.getExistingDirectory(\r\n self,\r\n \"Select Output Directory\",\r\n current_path,\r\n QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks\r\n )\r\n if directory:\r\n self.output_path_edit.setText(directory)\r\n log.info(f\"User selected output directory: {directory}\")\r\n\r\n\r\n # --- Processing Action Methods ---\r\n def start_processing(self):\r\n if self.processing_handler and self.processing_handler.is_running:\r\n log.warning(\"Start clicked, but processing is already running.\")\r\n self.statusBar().showMessage(\"Processing is already in progress.\", 3000)\r\n return\r\n if ProcessingHandler is None:\r\n self.statusBar().showMessage(\"Error: Processing components not loaded.\", 5000)\r\n return\r\n if not hasattr(self, 'current_asset_paths') or not self.current_asset_paths:\r\n self.statusBar().showMessage(\"No assets added to process.\", 3000)\r\n return\r\n input_paths = list(self.current_asset_paths)\r\n if not input_paths:\r\n self.statusBar().showMessage(\"No assets added to process.\", 3000)\r\n return\r\n selected_preset = self.preset_combo.currentText() # Use the processing combo box\r\n if not selected_preset:\r\n self.statusBar().showMessage(\"Please select a preset for processing.\", 3000)\r\n return\r\n overwrite = self.overwrite_checkbox.isChecked()\r\n num_workers = self.workers_spinbox.value()\r\n\r\n # --- Get Output Directory from UI and Validate ---\r\n output_dir_str = self.output_path_edit.text().strip()\r\n if not output_dir_str:\r\n self.statusBar().showMessage(\"Error: Output directory cannot be empty.\", 5000)\r\n log.error(\"Start processing failed: Output directory field is empty.\")\r\n return\r\n try:\r\n output_dir = Path(output_dir_str)\r\n # Attempt to create the directory if it doesn't exist\r\n output_dir.mkdir(parents=True, exist_ok=True)\r\n # Basic writability check (create and delete a temp file)\r\n # Note: This isn't foolproof due to potential race conditions/permissions issues\r\n # but provides a basic level of validation.\r\n temp_file = output_dir / f\".writable_check_{time.time()}\"\r\n temp_file.touch()\r\n temp_file.unlink()\r\n log.info(f\"Using validated output directory: {output_dir_str}\")\r\n except OSError as e:\r\n error_msg = f\"Error creating/accessing output directory '{output_dir_str}': {e}\"\r\n self.statusBar().showMessage(error_msg, 5000)\r\n log.error(error_msg)\r\n return\r\n except Exception as e: # Catch other potential Path errors or unexpected issues\r\n error_msg = f\"Invalid output directory path '{output_dir_str}': {e}\"\r\n self.statusBar().showMessage(error_msg, 5000)\r\n log.error(error_msg)\r\n return\r\n # --- End Output Directory Validation ---\r\n\r\n log.info(f\"Preparing to start processing {len(input_paths)} items to '{output_dir_str}'.\")\r\n self.set_controls_enabled(False)\r\n self.cancel_button.setEnabled(True)\r\n self.start_button.setText(\"Processing...\")\r\n self.progress_bar.setValue(0)\r\n self.progress_bar.setFormat(\"%p%\")\r\n self.setup_threads()\r\n if self.processing_thread and self.processing_handler:\r\n try: self.processing_thread.started.disconnect()\r\n except RuntimeError: pass\r\n self.processing_thread.started.connect(\r\n lambda: self.processing_handler.run_processing(\r\n input_paths, selected_preset, output_dir_str, overwrite, num_workers\r\n )\r\n )\r\n self.processing_thread.start()\r\n log.info(\"Processing thread started.\")\r\n self.statusBar().showMessage(f\"Processing {len(input_paths)} items...\", 0)\r\n else:\r\n log.error(\"Failed to start processing: Thread or handler not initialized.\")\r\n self.statusBar().showMessage(\"Error: Failed to initialize processing thread.\", 5000)\r\n self.set_controls_enabled(True)\r\n self.cancel_button.setEnabled(False)\r\n self.start_button.setText(\"Start Processing\")\r\n\r\n def cancel_processing(self):\r\n if self.processing_handler and self.processing_handler.is_running:\r\n log.info(\"Cancel button clicked. Requesting cancellation.\")\r\n self.statusBar().showMessage(\"Requesting cancellation...\", 3000)\r\n self.processing_handler.request_cancel()\r\n self.cancel_button.setEnabled(False)\r\n self.start_button.setText(\"Cancelling...\")\r\n else:\r\n log.warning(\"Cancel clicked, but no processing is running.\")\r\n self.statusBar().showMessage(\"Nothing to cancel.\", 3000)\r\n\r\n def clear_queue(self):\r\n \"\"\"Clears the current asset queue and the preview table.\"\"\"\r\n if self.processing_handler and self.processing_handler.is_running:\r\n self.statusBar().showMessage(\"Cannot clear queue while processing.\", 3000)\r\n return\r\n\r\n if hasattr(self, 'current_asset_paths') and self.current_asset_paths:\r\n log.info(f\"Clearing asset queue ({len(self.current_asset_paths)} items).\")\r\n self.current_asset_paths.clear()\r\n self.preview_table.setRowCount(0)\r\n self.statusBar().showMessage(\"Asset queue cleared.\", 3000)\r\n else:\r\n self.statusBar().showMessage(\"Asset queue is already empty.\", 3000)\r\n\r\n\r\n # --- Preview Update Method ---\r\n def update_preview(self):\r\n log.debug(f\"[{time.time():.4f}] update_preview triggered.\")\r\n\r\n # --- Handle Preview Toggle ---\r\n if hasattr(self, 'disable_preview_checkbox') and self.disable_preview_checkbox.isChecked():\r\n log.info(\"Detailed preview disabled. Showing simple input list.\")\r\n self.preview_table.setSortingEnabled(False)\r\n self.preview_table.setRowCount(0) # Clear previous\r\n self.preview_table.setColumnCount(1)\r\n self.preview_table.setHorizontalHeaderLabels([\"Input Path\"])\r\n self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch)\r\n\r\n if hasattr(self, 'current_asset_paths') and self.current_asset_paths:\r\n input_paths_list = sorted(list(self.current_asset_paths))\r\n self.preview_table.setRowCount(len(input_paths_list))\r\n for row, path_str in enumerate(input_paths_list):\r\n item = QTableWidgetItem(path_str)\r\n item.setToolTip(path_str) # Simple tooltip\r\n self.preview_table.setItem(row, 0, item)\r\n self.statusBar().showMessage(f\"Preview disabled. Showing {len(input_paths_list)} input assets.\", 3000)\r\n else:\r\n self.statusBar().showMessage(\"Preview disabled. No assets added.\", 3000)\r\n\r\n self.preview_table.setSortingEnabled(True)\r\n return # Stop here, do not run PredictionHandler\r\n # --- End Preview Toggle Handling ---\r\n\r\n # --- Proceed with Detailed Preview ---\r\n log.debug(\"Detailed preview enabled. Proceeding with prediction handler.\")\r\n # Reset table headers for detailed view\r\n self.preview_table.setColumnCount(4)\r\n self.preview_table.setHorizontalHeaderLabels([\"Status\", \"Original Path\", \"Predicted Name\", \"Details\"])\r\n self.preview_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Status\r\n self.preview_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Original Path\r\n self.preview_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Predicted Name\r\n self.preview_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Details (Slimmer)\r\n\r\n\r\n if self.prediction_handler and self.prediction_handler.is_running:\r\n log.warning(f\"[{time.time():.4f}] Preview update requested, but already running.\")\r\n # Optionally: Request cancel previous prediction? For now, just return.\r\n return\r\n if PredictionHandler is None:\r\n self.statusBar().showMessage(\"Error: Prediction components not loaded.\", 5000)\r\n return\r\n selected_preset = self.preset_combo.currentText() # Use the processing combo box\r\n if not selected_preset:\r\n log.debug(\"Update preview called with no processing preset selected.\")\r\n self.preview_table.setRowCount(0)\r\n self.statusBar().showMessage(\"Select a processing preset to update preview.\", 3000)\r\n return\r\n if not hasattr(self, 'current_asset_paths') or not self.current_asset_paths:\r\n log.debug(\"Update preview called with no assets tracked.\")\r\n self.preview_table.setRowCount(0)\r\n return\r\n input_paths = list(self.current_asset_paths)\r\n if not input_paths:\r\n log.debug(\"Update preview called but no input paths derived.\")\r\n self.preview_table.setRowCount(0)\r\n return\r\n\r\n log.info(f\"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items, Preset='{selected_preset}'\")\r\n self.statusBar().showMessage(f\"Updating preview for '{selected_preset}'...\", 0)\r\n self.preview_table.setRowCount(0) # Clear before starting prediction\r\n self.setup_threads() # Ensure threads are ready\r\n if self.prediction_thread and self.prediction_handler:\r\n try: self.prediction_thread.started.disconnect() # Disconnect previous lambda if any\r\n except RuntimeError: pass\r\n # Connect the lambda to start the prediction\r\n self.prediction_thread.started.connect(\r\n lambda: self.prediction_handler.run_prediction(input_paths, selected_preset)\r\n )\r\n log.debug(f\"[{time.time():.4f}] Starting prediction thread...\")\r\n self.prediction_thread.start()\r\n log.debug(f\"[{time.time():.4f}] Prediction thread start requested.\")\r\n else:\r\n log.error(f\"[{time.time():.4f}] Failed to start prediction: Thread or handler not initialized.\")\r\n self.statusBar().showMessage(\"Error: Failed to initialize prediction thread.\", 5000)\r\n\r\n # --- Threading and Processing Control ---\r\n def setup_threads(self):\r\n # Setup Processing Thread\r\n if ProcessingHandler and self.processing_thread is None:\r\n self.processing_thread = QThread(self)\r\n self.processing_handler = ProcessingHandler()\r\n self.processing_handler.moveToThread(self.processing_thread)\r\n self.processing_handler.progress_updated.connect(self.update_progress_bar)\r\n self.processing_handler.file_status_updated.connect(self.update_file_status)\r\n self.processing_handler.processing_finished.connect(self.on_processing_finished)\r\n self.processing_handler.status_message.connect(self.show_status_message)\r\n self.processing_handler.processing_finished.connect(self.processing_thread.quit)\r\n self.processing_handler.processing_finished.connect(self.processing_handler.deleteLater)\r\n self.processing_thread.finished.connect(self.processing_thread.deleteLater)\r\n self.processing_thread.finished.connect(self._reset_processing_thread_references)\r\n log.debug(\"Processing thread and handler set up.\")\r\n elif not ProcessingHandler:\r\n log.error(\"ProcessingHandler not available. Cannot set up processing thread.\")\r\n if hasattr(self, 'start_button'):\r\n self.start_button.setEnabled(False)\r\n self.start_button.setToolTip(\"Error: Backend processing components failed to load.\")\r\n\r\n # Setup Prediction Thread\r\n if PredictionHandler and self.prediction_thread is None:\r\n self.prediction_thread = QThread(self)\r\n self.prediction_handler = PredictionHandler()\r\n self.prediction_handler.moveToThread(self.prediction_thread)\r\n self.prediction_handler.prediction_results_ready.connect(self.on_prediction_results_ready) # Updated slot below\r\n self.prediction_handler.prediction_finished.connect(self.on_prediction_finished)\r\n self.prediction_handler.status_message.connect(self.show_status_message)\r\n self.prediction_handler.prediction_finished.connect(self.prediction_thread.quit)\r\n self.prediction_handler.prediction_finished.connect(self.prediction_handler.deleteLater)\r\n self.prediction_thread.finished.connect(self.prediction_thread.deleteLater)\r\n self.prediction_thread.finished.connect(self._reset_prediction_thread_references)\r\n log.debug(\"Prediction thread and handler set up.\")\r\n elif not PredictionHandler:\r\n log.error(\"PredictionHandler not available. Cannot set up prediction thread.\")\r\n\r\n @Slot()\r\n def _reset_processing_thread_references(self):\r\n log.debug(\"Resetting processing thread and handler references.\")\r\n self.processing_thread = None\r\n self.processing_handler = None\r\n\r\n @Slot()\r\n def _reset_prediction_thread_references(self):\r\n log.debug(\"Resetting prediction thread and handler references.\")\r\n self.prediction_thread = None\r\n self.prediction_handler = None\r\n\r\n @Slot(int, int)\r\n def update_progress_bar(self, current_count, total_count):\r\n if total_count > 0:\r\n percentage = int((current_count / total_count) * 100)\r\n self.progress_bar.setValue(percentage)\r\n self.progress_bar.setFormat(f\"%p% ({current_count}/{total_count})\")\r\n else:\r\n self.progress_bar.setValue(0)\r\n self.progress_bar.setFormat(\"0/0\")\r\n\r\n # Slot for prediction results (Updated for new format and coloring)\r\n @Slot(list)\r\n def on_prediction_results_ready(self, results: list):\r\n \"\"\"Populates the preview table with detailed prediction results.\"\"\"\r\n log.debug(f\"[{time.time():.4f}] on_prediction_results_ready received {len(results)} file details.\")\r\n self.preview_table.setSortingEnabled(False) # Disable sorting during population\r\n self.preview_table.setRowCount(0) # Clear previous results\r\n\r\n self.preview_table.setRowCount(len(results))\r\n for row, file_details in enumerate(results):\r\n # Ensure file_details is a dictionary before accessing keys\r\n if not isinstance(file_details, dict):\r\n log.warning(f\"Received non-dict item in prediction results: {file_details}\")\r\n # Add a row indicating an error (adjust column order)\r\n item_status = QTableWidgetItem(\"Error\")\r\n item_orig = QTableWidgetItem(\"[Invalid Data Received]\")\r\n item_pred_name = QTableWidgetItem(\"\")\r\n item_details_display = QTableWidgetItem(\"Invalid data format from handler\")\r\n self.preview_table.setItem(row, 0, item_status)\r\n self.preview_table.setItem(row, 1, item_orig)\r\n self.preview_table.setItem(row, 2, item_pred_name)\r\n self.preview_table.setItem(row, 3, item_details_display)\r\n continue\r\n\r\n # Extract data using the new keys\r\n original_path = file_details.get('original_path', '[Missing Path]')\r\n predicted_name = file_details.get('predicted_name', '') # Can be None or empty\r\n status = file_details.get('status', '[No Status]')\r\n details = file_details.get('details', '') # Tooltip info\r\n source_asset = file_details.get('source_asset', 'N/A') # For tooltip\r\n\r\n # Create items\r\n item_orig = QTableWidgetItem(original_path)\r\n item_pred_name = QTableWidgetItem(predicted_name if predicted_name else \"\") # Display empty if None\r\n item_status = QTableWidgetItem(status)\r\n item_details_display = QTableWidgetItem(details if details else \"\") # Show details directly for now\r\n\r\n # Set tooltips\r\n item_orig.setToolTip(f\"Source Asset: {source_asset}\\nFull Path: {original_path}\")\r\n item_status.setToolTip(details if details else status) # Use details for status tooltip\r\n item_details_display.setToolTip(details if details else \"\")\r\n\r\n # --- Apply Text Coloring based on Status ---\r\n color = None\r\n if status == \"Mapped\":\r\n color = QColor(\"#9dd9db\") # Light blue/teal\r\n elif status == \"Ignored\":\r\n color = QColor(\"#c1753d\") # Orange/brown\r\n elif status == \"Extra\":\r\n color = QColor(\"#cfdca4\") # Light green/yellow\r\n elif status == \"Unrecognised\":\r\n color = QColor(\"#92371f\") # User specified dark red/brown\r\n elif status == \"Model\":\r\n color = QColor(\"#a4b8dc\") # Light blue/grey (Example)\r\n elif status == \"Error\":\r\n color = QColor(Qt.GlobalColor.red) # Example for Error\r\n\r\n if color:\r\n item_status.setForeground(color)\r\n item_orig.setForeground(color)\r\n item_pred_name.setForeground(color)\r\n item_details_display.setForeground(color)\r\n # --- End Coloring ---\r\n\r\n # Set items in table (new column order)\r\n self.preview_table.setItem(row, 0, item_status)\r\n self.preview_table.setItem(row, 1, item_orig)\r\n self.preview_table.setItem(row, 2, item_pred_name)\r\n self.preview_table.setItem(row, 3, item_details_display)\r\n\r\n self.preview_table.sortItems(1, Qt.SortOrder.AscendingOrder) # Sort by original path (now column 1)\r\n self.preview_table.setSortingEnabled(True) # Re-enable sorting\r\n log.debug(f\"[{time.time():.4f}] Preview table populated with detailed results and coloring.\")\r\n\r\n\r\n @Slot()\r\n def on_prediction_finished(self):\r\n log.debug(f\"[{time.time():.4f}] Prediction finished signal received.\")\r\n\r\n @Slot(str, str, str)\r\n def update_file_status(self, input_path_str, status, message):\r\n # TODO: Update status bar or potentially find rows in table later\r\n status_text = f\"Asset '{Path(input_path_str).name}': {status.upper()}\"\r\n if status == \"failed\" and message: status_text += f\" - Error: {message}\"\r\n self.statusBar().showMessage(status_text, 5000)\r\n log.debug(f\"Received file status update: {input_path_str} - {status}\")\r\n\r\n @Slot(int, int, int)\r\n def on_processing_finished(self, processed_count, skipped_count, failed_count):\r\n log.info(f\"GUI received processing_finished signal: P={processed_count}, S={skipped_count}, F={failed_count}\")\r\n self.set_controls_enabled(True)\r\n self.cancel_button.setEnabled(False)\r\n self.start_button.setText(\"Start Processing\")\r\n\r\n @Slot(str, int)\r\n def show_status_message(self, message, timeout_ms):\r\n if timeout_ms > 0: self.statusBar().showMessage(message, timeout_ms)\r\n else: self.statusBar().showMessage(message)\r\n\r\n def set_controls_enabled(self, enabled: bool):\r\n \"\"\"Enables/disables input controls during processing.\"\"\"\r\n # Main panel controls\r\n self.preset_combo.setEnabled(enabled)\r\n self.start_button.setEnabled(enabled)\r\n self.setAcceptDrops(enabled)\r\n self.drag_drop_area.setEnabled(enabled)\r\n self.preview_table.setEnabled(enabled)\r\n # Editor panel controls (should generally be enabled unless processing)\r\n self.editor_panel.setEnabled(enabled) # Enable/disable the whole panel\r\n\r\n\r\n # --- Preset Editor Methods (Adapted from PresetEditorDialog) ---\r\n\r\n def _editor_add_list_item(self, list_widget: QListWidget):\r\n \"\"\"Adds an editable item to the specified list widget in the editor.\"\"\"\r\n text, ok = QInputDialog.getText(self, f\"Add Item\", \"Enter value:\")\r\n if ok and text:\r\n item = QListWidgetItem(text)\r\n # item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) # Already editable by default\r\n list_widget.addItem(item)\r\n self._mark_editor_unsaved()\r\n\r\n def _editor_remove_list_item(self, list_widget: QListWidget):\r\n \"\"\"Removes the selected item from the specified list widget in the editor.\"\"\"\r\n selected_items = list_widget.selectedItems()\r\n if not selected_items: return\r\n for item in selected_items: list_widget.takeItem(list_widget.row(item))\r\n self._mark_editor_unsaved()\r\n\r\n def _editor_add_table_row(self, table_widget: QTableWidget):\r\n \"\"\"Adds an empty row to the specified table widget in the editor.\"\"\"\r\n row_count = table_widget.rowCount()\r\n table_widget.insertRow(row_count)\r\n for col in range(table_widget.columnCount()): table_widget.setItem(row_count, col, QTableWidgetItem(\"\"))\r\n self._mark_editor_unsaved()\r\n\r\n def _editor_remove_table_row(self, table_widget: QTableWidget):\r\n \"\"\"Removes the selected row(s) from the specified table widget in the editor.\"\"\"\r\n selected_rows = sorted(list(set(index.row() for index in table_widget.selectedIndexes())), reverse=True)\r\n if not selected_rows:\r\n if table_widget.rowCount() > 0: selected_rows = [table_widget.rowCount() - 1]\r\n else: return\r\n for row in selected_rows: table_widget.removeRow(row)\r\n self._mark_editor_unsaved()\r\n\r\n def _mark_editor_unsaved(self):\r\n \"\"\"Marks changes in the editor panel as unsaved.\"\"\"\r\n if self._is_loading_editor: return\r\n self.editor_unsaved_changes = True\r\n self.editor_save_button.setEnabled(True)\r\n preset_name = Path(self.current_editing_preset_path).name if self.current_editing_preset_path else 'New Preset'\r\n self.setWindowTitle(f\"Asset Processor Tool - {preset_name}*\")\r\n\r\n def _connect_editor_change_signals(self):\r\n \"\"\"Connect signals from all editor widgets to mark_editor_unsaved.\"\"\"\r\n self.editor_preset_name.textChanged.connect(self._mark_editor_unsaved)\r\n self.editor_supplier_name.textChanged.connect(self._mark_editor_unsaved)\r\n self.editor_notes.textChanged.connect(self._mark_editor_unsaved)\r\n self.editor_separator.textChanged.connect(self._mark_editor_unsaved)\r\n self.editor_spin_base_name_idx.valueChanged.connect(self._mark_editor_unsaved)\r\n self.editor_spin_map_type_idx.valueChanged.connect(self._mark_editor_unsaved)\r\n # List/Table widgets are connected via helper functions\r\n\r\n def _check_editor_unsaved_changes(self) -> bool:\r\n \"\"\"Checks for unsaved changes in the editor and prompts the user. Returns True if should cancel action.\"\"\"\r\n if not self.editor_unsaved_changes: return False\r\n reply = QMessageBox.question(self, \"Unsaved Preset Changes\",\r\n \"You have unsaved changes in the preset editor. Discard them?\",\r\n QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,\r\n QMessageBox.StandardButton.Cancel)\r\n if reply == QMessageBox.StandardButton.Save: return not self._save_current_preset() # Return True (cancel) if save fails\r\n elif reply == QMessageBox.StandardButton.Discard: return False # Discarded, proceed\r\n else: return True # Cancel action\r\n\r\n def _set_editor_enabled(self, enabled: bool):\r\n \"\"\"Enables or disables all editor widgets.\"\"\"\r\n self.editor_tab_widget.setEnabled(enabled)\r\n # Also enable/disable save buttons based on editor state, not just processing state\r\n self.editor_save_button.setEnabled(enabled and self.editor_unsaved_changes)\r\n self.editor_save_as_button.setEnabled(enabled) # Save As is always possible if editor is enabled\r\n\r\n def _clear_editor(self):\r\n \"\"\"Clears the editor fields and resets state.\"\"\"\r\n self._is_loading_editor = True\r\n self.editor_preset_name.clear()\r\n self.editor_supplier_name.clear()\r\n self.editor_notes.clear()\r\n self.editor_separator.clear()\r\n self.editor_spin_base_name_idx.setValue(0)\r\n self.editor_spin_map_type_idx.setValue(1)\r\n self.editor_list_gloss_keywords.clear()\r\n self.editor_table_bit_depth_variants.setRowCount(0)\r\n self.editor_list_extra_patterns.clear()\r\n self.editor_table_map_type_mapping.setRowCount(0)\r\n self.editor_list_model_patterns.clear()\r\n self.editor_list_decal_keywords.clear()\r\n self.editor_table_archetype_rules.setRowCount(0)\r\n self.current_editing_preset_path = None\r\n self.editor_unsaved_changes = False\r\n self.editor_save_button.setEnabled(False)\r\n self.setWindowTitle(\"Asset Processor Tool\") # Reset window title\r\n self._set_editor_enabled(False)\r\n self._is_loading_editor = False\r\n\r\n def _populate_editor_from_data(self, preset_data: dict):\r\n \"\"\"Helper method to populate editor UI widgets from a preset data dictionary.\"\"\"\r\n self._is_loading_editor = True\r\n try:\r\n self.editor_preset_name.setText(preset_data.get(\"preset_name\", \"\"))\r\n self.editor_supplier_name.setText(preset_data.get(\"supplier_name\", \"\"))\r\n self.editor_notes.setText(preset_data.get(\"notes\", \"\"))\r\n naming_data = preset_data.get(\"source_naming\", {})\r\n self.editor_separator.setText(naming_data.get(\"separator\", \"_\"))\r\n indices = naming_data.get(\"part_indices\", {})\r\n self.editor_spin_base_name_idx.setValue(indices.get(\"base_name\", 0))\r\n self.editor_spin_map_type_idx.setValue(indices.get(\"map_type\", 1))\r\n self.editor_list_gloss_keywords.clear()\r\n self.editor_list_gloss_keywords.addItems(naming_data.get(\"glossiness_keywords\", []))\r\n self.editor_table_bit_depth_variants.setRowCount(0)\r\n bit_depth_vars = naming_data.get(\"bit_depth_variants\", {})\r\n for i, (map_type, pattern) in enumerate(bit_depth_vars.items()):\r\n self.editor_table_bit_depth_variants.insertRow(i)\r\n self.editor_table_bit_depth_variants.setItem(i, 0, QTableWidgetItem(map_type))\r\n self.editor_table_bit_depth_variants.setItem(i, 1, QTableWidgetItem(pattern))\r\n self.editor_list_extra_patterns.clear()\r\n self.editor_list_extra_patterns.addItems(preset_data.get(\"move_to_extra_patterns\", []))\r\n self.editor_table_map_type_mapping.setRowCount(0)\r\n map_mappings = preset_data.get(\"map_type_mapping\", [])\r\n # --- UPDATED for new dictionary format ---\r\n for i, mapping_dict in enumerate(map_mappings):\r\n if isinstance(mapping_dict, dict) and \"target_type\" in mapping_dict and \"keywords\" in mapping_dict:\r\n std_type = mapping_dict[\"target_type\"]\r\n keywords = mapping_dict[\"keywords\"]\r\n self.editor_table_map_type_mapping.insertRow(i)\r\n self.editor_table_map_type_mapping.setItem(i, 0, QTableWidgetItem(std_type))\r\n # Ensure keywords are strings before joining\r\n keywords_str = [str(k) for k in keywords if isinstance(k, str)]\r\n self.editor_table_map_type_mapping.setItem(i, 1, QTableWidgetItem(\", \".join(keywords_str)))\r\n else:\r\n log.warning(f\"Skipping invalid map_type_mapping item during editor population: {mapping_dict}\")\r\n # --- END UPDATE ---\r\n category_rules = preset_data.get(\"asset_category_rules\", {})\r\n self.editor_list_model_patterns.clear()\r\n self.editor_list_model_patterns.addItems(category_rules.get(\"model_patterns\", []))\r\n self.editor_list_decal_keywords.clear()\r\n self.editor_list_decal_keywords.addItems(category_rules.get(\"decal_keywords\", []))\r\n self.editor_table_archetype_rules.setRowCount(0)\r\n arch_rules = preset_data.get(\"archetype_rules\", [])\r\n for i, rule in enumerate(arch_rules):\r\n if isinstance(rule, (list, tuple)) and len(rule) == 2:\r\n arch_name, conditions = rule\r\n match_any = \", \".join(conditions.get(\"match_any\", []))\r\n match_all = \", \".join(conditions.get(\"match_all\", []))\r\n self.editor_table_archetype_rules.insertRow(i)\r\n self.editor_table_archetype_rules.setItem(i, 0, QTableWidgetItem(arch_name))\r\n self.editor_table_archetype_rules.setItem(i, 1, QTableWidgetItem(match_any))\r\n self.editor_table_archetype_rules.setItem(i, 2, QTableWidgetItem(match_all))\r\n finally:\r\n self._is_loading_editor = False\r\n\r\n def _load_preset_for_editing(self, file_path: Path):\r\n \"\"\"Loads the content of the selected preset file into the editor widgets.\"\"\"\r\n if not file_path or not file_path.is_file():\r\n self._clear_editor()\r\n return\r\n log.info(f\"Loading preset into editor: {file_path.name}\")\r\n try:\r\n with open(file_path, 'r', encoding='utf-8') as f: preset_data = json.load(f)\r\n self._populate_editor_from_data(preset_data)\r\n self._set_editor_enabled(True)\r\n self.current_editing_preset_path = file_path\r\n self.editor_unsaved_changes = False\r\n self.editor_save_button.setEnabled(False)\r\n self.setWindowTitle(f\"Asset Processor Tool - {file_path.name}\")\r\n log.info(f\"Preset '{file_path.name}' loaded into editor.\")\r\n except json.JSONDecodeError as json_err:\r\n log.error(f\"Invalid JSON in {file_path.name}: {json_err}\")\r\n QMessageBox.warning(self, \"Load Error\", f\"Failed to load preset '{file_path.name}'.\\nInvalid JSON structure:\\n{json_err}\")\r\n self._clear_editor()\r\n except Exception as e:\r\n log.exception(f\"Error loading preset file {file_path}: {e}\")\r\n QMessageBox.critical(self, \"Error\", f\"Could not load preset file:\\n{file_path}\\n\\nError: {e}\")\r\n self._clear_editor()\r\n\r\n def _load_selected_preset_for_editing(self, current_item: QListWidgetItem, previous_item: QListWidgetItem):\r\n \"\"\"Loads the preset currently selected in the editor list.\"\"\"\r\n if self._check_editor_unsaved_changes():\r\n # If user cancels, revert selection\r\n if previous_item:\r\n self.editor_preset_list.blockSignals(True)\r\n self.editor_preset_list.setCurrentItem(previous_item)\r\n self.editor_preset_list.blockSignals(False)\r\n return\r\n if current_item:\r\n self._load_preset_for_editing(current_item.data(Qt.ItemDataRole.UserRole))\r\n else:\r\n self._clear_editor() # Clear editor if selection is cleared\r\n\r\n def _gather_editor_data(self) -> dict:\r\n \"\"\"Gathers data from all editor UI widgets and returns a dictionary.\"\"\"\r\n preset_data = {}\r\n preset_data[\"preset_name\"] = self.editor_preset_name.text().strip()\r\n preset_data[\"supplier_name\"] = self.editor_supplier_name.text().strip()\r\n preset_data[\"notes\"] = self.editor_notes.toPlainText().strip()\r\n naming_data = {}\r\n naming_data[\"separator\"] = self.editor_separator.text()\r\n naming_data[\"part_indices\"] = { \"base_name\": self.editor_spin_base_name_idx.value(), \"map_type\": self.editor_spin_map_type_idx.value() }\r\n naming_data[\"glossiness_keywords\"] = [self.editor_list_gloss_keywords.item(i).text() for i in range(self.editor_list_gloss_keywords.count())]\r\n naming_data[\"bit_depth_variants\"] = {self.editor_table_bit_depth_variants.item(r, 0).text(): self.editor_table_bit_depth_variants.item(r, 1).text()\r\n for r in range(self.editor_table_bit_depth_variants.rowCount()) if self.editor_table_bit_depth_variants.item(r, 0) and self.editor_table_bit_depth_variants.item(r, 1)}\r\n preset_data[\"source_naming\"] = naming_data\r\n preset_data[\"move_to_extra_patterns\"] = [self.editor_list_extra_patterns.item(i).text() for i in range(self.editor_list_extra_patterns.count())]\r\n # --- UPDATED for new dictionary format ---\r\n map_mappings = []\r\n for r in range(self.editor_table_map_type_mapping.rowCount()):\r\n type_item = self.editor_table_map_type_mapping.item(r, 0)\r\n keywords_item = self.editor_table_map_type_mapping.item(r, 1)\r\n # Ensure both items exist and have text before processing\r\n if type_item and type_item.text() and keywords_item and keywords_item.text():\r\n target_type = type_item.text().strip()\r\n keywords = [k.strip() for k in keywords_item.text().split(',') if k.strip()]\r\n if target_type and keywords: # Only add if both parts are valid\r\n map_mappings.append({\"target_type\": target_type, \"keywords\": keywords})\r\n else:\r\n log.warning(f\"Skipping row {r} in map type mapping table due to empty target type or keywords.\")\r\n else:\r\n log.warning(f\"Skipping row {r} in map type mapping table due to missing items.\")\r\n preset_data[\"map_type_mapping\"] = map_mappings\r\n # --- END UPDATE ---\r\n category_rules = {}\r\n category_rules[\"model_patterns\"] = [self.editor_list_model_patterns.item(i).text() for i in range(self.editor_list_model_patterns.count())]\r\n category_rules[\"decal_keywords\"] = [self.editor_list_decal_keywords.item(i).text() for i in range(self.editor_list_decal_keywords.count())]\r\n preset_data[\"asset_category_rules\"] = category_rules\r\n arch_rules = []\r\n for r in range(self.editor_table_archetype_rules.rowCount()):\r\n name_item = self.editor_table_archetype_rules.item(r, 0)\r\n any_item = self.editor_table_archetype_rules.item(r, 1)\r\n all_item = self.editor_table_archetype_rules.item(r, 2)\r\n if name_item and any_item and all_item:\r\n match_any = [k.strip() for k in any_item.text().split(',') if k.strip()]\r\n match_all = [k.strip() for k in all_item.text().split(',') if k.strip()]\r\n arch_rules.append([name_item.text().strip(), {\"match_any\": match_any, \"match_all\": match_all}])\r\n preset_data[\"archetype_rules\"] = arch_rules\r\n return preset_data\r\n\r\n def _save_current_preset(self) -> bool:\r\n \"\"\"Saves the current editor content to the currently loaded file path.\"\"\"\r\n if not self.current_editing_preset_path: return self._save_preset_as()\r\n log.info(f\"Saving preset: {self.current_editing_preset_path.name}\")\r\n try:\r\n preset_data = self._gather_editor_data()\r\n if not preset_data.get(\"preset_name\"): QMessageBox.warning(self, \"Save Error\", \"Preset Name cannot be empty.\"); return False\r\n if not preset_data.get(\"supplier_name\"): QMessageBox.warning(self, \"Save Error\", \"Supplier Name cannot be empty.\"); return False\r\n content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False)\r\n with open(self.current_editing_preset_path, 'w', encoding='utf-8') as f: f.write(content_to_save)\r\n self.editor_unsaved_changes = False\r\n self.editor_save_button.setEnabled(False)\r\n self.setWindowTitle(f\"Asset Processor Tool - {self.current_editing_preset_path.name}\")\r\n self.presets_changed_signal.emit() # Signal that presets changed\r\n log.info(\"Preset saved successfully.\")\r\n # Refresh lists after save\r\n self.populate_presets()\r\n return True\r\n except Exception as e:\r\n log.exception(f\"Error saving preset file {self.current_editing_preset_path}: {e}\")\r\n QMessageBox.critical(self, \"Save Error\", f\"Could not save preset file:\\n{self.current_editing_preset_path}\\n\\nError: {e}\")\r\n return False\r\n\r\n def _save_preset_as(self) -> bool:\r\n \"\"\"Saves the current editor content to a new file chosen by the user.\"\"\"\r\n log.debug(\"Save As action triggered.\")\r\n try:\r\n preset_data = self._gather_editor_data()\r\n new_preset_name = preset_data.get(\"preset_name\")\r\n if not new_preset_name: QMessageBox.warning(self, \"Save As Error\", \"Preset Name cannot be empty.\"); return False\r\n if not preset_data.get(\"supplier_name\"): QMessageBox.warning(self, \"Save As Error\", \"Supplier Name cannot be empty.\"); return False\r\n content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False)\r\n suggested_name = f\"{new_preset_name}.json\"\r\n default_path = PRESETS_DIR / suggested_name\r\n file_path_str, _ = QFileDialog.getSaveFileName(self, \"Save Preset As\", str(default_path), \"JSON Files (*.json);;All Files (*)\")\r\n if not file_path_str: log.debug(\"Save As cancelled by user.\"); return False\r\n save_path = Path(file_path_str)\r\n if save_path.suffix.lower() != \".json\": save_path = save_path.with_suffix(\".json\")\r\n if save_path.exists() and save_path != self.current_editing_preset_path:\r\n reply = QMessageBox.warning(self, \"Confirm Overwrite\", f\"Preset '{save_path.name}' already exists. Overwrite?\", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)\r\n if reply == QMessageBox.StandardButton.No: log.debug(\"Save As overwrite cancelled.\"); return False\r\n log.info(f\"Saving preset as: {save_path.name}\")\r\n with open(save_path, 'w', encoding='utf-8') as f: f.write(content_to_save)\r\n self.current_editing_preset_path = save_path # Update current path\r\n self.editor_unsaved_changes = False\r\n self.editor_save_button.setEnabled(False)\r\n self.setWindowTitle(f\"Asset Processor Tool - {save_path.name}\")\r\n self.presets_changed_signal.emit() # Signal change\r\n log.info(\"Preset saved successfully (Save As).\")\r\n # Refresh lists and select the new item\r\n self.populate_presets()\r\n return True\r\n except Exception as e:\r\n log.exception(f\"Error saving preset file (Save As): {e}\")\r\n QMessageBox.critical(self, \"Save Error\", f\"Could not save preset file.\\n\\nError: {e}\")\r\n return False\r\n\r\n def _new_preset(self):\r\n \"\"\"Clears the editor and loads data from _template.json.\"\"\"\r\n log.debug(\"New Preset action triggered.\")\r\n if self._check_editor_unsaved_changes(): return\r\n self._clear_editor()\r\n if TEMPLATE_PATH.is_file():\r\n log.info(\"Loading new preset from _template.json\")\r\n try:\r\n with open(TEMPLATE_PATH, 'r', encoding='utf-8') as f: template_data = json.load(f)\r\n self._populate_editor_from_data(template_data)\r\n # Override specific fields for a new preset\r\n self.editor_preset_name.setText(\"NewPreset\")\r\n self.setWindowTitle(\"Asset Processor Tool - New Preset*\")\r\n except Exception as e:\r\n log.exception(f\"Error loading template preset file {TEMPLATE_PATH}: {e}\")\r\n QMessageBox.critical(self, \"Error\", f\"Could not load template preset file:\\n{TEMPLATE_PATH}\\n\\nError: {e}\")\r\n self._clear_editor()\r\n self.setWindowTitle(\"Asset Processor Tool - New Preset*\")\r\n else:\r\n log.warning(\"Presets/_template.json not found. Creating empty preset.\")\r\n self.setWindowTitle(\"Asset Processor Tool - New Preset*\")\r\n self.editor_preset_name.setText(\"NewPreset\")\r\n self.editor_supplier_name.setText(\"MySupplier\")\r\n self._set_editor_enabled(True)\r\n self.editor_unsaved_changes = True\r\n self.editor_save_button.setEnabled(True)\r\n\r\n def _delete_selected_preset(self):\r\n \"\"\"Deletes the currently selected preset file from the editor list after confirmation.\"\"\"\r\n current_item = self.editor_preset_list.currentItem()\r\n if not current_item: QMessageBox.information(self, \"Delete Preset\", \"Please select a preset from the list to delete.\"); return\r\n preset_path = current_item.data(Qt.ItemDataRole.UserRole)\r\n preset_name = preset_path.stem\r\n reply = QMessageBox.warning(self, \"Confirm Delete\", f\"Are you sure you want to permanently delete the preset '{preset_name}'?\", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)\r\n if reply == QMessageBox.StandardButton.Yes:\r\n log.info(f\"Deleting preset: {preset_path.name}\")\r\n try:\r\n preset_path.unlink()\r\n log.info(\"Preset deleted successfully.\")\r\n if self.current_editing_preset_path == preset_path: self._clear_editor()\r\n self.presets_changed_signal.emit() # Signal change\r\n # Refresh lists\r\n self.populate_presets()\r\n except Exception as e:\r\n log.exception(f\"Error deleting preset file {preset_path}: {e}\")\r\n QMessageBox.critical(self, \"Delete Error\", f\"Could not delete preset file:\\n{preset_path}\\n\\nError: {e}\")\r\n\r\n # --- Overridden Close Event ---\r\n def closeEvent(self, event):\r\n \"\"\"Overrides close event to check for unsaved changes in the editor.\"\"\"\r\n if self._check_editor_unsaved_changes():\r\n event.ignore() # Ignore close event if user cancels\r\n else:\r\n event.accept() # Accept close event\r\n\r\n# --- Main Execution ---\r\ndef run_gui():\r\n \"\"\"Initializes and runs the Qt application.\"\"\"\r\n app = QApplication(sys.argv)\r\n window = MainWindow()\r\n window.show()\r\n sys.exit(app.exec())\r\n\r\nif __name__ == \"__main__\":\r\n run_gui()"
}
]
}