Minor GUI refactor - Drag+drop issues introduced
This commit is contained in:
@@ -8,38 +8,16 @@ from PySide6.QtWidgets import (
|
||||
from PySide6.QtGui import QColor, QPalette, QMouseEvent # Added QMouseEvent
|
||||
from PySide6.QtCore import Qt, QEvent
|
||||
|
||||
# Assuming load_asset_definitions, load_file_type_definitions, load_supplier_settings
|
||||
# are in configuration.py at the root level.
|
||||
# Adjust the import path if configuration.py is located elsewhere relative to this file.
|
||||
# For example, if configuration.py is in the parent directory:
|
||||
# from ..configuration import load_asset_definitions, load_file_type_definitions, load_supplier_settings
|
||||
# Or if it's in the same directory (less likely for a root config file):
|
||||
# from .configuration import ...
|
||||
# Given the project structure, configuration.py is at the root.
|
||||
import sys
|
||||
import os
|
||||
# Add project root to sys.path to allow direct import of configuration
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
try:
|
||||
from configuration import (
|
||||
load_asset_definitions, save_asset_definitions,
|
||||
load_file_type_definitions, save_file_type_definitions,
|
||||
load_supplier_settings, save_supplier_settings
|
||||
)
|
||||
except ImportError as e:
|
||||
logging.error(f"Failed to import configuration functions: {e}. Ensure configuration.py is in the project root and accessible.")
|
||||
# Provide dummy functions if import fails, so the UI can still be tested somewhat
|
||||
def load_asset_definitions(): return {}
|
||||
def save_asset_definitions(data): pass
|
||||
def load_file_type_definitions(): return {}
|
||||
def save_file_type_definitions(data): pass
|
||||
def load_supplier_settings(): return {}
|
||||
# def save_supplier_settings(data): pass
|
||||
from PySide6.QtGui import QColor, QPalette, QMouseEvent
|
||||
from PySide6.QtCore import Qt, QEvent
|
||||
|
||||
# Import the Configuration class
|
||||
from configuration import Configuration, ConfigurationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DebugListWidget(QListWidget):
|
||||
def mousePressEvent(self, event: QMouseEvent): # QMouseEvent needs to be imported from PySide6.QtGui
|
||||
def mousePressEvent(self, event: QMouseEvent):
|
||||
logger.info(f"DebugListWidget.mousePressEvent: pos={event.pos()}")
|
||||
item = self.itemAt(event.pos())
|
||||
if item:
|
||||
@@ -50,8 +28,9 @@ class DebugListWidget(QListWidget):
|
||||
logger.info("DebugListWidget.mousePressEvent: super call finished.")
|
||||
|
||||
class DefinitionsEditorDialog(QDialog):
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, config: Configuration, parent=None):
|
||||
super().__init__(parent)
|
||||
self.config = config # Store the Configuration object
|
||||
self.setWindowTitle("Definitions Editor")
|
||||
self.setGeometry(200, 200, 800, 600) # x, y, width, height
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from pathlib import Path
|
||||
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
|
||||
from PySide6.QtCore import Qt, QModelIndex
|
||||
from configuration import Configuration, ConfigurationError, load_base_config # Keep load_base_config for SupplierSearchDelegate
|
||||
from configuration import Configuration, ConfigurationError # Keep load_base_config for SupplierSearchDelegate
|
||||
from PySide6.QtWidgets import QListWidgetItem
|
||||
|
||||
import json
|
||||
|
||||
@@ -10,7 +10,7 @@ from PySide6.QtWidgets import (
|
||||
from PySide6.QtCore import Slot as pyqtSlot, Signal as pyqtSignal # Use PySide6 equivalents
|
||||
|
||||
# Assuming configuration module exists and has relevant functions later
|
||||
from configuration import save_llm_config, ConfigurationError
|
||||
from configuration import ConfigurationError
|
||||
# For now, define path directly for initial structure
|
||||
LLM_CONFIG_PATH = "config/llm_settings.json"
|
||||
|
||||
@@ -280,20 +280,26 @@ class LLMEditorWidget(QWidget):
|
||||
|
||||
# 1.d. Save Updated Content
|
||||
try:
|
||||
save_llm_config(target_file_content) # Save the potentially modified target_file_content
|
||||
# Ensure the directory exists before saving
|
||||
import os
|
||||
os.makedirs(os.path.dirname(LLM_CONFIG_PATH), exist_ok=True)
|
||||
|
||||
with open(LLM_CONFIG_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(target_file_content, f, indent=4)
|
||||
|
||||
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()
|
||||
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}")
|
||||
except (IOError, OSError) as e:
|
||||
logger.error(f"Failed to write LLM settings file {LLM_CONFIG_PATH}: {e}")
|
||||
QMessageBox.critical(self, "Save Error", f"Could not write LLM settings file.\n\nError: {e}")
|
||||
self.save_button.setEnabled(True) # Keep save enabled
|
||||
self._unsaved_changes = True
|
||||
except Exception as e:
|
||||
|
||||
@@ -24,6 +24,9 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
Handles the interaction with an LLM for predicting asset structures
|
||||
based on a directory's file list. Inherits from BasePredictionHandler.
|
||||
"""
|
||||
# Define a constant for files not classified by the LLM
|
||||
FILE_UNCLASSIFIED_BY_LLM = "FILE_UNCLASSIFIED_BY_LLM"
|
||||
|
||||
# Signals (prediction_ready, prediction_error, status_update) are inherited
|
||||
|
||||
# Changed 'config: Configuration' to 'settings: dict'
|
||||
@@ -307,54 +310,67 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
valid_file_types = list(self.settings.get('file_type_definitions', {}).keys())
|
||||
asset_rules_map: Dict[str, AssetRule] = {} # Maps group_name to AssetRule
|
||||
|
||||
# --- Process Individual Files and Build Rules ---
|
||||
for file_data in response_data["individual_file_analysis"]:
|
||||
# --- Map LLM File Analysis for Quick Lookup ---
|
||||
llm_file_map: Dict[str, Dict[str, Any]] = {}
|
||||
for file_data in response_data.get("individual_file_analysis", []):
|
||||
if isinstance(file_data, dict):
|
||||
file_path_rel = file_data.get("relative_file_path")
|
||||
if file_path_rel and isinstance(file_path_rel, str):
|
||||
llm_file_map[file_path_rel] = file_data
|
||||
else:
|
||||
log.warning(f"Skipping LLM file data entry with missing or invalid 'relative_file_path': {file_data}")
|
||||
else:
|
||||
log.warning(f"Skipping invalid LLM file data entry (not a dict): {file_data}")
|
||||
|
||||
# --- Process Actual Input Files and Reconcile with LLM Data ---
|
||||
for file_path_rel in self.file_list:
|
||||
# Check for cancellation within the loop
|
||||
if self._is_cancelled:
|
||||
log.info("LLM prediction cancelled during response parsing (files).")
|
||||
return []
|
||||
|
||||
if not isinstance(file_data, dict):
|
||||
log.warning(f"Skipping invalid file data entry (not a dict): {file_data}")
|
||||
continue
|
||||
file_data = llm_file_map.pop(file_path_rel, None) # Get data if exists, remove from map
|
||||
|
||||
file_path_rel = file_data.get("relative_file_path")
|
||||
file_type = file_data.get("classified_file_type")
|
||||
group_name = file_data.get("proposed_asset_group_name") # Can be string or null
|
||||
if file_data:
|
||||
# --- File found in LLM output - Use LLM Classification ---
|
||||
file_type = file_data.get("classified_file_type")
|
||||
group_name = file_data.get("proposed_asset_group_name") # Can be string or null
|
||||
|
||||
# --- Validate File Data ---
|
||||
if not file_path_rel or not isinstance(file_path_rel, str):
|
||||
log.warning(f"Missing or invalid 'relative_file_path' in file data: {file_data}. Skipping file.")
|
||||
continue
|
||||
# Validate file_type against definitions, unless it's FILE_IGNORE
|
||||
if not file_type or not isinstance(file_type, str):
|
||||
log.warning(f"Missing or invalid 'classified_file_type' for file '{file_path_rel}' from LLM. Defaulting to {self.FILE_UNCLASSIFIED_BY_LLM}.")
|
||||
file_type = self.FILE_UNCLASSIFIED_BY_LLM
|
||||
elif file_type != "FILE_IGNORE" and file_type not in valid_file_types:
|
||||
log.warning(f"Invalid predicted_file_type '{file_type}' for file '{file_path_rel}' from LLM. Defaulting to EXTRA.")
|
||||
file_type = "EXTRA"
|
||||
|
||||
if not file_type or not isinstance(file_type, str):
|
||||
log.warning(f"Missing or invalid 'classified_file_type' for file '{file_path_rel}'. Skipping file.")
|
||||
continue
|
||||
# Handle FILE_IGNORE explicitly - do not create a rule for it
|
||||
if file_type == "FILE_IGNORE":
|
||||
log.debug(f"Ignoring file as per LLM prediction: {file_path_rel}")
|
||||
continue
|
||||
|
||||
# Handle FILE_IGNORE explicitly
|
||||
if file_type == "FILE_IGNORE":
|
||||
log.debug(f"Ignoring file as per LLM prediction: {file_path_rel}")
|
||||
continue # Skip creating a rule for this file
|
||||
# Determine group name and asset type
|
||||
if not group_name or not isinstance(group_name, str):
|
||||
log.warning(f"File '{file_path_rel}' has missing, null, or invalid 'proposed_asset_group_name' ({group_name}) from LLM. Assigning to default asset.")
|
||||
group_name = "Unclassified Files" # Default group name
|
||||
asset_type = "UtilityMap" # Default asset type for unclassified files (or another sensible default)
|
||||
else:
|
||||
asset_type = response_data["asset_group_classifications"].get(group_name)
|
||||
if not asset_type:
|
||||
log.warning(f"No classification found in 'asset_group_classifications' for group '{group_name}' (proposed for file '{file_path_rel}'). Assigning to default asset.")
|
||||
group_name = "Unclassified Files" # Default group name
|
||||
asset_type = "UtilityMap" # Default asset type
|
||||
elif asset_type not in valid_asset_types:
|
||||
log.warning(f"Invalid asset_type '{asset_type}' found in 'asset_group_classifications' for group '{group_name}'. Assigning to default asset.")
|
||||
group_name = "Unclassified Files" # Default group name
|
||||
asset_type = "UtilityMap" # Default asset type
|
||||
|
||||
# Validate file_type against definitions
|
||||
if file_type not in valid_file_types:
|
||||
log.warning(f"Invalid predicted_file_type '{file_type}' for file '{file_path_rel}'. Defaulting to EXTRA.")
|
||||
file_type = "EXTRA"
|
||||
|
||||
# --- Handle Grouping and Asset Type ---
|
||||
if not group_name or not isinstance(group_name, str):
|
||||
log.warning(f"File '{file_path_rel}' has missing, null, or invalid 'proposed_asset_group_name' ({group_name}). Cannot assign to an asset. Skipping file.")
|
||||
continue
|
||||
|
||||
asset_type = response_data["asset_group_classifications"].get(group_name)
|
||||
|
||||
if not asset_type:
|
||||
log.warning(f"No classification found in 'asset_group_classifications' for group '{group_name}' (proposed for file '{file_path_rel}'). Skipping file.")
|
||||
continue
|
||||
|
||||
if asset_type not in valid_asset_types:
|
||||
log.warning(f"Invalid asset_type '{asset_type}' found in 'asset_group_classifications' for group '{group_name}'. Skipping file '{file_path_rel}'.")
|
||||
continue
|
||||
else:
|
||||
# --- File NOT found in LLM output - Assign Default Classification ---
|
||||
log.warning(f"File '{file_path_rel}' from input list was NOT classified by LLM. Assigning type {self.FILE_UNCLASSIFIED_BY_LLM} and default asset.")
|
||||
file_type = self.FILE_UNCLASSIFIED_BY_LLM
|
||||
group_name = "Unclassified Files" # Default group name
|
||||
asset_type = "UtilityMap" # Default asset type
|
||||
|
||||
# --- Construct Absolute Path ---
|
||||
try:
|
||||
@@ -373,25 +389,34 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
# Create new AssetRule if this is the first file for this group
|
||||
log.debug(f"Creating new AssetRule for group '{group_name}' with type '{asset_type}'.")
|
||||
asset_rule = AssetRule(asset_name=group_name, asset_type=asset_type)
|
||||
asset_rule.parent_source = source_rule # Set parent back-reference
|
||||
source_rule.assets.append(asset_rule)
|
||||
asset_rules_map[group_name] = asset_rule
|
||||
# If asset_rule already exists, ensure its type is consistent or handle conflicts if necessary.
|
||||
# For now, we'll assume the first file dictates the asset type for the default group.
|
||||
# For LLM-classified groups, the type comes from asset_group_classifications.
|
||||
|
||||
# --- Create and Add File Rule ---
|
||||
file_rule = FileRule(
|
||||
file_path=file_path_abs,
|
||||
item_type=file_type,
|
||||
item_type_override=file_type, # Initial override based on LLM
|
||||
item_type_override=file_type, # Initial override based on classification (LLM or default)
|
||||
target_asset_name_override=group_name,
|
||||
output_format_override=None,
|
||||
resolution_override=None,
|
||||
channel_merge_instructions={}
|
||||
)
|
||||
file_rule.parent_asset = asset_rule # Set parent back-reference
|
||||
asset_rule.files.append(file_rule)
|
||||
log.debug(f"Added file '{file_path_rel}' (type: {file_type}) to asset '{group_name}'.")
|
||||
|
||||
# --- Handle LLM Hallucinations (Remaining entries in llm_file_map) ---
|
||||
for file_path_rel, file_data in llm_file_map.items():
|
||||
log.warning(f"LLM predicted file '{file_path_rel}' which was NOT in the actual input file list. Ignoring this hallucinated entry.")
|
||||
# No FileRule is created for this hallucinated file.
|
||||
|
||||
# Log if no assets were created
|
||||
if not source_rule.assets:
|
||||
log.warning(f"LLM prediction for '{self.input_source_identifier}' resulted in zero valid assets after parsing.")
|
||||
log.warning(f"LLM prediction for '{self.input_source_identifier}' resulted in zero valid assets after processing actual file list.")
|
||||
|
||||
return [source_rule] # Return list containing the single SourceRule
|
||||
|
||||
@@ -23,15 +23,8 @@ from .unified_view_model import UnifiedViewModel
|
||||
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
import configuration
|
||||
try:
|
||||
from configuration import ConfigurationError, load_base_config
|
||||
except ImportError:
|
||||
ConfigurationError = Exception
|
||||
load_base_config = None
|
||||
class configuration:
|
||||
PRESETS_DIR = "Presets"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
from configuration import Configuration, ConfigurationError # Import Configuration class and Error
|
||||
|
||||
class MainPanelWidget(QWidget):
|
||||
"""
|
||||
@@ -57,7 +50,7 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
blender_settings_changed = Signal(bool, str, str)
|
||||
|
||||
def __init__(self, unified_model: UnifiedViewModel, parent=None, file_type_keys: list[str] | None = None):
|
||||
def __init__(self, config: Configuration, unified_model: UnifiedViewModel, parent=None, file_type_keys: list[str] | None = None):
|
||||
"""
|
||||
Initializes the MainPanelWidget.
|
||||
|
||||
@@ -67,6 +60,7 @@ class MainPanelWidget(QWidget):
|
||||
file_type_keys: A list of available file type names (keys from FILE_TYPE_DEFINITIONS).
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self._config = config # Store the Configuration object
|
||||
self.unified_model = unified_model
|
||||
self.file_type_keys = file_type_keys if file_type_keys else []
|
||||
self.llm_processing_active = False
|
||||
@@ -91,21 +85,19 @@ class MainPanelWidget(QWidget):
|
||||
output_layout.addWidget(self.browse_output_button)
|
||||
main_layout.addLayout(output_layout)
|
||||
|
||||
if load_base_config:
|
||||
try:
|
||||
base_config = load_base_config()
|
||||
output_base_dir_config = base_config.get('OUTPUT_BASE_DIR', '../Asset_Processor_Output')
|
||||
default_output_dir = (self.project_root / output_base_dir_config).resolve()
|
||||
self.output_path_edit.setText(str(default_output_dir))
|
||||
log.info(f"MainPanelWidget: Default output directory set to: {default_output_dir}")
|
||||
except ConfigurationError as e:
|
||||
log.error(f"MainPanelWidget: Error reading base configuration for default output directory: {e}")
|
||||
self.output_path_edit.setText("")
|
||||
except Exception as e:
|
||||
log.exception(f"MainPanelWidget: Error setting default output directory: {e}")
|
||||
self.output_path_edit.setText("")
|
||||
else:
|
||||
log.warning("MainPanelWidget: load_base_config not available to set default output path.")
|
||||
try:
|
||||
# Access configuration directly from the stored object
|
||||
# Use the output_directory_pattern from the Configuration object
|
||||
output_pattern = self._config.output_directory_pattern
|
||||
# Assuming the pattern is relative to the project root for the default
|
||||
default_output_dir = (self.project_root / output_pattern).resolve()
|
||||
self.output_path_edit.setText(str(default_output_dir))
|
||||
log.info(f"MainPanelWidget: Default output directory set to: {default_output_dir} based on pattern '{output_pattern}'")
|
||||
except ConfigurationError as e:
|
||||
log.error(f"MainPanelWidget: Configuration Error setting default output directory: {e}")
|
||||
self.output_path_edit.setText("")
|
||||
except Exception as e:
|
||||
log.exception(f"MainPanelWidget: Unexpected Error setting default output directory: {e}")
|
||||
self.output_path_edit.setText("")
|
||||
|
||||
|
||||
@@ -180,19 +172,14 @@ class MainPanelWidget(QWidget):
|
||||
materials_layout.addWidget(self.browse_materials_blend_button)
|
||||
blender_layout.addLayout(materials_layout)
|
||||
|
||||
if load_base_config:
|
||||
try:
|
||||
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"MainPanelWidget: Error reading base configuration for default Blender paths: {e}")
|
||||
except Exception as e:
|
||||
log.error(f"MainPanelWidget: Error reading default Blender paths from config: {e}")
|
||||
else:
|
||||
log.warning("MainPanelWidget: load_base_config not available to set default Blender paths.")
|
||||
try:
|
||||
# Use hardcoded defaults as Configuration object does not expose these via public interface
|
||||
default_ng_path = ''
|
||||
default_mat_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 Exception as e:
|
||||
log.error(f"MainPanelWidget: Error setting default Blender paths: {e}")
|
||||
|
||||
|
||||
self.nodegroup_blend_path_input.setEnabled(False)
|
||||
|
||||
@@ -46,14 +46,13 @@ if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
try:
|
||||
from configuration import Configuration, ConfigurationError, load_base_config
|
||||
from configuration import Configuration, ConfigurationError
|
||||
|
||||
|
||||
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
|
||||
ConfigurationError = Exception
|
||||
AssetProcessor = None
|
||||
RuleBasedPredictionHandler = None
|
||||
@@ -97,8 +96,9 @@ class MainWindow(QMainWindow):
|
||||
start_prediction_signal = Signal(str, list, str)
|
||||
start_backend_processing = Signal(list, dict)
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config: Configuration):
|
||||
super().__init__()
|
||||
self.config = config # Store the Configuration object
|
||||
|
||||
self.setWindowTitle("Asset Processor Tool")
|
||||
self.resize(1200, 700)
|
||||
@@ -132,7 +132,7 @@ class MainWindow(QMainWindow):
|
||||
self.setCentralWidget(self.splitter)
|
||||
|
||||
# --- Create Models ---
|
||||
self.unified_model = UnifiedViewModel()
|
||||
self.unified_model = UnifiedViewModel(config=self.config)
|
||||
# --- Instantiate Handlers that depend on the model ---
|
||||
self.restructure_handler = AssetRestructureHandler(self.unified_model, self)
|
||||
|
||||
@@ -143,17 +143,16 @@ class MainWindow(QMainWindow):
|
||||
# --- Load File Type Definitions for Rule Editor ---
|
||||
file_type_keys = []
|
||||
try:
|
||||
base_cfg_data = load_base_config()
|
||||
if base_cfg_data and "FILE_TYPE_DEFINITIONS" in base_cfg_data:
|
||||
file_type_keys = list(base_cfg_data["FILE_TYPE_DEFINITIONS"].keys())
|
||||
log.info(f"Loaded {len(file_type_keys)} FILE_TYPE_DEFINITIONS keys for RuleEditor.")
|
||||
else:
|
||||
log.warning("FILE_TYPE_DEFINITIONS not found in base_config. RuleEditor item_type dropdown might be empty.")
|
||||
# Access configuration directly from the stored object using public methods
|
||||
file_type_defs = self.config.get_file_type_definitions_with_examples()
|
||||
file_type_keys = list(file_type_defs.keys())
|
||||
log.info(f"Loaded {len(file_type_keys)} FILE_TYPE_DEFINITIONS keys for RuleEditor.")
|
||||
except Exception as e:
|
||||
log.exception(f"Error loading FILE_TYPE_DEFINITIONS for RuleEditor: {e}")
|
||||
file_type_keys = [] # Ensure it's a list even on error
|
||||
|
||||
# Instantiate MainPanelWidget, passing the model, self (MainWindow) for context, and file_type_keys
|
||||
self.main_panel_widget = MainPanelWidget(self.unified_model, self, file_type_keys=file_type_keys)
|
||||
# Instantiate MainPanelWidget, passing the config, model, self (MainWindow) for context, and file_type_keys
|
||||
self.main_panel_widget = MainPanelWidget(config=self.config, unified_model=self.unified_model, parent=self, file_type_keys=file_type_keys)
|
||||
self.log_console = LogConsoleWidget(self)
|
||||
|
||||
# --- Create Left Pane with Static Selector and Stacked Editor ---
|
||||
@@ -215,8 +214,8 @@ class MainWindow(QMainWindow):
|
||||
}
|
||||
self.qt_key_to_ftd_map = {}
|
||||
try:
|
||||
base_settings = load_base_config()
|
||||
file_type_defs = base_settings.get('FILE_TYPE_DEFINITIONS', {})
|
||||
# Access configuration directly from the stored object using public methods
|
||||
file_type_defs = self.config.get_file_type_definitions_with_examples()
|
||||
for ftd_key, ftd_value in file_type_defs.items():
|
||||
if isinstance(ftd_value, dict) and 'keybind' in ftd_value:
|
||||
char_key = ftd_value['keybind']
|
||||
@@ -1340,6 +1339,7 @@ def run_gui():
|
||||
"""Initializes and runs the Qt application."""
|
||||
print("--- Reached run_gui() ---")
|
||||
from PySide6.QtGui import QKeySequence
|
||||
from configuration import Configuration # Import Configuration here for instantiation
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
@@ -1351,7 +1351,16 @@ def run_gui():
|
||||
|
||||
app.setPalette(palette)
|
||||
|
||||
window = MainWindow()
|
||||
# Create a Configuration instance and pass it to MainWindow
|
||||
try:
|
||||
config = Configuration()
|
||||
log.info("Configuration loaded successfully for GUI.")
|
||||
except Exception as e:
|
||||
log.critical(f"Failed to load configuration for GUI: {e}")
|
||||
QMessageBox.critical(None, "Configuration Error", f"Failed to load application configuration:\n{e}\n\nApplication will exit.")
|
||||
sys.exit(1) # Exit if configuration fails
|
||||
|
||||
window = MainWindow(config)
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# gui/unified_view_model.py
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot, QMimeData, QByteArray, QDataStream, QIODevice
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot, QMimeData, QByteArray, QDataStream, QIODevice, QPersistentModelIndex
|
||||
from PySide6.QtGui import QColor
|
||||
from pathlib import Path
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
from configuration import load_base_config
|
||||
from typing import List
|
||||
from configuration import Configuration # Import Configuration class
|
||||
|
||||
class CustomRoles:
|
||||
MapTypeRole = Qt.UserRole + 1
|
||||
@@ -46,8 +46,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# --- Drag and Drop MIME Type ---
|
||||
MIME_TYPE = "application/x-filerule-index-list"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
def __init__(self, config: Configuration, parent=None):
|
||||
super().__init__(parent)
|
||||
self._config = config # Store the Configuration object
|
||||
self._source_rules = []
|
||||
# self._display_mode removed
|
||||
self._asset_type_colors = {}
|
||||
@@ -59,9 +60,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
def _load_definitions(self):
|
||||
"""Loads configuration and caches colors and type keys."""
|
||||
try:
|
||||
base_config = load_base_config()
|
||||
asset_type_defs = base_config.get('ASSET_TYPE_DEFINITIONS', {})
|
||||
file_type_defs = base_config.get('FILE_TYPE_DEFINITIONS', {})
|
||||
# Access configuration directly from the stored object using public methods
|
||||
asset_type_defs = self._config.get_asset_type_definitions()
|
||||
file_type_defs = self._config.get_file_type_definitions_with_examples()
|
||||
|
||||
# Cache Asset Type Definitions (Keys and Colors)
|
||||
self._asset_type_keys = sorted(list(asset_type_defs.keys()))
|
||||
@@ -905,37 +906,22 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
encoded_data = QByteArray()
|
||||
stream = QDataStream(encoded_data, QIODevice.OpenModeFlag.WriteOnly)
|
||||
|
||||
dragged_file_info = []
|
||||
# Store QPersistentModelIndex for robustness
|
||||
persistent_indices = []
|
||||
for index in indexes:
|
||||
if not index.isValid() or index.column() != 0:
|
||||
continue
|
||||
item = index.internalPointer()
|
||||
if isinstance(item, FileRule):
|
||||
parent_index = self.parent(index)
|
||||
if parent_index.isValid():
|
||||
# Store: source_row, source_parent_row, source_grandparent_row
|
||||
# This allows reconstructing the index later
|
||||
grandparent_index = self.parent(parent_index)
|
||||
# Ensure grandparent_index is valid before accessing its row
|
||||
if grandparent_index.isValid():
|
||||
dragged_file_info.append((index.row(), parent_index.row(), grandparent_index.row()))
|
||||
else:
|
||||
# Handle case where grandparent is the root (shouldn't happen for FileRule, but safety)
|
||||
# Or if parent() failed unexpectedly
|
||||
log.warning(f"mimeData: Could not get valid grandparent index for FileRule at row {index.row()}, parent row {parent_index.row()}")
|
||||
if index.isValid() and index.column() == 0:
|
||||
item = index.internalPointer()
|
||||
if isinstance(item, FileRule):
|
||||
persistent_indices.append(QPersistentModelIndex(index))
|
||||
log.debug(f"mimeData: Added persistent index for file: {Path(item.file_path).name}")
|
||||
|
||||
else:
|
||||
log.warning(f"mimeData: Could not get parent index for FileRule at row {index.row()}")
|
||||
|
||||
# Write the number of items first, then each tuple
|
||||
stream.writeInt8(len(dragged_file_info))
|
||||
for info in dragged_file_info:
|
||||
stream.writeInt8(info[0])
|
||||
stream.writeInt8(info[1])
|
||||
stream.writeInt8(info[2])
|
||||
# Write the number of items first, then each persistent index
|
||||
stream.writeInt32(len(persistent_indices)) # Use writeInt32 for potentially more items
|
||||
for p_index in persistent_indices:
|
||||
stream.writeQPersistentModelIndex(p_index)
|
||||
|
||||
mime_data.setData(self.MIME_TYPE, encoded_data)
|
||||
log.debug(f"mimeData: Encoded {len(dragged_file_info)} FileRule indices.")
|
||||
log.debug(f"mimeData: Encoded {len(persistent_indices)} FileRule persistent indices.")
|
||||
return mime_data
|
||||
|
||||
def canDropMimeData(self, data: QMimeData, action: Qt.DropAction, row: int, column: int, parent: QModelIndex) -> bool:
|
||||
@@ -970,75 +956,48 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
encoded_data = data.data(self.MIME_TYPE)
|
||||
stream = QDataStream(encoded_data, QIODevice.OpenModeFlag.ReadOnly)
|
||||
|
||||
num_items = stream.readInt8()
|
||||
source_indices_info = []
|
||||
# Read QPersistentModelIndex objects
|
||||
persistent_indices = []
|
||||
num_items = stream.readInt32()
|
||||
log.debug(f"dropMimeData: Decoding {num_items} persistent indices.")
|
||||
for _ in range(num_items):
|
||||
source_row = stream.readInt8()
|
||||
source_parent_row = stream.readInt8()
|
||||
source_grandparent_row = stream.readInt8()
|
||||
source_indices_info.append((source_row, source_parent_row, source_grandparent_row))
|
||||
p_index = stream.readQPersistentModelIndex()
|
||||
if p_index.isValid():
|
||||
persistent_indices.append(p_index)
|
||||
else:
|
||||
log.warning("dropMimeData: Decoded invalid persistent index. Skipping.")
|
||||
|
||||
log.debug(f"dropMimeData: Decoded {len(source_indices_info)} source indices. Target Asset: '{target_asset_item.asset_name}'")
|
||||
log.debug(f"dropMimeData: Decoded {len(persistent_indices)} valid persistent indices. Target Asset: '{target_asset_item.asset_name}'")
|
||||
|
||||
if not source_indices_info:
|
||||
log.warning("dropMimeData: No valid source index information decoded.")
|
||||
if not persistent_indices:
|
||||
log.warning("dropMimeData: No valid persistent index information decoded.")
|
||||
return False
|
||||
|
||||
# Keep track of original parents that might become empty
|
||||
original_parents = set()
|
||||
original_parents_to_check = set()
|
||||
moved_files_new_indices = {}
|
||||
|
||||
# --- BEGIN FIX: Reconstruct all source indices BEFORE the move loop ---
|
||||
source_indices_to_process = []
|
||||
log.debug("Reconstructing initial source indices...")
|
||||
for src_row, src_parent_row, src_grandparent_row in source_indices_info:
|
||||
grandparent_index = self.index(src_grandparent_row, 0, QModelIndex())
|
||||
if not grandparent_index.isValid():
|
||||
log.error(f"dropMimeData: Failed initial reconstruction of grandparent index (row {src_grandparent_row}). Skipping item.")
|
||||
continue
|
||||
old_parent_index = self.index(src_parent_row, 0, grandparent_index)
|
||||
if not old_parent_index.isValid():
|
||||
log.error(f"dropMimeData: Failed initial reconstruction of old parent index (row {src_parent_row}). Skipping item.")
|
||||
continue
|
||||
source_file_index = self.index(src_row, 0, old_parent_index)
|
||||
# Process moves using the persistent indices
|
||||
for p_source_index in persistent_indices:
|
||||
# Convert persistent index back to a model index
|
||||
source_file_index = QModelIndex(p_source_index)
|
||||
|
||||
if not source_file_index.isValid():
|
||||
# Log the specific parent it failed under for better debugging
|
||||
parent_name = getattr(old_parent_index.internalPointer(), 'asset_name', 'Unknown Parent')
|
||||
log.error(f"dropMimeData: Failed initial reconstruction of source file index (original row {src_row}) under parent '{parent_name}'. Skipping item.")
|
||||
log.warning(f"dropMimeData: Persistent index is no longer valid. Skipping item.")
|
||||
continue
|
||||
|
||||
# Check if the reconstructed index actually points to a FileRule
|
||||
item_check = source_file_index.internalPointer()
|
||||
if isinstance(item_check, FileRule):
|
||||
source_indices_to_process.append(source_file_index)
|
||||
log.debug(f" Successfully reconstructed index for file: {Path(item_check.file_path).name}")
|
||||
else:
|
||||
log.warning(f"dropMimeData: Initial reconstructed index (row {src_row}) does not point to a FileRule. Skipping.")
|
||||
|
||||
log.debug(f"Successfully reconstructed {len(source_indices_to_process)} valid source indices.")
|
||||
# --- END FIX ---
|
||||
|
||||
|
||||
# Process moves using the pre-calculated valid indices
|
||||
for source_file_index in source_indices_to_process:
|
||||
# Get the file item (already validated during reconstruction)
|
||||
# Get the file item
|
||||
file_item = source_file_index.internalPointer()
|
||||
if not isinstance(file_item, FileRule):
|
||||
log.error(f"dropMimeData: Index points to non-FileRule item after conversion. Skipping.")
|
||||
continue
|
||||
|
||||
# Track original parent for cleanup (using the valid index)
|
||||
old_parent_index = self.parent(source_file_index)
|
||||
if old_parent_index.isValid():
|
||||
old_parent_asset = old_parent_index.internalPointer()
|
||||
if isinstance(old_parent_asset, AssetRule):
|
||||
# Need grandparent row for the tuple key
|
||||
grandparent_index = self.parent(old_parent_index)
|
||||
if grandparent_index.isValid():
|
||||
original_parents.add((grandparent_index.row(), old_parent_asset.asset_name))
|
||||
else:
|
||||
log.warning(f"Could not get grandparent index for original parent '{old_parent_asset.asset_name}' during cleanup tracking.")
|
||||
else:
|
||||
log.warning(f"Parent of file '{Path(file_item.file_path).name}' is not an AssetRule.")
|
||||
# Track original parent for cleanup using the parent back-reference
|
||||
old_parent_asset = getattr(file_item, 'parent_asset', None)
|
||||
if old_parent_asset and isinstance(old_parent_asset, AssetRule):
|
||||
original_parents_to_check.add(old_parent_asset)
|
||||
else:
|
||||
log.warning(f"Could not get valid parent index for file '{Path(file_item.file_path).name}' during cleanup tracking.")
|
||||
log.warning(f"dropMimeData: File '{Path(file_item.file_path).name}' has no valid parent asset reference for cleanup tracking.")
|
||||
|
||||
|
||||
# Perform the move using the model's method and the valid source_file_index
|
||||
@@ -1052,8 +1011,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
file_item.target_asset_name_override = target_asset_item.asset_name
|
||||
# Need the *new* index of the moved file to emit dataChanged
|
||||
try:
|
||||
# Find the new row of the file item within the target parent's list
|
||||
new_row = target_asset_item.files.index(file_item)
|
||||
new_file_index_col0 = self.index(new_row, 0, parent)
|
||||
# Create the index for the target asset column
|
||||
new_file_index_target_col = self.index(new_row, self.COL_TARGET_ASSET, parent)
|
||||
if new_file_index_target_col.isValid():
|
||||
moved_files_new_indices[file_item.file_path] = new_file_index_target_col
|
||||
@@ -1074,24 +1034,30 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
self.dataChanged.emit(new_index, new_index, [Qt.DisplayRole, Qt.EditRole])
|
||||
|
||||
# --- Cleanup: Remove any original parent AssetRules that are now empty ---
|
||||
log.debug(f"dropMimeData: Checking original parents for cleanup: {list(original_parents)}")
|
||||
for gp_row, asset_name in list(original_parents):
|
||||
log.debug(f"dropMimeData: Checking original parents for cleanup: {[asset.asset_name for asset in original_parents_to_check]}")
|
||||
# Convert set to list to iterate and allow removal from the model
|
||||
for asset_rule_to_check in list(original_parents_to_check):
|
||||
try:
|
||||
if 0 <= gp_row < len(self._source_rules):
|
||||
source_rule = self._source_rules[gp_row]
|
||||
# Find the asset rule within the correct source rule
|
||||
asset_rule_to_check = next((asset for asset in source_rule.assets if asset.asset_name == asset_name), None)
|
||||
|
||||
if asset_rule_to_check and not asset_rule_to_check.files and asset_rule_to_check != target_asset_item:
|
||||
log.info(f"dropMimeData: Attempting cleanup of now empty original parent: '{asset_rule_to_check.asset_name}'")
|
||||
if not self.removeAssetRule(asset_rule_to_check):
|
||||
log.warning(f"dropMimeData: Failed to remove empty original parent '{asset_rule_to_check.asset_name}'.")
|
||||
elif not asset_rule_to_check:
|
||||
log.warning(f"dropMimeData: Cleanup check failed. Could not find original parent asset '{asset_name}' in source rule at row {gp_row}.")
|
||||
# Re-check if the asset is still in the model and is now empty
|
||||
# Use parent back-reference to find the source rule
|
||||
source_rule = getattr(asset_rule_to_check, 'parent_source', None)
|
||||
if source_rule:
|
||||
# Check if the asset rule is still in its parent's list
|
||||
if asset_rule_to_check in source_rule.assets:
|
||||
if not asset_rule_to_check.files and asset_rule_to_check is not target_asset_item:
|
||||
log.info(f"dropMimeData: Attempting cleanup of now empty original parent: '{asset_rule_to_check.asset_name}'")
|
||||
if not self.removeAssetRule(asset_rule_to_check):
|
||||
log.warning(f"dropMimeData: Failed to remove empty original parent '{asset_rule_to_check.asset_name}'.")
|
||||
elif asset_rule_to_check.files:
|
||||
log.debug(f"dropMimeData: Original parent '{asset_rule_to_check.asset_name}' is not empty after moves. Skipping cleanup.")
|
||||
# If it's the target asset, we don't remove it
|
||||
else:
|
||||
log.warning(f"dropMimeData: Cleanup check failed. Original parent asset '{asset_rule_to_check.asset_name}' not found in its source rule's list.")
|
||||
else:
|
||||
log.warning(f"dropMimeData: Cleanup check failed. Invalid grandparent row index {gp_row} found in original_parents set.")
|
||||
log.warning(f"dropMimeData: Cleanup check failed. Original parent asset '{asset_rule_to_check.asset_name}' has no parent source reference.")
|
||||
|
||||
except Exception as e:
|
||||
log.exception(f"dropMimeData: Error during cleanup check for parent '{asset_name}' (gp_row {gp_row}): {e}")
|
||||
log.exception(f"dropMimeData: Error during cleanup check for parent '{asset_rule_to_check.asset_name}': {e}")
|
||||
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user