783 lines
42 KiB
Python
783 lines
42 KiB
Python
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, QComboBox,
|
|
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"
|
|
TEMPLATE_PATH = PRESETS_DIR / "_template.json"
|
|
APP_SETTINGS_PATH_LOCAL = project_root / "config" / "app_settings.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._ftd_keys = self._get_file_type_definition_keys()
|
|
self._clear_editor()
|
|
self._set_editor_enabled(False)
|
|
self.populate_presets()
|
|
|
|
# --- Connect Editor Signals ---
|
|
self._connect_editor_change_signals()
|
|
|
|
def _get_file_type_definition_keys(self) -> list[str]:
|
|
"""Loads FILE_TYPE_DEFINITIONS keys from app_settings.json."""
|
|
keys = []
|
|
try:
|
|
if APP_SETTINGS_PATH_LOCAL.is_file():
|
|
with open(APP_SETTINGS_PATH_LOCAL, 'r', encoding='utf-8') as f:
|
|
settings = json.load(f)
|
|
ftd = settings.get("FILE_TYPE_DEFINITIONS", {})
|
|
keys = list(ftd.keys())
|
|
log.debug(f"Successfully loaded {len(keys)} FILE_TYPE_DEFINITIONS keys.")
|
|
else:
|
|
log.error(f"app_settings.json not found at {APP_SETTINGS_PATH_LOCAL} for PresetEditorWidget.")
|
|
except json.JSONDecodeError as e:
|
|
log.error(f"Failed to parse app_settings.json in PresetEditorWidget: {e}")
|
|
except Exception as e:
|
|
log.error(f"Error loading FILE_TYPE_DEFINITIONS keys in PresetEditorWidget: {e}")
|
|
return keys
|
|
|
|
def _init_ui(self):
|
|
"""Initializes the UI elements for the preset editor."""
|
|
main_layout = QVBoxLayout(self)
|
|
main_layout.setContentsMargins(0, 0, 0, 0) # Let containers manage margins
|
|
main_layout.setSpacing(0) # No space between selector and editor containers
|
|
|
|
# Preset List and Controls
|
|
self.selector_container = QWidget()
|
|
selector_layout = QVBoxLayout(self.selector_container)
|
|
selector_layout.setContentsMargins(5, 5, 5, 5) # Margins for selector area
|
|
|
|
selector_layout.addWidget(QLabel("Presets:"))
|
|
self.editor_preset_list = QListWidget()
|
|
self.editor_preset_list.currentItemChanged.connect(self._load_selected_preset_for_editing)
|
|
selector_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)
|
|
selector_layout.addLayout(list_button_layout)
|
|
main_layout.addWidget(self.selector_container)
|
|
|
|
# Editor Tabs
|
|
self.json_editor_container = QWidget()
|
|
editor_layout = QVBoxLayout(self.json_editor_container)
|
|
editor_layout.setContentsMargins(5, 0, 5, 5) # Margins for editor area (no top margin)
|
|
|
|
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, 1) # Allow tabs to stretch
|
|
|
|
# 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)
|
|
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)
|
|
|
|
main_layout.addWidget(self.json_editor_container)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
# --- 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.")
|
|
|
|
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.")
|
|
|
|
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)
|
|
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)
|
|
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)
|
|
|
|
# --- 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)
|
|
|
|
if table_widget == self.editor_table_map_type_mapping:
|
|
# Column 0: Standard Type (QComboBox)
|
|
combo_box = QComboBox()
|
|
if self._ftd_keys:
|
|
combo_box.addItems(self._ftd_keys)
|
|
else:
|
|
log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping.")
|
|
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved)
|
|
table_widget.setCellWidget(row_count, 0, combo_box)
|
|
# Column 1: Input Keywords (QTableWidgetItem)
|
|
table_widget.setItem(row_count, 1, QTableWidgetItem(""))
|
|
else: # For other tables
|
|
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)
|
|
|
|
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."""
|
|
# Target the container holding the tabs and save buttons
|
|
self.json_editor_container.setEnabled(enabled)
|
|
# Save button state still depends on unsaved changes, but only if container is enabled
|
|
self.editor_save_button.setEnabled(enabled and self.editor_unsaved_changes)
|
|
|
|
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._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) # Clear before populating
|
|
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)
|
|
|
|
# Column 0: Standard Type (QComboBox)
|
|
combo_box = QComboBox()
|
|
if self._ftd_keys:
|
|
combo_box.addItems(self._ftd_keys)
|
|
if std_type in self._ftd_keys:
|
|
combo_box.setCurrentText(std_type)
|
|
else:
|
|
log.warning(f"Preset '{preset_data.get('preset_name', 'Unknown')}': target_type '{std_type}' not found in FILE_TYPE_DEFINITIONS. Selecting first available.")
|
|
if self._ftd_keys: combo_box.setCurrentIndex(0)
|
|
else:
|
|
log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping during population.")
|
|
|
|
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved)
|
|
self.editor_table_map_type_mapping.setCellWidget(i, 0, combo_box)
|
|
|
|
# Column 1: Input Keywords (QTableWidgetItem)
|
|
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)
|
|
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
|
|
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()):
|
|
target_type_widget = self.editor_table_map_type_mapping.cellWidget(r, 0)
|
|
keywords_item = self.editor_table_map_type_mapping.item(r, 1)
|
|
|
|
target_type = ""
|
|
if isinstance(target_type_widget, QComboBox):
|
|
target_type = target_type_widget.currentText()
|
|
elif self.editor_table_map_type_mapping.item(r, 0): # Fallback if item is not a widget
|
|
target_type_item = self.editor_table_map_type_mapping.item(r, 0)
|
|
if target_type_item:
|
|
target_type = target_type_item.text().strip()
|
|
|
|
if target_type and keywords_item and keywords_item.text():
|
|
keywords = [k.strip() for k in keywords_item.text().split(',') if k.strip()]
|
|
if keywords: # Ensure keywords list is not empty after stripping
|
|
map_mappings.append({"target_type": target_type, "keywords": keywords})
|
|
else:
|
|
log.warning(f"Skipping row {r} in map type mapping table due to empty keywords after processing for target_type '{target_type}'.")
|
|
else:
|
|
# Log if target_type is empty or keywords_item is problematic
|
|
if not target_type:
|
|
log.warning(f"Skipping row {r} in map type mapping table due to empty target_type.")
|
|
if not (keywords_item and keywords_item.text()):
|
|
log.warning(f"Skipping row {r} in map type mapping table for target_type '{target_type}' due to missing or empty keywords item.")
|
|
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.presets_changed_signal.emit()
|
|
log.info("Preset saved successfully.")
|
|
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
|
|
self.editor_unsaved_changes = False
|
|
self.editor_save_button.setEnabled(False)
|
|
self.presets_changed_signal.emit()
|
|
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")
|
|
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.editor_supplier_name.setText("MySupplier")
|
|
else:
|
|
log.warning("Presets/_template.json not found. Creating empty preset.")
|
|
self.editor_preset_name.setText("NewPreset")
|
|
self.editor_supplier_name.setText("MySupplier")
|
|
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()
|
|
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 ---
|