Config Updates - User settings - Saving Methods

This commit is contained in:
2025-05-13 10:32:19 +02:00
parent 383e904e1a
commit dec5d7d27f
14 changed files with 1279 additions and 974 deletions

View File

@@ -1,5 +1,7 @@
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,
@@ -14,10 +16,10 @@ 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_base_config
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_base_config
from ..configuration import load_base_config, save_user_config, ConfigurationError
# --- Custom Delegate for Color Editing ---
@@ -86,10 +88,29 @@ class ConfigEditorDialog(QDialog):
"""Loads settings from the configuration file."""
try:
self.settings = load_base_config()
print("Configuration loaded successfully.") # Debug print
# 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)
@@ -851,154 +872,162 @@ class ConfigEditorDialog(QDialog):
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 = {}
"""
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")
# 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)
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}")
# 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, QComboBox)):
# 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):
# Handle simple lists displayed as comma-separated strings
if key == "RESPECT_VARIANT_MAP_TYPES":
current_dict[k] = [item.strip() for item in widget.text().split(',') if item.strip()]
else:
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()
elif isinstance(widget, QComboBox):
# Special handling for RESOLUTION_THRESHOLD_FOR_JPG
if key == "RESOLUTION_THRESHOLD_FOR_JPG": # Use 'key' from the loop, not 'full_key'
selected_text = widget.currentText()
image_resolutions = new_settings.get('IMAGE_RESOLUTIONS', {})
if selected_text == "Never":
# Use a very large number so the comparison target_dim_px >= threshold is always false
current_dict[k] = 999999
elif selected_text == "Always":
current_dict[k] = 1 # Assuming 1 means always apply (any positive dimension >= 1)
elif selected_text in image_resolutions:
current_dict[k] = image_resolutions[selected_text]
else:
# Fallback or error handling if text is unexpected
print(f"Warning: Unexpected value '{selected_text}' for RESOLUTION_THRESHOLD_FOR_JPG. Saving as text.")
current_dict[k] = selected_text # Save original text as fallback
else:
# Default behavior for other combo boxes
current_dict[k] = widget.currentText()
else:
# Navigate to the next level
# Use full_key for error message consistency
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 and image resolutions)
elif key == "ASSET_TYPE_DEFINITIONS_TABLE":
table = widget
asset_defs = {}
for row in range(table.rowCount()):
try:
type_name_item = table.item(row, 0)
desc_item = table.item(row, 1)
color_item = table.item(row, 2) # Delegate stores color in EditRole
examples_item = table.item(row, 3)
if not type_name_item or not type_name_item.text():
print(f"Warning: Skipping row {row} in {key} due to missing type name.")
continue
type_name = type_name_item.text()
description = desc_item.text() if desc_item else ""
# Get color from EditRole data set by the delegate
color_str = color_item.data(Qt.EditRole) if color_item else "#ffffff"
examples_str = examples_item.text() if examples_item else ""
examples_list = [ex.strip() for ex in examples_str.split(',') if ex.strip()]
asset_defs[type_name] = {
"description": description,
"color": color_str,
"examples": examples_list
}
except Exception as e:
print(f"Error processing row {row} in {key}: {e}")
new_settings['ASSET_TYPE_DEFINITIONS'] = asset_defs # Overwrite with new data
elif key == "FILE_TYPE_DEFINITIONS_TABLE":
table = widget
file_defs = {}
for row in range(table.rowCount()):
try:
type_id_item = table.item(row, 0)
desc_item = table.item(row, 1)
color_item = table.item(row, 2) # Delegate stores color in EditRole
examples_item = table.item(row, 3)
std_type_item = table.item(row, 4)
bit_depth_item = table.item(row, 5)
if not type_id_item or not type_id_item.text():
print(f"Warning: Skipping row {row} in {key} due to missing type ID.")
continue
type_id = type_id_item.text()
description = desc_item.text() if desc_item else ""
# Get color from EditRole data set by the delegate
color_str = color_item.data(Qt.EditRole) if color_item else "#ffffff"
examples_str = examples_item.text() if examples_item else ""
examples_list = [ex.strip() for ex in examples_str.split(',') if ex.strip()]
standard_type = std_type_item.text() if std_type_item else ""
bit_depth_rule = bit_depth_item.text() if bit_depth_item else "respect" # Default?
file_defs[type_id] = {
"description": description,
"color": color_str,
"examples": examples_list,
"standard_type": standard_type,
"bit_depth_rule": bit_depth_rule
}
except Exception as e:
print(f"Error processing row {row} in {key}: {e}")
new_settings['FILE_TYPE_DEFINITIONS'] = file_defs # Overwrite with new data
elif key in ["IMAGE_RESOLUTIONS_TABLE", "MAP_BIT_DEPTH_RULES_TABLE"]:
# Still not implemented for these tables
print(f"Warning: Saving for table widget '{key}' is not fully implemented.")
pass # Defer saving complex table data
# Handle Map Merge Rules (more complex)
# Note: Changes made in the details form are NOT saved with this implementation
# due to deferred complexity in updating the list item's data and reconstructing the list.
elif key == "MAP_MERGE_RULES_DATA":
# The original data is stored, but changes in the details form are not reflected.
# A full implementation would require updating the rule data in the list item
# when the details form is edited and then reconstructing the list.
print(f"Warning: Saving for Map Merge Rules details is not fully implemented.")
pass # Defer saving changes from the details form
# 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}")
# 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."""

View File

@@ -1,6 +1,7 @@
# gui/llm_editor_widget.py
import json
import logging
import copy # Added for deepcopy
from PySide6.QtWidgets import (
QWidget, QVBoxLayout, QTabWidget, QPlainTextEdit, QGroupBox,
QHBoxLayout, QPushButton, QFormLayout, QLineEdit, QDoubleSpinBox,
@@ -24,6 +25,7 @@ class LLMEditorWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._unsaved_changes = False
self.original_llm_settings = {} # Initialize original_llm_settings
self._init_ui()
self._connect_signals()
self.save_button.setEnabled(False) # Initially disabled
@@ -131,6 +133,7 @@ class LLMEditorWidget(QWidget):
try:
with open(LLM_CONFIG_PATH, 'r', encoding='utf-8') as f:
settings = json.load(f)
self.original_llm_settings = copy.deepcopy(settings) # Store a deep copy
# Populate Prompt Settings
self.prompt_editor.setPlainText(settings.get("llm_predictor_prompt", ""))
@@ -159,9 +162,9 @@ class LLMEditorWidget(QWidget):
logger.info("LLM settings loaded successfully.")
except FileNotFoundError:
logger.warning(f"LLM settings file not found: {LLM_CONFIG_PATH}. Using defaults and disabling editor.")
logger.warning(f"LLM settings file not found: {LLM_CONFIG_PATH}. Using defaults.")
QMessageBox.warning(self, "Load Error",
f"LLM settings file not found:\n{LLM_CONFIG_PATH}\n\nPlease ensure the file exists. Using default values.")
f"LLM settings file not found:\n{LLM_CONFIG_PATH}\n\nNew settings will be created if you save.")
# Reset to defaults (optional, or leave fields empty)
self.prompt_editor.clear()
self.endpoint_url_edit.clear()
@@ -169,19 +172,21 @@ class LLMEditorWidget(QWidget):
self.model_name_edit.clear()
self.temperature_spinbox.setValue(0.7)
self.timeout_spinbox.setValue(120)
# self.setEnabled(False) # Disabling might be too harsh if user wants to create settings
self.original_llm_settings = {} # Start with empty original settings if file not found
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON from {LLM_CONFIG_PATH}: {e}")
QMessageBox.critical(self, "Load Error",
f"Failed to parse LLM settings file:\n{LLM_CONFIG_PATH}\n\nError: {e}\n\nPlease check the file for syntax errors. Editor will be disabled.")
self.setEnabled(False) # Disable editor on critical load error
self.original_llm_settings = {} # Reset original settings on JSON error
except Exception as e: # Catch other potential errors during loading/populating
logger.error(f"An unexpected error occurred loading LLM settings: {e}", exc_info=True)
QMessageBox.critical(self, "Load Error",
f"An unexpected error occurred while loading settings:\n{e}\n\nEditor will be disabled.")
self.setEnabled(False)
self.original_llm_settings = {} # Reset original settings on other errors
# Reset unsaved changes flag and disable save button after loading
@@ -201,26 +206,38 @@ class LLMEditorWidget(QWidget):
"""Gather data from UI, save to JSON file, and handle errors."""
logger.info("Attempting to save LLM settings...")
settings_dict = {}
# 1.a. Load Current Target File
target_file_content = {}
try:
with open(LLM_CONFIG_PATH, 'r', encoding='utf-8') as f:
target_file_content = json.load(f)
except FileNotFoundError:
logger.info(f"{LLM_CONFIG_PATH} not found. Will create a new one.")
target_file_content = {} # Start with an empty dict if file doesn't exist
except json.JSONDecodeError as e:
logger.error(f"Error decoding existing {LLM_CONFIG_PATH}: {e}. Starting with an empty config for save.")
QMessageBox.warning(self, "Warning",
f"Could not parse existing LLM settings file ({LLM_CONFIG_PATH}).\n"
f"Any pre-existing settings in that file might be overwritten if you save now.\nError: {e}")
target_file_content = {} # Start fresh if current file is corrupt
# 1.b. Gather current UI settings into current_llm_settings
current_llm_settings = {}
parsed_examples = []
has_errors = False
has_errors = False # For example parsing
# Gather API Settings
settings_dict["llm_endpoint_url"] = self.endpoint_url_edit.text().strip()
settings_dict["llm_api_key"] = self.api_key_edit.text() # Keep as is, don't strip
settings_dict["llm_model_name"] = self.model_name_edit.text().strip()
settings_dict["llm_temperature"] = self.temperature_spinbox.value()
settings_dict["llm_request_timeout"] = self.timeout_spinbox.value()
current_llm_settings["llm_endpoint_url"] = self.endpoint_url_edit.text().strip()
current_llm_settings["llm_api_key"] = self.api_key_edit.text() # Keep as is
current_llm_settings["llm_model_name"] = self.model_name_edit.text().strip()
current_llm_settings["llm_temperature"] = self.temperature_spinbox.value()
current_llm_settings["llm_request_timeout"] = self.timeout_spinbox.value()
current_llm_settings["llm_predictor_prompt"] = self.prompt_editor.toPlainText().strip()
# Gather Prompt Settings
settings_dict["llm_predictor_prompt"] = self.prompt_editor.toPlainText().strip()
# Gather and Parse Examples
for i in range(self.examples_tab_widget.count()):
example_editor = self.examples_tab_widget.widget(i)
if isinstance(example_editor, QTextEdit):
example_text = example_editor.toPlainText().strip()
if not example_text: # Skip empty examples silently
if not example_text:
continue
try:
parsed_example = json.loads(example_text)
@@ -231,40 +248,58 @@ class LLMEditorWidget(QWidget):
logger.warning(f"Invalid JSON in '{tab_name}': {e}. Skipping example.")
QMessageBox.warning(self, "Invalid Example",
f"The content in '{tab_name}' is not valid JSON and will not be saved.\n\nError: {e}\n\nPlease correct it or remove the tab.")
# Optionally switch to the tab with the error:
# self.examples_tab_widget.setCurrentIndex(i)
else:
logger.warning(f"Widget at index {i} in examples tab is not a QTextEdit. Skipping.")
logger.warning(f"Widget at index {i} in examples tab is not a QTextEdit. Skipping.")
if has_errors:
logger.warning("LLM settings not saved due to invalid JSON in examples.")
# Keep save button enabled if there were errors, allowing user to fix and retry
# self.save_button.setEnabled(True)
# self._unsaved_changes = True
return # Stop saving process
return
settings_dict["llm_predictor_examples"] = parsed_examples
current_llm_settings["llm_predictor_examples"] = parsed_examples
# Save the dictionary to file
# 1.c. Identify Changes and Update Target File Content
changed_settings_count = 0
for key, current_value in current_llm_settings.items():
original_value = self.original_llm_settings.get(key)
# Special handling for lists (e.g., examples) - direct comparison works
# For other types, direct comparison also works.
# This includes new keys present in current_llm_settings but not in original_llm_settings
if key not in self.original_llm_settings or current_value != original_value:
target_file_content[key] = current_value
logger.debug(f"Setting '{key}' changed or added. Old: '{original_value}', New: '{current_value}'")
changed_settings_count +=1
if changed_settings_count == 0 and self._unsaved_changes:
logger.info("Save called, but no actual changes detected compared to original loaded settings.")
# If _unsaved_changes was true, it means UI interaction happened,
# but values might have been reverted to original.
# We still proceed to save target_file_content as it might contain
# values from a file that was modified externally since last load.
# Or, if the file didn't exist, it will now be created with current UI values.
# 1.d. Save Updated Content
try:
save_llm_config(settings_dict)
save_llm_config(target_file_content) # Save the potentially modified target_file_content
QMessageBox.information(self, "Save Successful", f"LLM settings saved to:\n{LLM_CONFIG_PATH}")
# Update original_llm_settings to reflect the newly saved state
self.original_llm_settings = copy.deepcopy(target_file_content)
self.save_button.setEnabled(False)
self._unsaved_changes = False
self.settings_saved.emit() # Notify MainWindow or others
self.settings_saved.emit()
logger.info("LLM settings saved successfully.")
except ConfigurationError as e:
logger.error(f"Failed to save LLM settings: {e}")
QMessageBox.critical(self, "Save Error", f"Could not save LLM settings.\n\nError: {e}")
# Keep save button enabled as save failed
self.save_button.setEnabled(True)
self.save_button.setEnabled(True) # Keep save enabled
self._unsaved_changes = True
except Exception as e: # Catch unexpected errors during save
except Exception as e:
logger.error(f"An unexpected error occurred during LLM settings save: {e}", exc_info=True)
QMessageBox.critical(self, "Save Error", f"An unexpected error occurred while saving settings:\n{e}")
self.save_button.setEnabled(True)
self.save_button.setEnabled(True) # Keep save enabled
self._unsaved_changes = True
# --- Example Management Slots ---