1208 lines
63 KiB
Python
1208 lines
63 KiB
Python
|
|
import json
|
|
import os # Added for path operations
|
|
import copy # Added for deepcopy
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
|
|
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox,
|
|
QPushButton, QFileDialog, QLabel, QTableWidget,
|
|
QTableWidgetItem, QDialogButtonBox, QMessageBox, QListWidget,
|
|
QListWidgetItem, QFormLayout, QGroupBox, QStackedWidget,
|
|
QHeaderView, QSizePolicy
|
|
)
|
|
from PySide6.QtGui import QColor, QPainter
|
|
from PySide6.QtCore import Qt, QEvent
|
|
from PySide6.QtWidgets import QColorDialog, QStyledItemDelegate, QApplication
|
|
|
|
# Assuming configuration.py is in the parent directory or accessible
|
|
try:
|
|
from configuration import load_base_config, save_user_config, ConfigurationError
|
|
except ImportError:
|
|
# Fallback import for testing or different project structure
|
|
from ..configuration import load_base_config, save_user_config, ConfigurationError
|
|
|
|
|
|
# --- Custom Delegate for Color Editing ---
|
|
class ColorDelegate(QStyledItemDelegate):
|
|
def paint(self, painter: QPainter, option, index):
|
|
# Get color string from model data (EditRole is where we store it)
|
|
color_str = index.model().data(index, Qt.EditRole)
|
|
if isinstance(color_str, str) and color_str.startswith('#'):
|
|
color = QColor(color_str)
|
|
if color.isValid():
|
|
painter.fillRect(option.rect, color)
|
|
# Optionally draw text (e.g., the hex code) centered
|
|
# painter.drawText(option.rect, Qt.AlignCenter, color_str)
|
|
return # Prevent default painting
|
|
|
|
# Fallback to default painting if no valid color
|
|
super().paint(painter, option, index)
|
|
|
|
def createEditor(self, parent, option, index):
|
|
# No editor needed, handled by editorEvent
|
|
return None
|
|
|
|
def editorEvent(self, event, model, option, index):
|
|
if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
|
|
current_color_str = model.data(index, Qt.EditRole)
|
|
initial_color = QColor(current_color_str) if isinstance(current_color_str, str) else Qt.white
|
|
|
|
color = QColorDialog.getColor(initial_color, None, "Select Color")
|
|
|
|
if color.isValid():
|
|
new_color_str = color.name() # Get #RRGGBB format
|
|
model.setData(index, new_color_str, Qt.EditRole)
|
|
# Trigger update for the background role as well, although paint should handle it
|
|
# model.setData(index, QColor(new_color_str), Qt.BackgroundRole)
|
|
return True # Event handled
|
|
return False # Event not handled
|
|
|
|
def setModelData(self, editor, model, index):
|
|
# Not strictly needed as setData is called in editorEvent
|
|
pass
|
|
|
|
|
|
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() # Load settings FIRST
|
|
self.create_tabs() # THEN create widgets based on settings
|
|
self.populate_widgets_from_settings() # Populate widgets after creation
|
|
|
|
def load_settings(self):
|
|
"""Loads settings from the configuration file."""
|
|
try:
|
|
self.settings = load_base_config()
|
|
# Store a deep copy of the initial user-configurable settings for granular save.
|
|
# These are settings from the effective configuration (base + user + defs)
|
|
# that this dialog manages and are intended for user_settings.json.
|
|
# Exclude definitions that are stored in separate files or not directly managed here.
|
|
self.original_user_configurable_settings = {} # Initialize first
|
|
if self.settings: # Ensure settings were loaded
|
|
keys_to_copy = [
|
|
k for k in self.settings
|
|
if k not in ["ASSET_TYPE_DEFINITIONS", "FILE_TYPE_DEFINITIONS"]
|
|
]
|
|
# Create a temporary dictionary with only the keys to be copied
|
|
temp_original_settings = {
|
|
k: self.settings[k] for k in keys_to_copy if k in self.settings
|
|
}
|
|
self.original_user_configurable_settings = copy.deepcopy(temp_original_settings)
|
|
print("Original user-configurable settings (relevant parts) deep copied for comparison.") # Debug print
|
|
else:
|
|
# If self.settings is None or empty, original_user_configurable_settings remains an empty dict.
|
|
print("Settings not loaded or empty; original_user_configurable_settings initialized as empty.") # Debug print
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Loading Error", f"Failed to load configuration: {e}")
|
|
self.settings = {} # Use empty settings on failure
|
|
self.original_user_configurable_settings = {}
|
|
# Optionally disable save button or widgets if loading fails
|
|
self.button_box.button(QDialogButtonBox.Save).setEnabled(False)
|
|
|
|
def create_tabs(self):
|
|
"""Creates tabs based on the redesigned UI plan."""
|
|
if not self.settings:
|
|
return
|
|
|
|
# --- Create Tabs ---
|
|
self.tabs = {
|
|
"general": QWidget(),
|
|
"output_naming": QWidget(),
|
|
"image_processing": QWidget(),
|
|
"definitions": QWidget(),
|
|
"map_merging": QWidget(),
|
|
"postprocess_scripts": QWidget()
|
|
}
|
|
self.tab_widget.addTab(self.tabs["general"], "General")
|
|
self.tab_widget.addTab(self.tabs["output_naming"], "Output & Naming")
|
|
self.tab_widget.addTab(self.tabs["image_processing"], "Image Processing")
|
|
self.tab_widget.addTab(self.tabs["definitions"], "Definitions")
|
|
self.tab_widget.addTab(self.tabs["map_merging"], "Map Merging")
|
|
self.tab_widget.addTab(self.tabs["postprocess_scripts"], "Postprocess Scripts")
|
|
|
|
|
|
# --- Setup Layouts for Tabs ---
|
|
self.tab_layouts = {name: QVBoxLayout(tab) for name, tab in self.tabs.items()}
|
|
|
|
# --- Populate Tabs ---
|
|
self.populate_general_tab(self.tab_layouts["general"])
|
|
self.populate_output_naming_tab(self.tab_layouts["output_naming"])
|
|
self.populate_image_processing_tab(self.tab_layouts["image_processing"])
|
|
self.populate_definitions_tab(self.tab_layouts["definitions"])
|
|
self.populate_map_merging_tab(self.tab_layouts["map_merging"])
|
|
self.populate_postprocess_scripts_tab(self.tab_layouts["postprocess_scripts"])
|
|
|
|
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):
|
|
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): # Handle simple lists as comma-separated strings
|
|
widget = QLineEdit(", ".join(map(str, value)))
|
|
# Complex dicts/lists like ASSET_TYPE_DEFINITIONS, MAP_MERGE_RULES etc. are handled in dedicated methods
|
|
|
|
if widget:
|
|
parent_layout.addRow(label, 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_general_tab(self, layout):
|
|
"""Populates the General tab according to the plan."""
|
|
# Clear existing widgets in the layout first
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
widget = item.widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
sub_layout = item.layout()
|
|
if sub_layout:
|
|
# Basic clearing for sub-layouts like QHBoxLayout used below
|
|
while sub_layout.count():
|
|
sub_item = sub_layout.takeAt(0)
|
|
sub_widget = sub_item.widget()
|
|
if sub_widget:
|
|
sub_widget.deleteLater()
|
|
|
|
# Clear any potentially lingering widget references for this tab
|
|
self.widgets.pop("OUTPUT_BASE_DIR", None)
|
|
self.widgets.pop("EXTRA_FILES_SUBDIR", None)
|
|
self.widgets.pop("METADATA_FILENAME", None)
|
|
|
|
form_layout = QFormLayout()
|
|
|
|
# 1. OUTPUT_BASE_DIR: QLineEdit + QPushButton
|
|
output_dir_label = QLabel("Output Base Directory:")
|
|
output_dir_edit = QLineEdit()
|
|
output_dir_button = QPushButton("Browse...")
|
|
# Ensure lambda captures the correct widget reference
|
|
output_dir_button.clicked.connect(
|
|
lambda checked=False, w=output_dir_edit: self.browse_path(w, "OUTPUT_BASE_DIR", is_dir=True)
|
|
)
|
|
output_dir_layout = QHBoxLayout()
|
|
output_dir_layout.addWidget(output_dir_edit)
|
|
output_dir_layout.addWidget(output_dir_button)
|
|
form_layout.addRow(output_dir_label, output_dir_layout)
|
|
self.widgets["OUTPUT_BASE_DIR"] = output_dir_edit
|
|
|
|
# 2. EXTRA_FILES_SUBDIR: QLineEdit
|
|
extra_subdir_label = QLabel("Subdirectory for Extra Files:")
|
|
extra_subdir_edit = QLineEdit()
|
|
form_layout.addRow(extra_subdir_label, extra_subdir_edit)
|
|
self.widgets["EXTRA_FILES_SUBDIR"] = extra_subdir_edit
|
|
|
|
# 3. METADATA_FILENAME: QLineEdit
|
|
metadata_label = QLabel("Metadata Filename:")
|
|
metadata_edit = QLineEdit()
|
|
form_layout.addRow(metadata_label, metadata_edit)
|
|
self.widgets["METADATA_FILENAME"] = metadata_edit
|
|
|
|
layout.addLayout(form_layout)
|
|
layout.addStretch() # Keep stretch at the end
|
|
|
|
def populate_output_naming_tab(self, layout):
|
|
"""Populates the Output & Naming tab according to the plan."""
|
|
# Clear existing widgets in the layout first
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
widget = item.widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
sub_layout = item.layout()
|
|
if sub_layout:
|
|
# Basic clearing for sub-layouts
|
|
while sub_layout.count():
|
|
sub_item = sub_layout.takeAt(0)
|
|
sub_widget = sub_item.widget()
|
|
if sub_widget:
|
|
sub_widget.deleteLater()
|
|
sub_sub_layout = sub_item.layout()
|
|
if sub_sub_layout: # Clear nested layouts (like the button HBox)
|
|
while sub_sub_layout.count():
|
|
ss_item = sub_sub_layout.takeAt(0)
|
|
ss_widget = ss_item.widget()
|
|
if ss_widget:
|
|
ss_widget.deleteLater()
|
|
|
|
|
|
# Clear potentially lingering widget references for this tab
|
|
self.widgets.pop("TARGET_FILENAME_PATTERN", None)
|
|
self.widgets.pop("RESPECT_VARIANT_MAP_TYPES", None)
|
|
self.widgets.pop("ASPECT_RATIO_DECIMALS", None)
|
|
|
|
main_tab_layout = QVBoxLayout()
|
|
|
|
form_layout = QFormLayout()
|
|
|
|
# 1. TARGET_FILENAME_PATTERN: QLineEdit
|
|
target_filename_label = QLabel("Output Filename Pattern:")
|
|
target_filename_edit = QLineEdit()
|
|
target_filename_edit.setToolTip(
|
|
"Define the output filename structure.\n"
|
|
"Placeholders: {asset_name}, {map_type}, {resolution}, {variant}, {udim}"
|
|
)
|
|
form_layout.addRow(target_filename_label, target_filename_edit)
|
|
self.widgets["TARGET_FILENAME_PATTERN"] = target_filename_edit
|
|
|
|
# 2. RESPECT_VARIANT_MAP_TYPES: QLineEdit
|
|
respect_variant_label = QLabel("Map Types Respecting Variants (comma-separated):")
|
|
respect_variant_edit = QLineEdit()
|
|
form_layout.addRow(respect_variant_label, respect_variant_edit)
|
|
self.widgets["RESPECT_VARIANT_MAP_TYPES"] = respect_variant_edit
|
|
|
|
# 3. ASPECT_RATIO_DECIMALS: QSpinBox
|
|
aspect_ratio_label = QLabel("Aspect Ratio Precision (Decimals):")
|
|
aspect_ratio_spinbox = QSpinBox()
|
|
aspect_ratio_spinbox.setRange(0, 6) # Min: 0, Max: ~6
|
|
form_layout.addRow(aspect_ratio_label, aspect_ratio_spinbox)
|
|
self.widgets["ASPECT_RATIO_DECIMALS"] = aspect_ratio_spinbox
|
|
|
|
main_tab_layout.addLayout(form_layout)
|
|
|
|
layout.addLayout(main_tab_layout)
|
|
layout.addStretch() # Keep stretch at the end of the tab's main layout
|
|
|
|
|
|
def populate_image_processing_tab(self, layout):
|
|
"""Populates the Image Processing tab according to the plan."""
|
|
# Clear existing widgets in the layout first
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
widget = item.widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
sub_layout = item.layout()
|
|
if sub_layout:
|
|
# Basic clearing for sub-layouts
|
|
while sub_layout.count():
|
|
sub_item = sub_layout.takeAt(0)
|
|
sub_widget = sub_item.widget()
|
|
if sub_widget:
|
|
sub_widget.deleteLater()
|
|
sub_sub_layout = sub_item.layout()
|
|
if sub_sub_layout: # Clear nested layouts (like button HBox)
|
|
while sub_sub_layout.count():
|
|
ss_item = sub_sub_layout.takeAt(0)
|
|
ss_widget = ss_item.widget()
|
|
if ss_widget:
|
|
ss_widget.deleteLater()
|
|
|
|
# Clear potentially lingering widget references for this tab
|
|
keys_to_clear = [
|
|
"IMAGE_RESOLUTIONS_TABLE", "CALCULATE_STATS_RESOLUTION",
|
|
"PNG_COMPRESSION_LEVEL", "JPG_QUALITY",
|
|
"RESOLUTION_THRESHOLD_FOR_JPG", "OUTPUT_FORMAT_8BIT",
|
|
"OUTPUT_FORMAT_16BIT_PRIMARY", "OUTPUT_FORMAT_16BIT_FALLBACK"
|
|
]
|
|
for key in keys_to_clear:
|
|
self.widgets.pop(key, None)
|
|
|
|
main_tab_layout = QVBoxLayout()
|
|
|
|
# --- IMAGE_RESOLUTIONS Section ---
|
|
resolutions_layout = QVBoxLayout()
|
|
resolutions_label = QLabel("Defined Image Resolutions")
|
|
resolutions_layout.addWidget(resolutions_label)
|
|
|
|
resolutions_table = QTableWidget()
|
|
resolutions_table.setColumnCount(2)
|
|
resolutions_table.setHorizontalHeaderLabels(["Name", "Resolution (px)"])
|
|
resolutions_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Allow table to expand vertically
|
|
resolutions_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch) # Stretch Name column
|
|
resolutions_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents) # Resize Resolution column to contents
|
|
# TODO: Implement custom delegate for "Resolution (px)" column
|
|
# TODO: Connect add/remove buttons signals
|
|
resolutions_layout.addWidget(resolutions_table)
|
|
self.widgets["IMAGE_RESOLUTIONS_TABLE"] = resolutions_table
|
|
|
|
resolutions_button_layout = QHBoxLayout()
|
|
add_res_button = QPushButton("Add Row")
|
|
remove_res_button = QPushButton("Remove Row")
|
|
resolutions_button_layout.addWidget(add_res_button)
|
|
resolutions_button_layout.addWidget(remove_res_button)
|
|
resolutions_button_layout.addStretch() # Push buttons left
|
|
resolutions_layout.addLayout(resolutions_button_layout)
|
|
|
|
main_tab_layout.addLayout(resolutions_layout)
|
|
|
|
# --- Form Layout for other settings ---
|
|
form_layout = QFormLayout()
|
|
|
|
# CALCULATE_STATS_RESOLUTION: QComboBox
|
|
stats_res_label = QLabel("Resolution for Stats Calculation:")
|
|
stats_res_combo = QComboBox()
|
|
# Population deferred - will be populated from IMAGE_RESOLUTIONS_TABLE
|
|
form_layout.addRow(stats_res_label, stats_res_combo)
|
|
self.widgets["CALCULATE_STATS_RESOLUTION"] = stats_res_combo
|
|
|
|
# PNG_COMPRESSION_LEVEL: QSpinBox
|
|
png_level_label = QLabel("PNG Compression Level:")
|
|
png_level_spinbox = QSpinBox()
|
|
png_level_spinbox.setRange(0, 9)
|
|
form_layout.addRow(png_level_label, png_level_spinbox)
|
|
self.widgets["PNG_COMPRESSION_LEVEL"] = png_level_spinbox
|
|
|
|
# JPG_QUALITY: QSpinBox
|
|
jpg_quality_label = QLabel("JPG Quality:")
|
|
jpg_quality_spinbox = QSpinBox()
|
|
jpg_quality_spinbox.setRange(1, 100)
|
|
form_layout.addRow(jpg_quality_label, jpg_quality_spinbox)
|
|
self.widgets["JPG_QUALITY"] = jpg_quality_spinbox
|
|
|
|
# RESOLUTION_THRESHOLD_FOR_JPG: QComboBox
|
|
jpg_threshold_label = QLabel("Use JPG Above Resolution:")
|
|
jpg_threshold_combo = QComboBox()
|
|
# Population deferred - will be populated from IMAGE_RESOLUTIONS_TABLE + "Never"/"Always"
|
|
form_layout.addRow(jpg_threshold_label, jpg_threshold_combo)
|
|
self.widgets["RESOLUTION_THRESHOLD_FOR_JPG"] = jpg_threshold_combo
|
|
|
|
# OUTPUT_FORMAT_8BIT: QComboBox
|
|
format_8bit_label = QLabel("Output Format (8-bit):")
|
|
format_8bit_combo = QComboBox()
|
|
format_8bit_combo.addItems(["png", "jpg"])
|
|
form_layout.addRow(format_8bit_label, format_8bit_combo)
|
|
self.widgets["OUTPUT_FORMAT_8BIT"] = format_8bit_combo
|
|
|
|
# OUTPUT_FORMAT_16BIT_PRIMARY: QComboBox
|
|
format_16bit_primary_label = QLabel("Primary Output Format (16-bit+):")
|
|
format_16bit_primary_combo = QComboBox()
|
|
format_16bit_primary_combo.addItems(["png", "exr", "tif"])
|
|
form_layout.addRow(format_16bit_primary_label, format_16bit_primary_combo)
|
|
self.widgets["OUTPUT_FORMAT_16BIT_PRIMARY"] = format_16bit_primary_combo
|
|
|
|
# OUTPUT_FORMAT_16BIT_FALLBACK: QComboBox
|
|
format_16bit_fallback_label = QLabel("Fallback Output Format (16-bit+):")
|
|
format_16bit_fallback_combo = QComboBox()
|
|
format_16bit_fallback_combo.addItems(["png", "exr", "tif"])
|
|
form_layout.addRow(format_16bit_fallback_label, format_16bit_fallback_combo)
|
|
self.widgets["OUTPUT_FORMAT_16BIT_FALLBACK"] = format_16bit_fallback_combo
|
|
|
|
main_tab_layout.addLayout(form_layout)
|
|
|
|
layout.addLayout(main_tab_layout)
|
|
layout.addStretch() # Keep stretch at the end of the tab's main layout
|
|
|
|
def populate_definitions_tab(self, layout):
|
|
"""Populates the Definitions tab according to the plan."""
|
|
# Clear existing widgets in the layout first
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
widget = item.widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
sub_layout = item.layout()
|
|
if sub_layout:
|
|
# Recursively clear sub-layouts
|
|
while sub_layout.count():
|
|
sub_item = sub_layout.takeAt(0)
|
|
sub_widget = sub_item.widget()
|
|
if sub_widget:
|
|
sub_widget.deleteLater()
|
|
sub_sub_layout = sub_item.layout()
|
|
if sub_sub_layout:
|
|
# Clear nested layouts (like button HBox or inner tabs)
|
|
while sub_sub_layout.count():
|
|
ss_item = sub_sub_layout.takeAt(0)
|
|
ss_widget = ss_item.widget()
|
|
if ss_widget:
|
|
ss_widget.deleteLater()
|
|
# Add more levels if necessary, but this covers the planned structure
|
|
|
|
# Clear potentially lingering widget references for this tab
|
|
self.widgets.pop("DEFAULT_ASSET_CATEGORY", None)
|
|
self.widgets.pop("ASSET_TYPE_DEFINITIONS_TABLE", None)
|
|
self.widgets.pop("FILE_TYPE_DEFINITIONS_TABLE", None)
|
|
# Remove references to widgets no longer used in this tab's structure
|
|
self.widgets.pop("MAP_BIT_DEPTH_RULES_TABLE", None)
|
|
|
|
|
|
overall_layout = QVBoxLayout()
|
|
|
|
# --- Top Widget: DEFAULT_ASSET_CATEGORY ---
|
|
default_category_layout = QHBoxLayout() # Use QHBox for label + combo
|
|
default_category_label = QLabel("Default Asset Category:")
|
|
default_category_combo = QComboBox()
|
|
# Population is deferred, will happen in populate_widgets_from_settings
|
|
default_category_layout.addWidget(default_category_label)
|
|
default_category_layout.addWidget(default_category_combo)
|
|
default_category_layout.addStretch() # Push label/combo left
|
|
overall_layout.addLayout(default_category_layout)
|
|
self.widgets["DEFAULT_ASSET_CATEGORY"] = default_category_combo
|
|
|
|
# --- Bottom Widget: Inner QTabWidget ---
|
|
inner_tab_widget = QTabWidget()
|
|
inner_tab_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Allow inner tabs to expand
|
|
overall_layout.addWidget(inner_tab_widget)
|
|
|
|
# --- Inner Tab 1: Asset Types ---
|
|
asset_types_tab = QWidget()
|
|
asset_types_layout = QVBoxLayout(asset_types_tab)
|
|
inner_tab_widget.addTab(asset_types_tab, "Asset Types")
|
|
|
|
# Asset Types Table
|
|
asset_types_table = QTableWidget()
|
|
asset_types_table.setColumnCount(4)
|
|
asset_types_table.setHorizontalHeaderLabels(["Type Name", "Description", "Color", "Examples (comma-sep.)"])
|
|
asset_types_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Allow table to expand
|
|
# Set column resize modes
|
|
header = asset_types_table.horizontalHeader()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Type Name
|
|
header.setSectionResizeMode(1, QHeaderView.Stretch) # Description - Stretch
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Color
|
|
header.setSectionResizeMode(3, QHeaderView.Stretch) # Examples - Stretch
|
|
|
|
# Apply Color Delegate
|
|
color_delegate = ColorDelegate(self)
|
|
asset_types_table.setItemDelegateForColumn(2, color_delegate) # Column 2 is "Color"
|
|
|
|
# TODO: Implement custom delegate for "Examples" later
|
|
asset_types_layout.addWidget(asset_types_table)
|
|
self.widgets["ASSET_TYPE_DEFINITIONS_TABLE"] = asset_types_table
|
|
|
|
# Asset Types Add/Remove Buttons
|
|
asset_types_button_layout = QHBoxLayout()
|
|
add_asset_type_button = QPushButton("Add Row")
|
|
remove_asset_type_button = QPushButton("Remove Row")
|
|
# TODO: Connect button signals later
|
|
asset_types_button_layout.addWidget(add_asset_type_button)
|
|
asset_types_button_layout.addWidget(remove_asset_type_button)
|
|
asset_types_button_layout.addStretch()
|
|
asset_types_layout.addLayout(asset_types_button_layout)
|
|
|
|
# --- Inner Tab 2: File Types ---
|
|
file_types_tab = QWidget()
|
|
file_types_layout = QVBoxLayout(file_types_tab)
|
|
inner_tab_widget.addTab(file_types_tab, "File Types")
|
|
|
|
# File Types Table
|
|
file_types_table = QTableWidget()
|
|
file_types_table.setColumnCount(6)
|
|
file_types_table.setHorizontalHeaderLabels(["Type ID", "Description", "Color", "Examples (comma-sep.)", "Standard Type", "Bit Depth Rule"])
|
|
file_types_table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) # Allow table to expand
|
|
# Set column resize modes
|
|
header = file_types_table.horizontalHeader()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # Type ID
|
|
header.setSectionResizeMode(1, QHeaderView.Stretch) # Description - Stretch
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Color
|
|
header.setSectionResizeMode(3, QHeaderView.Stretch) # Examples - Stretch
|
|
header.setSectionResizeMode(4, QHeaderView.ResizeToContents) # Standard Type
|
|
header.setSectionResizeMode(5, QHeaderView.ResizeToContents) # Bit Depth Rule
|
|
|
|
# Apply Color Delegate (reuse instance or create new)
|
|
# color_delegate = ColorDelegate(self) # Reuse if appropriate
|
|
file_types_table.setItemDelegateForColumn(2, color_delegate) # Column 2 is "Color"
|
|
|
|
# TODO: Implement custom delegates for "Examples", "Standard Type", "Bit Depth Rule" later
|
|
file_types_layout.addWidget(file_types_table)
|
|
self.widgets["FILE_TYPE_DEFINITIONS_TABLE"] = file_types_table
|
|
|
|
# File Types Add/Remove Buttons
|
|
file_types_button_layout = QHBoxLayout()
|
|
add_file_type_button = QPushButton("Add Row")
|
|
remove_file_type_button = QPushButton("Remove Row")
|
|
# TODO: Connect button signals later
|
|
file_types_button_layout.addWidget(add_file_type_button)
|
|
file_types_button_layout.addWidget(remove_file_type_button)
|
|
file_types_button_layout.addStretch()
|
|
file_types_layout.addLayout(file_types_button_layout)
|
|
|
|
layout.addLayout(overall_layout)
|
|
layout.addStretch() # Keep stretch at the end of the tab's main layout
|
|
|
|
|
|
def populate_map_merging_tab(self, layout):
|
|
"""Populates the Map Merging tab according to the plan."""
|
|
# Clear existing widgets in the layout first
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
widget = item.widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
sub_layout = item.layout()
|
|
if sub_layout:
|
|
# Basic clearing for sub-layouts
|
|
while sub_layout.count():
|
|
sub_item = sub_layout.takeAt(0)
|
|
sub_widget = sub_item.widget()
|
|
if sub_widget:
|
|
sub_widget.deleteLater()
|
|
# Clear nested layouts if needed (e.g., button layout)
|
|
sub_sub_layout = sub_item.layout()
|
|
if sub_sub_layout:
|
|
while sub_sub_layout.count():
|
|
ss_item = sub_sub_layout.takeAt(0)
|
|
ss_widget = ss_item.widget()
|
|
if ss_widget:
|
|
ss_widget.deleteLater()
|
|
|
|
# Clear potentially lingering widget references for this tab
|
|
self.widgets.pop("MAP_MERGE_RULES_DATA", None)
|
|
# Clear references to the list and details group if they exist
|
|
if hasattr(self, 'merge_rules_list'):
|
|
del self.merge_rules_list
|
|
if hasattr(self, 'merge_rule_details_group'):
|
|
del self.merge_rule_details_group
|
|
if hasattr(self, 'merge_rule_details_layout'):
|
|
del self.merge_rule_details_layout
|
|
if hasattr(self, 'merge_rule_widgets'):
|
|
del self.merge_rule_widgets
|
|
|
|
|
|
# Layout: QHBoxLayout.
|
|
h_layout = QHBoxLayout()
|
|
layout.addLayout(h_layout)
|
|
|
|
# Left Side: QListWidget displaying output_map_type for each rule.
|
|
left_layout = QVBoxLayout()
|
|
left_layout.addWidget(QLabel("Merge Rules:"))
|
|
self.merge_rules_list = QListWidget()
|
|
self.merge_rules_list.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) # Allow list to expand vertically
|
|
self.merge_rules_list.currentItemChanged.connect(self.display_merge_rule_details)
|
|
left_layout.addWidget(self.merge_rules_list)
|
|
|
|
button_layout = QHBoxLayout()
|
|
add_button = QPushButton("Add Rule")
|
|
remove_button = QPushButton("Remove Rule")
|
|
# TODO: Connect add/remove buttons
|
|
button_layout.addWidget(add_button)
|
|
button_layout.addWidget(remove_button)
|
|
left_layout.addLayout(button_layout)
|
|
|
|
h_layout.addLayout(left_layout, 1) # Give list more space
|
|
|
|
# Right Side: QStackedWidget or dynamically populated QWidget showing details
|
|
self.merge_rule_details_group = QGroupBox("Rule Details")
|
|
self.merge_rule_details_group.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) # Allow groupbox to expand horizontally
|
|
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
|
|
|
|
if "MAP_MERGE_RULES" in self.settings:
|
|
self.populate_merge_rules_list(self.settings["MAP_MERGE_RULES"])
|
|
self.widgets["MAP_MERGE_RULES_DATA"] = self.settings["MAP_MERGE_RULES"] # Store original data reference
|
|
|
|
layout.addStretch()
|
|
|
|
|
|
def populate_postprocess_scripts_tab(self, layout):
|
|
"""Populates the Postprocess Scripts tab according to the plan."""
|
|
# Clear existing widgets in the layout first
|
|
while layout.count():
|
|
item = layout.takeAt(0)
|
|
widget = item.widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
sub_layout = item.layout()
|
|
if sub_layout:
|
|
# Basic clearing for sub-layouts (like the QHBoxLayouts used below)
|
|
while sub_layout.count():
|
|
sub_item = sub_layout.takeAt(0)
|
|
sub_widget = sub_item.widget()
|
|
if sub_widget:
|
|
sub_widget.deleteLater()
|
|
|
|
# Clear potentially lingering widget references for this tab
|
|
self.widgets.pop("DEFAULT_NODEGROUP_BLEND_PATH", None)
|
|
self.widgets.pop("DEFAULT_MATERIALS_BLEND_PATH", None)
|
|
self.widgets.pop("BLENDER_EXECUTABLE_PATH", None)
|
|
|
|
form_layout = QFormLayout()
|
|
|
|
# 1. DEFAULT_NODEGROUP_BLEND_PATH: QLineEdit + QPushButton
|
|
nodegroup_label = QLabel("Default Node Group Library (.blend):")
|
|
nodegroup_widget = QLineEdit()
|
|
nodegroup_button = QPushButton("Browse...")
|
|
nodegroup_button.clicked.connect(
|
|
lambda checked=False, w=nodegroup_widget: self.browse_path(w, "DEFAULT_NODEGROUP_BLEND_PATH")
|
|
)
|
|
nodegroup_layout = QHBoxLayout()
|
|
nodegroup_layout.addWidget(nodegroup_widget)
|
|
nodegroup_layout.addWidget(nodegroup_button)
|
|
form_layout.addRow(nodegroup_label, nodegroup_layout)
|
|
self.widgets["DEFAULT_NODEGROUP_BLEND_PATH"] = nodegroup_widget
|
|
|
|
# 2. DEFAULT_MATERIALS_BLEND_PATH: QLineEdit + QPushButton
|
|
materials_label = QLabel("Default Materials Library (.blend):")
|
|
materials_widget = QLineEdit()
|
|
materials_button = QPushButton("Browse...")
|
|
materials_button.clicked.connect(
|
|
lambda checked=False, w=materials_widget: self.browse_path(w, "DEFAULT_MATERIALS_BLEND_PATH")
|
|
)
|
|
materials_layout = QHBoxLayout()
|
|
materials_layout.addWidget(materials_widget)
|
|
materials_layout.addWidget(materials_button)
|
|
form_layout.addRow(materials_label, materials_layout)
|
|
self.widgets["DEFAULT_MATERIALS_BLEND_PATH"] = materials_widget
|
|
|
|
# 3. BLENDER_EXECUTABLE_PATH: QLineEdit + QPushButton
|
|
blender_label = QLabel("Blender Executable Path:")
|
|
blender_widget = QLineEdit()
|
|
blender_button = QPushButton("Browse...")
|
|
blender_button.clicked.connect(
|
|
lambda checked=False, w=blender_widget: self.browse_path(w, "BLENDER_EXECUTABLE_PATH")
|
|
)
|
|
blender_layout = QHBoxLayout()
|
|
blender_layout.addWidget(blender_widget)
|
|
blender_layout.addWidget(blender_button)
|
|
form_layout.addRow(blender_label, blender_layout)
|
|
self.widgets["BLENDER_EXECUTABLE_PATH"] = blender_widget
|
|
|
|
layout.addLayout(form_layout)
|
|
layout.addStretch()
|
|
|
|
def create_asset_definitions_table_widget(self, layout, definitions_data):
|
|
"""Creates a QTableWidget for editing asset type definitions."""
|
|
table = QTableWidget()
|
|
# Columns: "Type Name", "Description", "Color", "Examples (comma-sep.)"
|
|
table.setColumnCount(4)
|
|
table.setHorizontalHeaderLabels(["Type Name", "Description", "Color", "Examples (comma-sep.)"])
|
|
# Row count will be set when populating
|
|
|
|
# TODO: Implement "Add Row" and "Remove Row" buttons
|
|
# TODO: Implement custom delegate for "Color" column (QPushButton)
|
|
# TODO: Implement custom delegate for "Examples" column (QLineEdit)
|
|
|
|
layout.addWidget(table)
|
|
self.widgets["ASSET_TYPE_DEFINITIONS_TABLE"] = table
|
|
|
|
def create_file_type_definitions_table_widget(self, layout, definitions_data):
|
|
"""Creates a QTableWidget for editing file type definitions."""
|
|
table = QTableWidget()
|
|
# Columns: "Type ID", "Description", "Color", "Examples (comma-sep.)", "Standard Type", "Bit Depth Rule"
|
|
table.setColumnCount(6)
|
|
table.setHorizontalHeaderLabels(["Type ID", "Description", "Color", "Examples (comma-sep.)", "Standard Type", "Bit Depth Rule"])
|
|
# Row count will be set when populating
|
|
|
|
# TODO: Implement "Add Row" and "Remove Row" buttons
|
|
# TODO: Implement custom delegate for "Color" column (QPushButton)
|
|
# TODO: Implement custom delegate for "Examples" column (QLineEdit)
|
|
# TODO: Implement custom delegate for "Standard Type" column (QComboBox)
|
|
# TODO: Implement custom delegate for "Bit Depth Rule" column (QComboBox)
|
|
|
|
layout.addWidget(table)
|
|
self.widgets["FILE_TYPE_DEFINITIONS_TABLE"] = table
|
|
|
|
def create_image_resolutions_table_widget(self, layout, resolutions_data):
|
|
"""Creates a QTableWidget for editing image resolutions."""
|
|
table = QTableWidget()
|
|
# Columns: "Name", "Resolution (px)"
|
|
table.setColumnCount(2)
|
|
table.setHorizontalHeaderLabels(["Name", "Resolution (px)"])
|
|
# Row count will be set when populating
|
|
|
|
# TODO: Implement "Add Row" and "Remove Row" buttons
|
|
# TODO: Implement custom delegate for "Resolution (px)" column (e.g., QLineEdit with validation or two SpinBoxes)
|
|
|
|
layout.addWidget(table)
|
|
self.widgets["IMAGE_RESOLUTIONS_TABLE"] = table
|
|
|
|
def create_map_bit_depth_rules_table_widget(self, layout, rules_data: dict):
|
|
"""Creates a QTableWidget for editing map bit depth rules (Map Type -> Rule)."""
|
|
table = QTableWidget()
|
|
# Columns: "Map Type", "Rule (respect/force_8bit)"
|
|
table.setColumnCount(2)
|
|
table.setHorizontalHeaderLabels(["Map Type", "Rule (respect/force_8bit)"])
|
|
# Row count will be set when populating
|
|
|
|
# TODO: Implement "Add Row" and "Remove Row" buttons
|
|
# TODO: Implement custom delegate for "Rule" column (QComboBox)
|
|
|
|
layout.addWidget(table)
|
|
self.widgets["MAP_BIT_DEPTH_RULES_TABLE"] = table
|
|
|
|
|
|
def create_map_merge_rules_widget(self, layout, rules_data):
|
|
"""Creates the Map Merging UI (ListWidget + Details Form) according to the plan."""
|
|
# This method is called by populate_map_merging_tab and sets up the QHBoxLayout,
|
|
# ListWidget, and details group box. The details population is handled by
|
|
# display_merge_rule_details.
|
|
pass # Structure is already set up in populate_map_merging_tab
|
|
|
|
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:
|
|
# Use output_map_type for the display text
|
|
item_text = rule.get("output_map_type", "Unnamed Rule")
|
|
item = QListWidgetItem(item_text)
|
|
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 according to the plan."""
|
|
# 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:
|
|
# Rule Detail Form:
|
|
# output_map_type: QLineEdit. Label: "Output Map Type Name".
|
|
if "output_map_type" in rule_data:
|
|
label = QLabel("Output Map Type Name:")
|
|
widget = QLineEdit(rule_data["output_map_type"])
|
|
self.merge_rule_details_layout.addRow(label, widget)
|
|
self.merge_rule_widgets["output_map_type"] = widget
|
|
|
|
# inputs: QTableWidget (Fixed Rows: R, G, B, A. Columns: "Channel", "Input Map Type"). Label: "Channel Inputs".
|
|
if "inputs" in rule_data and isinstance(rule_data["inputs"], dict):
|
|
group = QGroupBox("Channel Inputs")
|
|
group_layout = QVBoxLayout(group)
|
|
input_table = QTableWidget(4, 2) # R, G, B, A rows, 2 columns
|
|
input_table.setHorizontalHeaderLabels(["Channel", "Input Map Type"])
|
|
input_table.setVerticalHeaderLabels(["R", "G", "B", "A"])
|
|
|
|
# Populate table with current input data
|
|
channels = ["R", "G", "B", "A"]
|
|
for i, channel in enumerate(channels):
|
|
input_map_type = rule_data["inputs"].get(channel, "")
|
|
input_table.setItem(i, 0, QTableWidgetItem(channel))
|
|
# TODO: Implement custom delegate for "Input Map Type" column (QComboBox)
|
|
input_table.setItem(i, 1, QTableWidgetItem(input_map_type)) # Placeholder
|
|
|
|
group_layout.addWidget(input_table)
|
|
self.merge_rule_details_layout.addRow(group)
|
|
self.merge_rule_widgets["inputs_table"] = input_table
|
|
|
|
|
|
# defaults: QTableWidget (Fixed Rows: R, G, B, A. Columns: "Channel", "Default Value"). Label: "Channel Defaults (if input missing)".
|
|
if "defaults" in rule_data and isinstance(rule_data["defaults"], dict):
|
|
group = QGroupBox("Channel Defaults (if input missing)")
|
|
group_layout = QVBoxLayout(group)
|
|
defaults_table = QTableWidget(4, 2) # R, G, B, A rows, 2 columns
|
|
defaults_table.setHorizontalHeaderLabels(["Channel", "Default Value"])
|
|
defaults_table.setVerticalHeaderLabels(["R", "G", "B", "A"])
|
|
|
|
# Populate table with current default data
|
|
channels = ["R", "G", "B", "A"]
|
|
for i, channel in enumerate(channels):
|
|
default_value = rule_data["defaults"].get(channel, 0.0)
|
|
defaults_table.setItem(i, 0, QTableWidgetItem(channel))
|
|
# TODO: Implement custom delegate for "Default Value" column (QDoubleSpinBox)
|
|
defaults_table.setItem(i, 1, QTableWidgetItem(str(default_value))) # Placeholder
|
|
|
|
group_layout.addWidget(defaults_table)
|
|
self.merge_rule_details_layout.addRow(group)
|
|
self.merge_rule_widgets["defaults_table"] = defaults_table
|
|
|
|
|
|
# output_bit_depth: QComboBox (Options: "respect_inputs", "force_8bit", "force_16bit"). Label: "Output Bit Depth".
|
|
if "output_bit_depth" in rule_data:
|
|
label = QLabel("Output Bit Depth:")
|
|
widget = QComboBox()
|
|
options = ["respect_inputs", "force_8bit", "force_16bit"]
|
|
widget.addItems(options)
|
|
if rule_data["output_bit_depth"] in options:
|
|
widget.setCurrentText(rule_data["output_bit_depth"])
|
|
self.merge_rule_details_layout.addRow(label, widget)
|
|
self.merge_rule_widgets["output_bit_depth"] = widget
|
|
|
|
# Add stretch to push widgets to the top
|
|
self.merge_rule_details_layout.addStretch()
|
|
|
|
|
|
def browse_path(self, widget, key, is_dir=False):
|
|
"""Opens a file or directory dialog based on the setting key and is_dir flag."""
|
|
if is_dir:
|
|
path = QFileDialog.getExistingDirectory(self, "Select Directory", widget.text())
|
|
elif 'BLEND_PATH' in key.upper():
|
|
path, _ = QFileDialog.getOpenFileName(self, "Select File", widget.text(), "Blender Files (*.blend)")
|
|
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, compares them to the original loaded settings,
|
|
and saves only the changed values to config/user_settings.json, preserving
|
|
other existing user settings.
|
|
"""
|
|
# 1a. Load Current Target File (user_settings.json)
|
|
# Assuming configuration.py and this file are structured such that
|
|
# 'config/user_settings.json' is the correct relative path from the workspace root.
|
|
# TODO: Ideally, get this path from the configuration module if it provides a constant.
|
|
user_settings_path = os.path.join("config", "user_settings.json")
|
|
|
|
target_file_content = {}
|
|
if os.path.exists(user_settings_path):
|
|
try:
|
|
with open(user_settings_path, 'r') as f:
|
|
target_file_content = json.load(f)
|
|
except json.JSONDecodeError:
|
|
QMessageBox.warning(self, "Warning",
|
|
f"File {user_settings_path} is corrupted or not valid JSON. "
|
|
f"It will be overwritten if changes are saved.")
|
|
target_file_content = {} # Start fresh if corrupted
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error Loading User Settings",
|
|
f"Failed to load {user_settings_path}: {e}. "
|
|
f"Proceeding with empty user settings for this save operation.")
|
|
target_file_content = {}
|
|
|
|
# 1b. Get current settings from UI by populating a full settings dictionary
|
|
# This `full_ui_state` will mirror the structure of `self.settings` but with current UI values.
|
|
full_ui_state = copy.deepcopy(self.settings) # Start with the loaded settings structure
|
|
|
|
# --- Populate full_ui_state from ALL widgets (adapted from original save_settings logic) ---
|
|
# This loop iterates through all widgets and updates `full_ui_state` with their current values.
|
|
# It handles simple widgets and complex ones like tables (though table data for definitions
|
|
# won't end up in user_settings.json).
|
|
for widget_config_key, widget_obj in self.widgets.items():
|
|
# `widget_config_key` is the key used in `self.widgets` (e.g., "OUTPUT_BASE_DIR", "IMAGE_RESOLUTIONS_TABLE")
|
|
# `keys_path` is for navigating the `full_ui_state` dictionary if `widget_config_key` implies a path.
|
|
# For most simple widgets, `widget_config_key` is a direct top-level key.
|
|
keys_path = widget_config_key.split('.')
|
|
current_level_dict = full_ui_state
|
|
|
|
# Navigate to the correct dictionary level if widget_config_key is a path like "general.foo"
|
|
# For simple keys like "OUTPUT_BASE_DIR", this loop runs once for the key itself.
|
|
for i, part_of_key in enumerate(keys_path):
|
|
if i == len(keys_path) - 1: # Last part of the key, time to set the value
|
|
# Handle simple widgets
|
|
if isinstance(widget_obj, QLineEdit):
|
|
if widget_config_key == "RESPECT_VARIANT_MAP_TYPES": # Special list handling
|
|
current_level_dict[part_of_key] = [item.strip() for item in widget_obj.text().split(',') if item.strip()]
|
|
else:
|
|
current_level_dict[part_of_key] = widget_obj.text()
|
|
elif isinstance(widget_obj, QSpinBox):
|
|
current_level_dict[part_of_key] = widget_obj.value()
|
|
elif isinstance(widget_obj, QDoubleSpinBox):
|
|
current_level_dict[part_of_key] = widget_obj.value()
|
|
elif isinstance(widget_obj, QCheckBox):
|
|
current_level_dict[part_of_key] = widget_obj.isChecked()
|
|
elif isinstance(widget_obj, QComboBox):
|
|
if widget_config_key == "RESOLUTION_THRESHOLD_FOR_JPG":
|
|
selected_text = widget_obj.currentText()
|
|
# Use image_resolutions from the potentially modified full_ui_state
|
|
image_resolutions_data = full_ui_state.get('IMAGE_RESOLUTIONS', {})
|
|
if selected_text == "Never": current_level_dict[part_of_key] = 999999
|
|
elif selected_text == "Always": current_level_dict[part_of_key] = 1
|
|
elif isinstance(image_resolutions_data, list): # Check if it's the list of [name, val]
|
|
found_res = next((res[1] for res in image_resolutions_data if res[0] == selected_text), None)
|
|
if found_res is not None: current_level_dict[part_of_key] = found_res
|
|
else: current_level_dict[part_of_key] = selected_text # Fallback
|
|
elif isinstance(image_resolutions_data, dict) and selected_text in image_resolutions_data: # Original format
|
|
current_level_dict[part_of_key] = image_resolutions_data[selected_text]
|
|
else: current_level_dict[part_of_key] = selected_text # Fallback
|
|
else:
|
|
current_level_dict[part_of_key] = widget_obj.currentText()
|
|
|
|
# Handle TableWidgets - only for those relevant to user_settings.json
|
|
# ASSET_TYPE_DEFINITIONS and FILE_TYPE_DEFINITIONS are handled by their own files.
|
|
# IMAGE_RESOLUTIONS and MAP_MERGE_RULES might be in user_settings.json.
|
|
elif widget_config_key == "IMAGE_RESOLUTIONS_TABLE" and isinstance(widget_obj, QTableWidget):
|
|
table = widget_obj
|
|
resolutions_list = []
|
|
for row in range(table.rowCount()):
|
|
name_item = table.item(row, 0)
|
|
res_item = table.item(row, 1)
|
|
if name_item and name_item.text() and res_item and res_item.text():
|
|
try:
|
|
# Assuming resolution value might be int or string like "1024" or "1k"
|
|
# For simplicity, store as string if not easily int.
|
|
# The config system should handle parsing later.
|
|
res_val_str = res_item.text()
|
|
try:
|
|
res_value = int(res_val_str)
|
|
except ValueError:
|
|
res_value = res_val_str # Keep as string if not simple int
|
|
resolutions_list.append([name_item.text(), res_value])
|
|
except Exception as e:
|
|
print(f"Skipping resolution row {row} due to error: {e}")
|
|
full_ui_state['IMAGE_RESOLUTIONS'] = resolutions_list # Key in settings is IMAGE_RESOLUTIONS
|
|
|
|
# MAP_MERGE_RULES are complex; current save logic is a pass-through.
|
|
# For granular save, if MAP_MERGE_RULES are edited, the full updated list should be in full_ui_state.
|
|
# The original code had a placeholder for MAP_MERGE_RULES_DATA.
|
|
# Assuming if MAP_MERGE_RULES are part of user_settings, their full structure from UI
|
|
# would be placed into full_ui_state['MAP_MERGE_RULES'].
|
|
# The current implementation of populate_map_merging_tab and display_merge_rule_details
|
|
# would need to ensure that `full_ui_state['MAP_MERGE_RULES']` is correctly updated
|
|
# if changes are made via the UI. The original save logic for this was:
|
|
# `elif key == "MAP_MERGE_RULES_DATA": pass`
|
|
# This means `full_ui_state['MAP_MERGE_RULES']` would retain its original loaded value unless
|
|
# other UI interactions (like Add/Remove Rule buttons, if implemented and connected) modify it.
|
|
# For now, we assume `full_ui_state['MAP_MERGE_RULES']` reflects the intended UI state.
|
|
|
|
else: # Navigate deeper
|
|
if part_of_key not in current_level_dict or not isinstance(current_level_dict[part_of_key], dict):
|
|
# This case should ideally not happen if widget keys match settings structure
|
|
# or if the base `full_ui_state` (from `self.settings`) had the correct structure.
|
|
# If a path implies a dict that doesn't exist, create it.
|
|
current_level_dict[part_of_key] = {}
|
|
current_level_dict = current_level_dict[part_of_key]
|
|
# --- End of populating full_ui_state ---
|
|
|
|
# 2. Identify Changes by comparing with self.original_user_configurable_settings
|
|
changed_settings_count = 0
|
|
for key_to_check, original_value in self.original_user_configurable_settings.items():
|
|
# `key_to_check` is a top-level key from the user-configurable settings
|
|
# (e.g., "OUTPUT_BASE_DIR", "PNG_COMPRESSION_LEVEL", "IMAGE_RESOLUTIONS").
|
|
current_value_from_ui = full_ui_state.get(key_to_check)
|
|
|
|
# Perform comparison. Python's default `!=` works for deep comparison
|
|
# of basic types, lists, and dicts if order doesn't matter for lists
|
|
# where it shouldn't (e.g. list of strings) or if dicts are canonical.
|
|
if current_value_from_ui != original_value:
|
|
# This setting has changed. Update it in target_file_content.
|
|
# This replaces the whole value for `key_to_check` in user_settings.
|
|
target_file_content[key_to_check] = copy.deepcopy(current_value_from_ui)
|
|
changed_settings_count += 1
|
|
print(f"Setting '{key_to_check}' changed. Old: {original_value}, New: {current_value_from_ui}")
|
|
|
|
|
|
# 3. Save Updated Content to user_settings.json
|
|
if changed_settings_count > 0 or not os.path.exists(user_settings_path):
|
|
# Save if there are changes or if the file didn't exist (to create it with defaults if any were set)
|
|
try:
|
|
save_user_config(target_file_content) # save_user_config is imported
|
|
QMessageBox.information(self, "Settings Saved",
|
|
f"User settings saved successfully to {user_settings_path}.\n"
|
|
f"{changed_settings_count} setting(s) updated. "
|
|
"Some changes may require an application restart.")
|
|
self.accept() # Close the dialog
|
|
except ConfigurationError as e:
|
|
QMessageBox.critical(self, "Saving Error", f"Failed to save user configuration: {e}")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Saving Error", f"An unexpected error occurred while saving: {e}")
|
|
else:
|
|
QMessageBox.information(self, "No Changes", "No changes were made to user-configurable settings.")
|
|
self.accept() # Close the dialog, or self.reject() if no changes means cancel
|
|
|
|
def populate_widgets_from_settings(self):
|
|
"""Populates the created widgets with loaded settings."""
|
|
if not self.settings or not self.widgets:
|
|
return
|
|
|
|
for key, value in self.settings.items():
|
|
# Handle simple settings directly if they have a corresponding widget
|
|
if key in self.widgets and isinstance(self.widgets[key], (QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QComboBox)):
|
|
widget = self.widgets[key]
|
|
if isinstance(widget, QLineEdit):
|
|
# Handle simple lists displayed as comma-separated strings
|
|
if key == "RESPECT_VARIANT_MAP_TYPES" and isinstance(value, list):
|
|
widget.setText(", ".join(map(str, value)))
|
|
elif isinstance(value, (str, int, float, bool)): # Also handle cases where simple types might be in QLineEdit
|
|
widget.setText(str(value))
|
|
elif isinstance(widget, QSpinBox) and isinstance(value, int):
|
|
widget.setValue(value)
|
|
elif isinstance(widget, QDoubleSpinBox) and isinstance(value, (int, float)):
|
|
widget.setValue(float(value))
|
|
elif isinstance(widget, QCheckBox) and isinstance(value, bool):
|
|
widget.setChecked(value)
|
|
elif isinstance(widget, QComboBox):
|
|
if value in [widget.itemText(i) for i in range(widget.count())]:
|
|
widget.setCurrentText(value)
|
|
|
|
|
|
# Handle complex structures with dedicated widgets (Tables and Lists)
|
|
elif key == "ASSET_TYPE_DEFINITIONS" and "ASSET_TYPE_DEFINITIONS_TABLE" in self.widgets:
|
|
self.populate_asset_definitions_table(self.widgets["ASSET_TYPE_DEFINITIONS_TABLE"], value)
|
|
elif key == "FILE_TYPE_DEFINITIONS" and "FILE_TYPE_DEFINITIONS_TABLE" in self.widgets:
|
|
self.populate_file_type_definitions_table(self.widgets["FILE_TYPE_DEFINITIONS_TABLE"], value)
|
|
elif key == "IMAGE_RESOLUTIONS" and "IMAGE_RESOLUTIONS_TABLE" in self.widgets:
|
|
self.populate_image_resolutions_table(self.widgets["IMAGE_RESOLUTIONS_TABLE"], value)
|
|
# Populate ComboBoxes that depend on Image Resolutions
|
|
resolution_names = [self.widgets["IMAGE_RESOLUTIONS_TABLE"].item(i, 0).text() for i in range(self.widgets["IMAGE_RESOLUTIONS_TABLE"].rowCount())]
|
|
if "CALCULATE_STATS_RESOLUTION" in self.widgets:
|
|
self.widgets["CALCULATE_STATS_RESOLUTION"].addItems(resolution_names)
|
|
if key in self.settings and self.settings["CALCULATE_STATS_RESOLUTION"] in resolution_names:
|
|
self.widgets["CALCULATE_STATS_RESOLUTION"].setCurrentText(self.settings["CALCULATE_STATS_RESOLUTION"])
|
|
if "RESOLUTION_THRESHOLD_FOR_JPG" in self.widgets:
|
|
jpg_threshold_options = ["Never", "Always"] + resolution_names
|
|
self.widgets["RESOLUTION_THRESHOLD_FOR_JPG"].addItems(jpg_threshold_options)
|
|
if key in self.settings and self.settings["RESOLUTION_THRESHOLD_FOR_JPG"] in jpg_threshold_options:
|
|
self.widgets["RESOLUTION_THRESHOLD_FOR_JPG"].setCurrentText(self.settings["RESOLUTION_THRESHOLD_FOR_JPG"])
|
|
|
|
elif key == "MAP_BIT_DEPTH_RULES" and "MAP_BIT_DEPTH_RULES_TABLE" in self.widgets:
|
|
self.populate_map_bit_depth_rules_table(self.widgets["MAP_BIT_DEPTH_RULES_TABLE"], value)
|
|
|
|
|
|
elif key == "MAP_MERGE_RULES" and hasattr(self, 'merge_rules_list'): # Check if the list widget exists
|
|
self.populate_merge_rules_list(value)
|
|
# Select the first item to display details if the list is not empty
|
|
if self.merge_rules_list.count() > 0:
|
|
self.merge_rules_list.setCurrentRow(0)
|
|
|
|
|
|
def populate_asset_definitions_table(self, table: QTableWidget, definitions_data: dict):
|
|
"""Populates the asset definitions table."""
|
|
table.setRowCount(len(definitions_data))
|
|
row = 0
|
|
for asset_type, details in definitions_data.items():
|
|
item_type_name = QTableWidgetItem(asset_type)
|
|
item_description = QTableWidgetItem(details.get("description", ""))
|
|
table.setItem(row, 0, item_type_name)
|
|
table.setItem(row, 1, item_description)
|
|
|
|
# Color column - Set item with color string as data
|
|
color_str = details.get("color", "#ffffff") # Default to white if missing
|
|
item_color = QTableWidgetItem() # No text needed, delegate handles paint
|
|
item_color.setData(Qt.EditRole, color_str) # Store hex string for delegate/editing
|
|
# item_color.setBackground(QColor(color_str)) # Optional: Set initial background via item
|
|
table.setItem(row, 2, item_color)
|
|
|
|
# Examples column
|
|
examples_list = details.get("examples", [])
|
|
examples_str = ", ".join(examples_list) if isinstance(examples_list, list) else ""
|
|
item_examples = QTableWidgetItem(examples_str)
|
|
table.setItem(row, 3, item_examples)
|
|
|
|
# Background color is now handled by the delegate's paint method based on data
|
|
|
|
row += 1
|
|
|
|
# After populating the Asset Types table, populate the DEFAULT_ASSET_CATEGORY ComboBox
|
|
if "DEFAULT_ASSET_CATEGORY" in self.widgets and isinstance(self.widgets["DEFAULT_ASSET_CATEGORY"], QComboBox):
|
|
asset_types = list(definitions_data.keys())
|
|
self.widgets["DEFAULT_ASSET_CATEGORY"].addItems(asset_types)
|
|
# Set the current value if it exists in settings
|
|
if "DEFAULT_ASSET_CATEGORY" in self.settings and self.settings["DEFAULT_ASSET_CATEGORY"] in asset_types:
|
|
self.widgets["DEFAULT_ASSET_CATEGORY"].setCurrentText(self.settings["DEFAULT_ASSET_CATEGORY"])
|
|
|
|
|
|
def populate_file_type_definitions_table(self, table: QTableWidget, definitions_data: dict):
|
|
"""Populates the file type definitions table."""
|
|
table.setRowCount(len(definitions_data))
|
|
row = 0
|
|
for file_type, details in definitions_data.items():
|
|
item_type_id = QTableWidgetItem(file_type)
|
|
item_description = QTableWidgetItem(details.get("description", ""))
|
|
table.setItem(row, 0, item_type_id)
|
|
table.setItem(row, 1, item_description)
|
|
|
|
# Color column - Set item with color string as data
|
|
color_str = details.get("color", "#ffffff") # Default to white if missing
|
|
item_color = QTableWidgetItem() # No text needed, delegate handles paint
|
|
item_color.setData(Qt.EditRole, color_str) # Store hex string for delegate/editing
|
|
# item_color.setBackground(QColor(color_str)) # Optional: Set initial background via item
|
|
table.setItem(row, 2, item_color)
|
|
|
|
# Examples column
|
|
examples_list = details.get("examples", [])
|
|
examples_str = ", ".join(examples_list) if isinstance(examples_list, list) else ""
|
|
item_examples = QTableWidgetItem(examples_str)
|
|
table.setItem(row, 3, item_examples)
|
|
|
|
# Standard Type column (simple QTableWidgetItem for now)
|
|
standard_type_str = details.get("standard_type", "")
|
|
item_standard_type = QTableWidgetItem(standard_type_str)
|
|
table.setItem(row, 4, item_standard_type)
|
|
|
|
# Bit Depth Rule column (simple QTableWidgetItem for now)
|
|
bit_depth_rule_str = details.get("bit_depth_rule", "")
|
|
item_bit_depth_rule = QTableWidgetItem(bit_depth_rule_str)
|
|
table.setItem(row, 5, item_bit_depth_rule)
|
|
|
|
# Background color is now handled by the delegate's paint method based on data
|
|
|
|
row += 1
|
|
|
|
def populate_image_resolutions_table(self, table: QTableWidget, resolutions_data: list):
|
|
"""Populates the image resolutions table."""
|
|
table.setRowCount(len(resolutions_data))
|
|
for row, resolution in enumerate(resolutions_data):
|
|
# Assuming resolution is a list/tuple like [name, resolution_string]
|
|
try:
|
|
if isinstance(resolution, (list, tuple)) and len(resolution) == 2:
|
|
name = str(resolution[0])
|
|
# Attempt to convert resolution value to string, handle potential errors
|
|
try:
|
|
res_value = str(resolution[1])
|
|
except Exception:
|
|
res_value = "Error: Invalid Value"
|
|
table.setItem(row, 0, QTableWidgetItem(name))
|
|
table.setItem(row, 1, QTableWidgetItem(res_value))
|
|
else:
|
|
# Handle unexpected format more clearly
|
|
table.setItem(row, 0, QTableWidgetItem(str(resolution)))
|
|
table.setItem(row, 1, QTableWidgetItem("Error: Invalid Format"))
|
|
except Exception as e:
|
|
# Catch any other unexpected errors during processing
|
|
print(f"Error populating resolution row {row}: {e}")
|
|
table.setItem(row, 0, QTableWidgetItem("Error"))
|
|
table.setItem(row, 1, QTableWidgetItem(f"Error: {e}"))
|
|
|
|
|
|
def populate_map_bit_depth_rules_table(self, table: QTableWidget, rules_data: dict):
|
|
"""Populates the map bit depth rules table."""
|
|
table.setRowCount(len(rules_data))
|
|
row = 0
|
|
for map_type, rule in rules_data.items():
|
|
table.setItem(row, 0, QTableWidgetItem(map_type))
|
|
table.setItem(row, 1, QTableWidgetItem(str(rule))) # Rule (respect/force_8bit)
|
|
row += 1
|
|
|
|
|
|
|
|
|
|
# Example usage (for testing the dialog independently)
|
|
if __name__ == '__main__':
|
|
# Use PySide6 instead of PyQt5 for consistency
|
|
from PySide6.QtWidgets import QApplication
|
|
import sys
|
|
|
|
app = QApplication(sys.argv)
|
|
dialog = ConfigEditorDialog()
|
|
dialog.exec() # Use exec() for PySide6 QDialog
|
|
sys.exit(app.exec()) # Use exec() for PySide6 QApplication |