GUI FIXES

This commit is contained in:
2025-05-01 19:16:37 +02:00
parent 51ff45bd5c
commit 2f8bbc3a7d
16 changed files with 1397 additions and 198 deletions

623
gui/config_editor_dialog.py Normal file
View File

@@ -0,0 +1,623 @@
# 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_())

View File

@@ -2,7 +2,16 @@
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
from PySide6.QtCore import Qt, QModelIndex
# Import the new config dictionaries
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
from configuration import load_base_config # Import load_base_config
import json
import logging
import os # Added for path manipulation if needed, though json.dump handles creation
from PySide6.QtWidgets import QCompleter # Added QCompleter
# Configure logging
log = logging.getLogger(__name__)
SUPPLIERS_CONFIG_PATH = "config/suppliers.json"
class LineEditDelegate(QStyledItemDelegate):
"""Delegate for editing string values using a QLineEdit."""
@@ -44,10 +53,16 @@ class ComboBoxDelegate(QStyledItemDelegate):
# Populate based on column using keys from config dictionaries
items_keys = None
if column == 2: # Asset-Type Override (AssetRule)
items_keys = list(ASSET_TYPE_DEFINITIONS.keys())
elif column == 4: # Item-Type Override (FileRule)
items_keys = list(FILE_TYPE_DEFINITIONS.keys())
try:
base_config = load_base_config() # Load base config
if column == 2: # Asset-Type Override (AssetRule)
items_keys = list(base_config.get('ASSET_TYPE_DEFINITIONS', {}).keys()) # Access from base_config
elif column == 4: # Item-Type Override (FileRule)
items_keys = list(base_config.get('FILE_TYPE_DEFINITIONS', {}).keys()) # Access from base_config
except Exception as e:
log.error(f"Error loading base config for ComboBoxDelegate: {e}")
items_keys = [] # Fallback to empty list on error
if items_keys:
for item_key in sorted(items_keys): # Sort keys alphabetically for consistency
@@ -88,16 +103,6 @@ class ComboBoxDelegate(QStyledItemDelegate):
def updateEditorGeometry(self, editor, option, index):
# Ensures the editor widget is placed correctly within the cell.
editor.setGeometry(option.rect)
# gui/delegates.py - New content to insert
import json
import logging
import os # Added for path manipulation if needed, though json.dump handles creation
from PySide6.QtWidgets import QCompleter # Added QCompleter
# Configure logging
log = logging.getLogger(__name__)
SUPPLIERS_CONFIG_PATH = "config/suppliers.json"
class SupplierSearchDelegate(QStyledItemDelegate):
"""

View File

@@ -39,22 +39,24 @@ if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
try:
from configuration import Configuration, ConfigurationError
from configuration import Configuration, ConfigurationError, load_base_config # Import Configuration and load_base_config
from asset_processor import AssetProcessor, AssetProcessingError
# from gui.processing_handler import ProcessingHandler # REMOVED Obsolete Handler
from gui.prediction_handler import PredictionHandler
import config as core_config # Import the config module
# Removed: import config as core_config # Import the config module
# PresetEditorDialog is no longer needed
except ImportError as e:
print(f"ERROR: Failed to import backend modules: {e}")
print(f"Ensure GUI is run from project root or backend modules are in PYTHONPATH.")
Configuration = None
load_base_config = None # Set to None if import fails
ConfigurationError = Exception
AssetProcessor = None
# ProcessingHandler = None # REMOVED Obsolete Handler
PredictionHandler = None
ConfigurationError = Exception
AssetProcessingError = Exception
# --- Constants ---
PRESETS_DIR = project_root / "presets"
TEMPLATE_PATH = PRESETS_DIR / "_template.json"
@@ -75,7 +77,7 @@ class QtLogHandler(logging.Handler, QObject):
log_record_received = Signal(str) # Signal emitting the formatted log string
def __init__(self, parent=None):
logging.Handler.__init__(self)
logging.Handler.__init__(self) # Call parent Handler init (level is optional)
QObject.__init__(self, parent) # Initialize QObject part
def emit(self, record):
@@ -373,16 +375,23 @@ class MainWindow(QMainWindow):
# --- Set Initial Output Path ---
try:
output_base_dir_config = getattr(core_config, 'OUTPUT_BASE_DIR', '../Asset_Processor_Output') # Default if not found
# Use load_base_config to get the default output directory
base_config = load_base_config()
output_base_dir_config = base_config.get('OUTPUT_BASE_DIR', '../Asset_Processor_Output') # Default if not found
# Resolve the path relative to the project root
default_output_dir = (project_root / output_base_dir_config).resolve()
self.output_path_edit.setText(str(default_output_dir))
log.info(f"Default output directory set to: {default_output_dir}")
except ConfigurationError as e:
log.error(f"Error reading base configuration for default output directory: {e}")
self.output_path_edit.setText("") # Clear on error
self.statusBar().showMessage(f"Error setting default output path: {e}", 5000)
except Exception as e:
log.error(f"Error setting default output directory: {e}")
log.exception(f"Error setting default output directory: {e}")
self.output_path_edit.setText("") # Clear on error
self.statusBar().showMessage(f"Error setting default output path: {e}", 5000)
# --- Drag and Drop Area ---
self.drag_drop_area = QFrame()
self.drag_drop_area.setFrameShape(QFrame.Shape.StyledPanel)
@@ -466,13 +475,18 @@ class MainWindow(QMainWindow):
# Initialize paths from config
try:
default_ng_path = getattr(core_config, 'DEFAULT_NODEGROUP_BLEND_PATH', '')
default_mat_path = getattr(core_config, 'DEFAULT_MATERIALS_BLEND_PATH', '')
# Use load_base_config to get default Blender paths
base_config = load_base_config()
default_ng_path = base_config.get('DEFAULT_NODEGROUP_BLEND_PATH', '')
default_mat_path = base_config.get('DEFAULT_MATERIALS_BLEND_PATH', '')
self.nodegroup_blend_path_input.setText(default_ng_path if default_ng_path else "")
self.materials_blend_path_input.setText(default_mat_path if default_mat_path else "")
except ConfigurationError as e:
log.error(f"Error reading base configuration for default Blender paths: {e}")
except Exception as e:
log.error(f"Error reading default Blender paths from config: {e}")
# Disable Blender controls initially if checkbox is unchecked
self.nodegroup_blend_path_input.setEnabled(False)
self.browse_nodegroup_blend_button.setEnabled(False)
@@ -1001,14 +1015,22 @@ class MainWindow(QMainWindow):
self._finalize_model_update()
else:
# Update status about remaining items
remaining_count = len(self._pending_predictions)
self.statusBar().showMessage(f"Prediction failed/finished for {Path(input_path).name}. Waiting for {remaining_count} more...", 5000)
completed_count = len(self._accumulated_rules)
pending_count = len(self._pending_predictions)
# total_count = completed_count + pending_count # This might be slightly off if some failed without rules
# We don't have the total count of *requested* predictions here easily,
# but we can use the initial number of items added.
total_requested = len(self.current_asset_paths) # Use the total number of items added
status_msg = f"Prediction finished for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_requested} requested)..."
self.statusBar().showMessage(status_msg, 5000)
log.debug(status_msg)
else:
log.debug(f"Prediction finished for '{input_path}', which was already processed.")
# Original status message might be misleading now, handled by accumulation logic.
# self.statusBar().showMessage("Preview updated.", 3000) # Removed
@Slot(str, str, str)
def update_file_status(self, input_path_str, status, message):
# TODO: Update status bar or potentially find rows in table later
@@ -1220,17 +1242,18 @@ class MainWindow(QMainWindow):
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", []))
self.editor_table_archetype_rules.setRowCount(0)
arch_rules = preset_data.get("archetype_rules", [])
for i, rule in enumerate(arch_rules):
if isinstance(rule, (list, tuple)) and len(rule) == 2:
arch_name, conditions = rule
match_any = ", ".join(conditions.get("match_any", []))
match_all = ", ".join(conditions.get("match_all", []))
self.editor_table_archetype_rules.insertRow(i)
self.editor_table_archetype_rules.setItem(i, 0, QTableWidgetItem(arch_name))
self.editor_table_archetype_rules.setItem(i, 1, QTableWidgetItem(match_any))
self.editor_table_archetype_rules.setItem(i, 2, QTableWidgetItem(match_all))
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 any_item and all_item:
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()]
arch_rules.append([name_item.text().strip(), {"match_any": match_any, "match_all": match_all}])
preset_data["archetype_rules"] = arch_rules
return preset_data
finally:
self._is_loading_editor = False
@@ -1424,15 +1447,17 @@ class MainWindow(QMainWindow):
QMessageBox.critical(self, "Error", f"Could not load template preset file:\n{TEMPLATE_PATH}\n\nError: {e}")
self._clear_editor()
self.setWindowTitle("Asset Processor Tool - New Preset*")
self.editor_supplier_name.setText("MySupplier") # Set a default supplier name
else:
log.warning("Presets/_template.json not found. Creating empty preset.")
self.setWindowTitle("Asset Processor Tool - New Preset*")
self.editor_preset_name.setText("NewPreset")
self.editor_supplier_name.setText("MySupplier")
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)
def _delete_selected_preset(self):
"""Deletes the currently selected preset file from the editor list after confirmation."""
current_item = self.editor_preset_list.currentItem()
@@ -1455,8 +1480,22 @@ class MainWindow(QMainWindow):
# --- Menu Bar Setup ---
def setup_menu_bar(self):
"""Creates the main menu bar and View menu."""
"""Creates the main menu bar and adds menus/actions."""
self.menu_bar = self.menuBar()
# --- File Menu (Optional, add if needed later) ---
# file_menu = self.menu_bar.addMenu("&File")
# Add actions like New, Open, Save, Exit
# --- Edit Menu ---
edit_menu = self.menu_bar.addMenu("&Edit")
# Preferences/Settings Action
self.preferences_action = QAction("&Preferences...", self)
self.preferences_action.triggered.connect(self._open_config_editor)
edit_menu.addAction(self.preferences_action)
# --- View Menu ---
view_menu = self.menu_bar.addMenu("&View")
# Log Console Action
@@ -1495,6 +1534,23 @@ class MainWindow(QMainWindow):
log.info("UI Log Handler Initialized.") # Log that the handler is ready
# --- Slots for Menu Actions and Logging ---
@Slot()
def _open_config_editor(self):
"""Opens the configuration editor dialog."""
log.debug("Opening configuration editor dialog.")
try:
from .config_editor_dialog import ConfigEditorDialog # Import locally to avoid circular dependency if needed
dialog = ConfigEditorDialog(self)
dialog.exec_() # Use exec_() to run as a modal dialog
log.debug("Configuration editor dialog closed.")
except ImportError:
log.error("Failed to import ConfigEditorDialog. Ensure gui/config_editor_dialog.py exists and is accessible.")
QMessageBox.critical(self, "Error", "Could not open configuration editor.\nRequired file not found or has errors.")
except Exception as e:
log.exception(f"Error opening configuration editor dialog: {e}")
QMessageBox.critical(self, "Error", f"An error occurred while opening the configuration editor:\n{e}")
@Slot(bool)
def _toggle_log_console_visibility(self, checked):
"""Shows or hides the log console widget based on menu action."""
@@ -1604,8 +1660,11 @@ class MainWindow(QMainWindow):
# Update status bar with progress
completed_count = len(self._accumulated_rules)
pending_count = len(self._pending_predictions)
total_count = completed_count + pending_count # This might be slightly off if some failed without rules
status_msg = f"Preview updated for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_count} requested)..."
# total_count = completed_count + pending_count # This might be slightly off if some failed without rules
# We don't have the total count of *requested* predictions here easily,
# but we can use the initial number of items added.
total_requested = len(self.current_asset_paths) # Use the total number of items added
status_msg = f"Preview finished for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_requested} requested)..."
self.statusBar().showMessage(status_msg, 5000)
log.debug(status_msg)

View File

@@ -21,23 +21,24 @@ if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
try:
from configuration import Configuration, ConfigurationError
from configuration import Configuration, ConfigurationError, load_base_config # Import Configuration, ConfigurationError, and load_base_config
# AssetProcessor might not be needed directly anymore if logic is moved here
# from asset_processor import AssetProcessor, AssetProcessingError
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType
import config as app_config # Import project's config module
# Import the new dictionaries directly for easier access
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
# Removed: import config as app_config # Import project's config module
# Removed: Import the new dictionaries directly for easier access
# Removed: from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
BACKEND_AVAILABLE = True
except ImportError as e:
print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}")
# Define placeholders if imports fail
Configuration = None
# AssetProcessor = None
load_base_config = None # Placeholder
ConfigurationError = Exception
# AssetProcessingError = Exception
SourceRule, AssetRule, FileRule, AssetType, ItemType = (None,)*5 # Placeholder for rule structures
app_config = None # Placeholder for config
SourceRule, AssetRule, FileRule = (None,)*3 # Placeholder for rule structures
# Removed: AssetType, ItemType = (None,)*2 # Placeholder for types
# Removed: app_config = None # Placeholder for config
BACKEND_AVAILABLE = False
log = logging.getLogger(__name__)
@@ -76,8 +77,9 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
if not file_list or not config:
log.warning("Classification skipped: Missing file list or config.")
return {}
# Access compiled regex directly from the config object
if not hasattr(config, 'compiled_map_keyword_regex') or not config.compiled_map_keyword_regex:
log.warning("Classification skipped: Missing compiled map keyword regex.")
log.warning("Classification skipped: Missing compiled map keyword regex in config.")
# Don't return yet, might still find extras
if not hasattr(config, 'compiled_extra_regex'):
log.warning("Configuration object missing 'compiled_extra_regex'. Cannot classify extra files.")
@@ -119,12 +121,14 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
# Access the full rule details directly from the config's map_type_mapping list using the index
matched_rule_details = None
try:
matched_rule_details = config.map_type_mapping[rule_index] # Access rule by index
# Access map_type_mapping using the property
map_type_mapping_list = config.map_type_mapping # Use the property
matched_rule_details = map_type_mapping_list[rule_index] # Access rule by index
is_gloss_flag = matched_rule_details.get('is_gloss_source', False) # Get flag or default False
log.debug(f" Associated rule details: {matched_rule_details}")
log.debug(f" 'is_gloss_source' flag from rule: {is_gloss_flag}")
except IndexError:
log.warning(f" Could not access map_type_mapping rule at index {rule_index}. Cannot determine 'is_gloss_source' flag.")
log.warning(f" Could not access map_type_mapping rule at index {rule_index} in config.settings. Cannot determine 'is_gloss_source' flag.")
is_gloss_flag = False # Default if rule cannot be accessed
# --- End DEBUG LOG ---
matched_item_type = target_type # The standard type (e.g., MAP_COL)
@@ -269,19 +273,19 @@ class PredictionHandler(QObject):
self.status_message.emit(f"Analyzing '{source_path.name}'...", 0)
config: Configuration | None = None
asset_type_definitions: Dict[str, Dict] = {}
file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
# Removed: asset_type_definitions: Dict[str, Dict] = {}
# Removed: file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
try:
config = Configuration(preset_name)
# Load allowed types from the project's config module (now dictionaries)
if app_config:
asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
else:
log.warning("Project config module not loaded. Cannot get type definitions.")
# Removed: Load allowed types from the project's config module (now dictionaries)
# Removed: if app_config:
# Removed: asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
# Removed: file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
# Removed: log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
# Removed: log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
# Removed: else:
# Removed: log.warning("Project config module not loaded. Cannot get type definitions.")
except ConfigurationError as e:
log.error(f"Failed to load configuration for preset '{preset_name}': {e}")
@@ -331,6 +335,10 @@ class PredictionHandler(QObject):
log.debug(f"Created SourceRule for identifier: {input_source_identifier} with supplier: {supplier_identifier}")
asset_rules = []
# Get allowed asset types from config's internal core settings
asset_type_definitions = config._core_settings.get('ASSET_TYPE_DEFINITIONS', {})
log.debug(f"Loaded AssetType Definitions from config: {list(asset_type_definitions.keys())}")
for asset_name, files_info in classified_assets.items():
if not files_info: continue # Skip empty asset groups
@@ -349,9 +357,10 @@ class PredictionHandler(QObject):
# Ensure the predicted type is allowed, fallback if necessary
# Now predicted_asset_type is already a string
if asset_type_definitions and predicted_asset_type not in asset_type_definitions:
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. Falling back.")
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS from config. Falling back.")
# Fallback logic: use the default from config if allowed, else first allowed type
default_type = getattr(app_config, 'DEFAULT_ASSET_CATEGORY', 'Surface')
# Access DEFAULT_ASSET_CATEGORY using the property
default_type = config.default_asset_category # Use the property
if default_type in asset_type_definitions:
predicted_asset_type = default_type
elif asset_type_definitions:
@@ -368,20 +377,38 @@ class PredictionHandler(QObject):
log.debug(f"Created AssetRule for asset: {asset_name} with type: {predicted_asset_type}")
file_rules = []
# Get allowed file types from config's internal core settings
file_type_definitions = config._core_settings.get('FILE_TYPE_DEFINITIONS', {})
log.debug(f"Loaded FileType Definitions (ItemTypes) from config: {list(file_type_definitions.keys())}")
for file_info in files_info:
# Determine FileRule level overrides/defaults
base_item_type = file_info['item_type'] # Type from classification (e.g., COL, NRM, EXTRA)
target_asset_name_override = file_info['asset_name'] # From classification
# Retrieve the standard_type from the config if available
standard_map_type = None
file_type_details = file_type_definitions.get(base_item_type)
if file_type_details:
standard_map_type = file_type_details.get('standard_type') # Try to get explicit standard_type
# If standard_type wasn't found in the definition, use the base_item_type itself
# (which is the alias in presets like Poliigon.json)
if standard_map_type is None and base_item_type in file_type_definitions: # Check base_item_type is a valid key
log.debug(f" No explicit 'standard_type' found for item type '{base_item_type}'. Using base_item_type itself as standard_map_type.")
standard_map_type = base_item_type # Fallback to using the base type (alias)
elif standard_map_type is None:
log.debug(f" No 'standard_type' found and base_item_type '{base_item_type}' not in definitions. Setting standard_map_type to None.")
# Determine the final item_type string (prefix maps, check if allowed)
final_item_type = base_item_type # Start with the base type
if not base_item_type.startswith("MAP_") and base_item_type not in ["FILE_IGNORE", "EXTRA", "MODEL"]:
# Prefix map types that don't already have it
final_item_type = f"MAP_{base_item_type}"
# Check if the final type is allowed (exists as a key in config)
# Check if the final type is allowed (exists as a key in config settings)
if file_type_definitions and final_item_type not in file_type_definitions and base_item_type not in ["FILE_IGNORE", "EXTRA"]:
log.warning(f"Predicted ItemType '{base_item_type}' (checked as '{final_item_type}') for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS. Setting base type to FILE_IGNORE.")
log.warning(f"Predicted ItemType '{base_item_type}' (checked as '{final_item_type}') for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS from config. Setting base type to FILE_IGNORE.")
final_item_type = "FILE_IGNORE" # Fallback base type to FILE_IGNORE string
# Output format is determined by the engine, not predicted here. Leave as None.
@@ -394,12 +421,37 @@ class PredictionHandler(QObject):
log.debug(f" Base Item Type (from classification): {base_item_type}")
log.debug(f" Final Item Type (for model): {final_item_type}")
log.debug(f" Target Asset Name Override: {target_asset_name_override}")
# --- DETAILED DEBUG LOG: Inspect standard_map_type assignment ---
log.debug(f" DEBUG: Processing file: {file_info['file_path']}")
log.debug(f" DEBUG: base_item_type = {base_item_type}")
log.debug(f" DEBUG: file_type_definitions keys = {list(file_type_definitions.keys())}")
# --- Fix: Use final_item_type (prefixed) for lookup, fallback to base_item_type (alias) ---
standard_map_type = None
# Use final_item_type (e.g., "MAP_AO") for the lookup
file_type_details = file_type_definitions.get(final_item_type)
log.debug(f" DEBUG: file_type_definitions.get({final_item_type}) = {file_type_details}") # Log lookup result
if file_type_details:
# Try to get explicit standard_type (might still be missing in some presets)
standard_map_type = file_type_details.get('standard_type')
log.debug(f" DEBUG: Explicit standard_type from details = {standard_map_type}")
# If standard_type wasn't found in the definition, use the base_item_type (alias)
# This handles presets like Poliigon.json where the alias is the target_type
if standard_map_type is None and final_item_type in file_type_definitions: # Check if the prefixed type was valid
log.debug(f" No explicit 'standard_type' found for item type '{final_item_type}'. Using base_item_type ('{base_item_type}') as standard_map_type.")
standard_map_type = base_item_type # Fallback to using the base type (alias)
elif standard_map_type is None:
log.debug(f" Could not determine standard_map_type for base '{base_item_type}' / final '{final_item_type}'. Setting to None.")
# --- End Fix ---
log.debug(f" DEBUG: Final standard_map_type variable value = {standard_map_type}") # Log final value
# --- END DETAILED DEBUG LOG ---
# Explicitly check and log the flag value from file_info
is_gloss_source_value = file_info.get('is_gloss_source', 'MISSING') # Get value or 'MISSING'
log.debug(f" Value for 'is_gloss_source' from file_info: {is_gloss_source_value}")
# --- End DEBUG LOG ---
# Pass the retrieved flag value to the constructor
# Pass the retrieved flag value and standard_map_type to the constructor
file_rule = FileRule(
file_path=file_info['file_path'], # This is static info based on input
item_type=final_item_type, # Set the new base item_type field
@@ -409,6 +461,7 @@ class PredictionHandler(QObject):
target_asset_name_override=target_asset_name_override,
output_format_override=output_format_override,
is_gloss_source=is_gloss_source_value if isinstance(is_gloss_source_value, bool) else False, # Pass the flag, ensure boolean
standard_map_type=standard_map_type, # Assign the determined standard_map_type
# --- Leave Static Fields as Default/None ---
resolution_override=None,
channel_merge_instructions={},

View File

@@ -26,15 +26,16 @@ try:
# Import the worker function from main.py
from main import process_single_asset_wrapper
# Import exceptions if needed for type hinting or specific handling
from configuration import ConfigurationError
from configuration import ConfigurationError, load_base_config # Import ConfigurationError and load_base_config
from asset_processor import AssetProcessingError
import config as core_config # <<< ADDED IMPORT
# Removed: import config as core_config # <<< ADDED IMPORT
BACKEND_AVAILABLE = True
except ImportError as e:
print(f"ERROR (ProcessingHandler): Failed to import backend modules/worker: {e}")
# Define placeholders if imports fail, so the GUI doesn't crash immediately
process_single_asset_wrapper = None
ConfigurationError = Exception
load_base_config = None # Placeholder
AssetProcessingError = Exception
BACKEND_AVAILABLE = False
@@ -70,7 +71,11 @@ class ProcessingHandler(QObject):
def is_running(self):
return self._is_running
def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,
# Removed _predict_single_asset method
@Slot(str, list, str, str, bool, int,
bool, str, str, bool, SourceRule) # Explicitly define types for the slot
def run_processing(self, input_source_identifier: str, original_input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,
run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str, verbose: bool, rules: SourceRule): # <<< ADDED verbose PARAM
"""
Starts the asset processing task and optionally runs Blender scripts afterwards.
@@ -84,13 +89,13 @@ class ProcessingHandler(QObject):
if not BACKEND_AVAILABLE or not process_single_asset_wrapper:
log.error("Backend modules or worker function not available. Cannot start processing.")
self.status_message.emit("Error: Backend components missing. Cannot process.", 5000)
self.processing_finished.emit(0, 0, len(input_paths)) # Emit finished with all failed
self.processing_finished.emit(0, 0, len(original_input_paths)) # Emit finished with all failed
return
self._is_running = True
self._cancel_requested = False
self._futures = {} # Reset futures
total_files = len(input_paths)
total_files = len(original_input_paths) # Use original_input_paths for total count
processed_count = 0
skipped_count = 0
failed_count = 0
@@ -105,9 +110,20 @@ class ProcessingHandler(QObject):
self._executor = executor # Store for potential cancellation
# Submit tasks
for input_path in input_paths:
for input_path in original_input_paths: # Iterate through the list of input paths
if self._cancel_requested: break # Check before submitting more
log.debug(f"Submitting task for: {input_path}")
# Pass the single SourceRule object to the worker
# --- DEBUG LOG: Inspect FileRule overrides before sending to worker ---
log.debug(f"ProcessingHandler: Inspecting rules for input '{input_path}' before submitting to worker:")
if rules: # Check if rules object exists
for asset_rule in rules.assets:
log.debug(f" Asset: {asset_rule.asset_name}")
for file_rule in asset_rule.files:
log.debug(f" File: {Path(file_rule.file_path).name}, ItemType: {file_rule.item_type}, Override: {file_rule.item_type_override}, StandardMap: {getattr(file_rule, 'standard_map_type', 'N/A')}")
else:
log.debug(" Rules object is None.")
# --- END DEBUG LOG ---
future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=verbose, rules=rules) # Pass verbose flag from GUI and rules
self._futures[future] = input_path # Map future back to input path
# Optionally emit "processing" status here
@@ -288,7 +304,14 @@ class ProcessingHandler(QObject):
def _find_blender_executable(self) -> Optional[str]:
"""Finds the Blender executable path from config or system PATH."""
try:
blender_exe_config = getattr(core_config, 'BLENDER_EXECUTABLE_PATH', None)
# Use load_base_config to get the Blender executable path
if load_base_config:
base_config = load_base_config()
blender_exe_config = base_config.get('BLENDER_EXECUTABLE_PATH', None)
else:
blender_exe_config = None
log.warning("load_base_config not available. Cannot read BLENDER_EXECUTABLE_PATH from config.")
if blender_exe_config:
p = Path(blender_exe_config)
if p.is_file():
@@ -306,6 +329,9 @@ class ProcessingHandler(QObject):
else:
log.warning("Could not find 'blender' in system PATH.")
return None
except ConfigurationError as e:
log.error(f"Error reading base configuration for Blender executable path: {e}")
return None
except Exception as e:
log.error(f"Error checking Blender executable path: {e}")
return None

View File

@@ -1,9 +1,11 @@
# gui/unified_view_model.py
import logging # Added for debugging
log = logging.getLogger(__name__) # Added for debugging
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal # Added Signal
from PySide6.QtGui import QColor # Added for background role
from pathlib import Path # Added for file_name extraction
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType import
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS # Added for coloring
from configuration import load_base_config # Import load_base_config
class UnifiedViewModel(QAbstractItemModel):
# --- Color Constants for Row Backgrounds ---
@@ -133,10 +135,10 @@ class UnifiedViewModel(QAbstractItemModel):
if not parent.isValid():
# Parent is invisible root. Children are SourceRules.
if row < len(self._source_rules):
child_item = self._source_rules[row]
return self.createIndex(row, column, child_item)
child_item = self._source_rules[row]
return self.createIndex(row, column, child_item)
else:
return QModelIndex() # Row out of bounds for top-level items
return QModelIndex() # Row out of bounds for top-level items
else:
# Parent is a valid index, get its item
parent_item = parent.internalPointer()
@@ -181,47 +183,57 @@ class UnifiedViewModel(QAbstractItemModel):
# Determine effective asset type
asset_type = item.asset_type_override if item.asset_type_override else item.asset_type
if asset_type:
type_info = ASSET_TYPE_DEFINITIONS.get(asset_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for asset type '{asset_type}' in config.")
return None # Fallback for invalid hex
try:
base_config = load_base_config() # Load base config
asset_type_definitions = base_config.get('ASSET_TYPE_DEFINITIONS', {}) # Get definitions
type_info = asset_type_definitions.get(asset_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for asset type '{asset_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for asset type '{asset_type}' in config.")
return None # Fallback if color key missing
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for asset type '{asset_type}' in config.")
return None # Fallback if color key missing
else:
# Optional: Add logging for missing asset type definition
# print(f"Warning: Asset type '{asset_type}' not found in ASSET_TYPE_DEFINITIONS.")
return None # Fallback if type not in config
# Optional: Add logging for missing asset type definition
# print(f"Warning: Asset type '{asset_type}' not found in ASSET_TYPE_DEFINITIONS.")
return None # Fallback if type not in config
except Exception: # Catch errors during config loading
return None # Fallback on error
else:
return None # Fallback if no asset_type determined
elif isinstance(item, FileRule):
# Determine effective item type: Prioritize override, then use base type
effective_item_type = item.item_type_override if item.item_type_override is not None else item.item_type
if effective_item_type:
type_info = FILE_TYPE_DEFINITIONS.get(effective_item_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for file type '{item_type}' in config.")
return None # Fallback for invalid hex
try:
base_config = load_base_config() # Load base config
file_type_definitions = base_config.get('FILE_TYPE_DEFINITIONS', {}) # Get definitions
type_info = file_type_definitions.get(effective_item_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for file type '{item_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for file type '{item_type}' in config.")
return None # Fallback if color key missing
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for file type '{item_type}' in config.")
return None # Fallback if color key missing
else:
# File types often don't have specific colors, so no warning needed unless debugging
return None # Fallback if type not in config
# File types often don't have specific colors, so no warning needed unless debugging
return None # Fallback if type not in config
except Exception: # Catch errors during config loading
return None # Fallback on error
else:
return None # Fallback if no item_type determined
else: # Other item types or if item is None
@@ -258,7 +270,14 @@ class UnifiedViewModel(QAbstractItemModel):
if column == self.COL_TARGET_ASSET:
return item.target_asset_name_override if item.target_asset_name_override is not None else ""
if column == self.COL_ITEM_TYPE:
return item.item_type_override if item.item_type_override else ""
# Reverted Logic: Display override if set, otherwise base type. Shows prefixed keys.
override = item.item_type_override
initial_type = item.item_type
if override is not None:
return override
else:
return initial_type if initial_type else ""
if column == self.COL_STATUS: return "" # Status (Not handled yet)
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
elif role == Qt.EditRole:
@@ -436,8 +455,47 @@ class UnifiedViewModel(QAbstractItemModel):
if new_value == "": new_value = None # Treat empty string as None
# Update item_type_override
if item.item_type_override != new_value:
item.item_type_override = new_value
changed = True
log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Original Override='{item.item_type_override}', Original Standard='{getattr(item, 'standard_map_type', 'N/A')}', New Value='{new_value}'") # DEBUG LOG - Added getattr for safety
old_override = item.item_type_override # Store old value for logging
item.item_type_override = new_value
changed = True
# --- BEGIN FIX: Update standard_map_type ---
try:
base_config = load_base_config()
file_type_definitions = base_config.get('FILE_TYPE_DEFINITIONS', {})
# Determine the type to look up (override first, then original)
type_to_lookup = new_value if new_value is not None else item.item_type
new_standard_type = None
if type_to_lookup:
type_info = file_type_definitions.get(type_to_lookup)
if type_info:
new_standard_type = type_info.get("standard_type")
# If standard_type itself is missing in the definition, treat as None or keep old? Let's default to None.
if new_standard_type is None:
log.warning(f"setData: No 'standard_type' defined for item type '{type_to_lookup}' in FILE_TYPE_DEFINITIONS.")
else:
log.warning(f"setData: Item type '{type_to_lookup}' not found in FILE_TYPE_DEFINITIONS.")
# Fallback: Keep the existing standard_map_type if lookup fails completely
new_standard_type = getattr(item, 'standard_map_type', None)
else:
# If both override and original type are None, standard type should be None
new_standard_type = None
# Update the standard_map_type if it changed or needs setting
current_standard_type = getattr(item, 'standard_map_type', None)
if current_standard_type != new_standard_type:
item.standard_map_type = new_standard_type
log.debug(f"setData: Updated standard_map_type from '{current_standard_type}' to '{new_standard_type}' for file '{Path(item.file_path).name}' based on type '{type_to_lookup}'")
# No need to set 'changed = True' again, already set above
except Exception as e:
log.exception(f"setData: Error updating standard_map_type for file '{Path(item.file_path).name}': {e}")
# --- END FIX ---
log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Final Override='{item.item_type_override}', Final Standard='{getattr(item, 'standard_map_type', 'N/A')}'") # DEBUG LOG - Updated
if changed: