import sys import os import json import logging from pathlib import Path from functools import partial from PySide6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QListWidget, QPushButton, QLabel, QTabWidget, QLineEdit, QTextEdit, QSpinBox, QTableWidget, QGroupBox, QFormLayout, QHeaderView, QAbstractItemView, QListWidgetItem, QTableWidgetItem, QMessageBox, QFileDialog, QInputDialog, QSizePolicy ) from PySide6.QtCore import Qt, Signal, QObject, Slot from PySide6.QtGui import QAction # Keep QAction if needed for context menus within editor later # --- Constants --- # Assuming project root is parent of the directory containing this file script_dir = Path(__file__).parent project_root = script_dir.parent PRESETS_DIR = project_root / "Presets" # Corrected path TEMPLATE_PATH = PRESETS_DIR / "_template.json" log = logging.getLogger(__name__) # --- Preset Editor Widget --- class PresetEditorWidget(QWidget): """ Widget dedicated to managing and editing presets. Contains the preset list, editor tabs, and save/load functionality. """ # Signal emitted when presets list changes (saved, deleted, new) presets_changed_signal = Signal() # Signal emitted when the selected preset (or LLM/Placeholder) changes # Emits: mode ("preset", "llm", "placeholder"), preset_name (str or None) preset_selection_changed_signal = Signal(str, str) def __init__(self, parent=None): super().__init__(parent) # --- Internal State --- self._last_valid_preset_name = None # Store the name of the last valid preset loaded self.current_editing_preset_path = None self.editor_unsaved_changes = False self._is_loading_editor = False # Flag to prevent signals during load # --- UI Setup --- self._init_ui() # --- Initial State --- self._clear_editor() # Clear/disable editor fields initially self._set_editor_enabled(False) # Disable editor initially self.populate_presets() # Populate preset list # --- Connect Editor Signals --- self._connect_editor_change_signals() def _init_ui(self): """Initializes the UI elements for the preset editor.""" editor_layout = QVBoxLayout(self) editor_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins # Preset List and Controls list_layout = QVBoxLayout() list_layout.addWidget(QLabel("Presets:")) self.editor_preset_list = QListWidget() self.editor_preset_list.currentItemChanged.connect(self._load_selected_preset_for_editing) list_layout.addWidget(self.editor_preset_list) list_button_layout = QHBoxLayout() self.editor_new_button = QPushButton("New") self.editor_delete_button = QPushButton("Delete") self.editor_new_button.clicked.connect(self._new_preset) self.editor_delete_button.clicked.connect(self._delete_selected_preset) list_button_layout.addWidget(self.editor_new_button) list_button_layout.addWidget(self.editor_delete_button) list_layout.addLayout(list_button_layout) editor_layout.addLayout(list_layout, 1) # Allow list to stretch # Editor Tabs self.editor_tab_widget = QTabWidget() self.editor_tab_general_naming = QWidget() self.editor_tab_mapping_rules = QWidget() self.editor_tab_widget.addTab(self.editor_tab_general_naming, "General & Naming") self.editor_tab_widget.addTab(self.editor_tab_mapping_rules, "Mapping & Rules") self._create_editor_general_tab() self._create_editor_mapping_tab() editor_layout.addWidget(self.editor_tab_widget, 3) # Allow tabs to stretch more # Save Buttons save_button_layout = QHBoxLayout() self.editor_save_button = QPushButton("Save") self.editor_save_as_button = QPushButton("Save As...") self.editor_save_button.setEnabled(False) # Disabled initially self.editor_save_button.clicked.connect(self._save_current_preset) self.editor_save_as_button.clicked.connect(self._save_preset_as) save_button_layout.addStretch() save_button_layout.addWidget(self.editor_save_button) save_button_layout.addWidget(self.editor_save_as_button) editor_layout.addLayout(save_button_layout) def _create_editor_general_tab(self): """Creates the widgets and layout for the 'General & Naming' editor tab.""" layout = QVBoxLayout(self.editor_tab_general_naming) form_layout = QFormLayout() form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow) # Basic Info self.editor_preset_name = QLineEdit() self.editor_supplier_name = QLineEdit() self.editor_notes = QTextEdit() self.editor_notes.setAcceptRichText(False) self.editor_notes.setFixedHeight(60) form_layout.addRow("Preset Name:", self.editor_preset_name) form_layout.addRow("Supplier Name:", self.editor_supplier_name) form_layout.addRow("Notes:", self.editor_notes) layout.addLayout(form_layout) # Source Naming Group naming_group = QGroupBox("Source File Naming Rules") naming_layout_outer = QVBoxLayout(naming_group) naming_layout_form = QFormLayout() self.editor_separator = QLineEdit() self.editor_separator.setMaxLength(1) self.editor_spin_base_name_idx = QSpinBox() self.editor_spin_base_name_idx.setMinimum(-1) self.editor_spin_map_type_idx = QSpinBox() self.editor_spin_map_type_idx.setMinimum(-1) naming_layout_form.addRow("Separator:", self.editor_separator) naming_layout_form.addRow("Base Name Index:", self.editor_spin_base_name_idx) naming_layout_form.addRow("Map Type Index:", self.editor_spin_map_type_idx) naming_layout_outer.addLayout(naming_layout_form) # Gloss Keywords List self._setup_list_widget_with_controls(naming_layout_outer, "Glossiness Keywords", "editor_list_gloss_keywords") # Bit Depth Variants Table self._setup_table_widget_with_controls(naming_layout_outer, "16-bit Variant Patterns", "editor_table_bit_depth_variants", ["Map Type", "Pattern"]) self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) layout.addWidget(naming_group) # Extra Files Group self._setup_list_widget_with_controls(layout, "Move to 'Extra' Folder Patterns", "editor_list_extra_patterns") layout.addStretch(1) def _create_editor_mapping_tab(self): """Creates the widgets and layout for the 'Mapping & Rules' editor tab.""" layout = QVBoxLayout(self.editor_tab_mapping_rules) # Map Type Mapping Group self._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.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) self.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Category Rules Group category_group = QGroupBox("Asset Category Rules") category_layout = QVBoxLayout(category_group) self._setup_list_widget_with_controls(category_layout, "Model File Patterns", "editor_list_model_patterns") self._setup_list_widget_with_controls(category_layout, "Decal Keywords", "editor_list_decal_keywords") layout.addWidget(category_group) # Archetype Rules Group self._setup_table_widget_with_controls(layout, "Archetype Rules", "editor_table_archetype_rules", ["Archetype Name", "Match Any (comma-sep)", "Match All (comma-sep)"]) self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) layout.addStretch(1) # --- Helper Functions for UI Setup (Moved into class) --- def _setup_list_widget_with_controls(self, parent_layout, label_text, attribute_name): """Adds a QListWidget with Add/Remove buttons to a layout.""" list_widget = QListWidget() list_widget.setAlternatingRowColors(True) list_widget.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed) setattr(self, attribute_name, list_widget) # Store list widget on the instance add_button = QPushButton("+") remove_button = QPushButton("-") add_button.setFixedWidth(30) remove_button.setFixedWidth(30) button_layout = QVBoxLayout() button_layout.addWidget(add_button) button_layout.addWidget(remove_button) button_layout.addStretch() list_layout = QHBoxLayout() list_layout.addWidget(list_widget) list_layout.addLayout(button_layout) group_box = QGroupBox(label_text) group_box_layout = QVBoxLayout(group_box) group_box_layout.addLayout(list_layout) parent_layout.addWidget(group_box) # Connections add_button.clicked.connect(partial(self._editor_add_list_item, list_widget)) remove_button.clicked.connect(partial(self._editor_remove_list_item, list_widget)) list_widget.itemChanged.connect(self._mark_editor_unsaved) # Mark unsaved on item edit def _setup_table_widget_with_controls(self, parent_layout, label_text, attribute_name, columns): """Adds a QTableWidget with Add/Remove buttons to a layout.""" table_widget = QTableWidget() table_widget.setColumnCount(len(columns)) table_widget.setHorizontalHeaderLabels(columns) table_widget.setAlternatingRowColors(True) setattr(self, attribute_name, table_widget) # Store table widget add_button = QPushButton("+ Row") remove_button = QPushButton("- Row") button_layout = QHBoxLayout() button_layout.addStretch() button_layout.addWidget(add_button) button_layout.addWidget(remove_button) group_box = QGroupBox(label_text) group_box_layout = QVBoxLayout(group_box) group_box_layout.addWidget(table_widget) group_box_layout.addLayout(button_layout) parent_layout.addWidget(group_box) # Connections add_button.clicked.connect(partial(self._editor_add_table_row, table_widget)) remove_button.clicked.connect(partial(self._editor_remove_table_row, table_widget)) table_widget.itemChanged.connect(self._mark_editor_unsaved) # Mark unsaved on item edit # --- Preset Population and Handling --- def populate_presets(self): """Scans presets dir and populates the editor list.""" log.debug("Populating preset list in PresetEditorWidget...") current_list_item = self.editor_preset_list.currentItem() current_list_selection_text = current_list_item.text() if current_list_item else None self.editor_preset_list.clear() log.debug("Preset list cleared.") # Add the "Select a Preset" placeholder item placeholder_item = QListWidgetItem("--- Select a Preset ---") placeholder_item.setFlags(placeholder_item.flags() & ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEditable) placeholder_item.setData(Qt.ItemDataRole.UserRole, "__PLACEHOLDER__") self.editor_preset_list.addItem(placeholder_item) log.debug("Added '--- Select a Preset ---' placeholder item.") # Add LLM Option llm_item = QListWidgetItem("- LLM Interpretation -") llm_item.setData(Qt.ItemDataRole.UserRole, "__LLM__") # Special identifier self.editor_preset_list.addItem(llm_item) log.debug("Added '- LLM Interpretation -' item.") if not PRESETS_DIR.is_dir(): msg = f"Error: Presets directory not found at {PRESETS_DIR}" log.error(msg) # Consider emitting a status signal to MainWindow? return presets = sorted([f for f in PRESETS_DIR.glob("*.json") if f.is_file() and not f.name.startswith('_')]) if not presets: msg = "Warning: No presets found in presets directory." log.warning(msg) else: for preset_path in presets: item = QListWidgetItem(preset_path.stem) item.setData(Qt.ItemDataRole.UserRole, preset_path) # Store full path self.editor_preset_list.addItem(item) log.info(f"Loaded {len(presets)} presets into editor list.") # Select the "Select a Preset" item by default log.debug("Preset list populated. Selecting '--- Select a Preset ---' item.") self.editor_preset_list.setCurrentItem(placeholder_item) # Select the placeholder item # --- Preset Editor Methods --- def _editor_add_list_item(self, list_widget: QListWidget): """Adds an editable item to the specified list widget in the editor.""" text, ok = QInputDialog.getText(self, f"Add Item", "Enter value:") if ok and text: item = QListWidgetItem(text) list_widget.addItem(item) self._mark_editor_unsaved() def _editor_remove_list_item(self, list_widget: QListWidget): """Removes the selected item from the specified list widget in the editor.""" selected_items = list_widget.selectedItems() if not selected_items: return for item in selected_items: list_widget.takeItem(list_widget.row(item)) self._mark_editor_unsaved() def _editor_add_table_row(self, table_widget: QTableWidget): """Adds an empty row to the specified table widget in the editor.""" row_count = table_widget.rowCount() table_widget.insertRow(row_count) for col in range(table_widget.columnCount()): table_widget.setItem(row_count, col, QTableWidgetItem("")) self._mark_editor_unsaved() def _editor_remove_table_row(self, table_widget: QTableWidget): """Removes the selected row(s) from the specified table widget in the editor.""" selected_rows = sorted(list(set(index.row() for index in table_widget.selectedIndexes())), reverse=True) if not selected_rows: if table_widget.rowCount() > 0: selected_rows = [table_widget.rowCount() - 1] else: return for row in selected_rows: table_widget.removeRow(row) self._mark_editor_unsaved() def _mark_editor_unsaved(self): """Marks changes in the editor panel as unsaved.""" if self._is_loading_editor: return self.editor_unsaved_changes = True self.editor_save_button.setEnabled(True) # Update window title (handled by MainWindow) - maybe emit signal? # preset_name = Path(self.current_editing_preset_path).name if self.current_editing_preset_path else 'New Preset' # self.window().setWindowTitle(f"Asset Processor Tool - {preset_name}*") # Access parent window def _connect_editor_change_signals(self): """Connect signals from all editor widgets to mark_editor_unsaved.""" self.editor_preset_name.textChanged.connect(self._mark_editor_unsaved) self.editor_supplier_name.textChanged.connect(self._mark_editor_unsaved) self.editor_notes.textChanged.connect(self._mark_editor_unsaved) self.editor_separator.textChanged.connect(self._mark_editor_unsaved) self.editor_spin_base_name_idx.valueChanged.connect(self._mark_editor_unsaved) self.editor_spin_map_type_idx.valueChanged.connect(self._mark_editor_unsaved) # List/Table widgets are connected via helper functions def check_unsaved_changes(self) -> bool: """ Checks for unsaved changes in the editor and prompts the user. Returns True if the calling action should be cancelled. (Called by MainWindow's closeEvent or before loading a new preset). """ if not self.editor_unsaved_changes: return False # No unsaved changes, proceed reply = QMessageBox.question(self, "Unsaved Preset Changes", # Use self as parent "You have unsaved changes in the preset editor. Discard them?", QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel, QMessageBox.StandardButton.Cancel) if reply == QMessageBox.StandardButton.Save: save_successful = self._save_current_preset() return not save_successful # Return True (cancel) if save fails elif reply == QMessageBox.StandardButton.Discard: return False # Discarded, proceed else: # Cancelled return True # Cancel the original action def _set_editor_enabled(self, enabled: bool): """Enables or disables all editor widgets.""" self.editor_tab_widget.setEnabled(enabled) self.editor_save_button.setEnabled(enabled and self.editor_unsaved_changes) self.editor_save_as_button.setEnabled(enabled) # Save As is always possible if editor is enabled def _clear_editor(self): """Clears the editor fields and resets state.""" self._is_loading_editor = True try: self.editor_preset_name.clear() self.editor_supplier_name.clear() self.editor_notes.clear() self.editor_separator.clear() self.editor_spin_base_name_idx.setValue(0) self.editor_spin_map_type_idx.setValue(1) self.editor_list_gloss_keywords.clear() self.editor_table_bit_depth_variants.setRowCount(0) self.editor_list_extra_patterns.clear() self.editor_table_map_type_mapping.setRowCount(0) self.editor_list_model_patterns.clear() self.editor_list_decal_keywords.clear() self.editor_table_archetype_rules.setRowCount(0) self.current_editing_preset_path = None self.editor_unsaved_changes = False self.editor_save_button.setEnabled(False) # self.window().setWindowTitle("Asset Processor Tool") # Reset window title (handled by MainWindow) self._set_editor_enabled(False) finally: self._is_loading_editor = False def _populate_editor_from_data(self, preset_data: dict): """Helper method to populate editor UI widgets from a preset data dictionary.""" self._is_loading_editor = True try: self.editor_preset_name.setText(preset_data.get("preset_name", "")) self.editor_supplier_name.setText(preset_data.get("supplier_name", "")) self.editor_notes.setText(preset_data.get("notes", "")) naming_data = preset_data.get("source_naming", {}) self.editor_separator.setText(naming_data.get("separator", "_")) indices = naming_data.get("part_indices", {}) self.editor_spin_base_name_idx.setValue(indices.get("base_name", 0)) self.editor_spin_map_type_idx.setValue(indices.get("map_type", 1)) self.editor_list_gloss_keywords.clear() self.editor_list_gloss_keywords.addItems(naming_data.get("glossiness_keywords", [])) self.editor_table_bit_depth_variants.setRowCount(0) bit_depth_vars = naming_data.get("bit_depth_variants", {}) for i, (map_type, pattern) in enumerate(bit_depth_vars.items()): self.editor_table_bit_depth_variants.insertRow(i) self.editor_table_bit_depth_variants.setItem(i, 0, QTableWidgetItem(map_type)) self.editor_table_bit_depth_variants.setItem(i, 1, QTableWidgetItem(pattern)) self.editor_list_extra_patterns.clear() self.editor_list_extra_patterns.addItems(preset_data.get("move_to_extra_patterns", [])) self.editor_table_map_type_mapping.setRowCount(0) map_mappings = preset_data.get("map_type_mapping", []) for i, mapping_dict in enumerate(map_mappings): if isinstance(mapping_dict, dict) and "target_type" in mapping_dict and "keywords" in mapping_dict: std_type = mapping_dict["target_type"] keywords = mapping_dict["keywords"] self.editor_table_map_type_mapping.insertRow(i) self.editor_table_map_type_mapping.setItem(i, 0, QTableWidgetItem(std_type)) keywords_str = [str(k) for k in keywords if isinstance(k, str)] self.editor_table_map_type_mapping.setItem(i, 1, QTableWidgetItem(", ".join(keywords_str))) else: log.warning(f"Skipping invalid map_type_mapping item during editor population: {mapping_dict}") category_rules = preset_data.get("asset_category_rules", {}) self.editor_list_model_patterns.clear() self.editor_list_model_patterns.addItems(category_rules.get("model_patterns", [])) self.editor_list_decal_keywords.clear() self.editor_list_decal_keywords.addItems(category_rules.get("decal_keywords", [])) # Archetype rules population (assuming table exists) self.editor_table_archetype_rules.setRowCount(0) arch_rules_data = preset_data.get("archetype_rules", []) for i, rule_entry in enumerate(arch_rules_data): # Handle both list and dict format for backward compatibility? Assuming list for now. if isinstance(rule_entry, (list, tuple)) and len(rule_entry) == 2: name, conditions = rule_entry if isinstance(conditions, dict): match_any = conditions.get("match_any", []) match_all = conditions.get("match_all", []) self.editor_table_archetype_rules.insertRow(i) self.editor_table_archetype_rules.setItem(i, 0, QTableWidgetItem(str(name))) self.editor_table_archetype_rules.setItem(i, 1, QTableWidgetItem(", ".join(map(str, match_any)))) self.editor_table_archetype_rules.setItem(i, 2, QTableWidgetItem(", ".join(map(str, match_all)))) else: log.warning(f"Skipping invalid archetype rule condition format: {conditions}") else: log.warning(f"Skipping invalid archetype rule format: {rule_entry}") finally: self._is_loading_editor = False def _load_preset_for_editing(self, file_path: Path): """Loads the content of the selected preset file into the editor widgets.""" if not file_path or not file_path.is_file(): self._clear_editor() return log.info(f"Loading preset into editor: {file_path.name}") try: with open(file_path, 'r', encoding='utf-8') as f: preset_data = json.load(f) self._populate_editor_from_data(preset_data) self._set_editor_enabled(True) self.current_editing_preset_path = file_path self.editor_unsaved_changes = False self.editor_save_button.setEnabled(False) # self.window().setWindowTitle(f"Asset Processor Tool - {file_path.name}") # Handled by MainWindow log.info(f"Preset '{file_path.name}' loaded into editor.") except json.JSONDecodeError as json_err: log.error(f"Invalid JSON in {file_path.name}: {json_err}") QMessageBox.warning(self, "Load Error", f"Failed to load preset '{file_path.name}'.\nInvalid JSON structure:\n{json_err}") self._clear_editor() except Exception as e: log.exception(f"Error loading preset file {file_path}: {e}") QMessageBox.critical(self, "Error", f"Could not load preset file:\n{file_path}\n\nError: {e}") self._clear_editor() @Slot(QListWidgetItem, QListWidgetItem) def _load_selected_preset_for_editing(self, current_item: QListWidgetItem, previous_item: QListWidgetItem): """Loads the preset currently selected in the editor list and emits selection change signal.""" log.debug(f"PresetEditor: currentItemChanged signal triggered. current: {current_item.text() if current_item else 'None'}") mode = "placeholder" preset_name = None # Check for unsaved changes before proceeding if self.check_unsaved_changes(): # If user cancels, revert selection if previous_item: log.debug("Unsaved changes check cancelled. Reverting selection.") self.editor_preset_list.blockSignals(True) self.editor_preset_list.setCurrentItem(previous_item) self.editor_preset_list.blockSignals(False) return # Stop processing # Determine mode and preset name based on selection if current_item: item_data = current_item.data(Qt.ItemDataRole.UserRole) if item_data == "__PLACEHOLDER__": log.debug("Placeholder item selected.") self._clear_editor() self._set_editor_enabled(False) mode = "placeholder" self._last_valid_preset_name = None # Clear last valid name elif item_data == "__LLM__": log.debug("LLM Interpretation item selected.") self._clear_editor() self._set_editor_enabled(False) mode = "llm" # Keep _last_valid_preset_name as it was elif isinstance(item_data, Path): log.debug(f"Loading preset for editing: {current_item.text()}") preset_path = item_data self._load_preset_for_editing(preset_path) self._last_valid_preset_name = preset_path.stem # Store the name mode = "preset" preset_name = self._last_valid_preset_name else: log.error(f"Invalid data type for preset path: {type(item_data)}. Clearing editor.") self._clear_editor() self._set_editor_enabled(False) mode = "placeholder" # Treat as placeholder on error self._last_valid_preset_name = None else: log.debug("No preset selected. Clearing editor.") self._clear_editor() self._set_editor_enabled(False) mode = "placeholder" self._last_valid_preset_name = None # Emit the signal regardless of what was selected log.debug(f"Emitting preset_selection_changed_signal: mode='{mode}', preset_name='{preset_name}'") self.preset_selection_changed_signal.emit(mode, preset_name) def _gather_editor_data(self) -> dict: """Gathers data from all editor UI widgets and returns a dictionary.""" preset_data = {} preset_data["preset_name"] = self.editor_preset_name.text().strip() preset_data["supplier_name"] = self.editor_supplier_name.text().strip() preset_data["notes"] = self.editor_notes.toPlainText().strip() naming_data = {} naming_data["separator"] = self.editor_separator.text() naming_data["part_indices"] = { "base_name": self.editor_spin_base_name_idx.value(), "map_type": self.editor_spin_map_type_idx.value() } naming_data["glossiness_keywords"] = [self.editor_list_gloss_keywords.item(i).text() for i in range(self.editor_list_gloss_keywords.count())] 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() 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)} preset_data["source_naming"] = naming_data preset_data["move_to_extra_patterns"] = [self.editor_list_extra_patterns.item(i).text() for i in range(self.editor_list_extra_patterns.count())] map_mappings = [] for r in range(self.editor_table_map_type_mapping.rowCount()): type_item = self.editor_table_map_type_mapping.item(r, 0) keywords_item = self.editor_table_map_type_mapping.item(r, 1) if type_item and type_item.text() and keywords_item and keywords_item.text(): target_type = type_item.text().strip() keywords = [k.strip() for k in keywords_item.text().split(',') if k.strip()] if target_type and keywords: map_mappings.append({"target_type": target_type, "keywords": keywords}) else: log.warning(f"Skipping row {r} in map type mapping table due to empty target type or keywords.") else: log.warning(f"Skipping row {r} in map type mapping table due to missing items.") preset_data["map_type_mapping"] = map_mappings category_rules = {} category_rules["model_patterns"] = [self.editor_list_model_patterns.item(i).text() for i in range(self.editor_list_model_patterns.count())] category_rules["decal_keywords"] = [self.editor_list_decal_keywords.item(i).text() for i in range(self.editor_list_decal_keywords.count())] preset_data["asset_category_rules"] = category_rules arch_rules = [] for r in range(self.editor_table_archetype_rules.rowCount()): name_item = self.editor_table_archetype_rules.item(r, 0) any_item = self.editor_table_archetype_rules.item(r, 1) all_item = self.editor_table_archetype_rules.item(r, 2) if name_item and name_item.text() and any_item and all_item: # Check name has text match_any = [k.strip() for k in any_item.text().split(',') if k.strip()] match_all = [k.strip() for k in all_item.text().split(',') if k.strip()] # Only add if name is present and at least one condition list is non-empty? Or allow empty conditions? # Let's allow empty conditions for now. arch_rules.append([name_item.text().strip(), {"match_any": match_any, "match_all": match_all}]) else: log.warning(f"Skipping row {r} in archetype rules table due to missing items or empty name.") preset_data["archetype_rules"] = arch_rules return preset_data def _save_current_preset(self) -> bool: """Saves the current editor content to the currently loaded file path.""" if not self.current_editing_preset_path: return self._save_preset_as() log.info(f"Saving preset: {self.current_editing_preset_path.name}") try: preset_data = self._gather_editor_data() if not preset_data.get("preset_name"): QMessageBox.warning(self, "Save Error", "Preset Name cannot be empty."); return False if not preset_data.get("supplier_name"): QMessageBox.warning(self, "Save Error", "Supplier Name cannot be empty."); return False content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False) with open(self.current_editing_preset_path, 'w', encoding='utf-8') as f: f.write(content_to_save) self.editor_unsaved_changes = False self.editor_save_button.setEnabled(False) # self.window().setWindowTitle(f"Asset Processor Tool - {self.current_editing_preset_path.name}") # Handled by MainWindow self.presets_changed_signal.emit() # Signal that presets changed log.info("Preset saved successfully.") # Refresh list within the editor self.populate_presets() # Reselect the saved item items = self.editor_preset_list.findItems(self.current_editing_preset_path.stem, Qt.MatchFlag.MatchExactly) if items: self.editor_preset_list.setCurrentItem(items[0]) return True except Exception as e: log.exception(f"Error saving preset file {self.current_editing_preset_path}: {e}") QMessageBox.critical(self, "Save Error", f"Could not save preset file:\n{self.current_editing_preset_path}\n\nError: {e}") return False def _save_preset_as(self) -> bool: """Saves the current editor content to a new file chosen by the user.""" log.debug("Save As action triggered.") try: preset_data = self._gather_editor_data() new_preset_name = preset_data.get("preset_name") if not new_preset_name: QMessageBox.warning(self, "Save As Error", "Preset Name cannot be empty."); return False if not preset_data.get("supplier_name"): QMessageBox.warning(self, "Save As Error", "Supplier Name cannot be empty."); return False content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False) suggested_name = f"{new_preset_name}.json" default_path = PRESETS_DIR / suggested_name file_path_str, _ = QFileDialog.getSaveFileName(self, "Save Preset As", str(default_path), "JSON Files (*.json);;All Files (*)") if not file_path_str: log.debug("Save As cancelled by user."); return False save_path = Path(file_path_str) if save_path.suffix.lower() != ".json": save_path = save_path.with_suffix(".json") if save_path.exists() and save_path != self.current_editing_preset_path: reply = QMessageBox.warning(self, "Confirm Overwrite", f"Preset '{save_path.name}' already exists. Overwrite?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No) if reply == QMessageBox.StandardButton.No: log.debug("Save As overwrite cancelled."); return False log.info(f"Saving preset as: {save_path.name}") with open(save_path, 'w', encoding='utf-8') as f: f.write(content_to_save) self.current_editing_preset_path = save_path # Update current path self.editor_unsaved_changes = False self.editor_save_button.setEnabled(False) # self.window().setWindowTitle(f"Asset Processor Tool - {save_path.name}") # Handled by MainWindow self.presets_changed_signal.emit() # Signal change log.info("Preset saved successfully (Save As).") # Refresh list and select the new item self.populate_presets() items = self.editor_preset_list.findItems(save_path.stem, Qt.MatchFlag.MatchExactly) if items: self.editor_preset_list.setCurrentItem(items[0]) return True except Exception as e: log.exception(f"Error saving preset file (Save As): {e}") QMessageBox.critical(self, "Save Error", f"Could not save preset file.\n\nError: {e}") return False def _new_preset(self): """Clears the editor and loads data from _template.json.""" log.debug("New Preset action triggered.") if self.check_unsaved_changes(): return # Check unsaved changes first self._clear_editor() if TEMPLATE_PATH.is_file(): log.info("Loading new preset from _template.json") try: with open(TEMPLATE_PATH, 'r', encoding='utf-8') as f: template_data = json.load(f) self._populate_editor_from_data(template_data) # Override specific fields for a new preset self.editor_preset_name.setText("NewPreset") # self.window().setWindowTitle("Asset Processor Tool - New Preset*") # Handled by MainWindow except Exception as e: log.exception(f"Error loading template preset file {TEMPLATE_PATH}: {e}") QMessageBox.critical(self, "Error", f"Could not load template preset file:\n{TEMPLATE_PATH}\n\nError: {e}") self._clear_editor() # self.window().setWindowTitle("Asset Processor Tool - New Preset*") # Handled by MainWindow self.editor_supplier_name.setText("MySupplier") # Set a default supplier name else: log.warning("Presets/_template.json not found. Creating empty preset.") # self.window().setWindowTitle("Asset Processor Tool - New Preset*") # Handled by MainWindow self.editor_preset_name.setText("NewPreset") self.editor_supplier_name.setText("MySupplier") # Set a default supplier name self._set_editor_enabled(True) self.editor_unsaved_changes = True self.editor_save_button.setEnabled(True) # Select the placeholder item to avoid auto-loading the "NewPreset" placeholder_item = self.editor_preset_list.findItems("--- Select a Preset ---", Qt.MatchFlag.MatchExactly) if placeholder_item: self.editor_preset_list.setCurrentItem(placeholder_item[0]) # Emit selection change for the new state (effectively placeholder) self.preset_selection_changed_signal.emit("placeholder", None) def _delete_selected_preset(self): """Deletes the currently selected preset file from the editor list after confirmation.""" current_item = self.editor_preset_list.currentItem() if not current_item: QMessageBox.information(self, "Delete Preset", "Please select a preset from the list to delete."); return item_data = current_item.data(Qt.ItemDataRole.UserRole) # Ensure it's a real preset path before attempting delete if not isinstance(item_data, Path): QMessageBox.information(self, "Delete Preset", "Cannot delete placeholder or LLM option.") return preset_path = item_data preset_name = preset_path.stem 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) if reply == QMessageBox.StandardButton.Yes: log.info(f"Deleting preset: {preset_path.name}") try: preset_path.unlink() log.info("Preset deleted successfully.") if self.current_editing_preset_path == preset_path: self._clear_editor() self.presets_changed_signal.emit() # Signal change # Refresh list self.populate_presets() except Exception as e: log.exception(f"Error deleting preset file {preset_path}: {e}") QMessageBox.critical(self, "Delete Error", f"Could not delete preset file:\n{preset_path}\n\nError: {e}") # --- Public Access Methods for MainWindow --- def get_selected_preset_mode(self) -> tuple[str, str | None]: """ Returns the current selection mode and preset name (if applicable). Returns: tuple(mode_string, preset_name_string_or_None) mode_string can be "preset", "llm", "placeholder" """ current_item = self.editor_preset_list.currentItem() if current_item: item_data = current_item.data(Qt.ItemDataRole.UserRole) if item_data == "__PLACEHOLDER__": return "placeholder", None elif item_data == "__LLM__": return "llm", None elif isinstance(item_data, Path): return "preset", item_data.stem return "placeholder", None # Default or if no item selected def get_last_valid_preset_name(self) -> str | None: """ Returns the name (stem) of the last valid preset that was loaded. Used by delegates to populate dropdowns based on the original context. """ return self._last_valid_preset_name # --- Slots for MainWindow Interaction ---