# gui/unified_view_model.py 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", "Asset-Type Override", # Renamed "Supplier Override" "Target Asset Name Override", "Item-Type Override", "Status", "Output Path" ] COL_NAME = 0 COL_SUPPLIER = 1 COL_ASSET_TYPE = 2 COL_TARGET_ASSET = 3 COL_ITEM_TYPE = 4 COL_STATUS = 5 COL_OUTPUT_PATH = 6 def __init__(self, parent=None): super().__init__(parent) self._source_rules = [] # Now stores a list of SourceRule objects def load_data(self, source_rules_list: list): # Accepts a list """Loads or reloads the model with a list of SourceRule objects.""" self.beginResetModel() self._source_rules = source_rules_list if source_rules_list else [] # Assign the new list # Ensure back-references for parent lookup are set on the NEW items for source_rule in self._source_rules: for asset_rule in source_rule.assets: asset_rule.parent_source = source_rule # Set parent SourceRule for file_rule in asset_rule.files: file_rule.parent_asset = asset_rule # Set parent AssetRule self.endResetModel() def clear_data(self): """Clears the model data.""" self.beginResetModel() self._source_rules = [] # Clear the list self.endResetModel() def get_all_source_rules(self) -> list: """Returns the internal list of SourceRule objects.""" return self._source_rules def rowCount(self, parent: QModelIndex = QModelIndex()) -> int: """Returns the number of rows under the given parent.""" if not parent.isValid(): # Parent is the invisible root. Children are the SourceRules. return len(self._source_rules) parent_item = parent.internalPointer() if isinstance(parent_item, SourceRule): # Parent is a SourceRule. Children are AssetRules. return len(parent_item.assets) elif isinstance(parent_item, AssetRule): # Parent is an AssetRule. Children are FileRules. return len(parent_item.files) elif isinstance(parent_item, FileRule): return 0 # FileRules have no children return 0 # Should not happen for valid items def columnCount(self, parent: QModelIndex = QModelIndex()) -> int: """Returns the number of columns.""" return len(self.Columns) def parent(self, index: QModelIndex) -> QModelIndex: """Returns the parent of the model item with the given index.""" if not index.isValid(): return QModelIndex() child_item = index.internalPointer() if child_item is None: return QModelIndex() # Determine the parent based on the item type if isinstance(child_item, SourceRule): # Parent is the invisible root return QModelIndex() elif isinstance(child_item, AssetRule): # Parent is a SourceRule. Find its row in the _source_rules list. parent_item = getattr(child_item, 'parent_source', None) if parent_item and parent_item in self._source_rules: try: parent_row = self._source_rules.index(parent_item) return self.createIndex(parent_row, 0, parent_item) except ValueError: return QModelIndex() # Should not happen if parent_source is correct else: return QModelIndex() # Parent SourceRule not found or reference missing elif isinstance(child_item, FileRule): # Parent is an AssetRule. Find its row within its parent SourceRule. parent_item = getattr(child_item, 'parent_asset', None) # Get parent AssetRule if parent_item: grandparent_item = getattr(parent_item, 'parent_source', None) # Get the SourceRule if grandparent_item: try: parent_row = grandparent_item.assets.index(parent_item) # We need the index of the grandparent (SourceRule) to create the parent index grandparent_row = self._source_rules.index(grandparent_item) return self.createIndex(parent_row, 0, parent_item) # Create index for the AssetRule parent except ValueError: return QModelIndex() # Parent AssetRule or Grandparent SourceRule not found in respective lists else: return QModelIndex() # Grandparent (SourceRule) reference missing else: return QModelIndex() # Parent AssetRule reference missing return QModelIndex() # Should not be reached def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex: """Returns the index of the item in the model specified by the given row, column and parent index.""" if not self.hasIndex(row, column, parent): return QModelIndex() parent_item = None if not parent.isValid(): # Parent is invisible root. Children are SourceRules. if row < len(self._source_rules): child_item = self._source_rules[row] return self.createIndex(row, column, child_item) else: return QModelIndex() # Row out of bounds for top-level items else: # Parent is a valid index, get its item parent_item = parent.internalPointer() child_item = None if isinstance(parent_item, SourceRule): # Parent is SourceRule. Children are AssetRules. if row < len(parent_item.assets): child_item = parent_item.assets[row] # Ensure parent reference is set if not hasattr(child_item, 'parent_source'): child_item.parent_source = parent_item elif isinstance(parent_item, AssetRule): # Parent is AssetRule. Children are FileRules. if row < len(parent_item.files): child_item = parent_item.files[row] # Ensure parent reference is set if not hasattr(child_item, 'parent_asset'): child_item.parent_asset = parent_item if child_item: # Create index for the child item under the parent return self.createIndex(row, column, child_item) else: # Invalid row or parent type has no children (FileRule) return QModelIndex() def data(self, index: QModelIndex, role: int = Qt.DisplayRole): """Returns the data stored under the given role for the item referred to by the index.""" if not index.isValid(): # Check only index validity, data list might be empty but valid return None item = index.internalPointer() column = index.column() # --- 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 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 "" 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: 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 if column == self.COL_TARGET_ASSET: return item.target_asset_name_override if item.target_asset_name_override is not None else "" if column == self.COL_ITEM_TYPE: return item.item_type_override if item.item_type_override else "" 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: if column == self.COL_TARGET_ASSET: return item.target_asset_name_override if item.target_asset_name_override is not None else "" if column == self.COL_ITEM_TYPE: return item.item_type_override # Return string or None return None # Default for FileRule 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.""" if not index.isValid() or role != Qt.EditRole: # Check only index and role return False item = index.internalPointer() if item is None: # Extra check for safety return False column = index.column() changed = False # --- Handle different item types --- if isinstance(item, SourceRule): # If SourceRule is editable if column == self.COL_SUPPLIER: # 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 elif isinstance(item, AssetRule): if column == self.COL_ASSET_TYPE: # Delegate provides string value (e.g., "Surface", "Model") or None new_value = str(value) if value is not None else None if new_value == "": new_value = None # Treat empty string as None # Update asset_type_override if item.asset_type_override != new_value: item.asset_type_override = new_value changed = True elif isinstance(item, FileRule): if column == self.COL_TARGET_ASSET: # Target Asset Name Override # 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 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 if new_value == "": new_value = None # Treat empty string as None # Update item_type_override if item.item_type_override != new_value: item.item_type_override = new_value changed = True if changed: # Emit dataChanged for the specific index and affected roles self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole]) return True return False def flags(self, index: QModelIndex) -> Qt.ItemFlags: """Returns the item flags for the given index.""" if not index.isValid(): return Qt.NoItemFlags # No flags for invalid index # Start with default flags for a valid item default_flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable item = index.internalPointer() column = index.column() can_edit = False # Determine editability based on item type and column if isinstance(item, SourceRule): # If SourceRule is displayed/editable if column == 1: can_edit = True elif isinstance(item, AssetRule): if column == 2: can_edit = True elif isinstance(item, FileRule): if column == 3: can_edit = True if column == 4: can_edit = True if can_edit: return default_flags | Qt.ItemIsEditable else: return default_flags def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole): """Returns the data for the given role and section in the header.""" if orientation == Qt.Horizontal and role == Qt.DisplayRole: if 0 <= section < len(self.Columns): return self.Columns[section] # Optionally handle Vertical header (row numbers) # if orientation == Qt.Vertical and role == Qt.DisplayRole: # return str(section + 1) return None # Helper to get item from index def getItem(self, index: QModelIndex): """Safely returns the item associated with the index.""" if index.isValid(): item = index.internalPointer() if item: # Ensure internal pointer is not None return item return None # Return None for invalid index or None pointer