# gui/config_editor_dialog.py import json from PySide6.QtWidgets import ( # Changed from PyQt5 QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget, QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox, QPushButton, QFileDialog, QLabel, QTableWidget, # Removed QColorDialog QTableWidgetItem, QDialogButtonBox, QMessageBox, QListWidget, QListWidgetItem, QFormLayout, QGroupBox ) from PySide6.QtGui import QColor # Changed from PyQt5 from PySide6.QtCore import Qt # Changed from PyQt5 from PySide6.QtWidgets import QColorDialog # Import QColorDialog separately for PySide6 # Assuming configuration.py is in the parent directory or accessible # Adjust import path if necessary try: from configuration import load_base_config, save_base_config except ImportError: # Fallback import for testing or different project structure from ..configuration import load_base_config, save_base_config class ConfigEditorDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Configuration Editor") self.setGeometry(100, 100, 800, 600) self.settings = {} self.widgets = {} # Dictionary to hold references to created widgets self.main_layout = QVBoxLayout(self) self.tab_widget = QTabWidget() self.main_layout.addWidget(self.tab_widget) self.button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel) self.button_box.accepted.connect(self.save_settings) self.button_box.rejected.connect(self.reject) self.main_layout.addWidget(self.button_box) self.load_settings() self.create_tabs() self.populate_widgets() def load_settings(self): """Loads settings from the configuration file.""" try: self.settings = load_base_config() print("Configuration loaded successfully.") # Debug print except Exception as e: QMessageBox.critical(self, "Loading Error", f"Failed to load configuration: {e}") self.settings = {} # Use empty settings on failure def create_tabs(self): """Creates tabs based on logical groupings of settings.""" if not self.settings: return # --- Create Tabs --- self.tabs = { "definitions": QWidget(), "paths_output": QWidget(), "image_settings": QWidget(), "blender": QWidget(), "misc": QWidget() # For settings that don't fit elsewhere } self.tab_widget.addTab(self.tabs["definitions"], "Definitions") self.tab_widget.addTab(self.tabs["paths_output"], "Paths & Output") self.tab_widget.addTab(self.tabs["image_settings"], "Image Settings") self.tab_widget.addTab(self.tabs["blender"], "Blender") self.tab_widget.addTab(self.tabs["misc"], "Miscellaneous") # --- Setup Layouts for Tabs --- self.tab_layouts = {name: QVBoxLayout(tab) for name, tab in self.tabs.items()} # --- Populate Tabs --- self.populate_definitions_tab(self.tab_layouts["definitions"]) self.populate_paths_output_tab(self.tab_layouts["paths_output"]) self.populate_image_settings_tab(self.tab_layouts["image_settings"]) self.populate_blender_tab(self.tab_layouts["blender"]) self.populate_misc_tab(self.tab_layouts["misc"]) def create_widget_for_setting(self, parent_layout, key, value, setting_key_prefix=""): """Creates an appropriate widget for a single setting key-value pair.""" full_key = f"{setting_key_prefix}{key}" if setting_key_prefix else key label_text = key.replace('_', ' ').title() label = QLabel(label_text + ":") widget = None layout_to_add = None # Use this for widgets needing extra controls (like browse button) if isinstance(value, str): if 'PATH' in key.upper() or 'DIR' in key.upper() or key == "BLENDER_EXECUTABLE_PATH": widget = QLineEdit(value) button = QPushButton("Browse...") # Determine if it's a file or directory browse is_dir = 'DIR' in key.upper() button.clicked.connect(lambda checked, w=widget, k=full_key, is_dir=is_dir: self.browse_path(w, k, is_dir)) h_layout = QHBoxLayout() h_layout.addWidget(widget) h_layout.addWidget(button) layout_to_add = h_layout elif 'COLOR' in key.upper() or 'COLOUR' in key.upper(): widget = QLineEdit(value) button = QPushButton("Pick Color...") button.clicked.connect(lambda checked, w=widget: self.pick_color(w)) h_layout = QHBoxLayout() h_layout.addWidget(widget) h_layout.addWidget(button) layout_to_add = h_layout else: widget = QLineEdit(value) elif isinstance(value, int): widget = QSpinBox() widget.setRange(-2147483648, 2147483647) widget.setValue(value) elif isinstance(value, float): widget = QDoubleSpinBox() widget.setRange(-1.7976931348623157e+308, 1.7976931348623157e+308) widget.setValue(value) elif isinstance(value, bool): widget = QCheckBox() widget.setChecked(value) elif isinstance(value, list) and key != "MAP_MERGE_RULES": # Handle simple lists (excluding complex ones) # Assuming list of strings or simple types widget = QLineEdit(", ".join(map(str, value))) # Complex dicts/lists like ASSET_TYPE_DEFINITIONS, MAP_MERGE_RULES etc. are handled in dedicated methods if widget or layout_to_add: if layout_to_add: parent_layout.addRow(label, layout_to_add) else: parent_layout.addRow(label, widget) # Store reference using the full key only if a widget was created if widget: self.widgets[full_key] = widget else: # Optionally handle unsupported types or log a warning # print(f"Skipping widget creation for key '{full_key}' with unsupported type: {type(value)}") pass def populate_definitions_tab(self, layout): """Populates the Definitions tab.""" if "ASSET_TYPE_DEFINITIONS" in self.settings: group = QGroupBox("Asset Type Definitions") group_layout = QVBoxLayout(group) self.create_asset_definitions_widget(group_layout, self.settings["ASSET_TYPE_DEFINITIONS"]) layout.addWidget(group) if "FILE_TYPE_DEFINITIONS" in self.settings: group = QGroupBox("File Type Definitions") group_layout = QVBoxLayout(group) self.create_file_type_definitions_widget(group_layout, self.settings["FILE_TYPE_DEFINITIONS"]) layout.addWidget(group) # Add STANDARD_MAP_TYPES and RESPECT_VARIANT_MAP_TYPES here form_layout = QFormLayout() if "STANDARD_MAP_TYPES" in self.settings: self.create_widget_for_setting(form_layout, "STANDARD_MAP_TYPES", self.settings["STANDARD_MAP_TYPES"]) if "RESPECT_VARIANT_MAP_TYPES" in self.settings: self.create_widget_for_setting(form_layout, "RESPECT_VARIANT_MAP_TYPES", self.settings["RESPECT_VARIANT_MAP_TYPES"]) if "DEFAULT_ASSET_CATEGORY" in self.settings: self.create_widget_for_setting(form_layout, "DEFAULT_ASSET_CATEGORY", self.settings["DEFAULT_ASSET_CATEGORY"]) layout.addLayout(form_layout) layout.addStretch() def populate_paths_output_tab(self, layout): """Populates the Paths & Output tab.""" form_layout = QFormLayout() keys_to_include = [ "OUTPUT_BASE_DIR", "EXTRA_FILES_SUBDIR", "METADATA_FILENAME", "TARGET_FILENAME_PATTERN", "TEMP_DIR_PREFIX" ] for key in keys_to_include: if key in self.settings: self.create_widget_for_setting(form_layout, key, self.settings[key]) layout.addLayout(form_layout) layout.addStretch() def populate_image_settings_tab(self, layout): """Populates the Image Settings tab.""" form_layout = QFormLayout() simple_keys = [ "PNG_COMPRESSION_LEVEL", "JPG_QUALITY", "RESOLUTION_THRESHOLD_FOR_JPG", "ASPECT_RATIO_DECIMALS", "CALCULATE_STATS_RESOLUTION", "OUTPUT_FORMAT_16BIT_PRIMARY", "OUTPUT_FORMAT_16BIT_FALLBACK", "OUTPUT_FORMAT_8BIT" ] for key in simple_keys: if key in self.settings: self.create_widget_for_setting(form_layout, key, self.settings[key]) layout.addLayout(form_layout) # Add complex widgets if "IMAGE_RESOLUTIONS" in self.settings: group = QGroupBox("Image Resolutions") group_layout = QVBoxLayout(group) self.create_image_resolutions_widget(group_layout, self.settings["IMAGE_RESOLUTIONS"]) layout.addWidget(group) if "MAP_BIT_DEPTH_RULES" in self.settings: group = QGroupBox("Map Bit Depth Rules") group_layout = QVBoxLayout(group) self.create_map_bit_depth_rules_widget(group_layout, self.settings["MAP_BIT_DEPTH_RULES"]) layout.addWidget(group) if "MAP_MERGE_RULES" in self.settings: group = QGroupBox("Map Merge Rules") group_layout = QVBoxLayout(group) self.create_map_merge_rules_widget(group_layout, self.settings["MAP_MERGE_RULES"]) layout.addWidget(group) layout.addStretch() def populate_blender_tab(self, layout): """Populates the Blender tab.""" form_layout = QFormLayout() keys_to_include = [ "DEFAULT_NODEGROUP_BLEND_PATH", "DEFAULT_MATERIALS_BLEND_PATH", "BLENDER_EXECUTABLE_PATH" ] for key in keys_to_include: if key in self.settings: self.create_widget_for_setting(form_layout, key, self.settings[key]) layout.addLayout(form_layout) layout.addStretch() def populate_misc_tab(self, layout): """Populates the Miscellaneous tab with any remaining settings.""" form_layout = QFormLayout() handled_keys = set() # Collect keys handled by other tabs handled_keys.update([ "ASSET_TYPE_DEFINITIONS", "FILE_TYPE_DEFINITIONS", "STANDARD_MAP_TYPES", "RESPECT_VARIANT_MAP_TYPES", "DEFAULT_ASSET_CATEGORY", "OUTPUT_BASE_DIR", "EXTRA_FILES_SUBDIR", "METADATA_FILENAME", "TARGET_FILENAME_PATTERN", "TEMP_DIR_PREFIX", "PNG_COMPRESSION_LEVEL", "JPG_QUALITY", "RESOLUTION_THRESHOLD_FOR_JPG", "ASPECT_RATIO_DECIMALS", "CALCULATE_STATS_RESOLUTION", "OUTPUT_FORMAT_16BIT_PRIMARY", "OUTPUT_FORMAT_16BIT_FALLBACK", "OUTPUT_FORMAT_8BIT", "IMAGE_RESOLUTIONS", "MAP_BIT_DEPTH_RULES", "MAP_MERGE_RULES", "DEFAULT_NODEGROUP_BLEND_PATH", "DEFAULT_MATERIALS_BLEND_PATH", "BLENDER_EXECUTABLE_PATH" ]) for key, value in self.settings.items(): if key not in handled_keys: # Only create widgets for simple types here if isinstance(value, (str, int, float, bool, list)): # Check if list is simple is_simple_list = isinstance(value, list) and (not value or not isinstance(value[0], (dict, list))) if not isinstance(value, list) or is_simple_list: self.create_widget_for_setting(form_layout, key, value) handled_keys.add(key) # Mark as handled if form_layout.rowCount() == 0: layout.addWidget(QLabel("No miscellaneous settings found.")) layout.addLayout(form_layout) layout.addStretch() # Remove the old create_widgets_for_section method as it's replaced # def create_widgets_for_section(self, layout, section_data, section_key): # ... (old implementation removed) ... def create_asset_definitions_widget(self, layout, definitions_data): """Creates a widget for editing asset type definitions.""" table = QTableWidget() table.setColumnCount(3) # Asset Type, Description, Color table.setHorizontalHeaderLabels(["Asset Type", "Description", "Color"]) table.setRowCount(len(definitions_data)) row = 0 for asset_type, details in definitions_data.items(): table.setItem(row, 0, QTableWidgetItem(asset_type)) table.setItem(row, 1, QTableWidgetItem(details.get("description", ""))) color_widget = QLineEdit(details.get("color", "")) color_button = QPushButton("Pick Color...") color_button.clicked.connect(lambda checked, w=color_widget: self.pick_color(w)) h_layout = QHBoxLayout() h_layout.addWidget(color_widget) h_layout.addWidget(color_button) cell_widget = QWidget() cell_widget.setLayout(h_layout) table.setCellWidget(row, 2, cell_widget) row += 1 table.horizontalHeader().setStretchLastSection(True) layout.addWidget(table) self.widgets["DEFINITION_SETTINGS.ASSET_TYPE_DEFINITIONS"] = table # Store table reference def create_file_type_definitions_widget(self, layout, definitions_data): """Creates a widget for editing file type definitions.""" table = QTableWidget() table.setColumnCount(3) # File Type, Description, Color table.setHorizontalHeaderLabels(["File Type", "Description", "Color"]) table.setRowCount(len(definitions_data)) row = 0 for file_type, details in definitions_data.items(): table.setItem(row, 0, QTableWidgetItem(file_type)) table.setItem(row, 1, QTableWidgetItem(details.get("description", ""))) color_widget = QLineEdit(details.get("color", "")) color_button = QPushButton("Pick Color...") color_button.clicked.connect(lambda checked, w=color_widget: self.pick_color(w)) h_layout = QHBoxLayout() h_layout.addWidget(color_widget) h_layout.addWidget(color_button) cell_widget = QWidget() cell_widget.setLayout(h_layout) table.setCellWidget(row, 2, cell_widget) row += 1 table.horizontalHeader().setStretchLastSection(True) layout.addWidget(table) self.widgets["DEFINITION_SETTINGS.FILE_TYPE_DEFINITIONS"] = table # Store table reference def create_image_resolutions_widget(self, layout, resolutions_data): """Creates a widget for editing image resolutions.""" table = QTableWidget() table.setColumnCount(2) # Width, Height table.setHorizontalHeaderLabels(["Width", "Height"]) table.setRowCount(len(resolutions_data)) for row, resolution in enumerate(resolutions_data): table.setItem(row, 0, QTableWidgetItem(str(resolution[0]))) table.setItem(row, 1, QTableWidgetItem(str(resolution[1]))) table.horizontalHeader().setStretchLastSection(True) layout.addWidget(table) self.widgets["IMAGE_PROCESSING_SETTINGS.IMAGE_RESOLUTIONS"] = table # Store table reference def create_map_bit_depth_rules_widget(self, layout, rules_data: dict): """Creates a widget for editing map bit depth rules (Map Type -> Rule).""" table = QTableWidget() table.setColumnCount(2) # Map Type, Rule table.setHorizontalHeaderLabels(["Map Type", "Rule (respect/force_8bit)"]) table.setRowCount(len(rules_data)) # Iterate through dictionary items (key-value pairs) for row, (map_type, rule_string) in enumerate(rules_data.items()): table.setItem(row, 0, QTableWidgetItem(map_type)) # Optionally use a ComboBox for the rule selection later table.setItem(row, 1, QTableWidgetItem(str(rule_string))) table.horizontalHeader().setStretchLastSection(True) layout.addWidget(table) # Store reference using a more specific key if needed, or handle in save_settings self.widgets["MAP_BIT_DEPTH_RULES_TABLE"] = table # Use a distinct key for the table widget def create_map_merge_rules_widget(self, layout, rules_data): """Creates a widget for editing map merge rules.""" # This is a more complex structure (list of dicts) # Using a ListWidget to select rules and a separate form to edit details h_layout = QHBoxLayout() layout.addLayout(h_layout) self.merge_rules_list = QListWidget() self.merge_rules_list.currentItemChanged.connect(self.display_merge_rule_details) h_layout.addWidget(self.merge_rules_list, 1) # Give list more space self.merge_rule_details_group = QGroupBox("Rule Details") self.merge_rule_details_layout = QFormLayout(self.merge_rule_details_group) h_layout.addWidget(self.merge_rule_details_group, 2) # Give details form more space self.merge_rule_widgets = {} # Widgets for the currently displayed rule self.populate_merge_rules_list(rules_data) self.widgets["IMAGE_PROCESSING_SETTINGS.MAP_MERGE_RULES"] = rules_data # Store original data reference def populate_merge_rules_list(self, rules_data): """Populates the list widget with map merge rules.""" self.merge_rules_list.clear() for rule in rules_data: item = QListWidgetItem(rule.get("output_name", "Unnamed Rule")) item.setData(Qt.UserRole, rule) # Store the rule dictionary in the item self.merge_rules_list.addItem(item) def display_merge_rule_details(self, current, previous): """Displays details of the selected merge rule.""" # Clear previous widgets for i in reversed(range(self.merge_rule_details_layout.count())): widget_item = self.merge_rule_details_layout.itemAt(i) if widget_item: widget = widget_item.widget() if widget: widget.deleteLater() layout = widget_item.layout() if layout: # Recursively delete widgets in layout while layout.count(): item = layout.takeAt(0) widget = item.widget() if widget: widget.deleteLater() elif item.layout(): # Handle nested layouts if necessary pass # For simplicity, assuming no deeply nested layouts here self.merge_rule_widgets.clear() if current: rule_data = current.data(Qt.UserRole) if rule_data: for key, value in rule_data.items(): label = QLabel(key.replace('_', ' ').title() + ":") if isinstance(value, str): widget = QLineEdit(value) elif isinstance(value, (int, float)): if isinstance(value, int): widget = QSpinBox() widget.setRange(-2147483648, 2147483647) widget.setValue(value) else: widget = QDoubleSpinBox() widget.setRange(-1.7976931348623157e+308, 1.7976931348623157e+308) widget.setValue(value) elif isinstance(value, bool): widget = QCheckBox() widget.setChecked(value) elif isinstance(value, list): # Assuming list of strings or simple types for now widget = QLineEdit(", ".join(map(str, value))) elif isinstance(value, dict): # Assuming simple key-value dicts for now widget = QLineEdit(json.dumps(value)) # Display as JSON string else: widget = QLabel(f"Unsupported type: {type(value)}") self.merge_rule_details_layout.addRow(label, widget) self.merge_rule_widgets[key] = widget # Store widget reference def populate_widgets(self): """Populates the created widgets with loaded settings (for simple types).""" # This method is less critical with the recursive create_widgets_for_section # but could be used for specific post-creation population if needed. pass def browse_path(self, widget, key): """Opens a file or directory dialog based on the setting key.""" if 'DIR' in key.upper(): path = QFileDialog.getExistingDirectory(self, "Select Directory", widget.text()) else: path, _ = QFileDialog.getOpenFileName(self, "Select File", widget.text()) if path: widget.setText(path) def pick_color(self, widget): """Opens a color dialog and sets the selected color in the widget.""" color = QColorDialog.getColor(QColor(widget.text())) if color.isValid(): widget.setText(color.name()) # Get color as hex string def save_settings(self): """Reads values from widgets and saves them to the configuration file.""" new_settings = {} # Reconstruct the settings dictionary from widgets # This requires iterating through the widgets and mapping them back # to the original structure. This is a simplified approach and might # need refinement for complex nested structures or dynamic lists/tables. # Start with a deep copy of the original settings structure to preserve # sections/keys that might not have dedicated widgets (though ideally all should) import copy new_settings = copy.deepcopy(self.settings) # Iterate through the stored widgets and update the new_settings dictionary for key, widget in self.widgets.items(): # Handle simple widgets if isinstance(widget, (QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox)): # Split the key to navigate the dictionary structure keys = key.split('.') current_dict = new_settings for i, k in enumerate(keys): if i == len(keys) - 1: # This is the final key, update the value if isinstance(widget, QLineEdit): current_dict[k] = widget.text() elif isinstance(widget, QSpinBox): current_dict[k] = widget.value() elif isinstance(widget, QDoubleSpinBox): current_dict[k] = widget.value() elif isinstance(widget, QCheckBox): current_dict[k] = widget.isChecked() else: # Navigate to the next level if k not in current_dict or not isinstance(current_dict[k], dict): # This should not happen if create_tabs is correct, but handle defensively print(f"Warning: Structure mismatch for key part '{k}' in '{key}'") break # Stop processing this key current_dict = current_dict[k] # Handle TableWidgets (for definitions, resolutions, bit depth rules) elif isinstance(widget, QTableWidget): keys = key.split('.') if len(keys) >= 2: section_key = keys[0] list_key = keys[1] if section_key in new_settings and list_key in new_settings[section_key]: if list_key == "ASSET_TYPE_DEFINITIONS": new_definitions = {} for row in range(widget.rowCount()): asset_type_item = widget.item(row, 0) description_item = widget.item(row, 1) color_widget_container = widget.cellWidget(row, 2) if asset_type_item and color_widget_container: asset_type = asset_type_item.text() description = description_item.text() if description_item else "" color_widget = color_widget_container.findChild(QLineEdit) if color_widget: color = color_widget.text() new_definitions[asset_type] = {"description": description, "color": color} new_settings[section_key][list_key] = new_definitions elif list_key == "FILE_TYPE_DEFINITIONS": new_definitions = {} for row in range(widget.rowCount()): file_type_item = widget.item(row, 0) description_item = widget.item(row, 1) color_widget_container = widget.cellWidget(row, 2) if file_type_item and color_widget_container: file_type = file_type_item.text() description = description_item.text() if description_item else "" color_widget = color_widget_container.findChild(QLineEdit) if color_widget: color = color_widget.text() new_definitions[file_type] = {"description": description, "color": color} new_settings[section_key][list_key] = new_definitions elif list_key == "IMAGE_RESOLUTIONS": new_resolutions = [] for row in range(widget.rowCount()): width_item = widget.item(row, 0) height_item = widget.item(row, 1) if width_item and height_item: try: width = int(width_item.text()) height = int(height_item.text()) new_resolutions.append([width, height]) except ValueError: print(f"Warning: Invalid resolution value at row {row}") new_settings[section_key][list_key] = new_resolutions elif list_key == "MAP_BIT_DEPTH_RULES": new_rules = [] for row in range(widget.rowCount()): pattern_item = widget.item(row, 0) bit_depth_item = widget.item(row, 1) if pattern_item and bit_depth_item: try: bit_depth = int(bit_depth_item.text()) new_rules.append({"pattern": pattern_item.text(), "bit_depth": bit_depth}) except ValueError: print(f"Warning: Invalid bit depth value at row {row}") new_settings[section_key][list_key] = new_rules # Handle Map Merge Rules (more complex) # This requires reading from the details form for the currently selected item # and updating the corresponding dictionary in the original list stored in self.widgets elif key == "IMAGE_PROCESSING_SETTINGS.MAP_MERGE_RULES": # The original list is stored in self.widgets["IMAGE_PROCESSING_SETTINGS.MAP_MERGE_RULES"] # We need to iterate through the list widget items and update the corresponding # dictionary in the list based on the details form if that item was selected and edited. # A simpler approach for now is to just read the currently displayed rule details # and update the corresponding item in the list widget's data, then reconstruct the list. # Reconstruct the list from the list widget items' data new_merge_rules = [] for i in range(self.merge_rules_list.count()): item = self.merge_rules_list.item(i) rule_data = item.data(Qt.UserRole) if rule_data: # If this item is the currently selected one, update its data from the details widgets if item == self.merge_rules_list.currentItem(): updated_rule_data = {} for detail_key, detail_widget in self.merge_rule_widgets.items(): if isinstance(detail_widget, QLineEdit): updated_rule_data[detail_key] = detail_widget.text() elif isinstance(detail_widget, QSpinBox): updated_rule_data[detail_key] = detail_widget.value() elif isinstance(detail_widget, QDoubleSpinBox): updated_rule_data[detail_key] = detail_widget.value() elif isinstance(detail_widget, QCheckBox): updated_rule_data[detail_key] = detail_widget.isChecked() # Add handling for other widget types in details form if needed # Merge updated data with original data (in case some fields weren't in details form) rule_data.update(updated_rule_data) new_merge_rules.append(rule_data) # Update the new_settings dictionary with the reconstructed list keys = key.split('.') if len(keys) == 2: section_key = keys[0] list_key = keys[1] if section_key in new_settings and list_key in new_settings[section_key]: new_settings[section_key][list_key] = new_merge_rules # Save the new settings try: save_base_config(new_settings) QMessageBox.information(self, "Settings Saved", "Configuration saved successfully.\nRestart the application to apply changes.") self.accept() # Close the dialog except Exception as e: QMessageBox.critical(self, "Saving Error", f"Failed to save configuration: {e}") # Example usage (for testing the dialog independently) if __name__ == '__main__': from PyQt5.QtWidgets import QApplication import sys app = QApplication(sys.argv) dialog = ConfigEditorDialog() dialog.exec_() sys.exit(app.exec_())