Various Issue Completions

#10
#9
#8
#7
#6
#5
This commit is contained in:
2025-05-01 15:44:40 +02:00
parent 26e1a769ce
commit a5be50b587
31 changed files with 589 additions and 123 deletions

View File

@@ -1,7 +1,8 @@
# gui/delegates.py
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
from PySide6.QtCore import Qt, QModelIndex
from config import ALLOWED_ASSET_TYPES, ALLOWED_FILE_TYPES # Import config lists
# Import the new config dictionaries
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
class LineEditDelegate(QStyledItemDelegate):
"""Delegate for editing string values using a QLineEdit."""
@@ -41,17 +42,17 @@ class ComboBoxDelegate(QStyledItemDelegate):
# Add a "clear" option first, associating None with it.
editor.addItem("---", None) # UserData = None
# Populate based on column using lists from config
items_list = None
# Populate based on column using keys from config dictionaries
items_keys = None
if column == 2: # Asset-Type Override (AssetRule)
items_list = ALLOWED_ASSET_TYPES
items_keys = list(ASSET_TYPE_DEFINITIONS.keys())
elif column == 4: # Item-Type Override (FileRule)
items_list = ALLOWED_FILE_TYPES
items_keys = list(FILE_TYPE_DEFINITIONS.keys())
if items_list:
for item_str in items_list:
# Add item with the string itself as text and UserData
editor.addItem(item_str, item_str)
if items_keys:
for item_key in sorted(items_keys): # Sort keys alphabetically for consistency
# Add item with the key string itself as text and UserData
editor.addItem(item_key, item_key)
else:
# If the delegate is incorrectly applied to another column,
# it will just have the "---" option.
@@ -86,4 +87,100 @@ class ComboBoxDelegate(QStyledItemDelegate):
def updateEditorGeometry(self, editor, option, index):
# Ensures the editor widget is placed correctly within the cell.
editor.setGeometry(option.rect)
# gui/delegates.py - New content to insert
import json
import logging
import os # Added for path manipulation if needed, though json.dump handles creation
from PySide6.QtWidgets import QCompleter # Added QCompleter
# Configure logging
log = logging.getLogger(__name__)
SUPPLIERS_CONFIG_PATH = "config/suppliers.json"
class SupplierSearchDelegate(QStyledItemDelegate):
"""
Delegate for editing supplier names using a QLineEdit with auto-completion.
Loads known suppliers from config/suppliers.json and allows adding new ones.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.known_suppliers = self._load_suppliers()
def _load_suppliers(self):
"""Loads the list of known suppliers from the JSON config file."""
try:
with open(SUPPLIERS_CONFIG_PATH, 'r') as f:
suppliers = json.load(f)
if isinstance(suppliers, list):
# Ensure all items are strings
return sorted([str(s) for s in suppliers if isinstance(s, str)])
else:
log.warning(f"'{SUPPLIERS_CONFIG_PATH}' does not contain a valid list. Starting fresh.")
return []
except FileNotFoundError:
log.info(f"'{SUPPLIERS_CONFIG_PATH}' not found. Starting with an empty supplier list.")
return []
except json.JSONDecodeError:
log.error(f"Error decoding JSON from '{SUPPLIERS_CONFIG_PATH}'. Starting fresh.", exc_info=True)
return []
except Exception as e:
log.error(f"An unexpected error occurred loading '{SUPPLIERS_CONFIG_PATH}': {e}", exc_info=True)
return []
def _save_suppliers(self):
"""Saves the current list of known suppliers back to the JSON config file."""
try:
# Ensure the directory exists (though write_to_file handled initial creation)
os.makedirs(os.path.dirname(SUPPLIERS_CONFIG_PATH), exist_ok=True)
with open(SUPPLIERS_CONFIG_PATH, 'w') as f:
json.dump(self.known_suppliers, f, indent=4) # Save sorted list with indentation
log.debug(f"Successfully saved updated supplier list to '{SUPPLIERS_CONFIG_PATH}'.")
except IOError as e:
log.error(f"Could not write to '{SUPPLIERS_CONFIG_PATH}': {e}", exc_info=True)
except Exception as e:
log.error(f"An unexpected error occurred saving '{SUPPLIERS_CONFIG_PATH}': {e}", exc_info=True)
def createEditor(self, parent, option, index):
"""Creates the QLineEdit editor with a QCompleter."""
editor = QLineEdit(parent)
completer = QCompleter(self.known_suppliers, editor)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains) # More flexible matching
completer.setCompletionMode(QCompleter.PopupCompletion) # Standard popup
editor.setCompleter(completer)
return editor
def setEditorData(self, editor: QLineEdit, index: QModelIndex):
"""Sets the editor's initial data from the model."""
# Use EditRole as defined in the model's data() method for supplier
value = index.model().data(index, Qt.EditRole)
editor.setText(str(value) if value is not None else "")
def setModelData(self, editor: QLineEdit, model, index: QModelIndex):
"""Commits the editor's data back to the model and handles new suppliers."""
final_text = editor.text().strip()
value_to_set = final_text if final_text else None # Set None if empty after stripping
# Set data in the model first
model.setData(index, value_to_set, Qt.EditRole)
# Add new supplier if necessary
if final_text and final_text not in self.known_suppliers:
log.info(f"Adding new supplier '{final_text}' to known list.")
self.known_suppliers.append(final_text)
self.known_suppliers.sort() # Keep the list sorted
# Update the completer's model immediately
completer = editor.completer()
if completer:
completer.model().setStringList(self.known_suppliers)
# Save the updated list back to the file
self._save_suppliers()
def updateEditorGeometry(self, editor, option, index):
"""Ensures the editor widget is placed correctly."""
editor.setGeometry(option.rect)

View File

@@ -29,6 +29,7 @@ from rule_structure import SourceRule, AssetRule, FileRule # Import Rule Structu
# Removed: from gui.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel
# Removed: from gui.rule_hierarchy_model import RuleHierarchyModel
from gui.unified_view_model import UnifiedViewModel # Import the new unified model
from gui.delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate # Import delegates
from gui.delegates import LineEditDelegate, ComboBoxDelegate # Import delegates
# --- Backend Imports ---
@@ -171,6 +172,9 @@ class MainWindow(QMainWindow):
# --- Internal State ---
self.current_asset_paths = set() # Store unique paths of assets added
self._pending_predictions = set() # Track input paths awaiting prediction results
self._accumulated_rules = {} # Store {input_path: SourceRule} as results arrive
self._source_file_lists = {} # Store {input_path: [file_list]} for context
# Removed: self.rule_hierarchy_model = RuleHierarchyModel()
# Removed: self._current_source_rule = None # The new model will hold the data
@@ -218,7 +222,7 @@ class MainWindow(QMainWindow):
# --- Connect Editor Signals ---
self._connect_editor_change_signals()
# --- Adjust Splitter ---
# --- Adjust Splitter ---
self.splitter.setSizes([400, 800]) # Initial size ratio
# --- UI Setup Methods ---
@@ -402,10 +406,11 @@ class MainWindow(QMainWindow):
# Instantiate Delegates
lineEditDelegate = LineEditDelegate(self.unified_view)
comboBoxDelegate = ComboBoxDelegate(self.unified_view)
supplierSearchDelegate = SupplierSearchDelegate(self.unified_view) # Instantiate the new delegate
# Set Delegates for Columns (adjust column indices as per UnifiedViewModel)
# Assuming columns are: Name (0), Supplier (1), AssetType (2), TargetAsset (3), ItemType (4)
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, lineEditDelegate)
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, supplierSearchDelegate) # Use the new delegate for Supplier
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ASSET_TYPE, comboBoxDelegate)
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_TARGET_ASSET, lineEditDelegate)
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, comboBoxDelegate)
@@ -667,6 +672,10 @@ class MainWindow(QMainWindow):
if file_list is not None: # Check if extraction was successful (not None)
log.debug(f"Extracted {len(file_list)} files for {input_path_str}. Emitting signal.")
log.info(f"VERIFY: Extracted file list for '{input_path_str}'. Count: {len(file_list)}. Emitting prediction signal.") # DEBUG Verify
# Store file list and mark as pending before emitting
self._source_file_lists[input_path_str] = file_list
self._pending_predictions.add(input_path_str)
log.debug(f"Added '{input_path_str}' to pending predictions. Current pending: {self._pending_predictions}")
self.start_prediction_signal.emit(input_path_str, file_list, selected_preset)
else:
log.warning(f"Skipping prediction for {input_path_str} due to extraction error.")
@@ -844,7 +853,12 @@ class MainWindow(QMainWindow):
self.current_asset_paths.clear()
# self.preview_model.clear_data() # Old model removed
self.unified_model.clear_data() # Clear the new model data
self.statusBar().showMessage("Asset queue cleared.", 3000)
# Clear accumulation state
self._pending_predictions.clear()
self._accumulated_rules.clear()
self._source_file_lists.clear()
log.info("Cleared accumulation state (_pending_predictions, _accumulated_rules, _source_file_lists).")
self.statusBar().showMessage("Asset queue and prediction state cleared.", 3000)
else:
self.statusBar().showMessage("Asset queue is already empty.", 3000)
@@ -924,6 +938,15 @@ class MainWindow(QMainWindow):
log.info(f"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items using Preset='{selected_preset}'")
self.statusBar().showMessage(f"Updating preview for '{selected_preset}'...", 0)
# --- Reset Accumulation State for this batch ---
log.debug("Clearing accumulated rules for new preview batch.")
self._accumulated_rules.clear()
# Reset pending predictions to only include paths in this update request
self._pending_predictions = set(input_paths)
log.debug(f"Reset pending predictions for batch: {self._pending_predictions}")
# Keep _source_file_lists, it might contain lists for paths already processed
# Clearing is handled by model's set_data now, no need to clear table view directly
if self.prediction_thread and self.prediction_handler:
# REMOVED Placeholder SourceRule creation
@@ -981,8 +1004,10 @@ class MainWindow(QMainWindow):
# Connect the new signal to the handler's run_prediction slot using QueuedConnection
self.start_prediction_signal.connect(self.prediction_handler.run_prediction, Qt.ConnectionType.QueuedConnection)
# Removed: self.prediction_handler.prediction_results_ready.connect(self.on_prediction_results_ready) # Old signal
self.prediction_handler.rule_hierarchy_ready.connect(self._on_rule_hierarchy_ready) # Connect the LIST signal
self.prediction_handler.prediction_finished.connect(self.on_prediction_finished)
# Assume PredictionHandler.rule_hierarchy_ready signal is changed to Signal(str, list) -> input_path, rules_list
self.prediction_handler.rule_hierarchy_ready.connect(self._on_rule_hierarchy_ready) # Connect the LIST signal (now with input_path)
# Assume PredictionHandler.prediction_finished signal is changed to Signal(str) -> input_path
self.prediction_handler.prediction_finished.connect(self.on_prediction_finished) # Connect finish signal (now with input_path)
self.prediction_handler.status_message.connect(self.show_status_message)
# --- REMOVED connections causing thread/handler cleanup ---
# self.prediction_handler.prediction_finished.connect(self.prediction_thread.quit)
@@ -1027,12 +1052,30 @@ class MainWindow(QMainWindow):
# # This is no longer needed as _on_rule_hierarchy_ready handles data loading for the new model.
# pass
@Slot()
def on_prediction_finished(self):
log.info(f"[{time.time():.4f}] --> Prediction finished signal received.")
# Optionally update status bar or re-enable controls if needed after prediction finishes
# (Controls are primarily managed by processing_finished, but prediction is a separate background task)
self.statusBar().showMessage("Preview updated.", 3000)
# Slot signature assumes prediction_finished signal is updated to emit input_path: Signal(str)
# Slot signature assumes prediction_finished signal is updated to emit input_path: Signal(str)
@Slot(str)
def on_prediction_finished(self, input_path: str):
"""Handles the completion (potentially failure) of a single prediction task."""
log.info(f"[{time.time():.4f}] --> Prediction finished signal received for: {input_path}")
# Ensure path is removed from pending even if rule_hierarchy_ready wasn't emitted (e.g., critical error)
if input_path in self._pending_predictions:
log.warning(f"Prediction finished for '{input_path}', but it was still marked as pending. Removing.")
self._pending_predictions.discard(input_path)
# Check if this was the last pending item after an error
if not self._pending_predictions:
log.info("Prediction finished, and no more predictions are pending (potentially due to error). Finalizing model update.")
self._finalize_model_update()
else:
# Update status about remaining items
remaining_count = len(self._pending_predictions)
self.statusBar().showMessage(f"Prediction failed/finished for {Path(input_path).name}. Waiting for {remaining_count} more...", 5000)
else:
log.debug(f"Prediction finished for '{input_path}', which was already processed.")
# Original status message might be misleading now, handled by accumulation logic.
# self.statusBar().showMessage("Preview updated.", 3000) # Removed
@Slot(str, str, str)
def update_file_status(self, input_path_str, status, message):
@@ -1580,18 +1623,74 @@ class MainWindow(QMainWindow):
# @Slot(object)
# def _on_rule_updated(self, rule_object): ...
@Slot(list) # Changed signature to accept list
# Slot signature assumes rule_hierarchy_ready signal is updated to emit input_path: Signal(str, list)
# Slot signature matches rule_hierarchy_ready = Signal(list)
@Slot(list)
def _on_rule_hierarchy_ready(self, source_rules_list: list):
log.debug(f"--> Entered _on_rule_hierarchy_ready with {len(source_rules_list)} SourceRule(s)")
"""Receives the generated list of SourceRule hierarchies and updates the unified view model."""
# Removed: log.info(f"Received rule hierarchy ready signal for input: {source_rule.input_path}")
# Removed: self._current_source_rule = source_rule # This concept might need rethinking if processing needs a specific rule
# Removed: self.rule_hierarchy_model.set_root_rule(source_rule)
# Removed: self.hierarchy_tree_view.expandToDepth(0)
"""Receives prediction results (a list containing one SourceRule) for a single input path,
accumulates them, and updates the model when all are ready."""
# Load the LIST of data into the new UnifiedViewModel
self.unified_model.load_data(source_rules_list) # Pass the list
log.debug("Unified view model updated with new list of SourceRules.")
# --- Extract input_path from the received rule ---
input_path = None
source_rule = None
if source_rules_list and isinstance(source_rules_list[0], SourceRule):
source_rule = source_rules_list[0]
input_path = source_rule.input_path
log.debug(f"--> Entered _on_rule_hierarchy_ready for '{input_path}' with {len(source_rules_list)} SourceRule(s)")
elif source_rules_list:
log.error(f"Received non-SourceRule object in list: {type(source_rules_list[0])}. Cannot process.")
# Attempt to find which pending prediction this might correspond to? Difficult.
# For now, we can't reliably remove from pending without the path.
return
else:
# This case might happen if prediction failed critically before creating a rule.
# The prediction_finished signal (which now includes input_path) should handle removing from pending.
log.warning("Received empty source_rules_list in _on_rule_hierarchy_ready. Prediction likely failed.")
return # Nothing to accumulate
if input_path is None:
log.error("Could not determine input_path from received source_rules_list. Aborting accumulation.")
return
if input_path not in self._pending_predictions:
log.warning(f"Received rule hierarchy for '{input_path}', but it was not in the pending set. Ignoring stale result? Pending: {self._pending_predictions}")
return # Ignore if not expected
# --- Accumulate Result ---
if source_rule: # Check if we successfully got the rule object
self._accumulated_rules[input_path] = source_rule
log.debug(f"Accumulated rule for '{input_path}'. Total accumulated: {len(self._accumulated_rules)}")
else:
# This path is already handled by the initial checks, but log just in case.
log.warning(f"No valid SourceRule found for '{input_path}' to accumulate.")
# --- Mark as Completed ---
self._pending_predictions.discard(input_path)
log.debug(f"Removed '{input_path}' from pending predictions. Remaining: {self._pending_predictions}")
# --- Check for Completion ---
if not self._pending_predictions:
log.info("All pending predictions received. Finalizing model update.")
self._finalize_model_update()
else:
# Update status bar with progress
completed_count = len(self._accumulated_rules)
pending_count = len(self._pending_predictions)
total_count = completed_count + pending_count # This might be slightly off if some failed without rules
status_msg = f"Preview updated for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_count} requested)..."
self.statusBar().showMessage(status_msg, 5000)
log.debug(status_msg)
def _finalize_model_update(self):
"""Combines accumulated rules and updates the UI model and view."""
log.debug("Entering _finalize_model_update")
final_rules = list(self._accumulated_rules.values())
log.info(f"Finalizing model with {len(final_rules)} accumulated SourceRule(s).")
# Load the FINAL LIST of data into the UnifiedViewModel
self.unified_model.load_data(final_rules)
log.debug("Unified view model updated with final list of SourceRules.")
# Resize columns to fit content after loading data
for col in range(self.unified_model.columnCount()):
@@ -1599,6 +1698,8 @@ class MainWindow(QMainWindow):
log.debug("Unified view columns resized to contents.")
self.unified_view.expandToDepth(1) # Expand Source -> Asset level
self.statusBar().showMessage(f"Preview complete for {len(final_rules)} asset(s).", 5000)
# --- Main Execution ---
def run_gui():

View File

@@ -26,8 +26,8 @@ try:
# from asset_processor import AssetProcessor, AssetProcessingError
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType
import config as app_config # Import project's config module
# Import the lists directly for easier access
from config import ALLOWED_ASSET_TYPES, ALLOWED_FILE_TYPES
# Import the new dictionaries directly for easier access
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
BACKEND_AVAILABLE = True
except ImportError as e:
print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}")
@@ -209,8 +209,8 @@ class PredictionHandler(QObject):
# --- Signals ---
# Emitted when the hierarchical rule structure is ready for a single source
rule_hierarchy_ready = Signal(list) # Emits a LIST containing ONE SourceRule object
# Emitted when prediction/hierarchy generation for a source is done
prediction_finished = Signal()
# Emitted when prediction/hierarchy generation for a source is done (emits the input_source_identifier)
prediction_finished = Signal(str)
# Emitted for status updates
status_message = Signal(str, int)
@@ -261,7 +261,7 @@ class PredictionHandler(QObject):
log.warning(f"Input source path does not exist: '{input_source_identifier}'. Skipping prediction.")
self.status_message.emit("Input path not found.", 3000)
self.rule_hierarchy_ready.emit([])
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
return
@@ -269,30 +269,30 @@ class PredictionHandler(QObject):
self.status_message.emit(f"Analyzing '{source_path.name}'...", 0)
config: Configuration | None = None
allowed_asset_types: List[str] = []
allowed_file_types: List[str] = [] # These are ItemType names
asset_type_definitions: Dict[str, Dict] = {}
file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
try:
config = Configuration(preset_name)
# Load allowed types from the project's config module
# Load allowed types from the project's config module (now dictionaries)
if app_config:
allowed_asset_types = getattr(app_config, 'ALLOWED_ASSET_TYPES', [])
allowed_file_types = getattr(app_config, 'ALLOWED_FILE_TYPES', [])
log.debug(f"Loaded allowed AssetTypes: {allowed_asset_types}")
log.debug(f"Loaded allowed FileTypes (ItemTypes): {allowed_file_types}")
asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
else:
log.warning("Project config module not loaded. Cannot get allowed types.")
log.warning("Project config module not loaded. Cannot get type definitions.")
except ConfigurationError as e:
log.error(f"Failed to load configuration for preset '{preset_name}': {e}")
self.status_message.emit(f"Error loading preset '{preset_name}': {e}", 5000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
except Exception as e:
log.exception(f"Unexpected error loading configuration or allowed types for preset '{preset_name}': {e}")
self.status_message.emit(f"Unexpected error loading preset '{preset_name}'.", 5000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
@@ -303,7 +303,7 @@ class PredictionHandler(QObject):
except Exception as e:
log.exception(f"Error during file classification for source '{input_source_identifier}': {e}")
self.status_message.emit(f"Error classifying files: {e}", 5000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
@@ -311,7 +311,7 @@ class PredictionHandler(QObject):
log.warning(f"Classification yielded no assets for source '{input_source_identifier}'.")
self.status_message.emit("No assets identified from files.", 3000)
self.rule_hierarchy_ready.emit([]) # Emit empty list
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
@@ -348,16 +348,16 @@ class PredictionHandler(QObject):
# Ensure the predicted type is allowed, fallback if necessary
# Now predicted_asset_type is already a string
if allowed_asset_types and predicted_asset_type not in allowed_asset_types:
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ALLOWED_ASSET_TYPES. Falling back.")
if asset_type_definitions and predicted_asset_type not in asset_type_definitions:
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. Falling back.")
# Fallback logic: use the default from config if allowed, else first allowed type
default_type = getattr(app_config, 'DEFAULT_ASSET_CATEGORY', 'Surface')
if default_type in allowed_asset_types:
if default_type in asset_type_definitions:
predicted_asset_type = default_type
elif allowed_asset_types:
predicted_asset_type = allowed_asset_types[0]
elif asset_type_definitions:
predicted_asset_type = list(asset_type_definitions.keys())[0] # Use first key
else:
pass # Keep the original prediction if allowed list is empty
pass # Keep the original prediction if definitions are empty
asset_rule = AssetRule(
@@ -370,35 +370,42 @@ class PredictionHandler(QObject):
file_rules = []
for file_info in files_info:
# Determine FileRule level overrides/defaults
item_type_override = file_info['item_type'] # From classification
base_item_type = file_info['item_type'] # Type from classification (e.g., COL, NRM, EXTRA)
target_asset_name_override = file_info['asset_name'] # From classification
# Ensure the predicted item type is allowed (check against prefixed version), skipping EXTRA and FILE_IGNORE
# Only prefix if it's a map type that doesn't already have the prefix
prefixed_item_type = f"MAP_{item_type_override}" if not item_type_override.startswith("MAP_") and item_type_override not in ["FILE_IGNORE", "EXTRA", "MODEL"] else item_type_override
# Check if the (potentially prefixed) type is allowed, but only if it's not supposed to be ignored or extra
if allowed_file_types and prefixed_item_type not in allowed_file_types and item_type_override not in ["FILE_IGNORE", "EXTRA"]:
log.warning(f"Predicted ItemType '{item_type_override}' (checked as '{prefixed_item_type}') for file '{file_info['file_path']}' is not in ALLOWED_FILE_TYPES. Setting to FILE_IGNORE.")
item_type_override = "FILE_IGNORE" # Fallback to FILE_IGNORE string
# Determine the final item_type string (prefix maps, check if allowed)
final_item_type = base_item_type # Start with the base type
if not base_item_type.startswith("MAP_") and base_item_type not in ["FILE_IGNORE", "EXTRA", "MODEL"]:
# Prefix map types that don't already have it
final_item_type = f"MAP_{base_item_type}"
# Check if the final type is allowed (exists as a key in config)
if file_type_definitions and final_item_type not in file_type_definitions and base_item_type not in ["FILE_IGNORE", "EXTRA"]:
log.warning(f"Predicted ItemType '{base_item_type}' (checked as '{final_item_type}') for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS. Setting base type to FILE_IGNORE.")
final_item_type = "FILE_IGNORE" # Fallback base type to FILE_IGNORE string
# Output format is determined by the engine, not predicted here. Leave as None.
output_format_override = None
# User override for item type starts as None
item_type_override = None
# --- DEBUG LOG: Inspect data before FileRule creation ---
log.debug(f" Creating FileRule for: {file_info['file_path']}")
log.debug(f" Using item_type_override: {item_type_override}")
log.debug(f" Using target_asset_name_override: {target_asset_name_override}")
log.debug(f" Base Item Type (from classification): {base_item_type}")
log.debug(f" Final Item Type (for model): {final_item_type}")
log.debug(f" Target Asset Name Override: {target_asset_name_override}")
# Explicitly check and log the flag value from file_info
is_gloss_source_value = file_info.get('is_gloss_source', 'MISSING') # Get value or 'MISSING'
log.debug(f" Value for 'is_gloss_source' from file_info: {is_gloss_source_value}")
# --- End DEBUG LOG ---
# TODO: Need to verify FileRule constructor accepts is_gloss_source
# and pass is_gloss_source_value if it does.
# Pass the retrieved flag value to the constructor
file_rule = FileRule(
file_path=file_info['file_path'], # This is static info based on input
item_type=final_item_type, # Set the new base item_type field
# --- Populate ONLY Overridable Fields ---
item_type_override=item_type_override,
# Initialize override with the classified type for display
item_type_override=final_item_type,
target_asset_name_override=target_asset_name_override,
output_format_override=output_format_override,
is_gloss_source=is_gloss_source_value if isinstance(is_gloss_source_value, bool) else False, # Pass the flag, ensure boolean
@@ -421,7 +428,7 @@ class PredictionHandler(QObject):
log.exception(f"Error building rule hierarchy for source '{input_source_identifier}': {e}")
self.status_message.emit(f"Error building rules: {e}", 5000)
# Don't emit hierarchy, just finish
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
# Removed erroneous temp_dir_obj cleanup
return
@@ -439,7 +446,7 @@ class PredictionHandler(QObject):
# Removed prediction_results_ready signal emission
self.status_message.emit(f"Analysis complete for '{input_source_identifier}'.", 3000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
# Removed temp_dir_obj cleanup - not relevant here
log.info(f"[{time.time():.4f}][T:{thread_id}] <-- Exiting PredictionHandler.run_prediction.")

View File

@@ -1,15 +1,22 @@
# gui/unified_view_model.py
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal # Added Signal
from PySide6.QtGui import QColor # Added for background role
from pathlib import Path # Added for file_name extraction
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType import
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS # Added for coloring
class UnifiedViewModel(QAbstractItemModel):
# --- Color Constants for Row Backgrounds ---
# Old colors removed, using config now + fixed source color
SOURCE_RULE_COLOR = QColor("#306091") # Fixed color for SourceRule rows
# -----------------------------------------
"""
A QAbstractItemModel for displaying and editing the hierarchical structure
of SourceRule -> AssetRule -> FileRule.
"""
Columns = [
"Name", "Supplier Override", "Asset-Type Override",
"Name", "Supplier", "Asset-Type Override", # Renamed "Supplier Override"
"Target Asset Name Override", "Item-Type Override",
"Status", "Output Path"
]
@@ -165,55 +172,101 @@ class UnifiedViewModel(QAbstractItemModel):
item = index.internalPointer()
column = index.column()
# --- Handle different item types ---
if isinstance(item, SourceRule): # This might only be relevant if SourceRule is displayed
if role == Qt.DisplayRole:
if column == 0: return item.input_path
# Use supplier_override if set, otherwise empty string
if column == self.COL_SUPPLIER: return item.supplier_override if item.supplier_override is not None else ""
# Other columns return None or "" for SourceRule
elif role == Qt.EditRole:
# Return supplier_override for editing
if column == self.COL_SUPPLIER: return item.supplier_override if item.supplier_override is not None else ""
# --- Handle Background Role ---
if role == Qt.BackgroundRole:
# item is already fetched at line 172
if isinstance(item, SourceRule):
return self.SOURCE_RULE_COLOR # Use the class constant
elif isinstance(item, AssetRule):
# Determine effective asset type
asset_type = item.asset_type_override if item.asset_type_override else item.asset_type
if asset_type:
type_info = ASSET_TYPE_DEFINITIONS.get(asset_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for asset type '{asset_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for asset type '{asset_type}' in config.")
return None # Fallback if color key missing
else:
# Optional: Add logging for missing asset type definition
# print(f"Warning: Asset type '{asset_type}' not found in ASSET_TYPE_DEFINITIONS.")
return None # Fallback if type not in config
else:
return None # Fallback if no asset_type determined
elif isinstance(item, FileRule):
# Determine effective item type: Prioritize override, then use base type
effective_item_type = item.item_type_override if item.item_type_override is not None else item.item_type
if effective_item_type:
type_info = FILE_TYPE_DEFINITIONS.get(effective_item_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for file type '{item_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for file type '{item_type}' in config.")
return None # Fallback if color key missing
else:
# File types often don't have specific colors, so no warning needed unless debugging
return None # Fallback if type not in config
else:
return None # Fallback if no item_type determined
else: # Other item types or if item is None
return None
# --- Handle other roles (Display, Edit, etc.) ---
if isinstance(item, SourceRule):
if role == Qt.DisplayRole or role == Qt.EditRole: # Combine Display and Edit logic
if column == self.COL_NAME:
return Path(item.input_path).name # Display only basename for SourceRule
elif column == self.COL_SUPPLIER:
# Return override if set, otherwise the original identifier, else empty string
display_value = item.supplier_override if item.supplier_override is not None else item.supplier_identifier
return display_value if display_value is not None else ""
# Other columns return None or "" for SourceRule in Display/Edit roles
return None # Default for SourceRule for other roles/columns
elif isinstance(item, AssetRule):
if role == Qt.DisplayRole:
if column == self.COL_NAME: return item.asset_name
# Use asset_type_override if set, otherwise fall back to predicted asset_type
if column == self.COL_ASSET_TYPE:
display_value = item.asset_type_override if item.asset_type_override is not None else item.asset_type
return display_value if display_value else ""
# Placeholder columns
if column == self.COL_STATUS: return "" # Status (Not handled yet)
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
elif role == Qt.EditRole:
# Return asset_type_override for editing (delegate expects string or None)
if column == self.COL_ASSET_TYPE:
return item.asset_type_override # Return string or None
return None # Default for AssetRule
elif isinstance(item, FileRule):
if role == Qt.DisplayRole:
if column == self.COL_NAME: return Path(item.file_path).name # Display only filename
# Use target_asset_name_override if set, otherwise empty string
if column == self.COL_TARGET_ASSET:
return item.target_asset_name_override if item.target_asset_name_override is not None else ""
# Use item_type_override if set, otherwise empty string (assuming predicted isn't stored directly)
if column == self.COL_ITEM_TYPE:
# Assuming item_type_override stores the string name of the ItemType enum
return item.item_type_override if item.item_type_override else ""
if column == self.COL_STATUS: return "" # Status (Not handled yet)
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
elif role == Qt.EditRole:
# Return target_asset_name_override for editing
if column == self.COL_TARGET_ASSET: return item.target_asset_name_override if item.target_asset_name_override is not None else ""
# Return item_type_override for editing (delegate expects string or None)
if column == self.COL_ITEM_TYPE: return item.item_type_override # Return string or None
return None # Default for FileRule
return None # Should not be reached if item is one of the known types
return None # Default return if role/item combination not handled
def setData(self, index: QModelIndex, value, role: int = Qt.EditRole) -> bool:
"""Sets the role data for the item at index to value."""
@@ -229,10 +282,17 @@ class UnifiedViewModel(QAbstractItemModel):
# --- Handle different item types ---
if isinstance(item, SourceRule): # If SourceRule is editable
if column == self.COL_SUPPLIER:
# Ensure value is string or None
new_value = str(value).strip() if value is not None else None
if new_value == "": new_value = None # Treat empty string as None
# Update supplier_override
# Get the new value, strip whitespace, treat empty as None
new_value = str(value).strip() if value is not None and str(value).strip() else None
# Get the original identifier (assuming it exists on SourceRule)
original_identifier = getattr(item, 'supplier_identifier', None)
# If the new value is the same as the original, clear the override
if new_value == original_identifier:
new_value = None # Effectively removes the override
# Update supplier_override only if it's different
if item.supplier_override != new_value:
item.supplier_override = new_value
changed = True
@@ -254,8 +314,122 @@ class UnifiedViewModel(QAbstractItemModel):
if new_value == "": new_value = None # Treat empty string as None
# Update target_asset_name_override
if item.target_asset_name_override != new_value:
old_value = item.target_asset_name_override # Store old value for potential revert/comparison
item.target_asset_name_override = new_value
changed = True
# --- Start: New Direct Model Restructuring Logic ---
old_parent_asset = getattr(item, 'parent_asset', None)
if old_parent_asset: # Ensure we have the old parent
source_rule = getattr(old_parent_asset, 'parent_source', None)
if source_rule: # Ensure we have the grandparent
new_target_name = new_value # Can be None or a string
# Get old parent index and source row
try:
grandparent_row = self._source_rules.index(source_rule)
old_parent_row = source_rule.assets.index(old_parent_asset)
source_row = old_parent_asset.files.index(item)
old_parent_index = self.createIndex(old_parent_row, 0, old_parent_asset)
grandparent_index = self.createIndex(grandparent_row, 0, source_rule) # Needed for insert/remove parent
except ValueError:
print("Error: Could not find item, parent, or grandparent in model structure during setData.")
item.target_asset_name_override = old_value # Revert data change
return False # Indicate failure
target_parent_asset = None
target_parent_index = QModelIndex()
target_parent_row = -1 # Row within source_rule.assets
target_row = -1 # Row within target_parent_asset.files
move_occurred = False # Flag to track if a move happened
# 1. Find existing target parent
if new_target_name: # Only search if a specific target is given
for i, asset in enumerate(source_rule.assets):
if asset.asset_name == new_target_name:
target_parent_asset = asset
target_parent_row = i
target_parent_index = self.createIndex(target_parent_row, 0, target_parent_asset)
break
# 2. Handle Move/Creation
if target_parent_asset:
# --- Move to Existing Parent ---
if target_parent_asset != old_parent_asset: # Don't move if target is the same as old parent
target_row = len(target_parent_asset.files) # Append to the end
# print(f"DEBUG: Moving {Path(item.file_path).name} from {old_parent_asset.asset_name} ({source_row}) to {target_parent_asset.asset_name} ({target_row})")
self.beginMoveRows(old_parent_index, source_row, source_row, target_parent_index, target_row)
# Restructure internal data
old_parent_asset.files.pop(source_row)
target_parent_asset.files.append(item)
item.parent_asset = target_parent_asset # Update parent reference
self.endMoveRows()
move_occurred = True
else:
# Target is the same as the old parent. No move needed.
pass
elif new_target_name: # Only create if a *new* specific target name was given
# --- Create New Parent and Move ---
# print(f"DEBUG: Creating new parent '{new_target_name}' and moving {Path(item.file_path).name}")
# Create new AssetRule
new_asset_rule = AssetRule(asset_name=new_target_name)
new_asset_rule.asset_type = old_parent_asset.asset_type # Copy type from old parent
new_asset_rule.asset_type_override = old_parent_asset.asset_type_override # Copy override too
new_asset_rule.parent_source = source_rule # Set parent reference
# Determine insertion row for the new parent (e.g., append)
new_parent_row = len(source_rule.assets)
# print(f"DEBUG: Inserting new parent at row {new_parent_row} under {Path(source_rule.input_path).name}")
# Emit signals for inserting the new parent row
self.beginInsertRows(grandparent_index, new_parent_row, new_parent_row)
source_rule.assets.insert(new_parent_row, new_asset_rule) # Insert into data structure
self.endInsertRows()
# Get index for the newly inserted parent
target_parent_index = self.createIndex(new_parent_row, 0, new_asset_rule)
target_row = 0 # Insert file at the beginning of the new parent (for signal)
# Emit signals for moving the file row
# print(f"DEBUG: Moving {Path(item.file_path).name} from {old_parent_asset.asset_name} ({source_row}) to new {new_asset_rule.asset_name} ({target_row})")
self.beginMoveRows(old_parent_index, source_row, source_row, target_parent_index, target_row)
# Restructure internal data
old_parent_asset.files.pop(source_row)
new_asset_rule.files.append(item) # Append is fine, target_row=0 was for signal
item.parent_asset = new_asset_rule # Update parent reference
self.endMoveRows()
move_occurred = True
# Update target_parent_asset for potential cleanup check later
target_parent_asset = new_asset_rule
else: # new_target_name is None or empty
# No move happens when the override is simply cleared.
pass
# 3. Cleanup Empty Old Parent (only if a move occurred and old parent is empty)
if move_occurred and not old_parent_asset.files:
# print(f"DEBUG: Removing empty old parent {old_parent_asset.asset_name}")
try:
# Find the row of the old parent again, as it might have shifted
old_parent_row_for_removal = source_rule.assets.index(old_parent_asset)
# print(f"DEBUG: Removing parent at row {old_parent_row_for_removal} under {Path(source_rule.input_path).name}")
self.beginRemoveRows(grandparent_index, old_parent_row_for_removal, old_parent_row_for_removal)
source_rule.assets.pop(old_parent_row_for_removal)
self.endRemoveRows()
except ValueError:
print(f"Error: Could not find old parent '{old_parent_asset.asset_name}' for removal.")
# Log error, but continue
else:
print("Error: Could not find grandparent SourceRule during setData restructuring.")
item.target_asset_name_override = old_value # Revert
return False
else:
print("Error: Could not find parent AssetRule during setData restructuring.")
item.target_asset_name_override = old_value # Revert
return False
# --- End: New Direct Model Restructuring Logic ---
elif column == self.COL_ITEM_TYPE: # Item-Type Override
# Delegate provides string value (e.g., "MAP_COL") or None
new_value = str(value) if value is not None else None