@@ -208,6 +208,57 @@ class SupplierSearchDelegate(QStyledItemDelegate):
|
||||
# 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)
|
||||
class ItemTypeSearchDelegate(QStyledItemDelegate):
|
||||
"""
|
||||
Delegate for editing item types using a QLineEdit with auto-completion.
|
||||
Loads known item types from the UnifiedViewModel's cached keys.
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
# No persistent list needed here, suggestions come from the model
|
||||
|
||||
def createEditor(self, parent, option, index: QModelIndex):
|
||||
"""Creates the QLineEdit editor with a QCompleter."""
|
||||
editor = QLineEdit(parent)
|
||||
model = index.model()
|
||||
item_keys = []
|
||||
|
||||
# Get keys directly from the UnifiedViewModel
|
||||
if hasattr(model, '_file_type_keys'):
|
||||
try:
|
||||
item_keys = model._file_type_keys # Use cached keys
|
||||
except Exception as e:
|
||||
log.error(f"Error getting _file_type_keys from model in ItemTypeSearchDelegate: {e}")
|
||||
item_keys = []
|
||||
else:
|
||||
log.warning("ItemTypeSearchDelegate: Model is missing _file_type_keys attribute. Suggestions will be empty.")
|
||||
|
||||
completer = QCompleter(item_keys, editor)
|
||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
completer.setFilterMode(Qt.MatchContains)
|
||||
completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||
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 item type override
|
||||
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."""
|
||||
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
|
||||
# The model's setData handles updating the override and standard_map_type
|
||||
model.setData(index, value_to_set, Qt.EditRole)
|
||||
# DO NOT add to a persistent list or save back to config
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
"""Ensures the editor widget is placed correctly."""
|
||||
editor.setGeometry(option.rect)
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
import functools # Ensure functools is imported directly for partial
|
||||
from functools import partial
|
||||
|
||||
from PySide6.QtWidgets import QApplication # Added for processEvents
|
||||
@@ -19,18 +20,23 @@ from PySide6.QtGui import QColor, QAction, QPalette, QClipboard, QGuiApplication
|
||||
|
||||
# --- Local GUI Imports ---
|
||||
# Import delegates and models needed by the panel
|
||||
from .delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate
|
||||
from .delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate, ItemTypeSearchDelegate # Added ItemTypeSearchDelegate
|
||||
from .unified_view_model import UnifiedViewModel # Assuming UnifiedViewModel is passed in
|
||||
|
||||
# --- Backend Imports ---
|
||||
# Import Rule Structures if needed for context menus etc.
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
# Import config loading if defaults are needed directly here (though better passed from MainWindow)
|
||||
# Import configuration directly for PRESETS_DIR access
|
||||
import configuration
|
||||
try:
|
||||
from configuration import ConfigurationError, load_base_config
|
||||
except ImportError:
|
||||
ConfigurationError = Exception
|
||||
load_base_config = None
|
||||
# Define PRESETS_DIR fallback if configuration module fails to load entirely
|
||||
class configuration:
|
||||
PRESETS_DIR = "Presets" # Fallback path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,6 +63,7 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
# Request to re-interpret selected items using LLM
|
||||
llm_reinterpret_requested = Signal(list) # Emits list of source paths
|
||||
preset_reinterpret_requested = Signal(list, str) # Emits list[source_paths], preset_name
|
||||
|
||||
# Notify when the output directory changes
|
||||
output_dir_changed = Signal(str)
|
||||
@@ -130,12 +137,14 @@ class MainPanelWidget(QWidget):
|
||||
# TODO: Revisit ComboBoxDelegate dependency
|
||||
comboBoxDelegate = ComboBoxDelegate(self) # Pass only parent (self)
|
||||
supplierSearchDelegate = SupplierSearchDelegate(self) # Pass parent
|
||||
itemTypeSearchDelegate = ItemTypeSearchDelegate(self) # Instantiate new delegate
|
||||
|
||||
# Set Delegates for Columns
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, supplierSearchDelegate)
|
||||
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)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, itemTypeSearchDelegate) # Use ItemTypeSearchDelegate
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_NAME, lineEditDelegate) # Assign LineEditDelegate for AssetRule names
|
||||
|
||||
# Configure View Appearance
|
||||
self.unified_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
@@ -156,6 +165,17 @@ class MainPanelWidget(QWidget):
|
||||
# Enable custom context menu
|
||||
self.unified_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
|
||||
# --- Enable Drag and Drop ---
|
||||
self.unified_view.setDragEnabled(True)
|
||||
self.unified_view.setAcceptDrops(True)
|
||||
self.unified_view.setDropIndicatorShown(True)
|
||||
self.unified_view.setDefaultDropAction(Qt.MoveAction)
|
||||
# Use InternalMove for handling drops within the model itself
|
||||
self.unified_view.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
# Ensure ExtendedSelection is set (already done above, but good practice)
|
||||
self.unified_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
# --- End Drag and Drop ---
|
||||
|
||||
# Add the Unified View to the main layout
|
||||
main_layout.addWidget(self.unified_view, 1) # Give it stretch factor 1
|
||||
|
||||
@@ -240,11 +260,11 @@ class MainPanelWidget(QWidget):
|
||||
bottom_controls_layout.addWidget(self.workers_spinbox)
|
||||
bottom_controls_layout.addStretch(1)
|
||||
|
||||
# --- LLM Re-interpret Button ---
|
||||
self.llm_reinterpret_button = QPushButton("Re-interpret Selected with LLM")
|
||||
self.llm_reinterpret_button.setToolTip("Re-run LLM interpretation on the selected source items.")
|
||||
self.llm_reinterpret_button.setEnabled(False) # Initially disabled
|
||||
bottom_controls_layout.addWidget(self.llm_reinterpret_button)
|
||||
# --- LLM Re-interpret Button (Removed, functionality moved to context menu) ---
|
||||
# self.llm_reinterpret_button = QPushButton("Re-interpret Selected with LLM")
|
||||
# self.llm_reinterpret_button.setToolTip("Re-run LLM interpretation on the selected source items.")
|
||||
# self.llm_reinterpret_button.setEnabled(False) # Initially disabled
|
||||
# bottom_controls_layout.addWidget(self.llm_reinterpret_button)
|
||||
|
||||
self.clear_queue_button = QPushButton("Clear Queue")
|
||||
self.start_button = QPushButton("Start Processing")
|
||||
@@ -263,7 +283,6 @@ class MainPanelWidget(QWidget):
|
||||
self.output_path_edit.editingFinished.connect(self._on_output_path_changed) # Emit signal when user finishes editing
|
||||
|
||||
# Unified View
|
||||
self.unified_view.selectionModel().selectionChanged.connect(self._update_llm_reinterpret_button_state)
|
||||
self.unified_view.customContextMenuRequested.connect(self._show_unified_view_context_menu)
|
||||
|
||||
# Blender Controls
|
||||
@@ -280,7 +299,7 @@ class MainPanelWidget(QWidget):
|
||||
self.clear_queue_button.clicked.connect(self.clear_queue_requested) # Emit signal directly
|
||||
self.start_button.clicked.connect(self._on_start_processing_clicked) # Use slot to gather data
|
||||
self.cancel_button.clicked.connect(self.cancel_requested) # Emit signal directly
|
||||
self.llm_reinterpret_button.clicked.connect(self._on_llm_reinterpret_clicked) # Use slot to gather data
|
||||
# self.llm_reinterpret_button.clicked.connect(self._on_llm_reinterpret_clicked) # Removed button connection
|
||||
|
||||
# --- Slots for Internal UI Logic ---
|
||||
|
||||
@@ -371,79 +390,98 @@ class MainPanelWidget(QWidget):
|
||||
}
|
||||
self.process_requested.emit(settings)
|
||||
|
||||
@Slot()
|
||||
def _update_llm_reinterpret_button_state(self):
|
||||
"""Enables/disables the LLM re-interpret button based on selection and LLM status."""
|
||||
selection_model = self.unified_view.selectionModel()
|
||||
has_selection = selection_model is not None and selection_model.hasSelection()
|
||||
# Enable only if there's a selection AND LLM is not currently active
|
||||
self.llm_reinterpret_button.setEnabled(has_selection and not self.llm_processing_active)
|
||||
# Removed _update_llm_reinterpret_button_state as the button is removed.
|
||||
# Context menu actions will handle their own enabled state or rely on _on_llm_reinterpret_clicked checks.
|
||||
|
||||
def _get_unique_source_dirs_from_selection(self, selected_indexes: list[QModelIndex]) -> set[str]:
|
||||
"""
|
||||
Extracts unique, valid source directory/zip paths from the selected QModelIndex list.
|
||||
Traverses up the model hierarchy to find the parent SourceRule for each index.
|
||||
"""
|
||||
unique_source_dirs = set()
|
||||
model = self.unified_view.model()
|
||||
if not model:
|
||||
log.error("Unified view model not found.")
|
||||
return unique_source_dirs
|
||||
|
||||
processed_source_paths = set() # To avoid processing duplicates if multiple cells of the same source are selected
|
||||
|
||||
for index in selected_indexes:
|
||||
if not index.isValid():
|
||||
continue
|
||||
|
||||
# Use the model's getItem method for robust node retrieval
|
||||
item_node = model.getItem(index)
|
||||
source_rule_node = None
|
||||
|
||||
# Find the parent SourceRule node by traversing upwards using the index
|
||||
source_rule_node = None
|
||||
current_index = index # Start with the index of the selected item
|
||||
while current_index.isValid():
|
||||
current_item = model.getItem(current_index)
|
||||
if isinstance(current_item, SourceRule):
|
||||
source_rule_node = current_item
|
||||
break
|
||||
current_index = model.parent(current_index) # Move to the parent index
|
||||
# If loop finishes without break, source_rule_node remains None
|
||||
|
||||
if source_rule_node:
|
||||
# Use input_path attribute as defined in SourceRule
|
||||
source_path = getattr(source_rule_node, 'input_path', None)
|
||||
if source_path and source_path not in processed_source_paths:
|
||||
source_path_obj = Path(source_path)
|
||||
# Check if it's a directory or a zip file (common input types)
|
||||
if source_path_obj.is_dir() or (source_path_obj.is_file() and source_path_obj.suffix.lower() == '.zip'):
|
||||
log.debug(f"Identified source path for re-interpretation: {source_path}")
|
||||
unique_source_dirs.add(source_path)
|
||||
processed_source_paths.add(source_path)
|
||||
else:
|
||||
log.warning(f"Selected item's source path is not a directory or zip file: {source_path}")
|
||||
elif not source_path:
|
||||
log.warning(f"Parent SourceRule found for index {index.row()},{index.column()} but has no 'input_path' attribute.")
|
||||
|
||||
else:
|
||||
log.warning(f"Could not find parent SourceRule for selected index: {index.row()},{index.column()} (Node type: {type(item_node).__name__})")
|
||||
|
||||
return unique_source_dirs
|
||||
|
||||
@Slot()
|
||||
def _on_llm_reinterpret_clicked(self):
|
||||
"""Gathers selected source paths and emits the llm_reinterpret_requested signal."""
|
||||
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
||||
if not selected_indexes:
|
||||
return
|
||||
|
||||
"""Gathers selected source paths and emits the llm_reinterpret_requested signal. (Triggered by context menu)"""
|
||||
if self.llm_processing_active:
|
||||
QMessageBox.warning(self, "Busy", "LLM processing is already in progress. Please wait.")
|
||||
return
|
||||
|
||||
unique_source_dirs = set()
|
||||
processed_source_paths = set() # Track processed source paths to avoid duplicates
|
||||
for index in selected_indexes:
|
||||
if not index.isValid(): continue
|
||||
item_node = index.internalPointer()
|
||||
if not item_node: continue
|
||||
|
||||
# Traverse up to find the SourceRule node (Simplified traversal)
|
||||
source_node = None
|
||||
current_node = item_node
|
||||
while current_node is not None:
|
||||
if isinstance(current_node, SourceRule):
|
||||
source_node = current_node
|
||||
break
|
||||
# Simplified parent traversal - adjust if model structure is different
|
||||
parent_attr = getattr(current_node, 'parent', None) # Check for generic 'parent'
|
||||
if callable(parent_attr): # Check if parent is a method (like in QStandardItemModel)
|
||||
current_node = parent_attr()
|
||||
elif parent_attr: # Check if parent is an attribute
|
||||
current_node = parent_attr
|
||||
else: # Try specific parent attributes if generic fails
|
||||
parent_source = getattr(current_node, 'parent_source', None)
|
||||
if parent_source:
|
||||
current_node = parent_source
|
||||
else:
|
||||
parent_asset = getattr(current_node, 'parent_asset', None)
|
||||
if parent_asset:
|
||||
current_node = parent_asset
|
||||
else: # Reached top or unexpected node type
|
||||
current_node = None
|
||||
|
||||
|
||||
if source_node and hasattr(source_node, 'input_path') and source_node.input_path:
|
||||
source_path_str = source_node.input_path
|
||||
if source_path_str in processed_source_paths:
|
||||
continue
|
||||
source_path_obj = Path(source_path_str)
|
||||
if source_path_obj.is_dir() or (source_path_obj.is_file() and source_path_obj.suffix.lower() == '.zip'):
|
||||
unique_source_dirs.add(source_path_str)
|
||||
processed_source_paths.add(source_path_str)
|
||||
else:
|
||||
log.warning(f"Skipping non-directory/zip source for re-interpretation: {source_path_str}")
|
||||
# else: # Reduce log noise
|
||||
# log.warning(f"Could not determine valid SourceRule or input_path for selected index: {index.row()},{index.column()} (Item type: {type(item_node).__name__})")
|
||||
|
||||
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
||||
unique_source_dirs = self._get_unique_source_dirs_from_selection(selected_indexes)
|
||||
|
||||
if not unique_source_dirs:
|
||||
# self.statusBar().showMessage("No valid source directories found for selected items.", 5000) # Status bar is in MainWindow
|
||||
log.warning("No valid source directories found for selected items to re-interpret.")
|
||||
log.warning("No valid source directories found for selected items to re-interpret with LLM.")
|
||||
# Optionally show status bar message via MainWindow reference if available
|
||||
return
|
||||
|
||||
log.info(f"Emitting llm_reinterpret_requested for {len(unique_source_dirs)} paths.")
|
||||
self.llm_reinterpret_requested.emit(list(unique_source_dirs))
|
||||
|
||||
|
||||
@Slot(str, QModelIndex)
|
||||
def _on_reinterpret_preset_selected(self, preset_name: str, index: QModelIndex):
|
||||
"""Handles the selection of a preset from the re-interpret context sub-menu."""
|
||||
log.info(f"Preset re-interpretation requested: Preset='{preset_name}', Index='{index.row()},{index.column()}'")
|
||||
# Reuse logic from _on_llm_reinterpret_clicked to get selected source paths
|
||||
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
||||
# Use the helper method to get all selected source paths, not just the one clicked
|
||||
unique_source_dirs = self._get_unique_source_dirs_from_selection(selected_indexes)
|
||||
|
||||
if not unique_source_dirs:
|
||||
log.warning("No valid source directories found for selected items to re-interpret with preset.")
|
||||
# Optionally show status bar message via MainWindow reference if available
|
||||
return
|
||||
|
||||
log.info(f"Emitting preset_reinterpret_requested for {len(unique_source_dirs)} paths with preset '{preset_name}'.")
|
||||
self.preset_reinterpret_requested.emit(list(unique_source_dirs), preset_name)
|
||||
|
||||
|
||||
@Slot(QPoint)
|
||||
def _show_unified_view_context_menu(self, point: QPoint):
|
||||
"""Shows the context menu for the unified view."""
|
||||
@@ -451,31 +489,90 @@ class MainPanelWidget(QWidget):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
item_node = index.internalPointer()
|
||||
is_source_item = isinstance(item_node, SourceRule)
|
||||
model = self.unified_view.model()
|
||||
if not model: return
|
||||
item_node = model.getItem(index) # Use model's method
|
||||
|
||||
# Find the SourceRule node associated with the clicked index
|
||||
# Find the SourceRule node associated with the clicked index
|
||||
source_rule_node = None
|
||||
current_index = index # Start with the clicked index
|
||||
while current_index.isValid():
|
||||
current_item = model.getItem(current_index)
|
||||
if isinstance(current_item, SourceRule):
|
||||
source_rule_node = current_item
|
||||
break
|
||||
current_index = model.parent(current_index) # Move to the parent index
|
||||
# If loop finishes without break, source_rule_node remains None
|
||||
|
||||
menu = QMenu(self)
|
||||
|
||||
if is_source_item:
|
||||
# --- Re-interpret Menu ---
|
||||
if source_rule_node: # Only show if we clicked on or within a SourceRule item
|
||||
reinterpet_menu = menu.addMenu("Re-interpret selected source")
|
||||
|
||||
# Get Preset Names (Option B: Direct File Listing)
|
||||
preset_names = []
|
||||
try:
|
||||
presets_dir = configuration.PRESETS_DIR
|
||||
if os.path.isdir(presets_dir):
|
||||
for filename in os.listdir(presets_dir):
|
||||
if filename.endswith(".json") and filename != "_template.json":
|
||||
preset_name = os.path.splitext(filename)[0]
|
||||
preset_names.append(preset_name)
|
||||
preset_names.sort() # Sort alphabetically
|
||||
else:
|
||||
log.warning(f"Presets directory not found or not a directory: {presets_dir}")
|
||||
except Exception as e:
|
||||
log.exception(f"Error listing presets in {configuration.PRESETS_DIR}: {e}")
|
||||
|
||||
# Populate Sub-Menu with Presets
|
||||
if preset_names:
|
||||
for preset_name in preset_names:
|
||||
preset_action = QAction(preset_name, self)
|
||||
# Pass the preset name and the *clicked* index (though the slot will get all selected)
|
||||
preset_action.triggered.connect(functools.partial(self._on_reinterpret_preset_selected, preset_name, index))
|
||||
reinterpet_menu.addAction(preset_action)
|
||||
else:
|
||||
no_presets_action = QAction("No presets found", self)
|
||||
no_presets_action.setEnabled(False)
|
||||
reinterpet_menu.addAction(no_presets_action)
|
||||
|
||||
|
||||
# Add LLM Option (Static)
|
||||
reinterpet_menu.addSeparator()
|
||||
llm_action = QAction("LLM", self)
|
||||
# Connect to the existing slot that handles LLM re-interpretation requests
|
||||
llm_action.triggered.connect(self._on_llm_reinterpret_clicked)
|
||||
# Disable if LLM is currently processing
|
||||
llm_action.setEnabled(not self.llm_processing_active)
|
||||
reinterpet_menu.addAction(llm_action)
|
||||
|
||||
menu.addSeparator() # Separator before other actions
|
||||
|
||||
# --- Other Actions (like Copy LLM Example) ---
|
||||
if source_rule_node: # Check again if it's a source item for this action
|
||||
copy_llm_example_action = QAction("Copy LLM Example to Clipboard", self)
|
||||
copy_llm_example_action.setToolTip("Copies a JSON structure representing the input files and predicted output, suitable for LLM examples.")
|
||||
copy_llm_example_action.triggered.connect(lambda: self._copy_llm_example_to_clipboard(index))
|
||||
# Pass the found source_rule_node
|
||||
copy_llm_example_action.triggered.connect(lambda: self._copy_llm_example_to_clipboard(source_rule_node))
|
||||
menu.addAction(copy_llm_example_action)
|
||||
menu.addSeparator()
|
||||
# menu.addSeparator() # Removed redundant separator
|
||||
|
||||
# Add other actions...
|
||||
# Add other general actions here if needed...
|
||||
|
||||
if not menu.isEmpty():
|
||||
menu.exec(self.unified_view.viewport().mapToGlobal(point))
|
||||
|
||||
@Slot(QModelIndex)
|
||||
def _copy_llm_example_to_clipboard(self, index: QModelIndex):
|
||||
"""Copies a JSON structure for the selected source item to the clipboard."""
|
||||
if not index.isValid(): return
|
||||
item_node = index.internalPointer()
|
||||
if not isinstance(item_node, SourceRule): return
|
||||
@Slot(SourceRule) # Accept SourceRule directly
|
||||
def _copy_llm_example_to_clipboard(self, source_rule_node: SourceRule | None):
|
||||
"""Copies a JSON structure for the given SourceRule node to the clipboard."""
|
||||
if not source_rule_node:
|
||||
log.warning(f"No SourceRule node provided to copy LLM example.")
|
||||
return
|
||||
|
||||
source_rule: SourceRule = item_node
|
||||
# We already have the source_rule_node passed in
|
||||
source_rule: SourceRule = source_rule_node
|
||||
log.info(f"Attempting to generate LLM example JSON for source: {source_rule.input_path}")
|
||||
|
||||
all_file_paths = []
|
||||
@@ -570,11 +667,8 @@ class MainPanelWidget(QWidget):
|
||||
self.materials_blend_path_input.setEnabled(blender_paths_enabled)
|
||||
self.browse_materials_blend_button.setEnabled(blender_paths_enabled)
|
||||
|
||||
# Update LLM button state explicitly when controls are enabled/disabled
|
||||
if enabled:
|
||||
self._update_llm_reinterpret_button_state()
|
||||
else:
|
||||
self.llm_reinterpret_button.setEnabled(False)
|
||||
# LLM button removed, no need to update its state here.
|
||||
# Context menu actions enable/disable themselves based on context (e.g., llm_processing_active).
|
||||
|
||||
|
||||
@Slot(bool)
|
||||
@@ -594,9 +688,9 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
@Slot(bool)
|
||||
def set_llm_processing_status(self, active: bool):
|
||||
"""Informs the panel whether LLM processing is active."""
|
||||
"""Informs the panel whether LLM processing is active (used for context menu state)."""
|
||||
self.llm_processing_active = active
|
||||
self._update_llm_reinterpret_button_state() # Update button state based on new status
|
||||
# No button state to update directly, but context menu will check this flag when built.
|
||||
|
||||
# TODO: Add method to get current output path if needed by MainWindow before processing
|
||||
def get_output_directory(self) -> str:
|
||||
|
||||
@@ -189,6 +189,7 @@ class MainWindow(QMainWindow):
|
||||
self.main_panel_widget.output_dir_changed.connect(self._on_output_dir_changed)
|
||||
self.main_panel_widget.blender_settings_changed.connect(self._on_blender_settings_changed)
|
||||
|
||||
self.main_panel_widget.preset_reinterpret_requested.connect(self._on_preset_reinterpret_requested)
|
||||
# --- Connect Signals from LLMInteractionHandler ---
|
||||
self.llm_interaction_handler.llm_prediction_ready.connect(self._on_llm_prediction_ready_from_handler)
|
||||
self.llm_interaction_handler.llm_prediction_error.connect(self._on_prediction_error) # Use common error slot
|
||||
@@ -621,6 +622,67 @@ class MainWindow(QMainWindow):
|
||||
"materials_blend_path": mat_path
|
||||
}
|
||||
log.debug(f"MainWindow stored Blender settings: {self._current_blender_settings}")
|
||||
@Slot(list, str)
|
||||
def _on_preset_reinterpret_requested(self, source_paths: list, preset_name: str):
|
||||
"""Handles the preset_reinterpret_requested signal from MainPanelWidget."""
|
||||
log.info(f"Received preset re-interpret request for {len(source_paths)} paths using preset '{preset_name}'.")
|
||||
|
||||
if not source_paths:
|
||||
self.statusBar().showMessage("No valid source directories selected for preset re-interpretation.", 5000)
|
||||
return
|
||||
|
||||
# Check if rule-based prediction is already running (optional, handler might manage internally)
|
||||
# Note: QueuedConnection on the signal helps, but check anyway for immediate feedback/logging
|
||||
# TODO: Add is_running() method to RuleBasedPredictionHandler if needed for this check
|
||||
if self.prediction_handler and hasattr(self.prediction_handler, 'is_running') and self.prediction_handler.is_running():
|
||||
log.warning("Rule-based prediction is already running. Queuing re-interpretation request.")
|
||||
# Proceed, relying on QueuedConnection
|
||||
|
||||
# Ensure the prediction thread is running
|
||||
if self.prediction_thread and not self.prediction_thread.isRunning():
|
||||
log.debug("Starting prediction thread for preset re-interpretation.")
|
||||
self.prediction_thread.start()
|
||||
elif not self.prediction_thread:
|
||||
log.error("Prediction thread not initialized. Cannot perform preset re-interpretation.")
|
||||
self.statusBar().showMessage("Error: Prediction system not ready.", 5000)
|
||||
return
|
||||
|
||||
|
||||
# Trigger prediction for each path
|
||||
self.statusBar().showMessage(f"Starting re-interpretation for {len(source_paths)} item(s) using preset '{preset_name}'...", 0)
|
||||
for input_path_str in source_paths:
|
||||
# Mark as pending (use existing mechanism)
|
||||
self._pending_predictions.add(input_path_str)
|
||||
self._completed_predictions.discard(input_path_str) # Ensure it's not marked completed
|
||||
|
||||
# Update status in model (Requires update_status method in UnifiedViewModel)
|
||||
try:
|
||||
if hasattr(self.unified_model, 'update_status'):
|
||||
self.unified_model.update_status(input_path_str, "Re-interpreting...")
|
||||
else:
|
||||
log.warning("UnifiedViewModel does not have 'update_status' method. Cannot update status visually.")
|
||||
except Exception as e:
|
||||
log.exception(f"Error calling unified_model.update_status for {input_path_str}: {e}")
|
||||
|
||||
|
||||
# Extract file list (needed by RuleBasedPredictionHandler)
|
||||
file_list = self._extract_file_list(input_path_str)
|
||||
if file_list is not None:
|
||||
log.debug(f"Emitting start_prediction_signal for re-interpretation: Path='{input_path_str}', Preset='{preset_name}'")
|
||||
# Use the existing signal connected to RuleBasedPredictionHandler
|
||||
self.start_prediction_signal.emit(input_path_str, file_list, preset_name)
|
||||
else:
|
||||
log.warning(f"Skipping re-interpretation for {input_path_str} due to extraction error.")
|
||||
# Update status in model to reflect error (Requires update_status method)
|
||||
try:
|
||||
if hasattr(self.unified_model, 'update_status'):
|
||||
self.unified_model.update_status(input_path_str, "Error extracting files")
|
||||
else:
|
||||
log.warning("UnifiedViewModel does not have 'update_status' method. Cannot update error status visually.")
|
||||
except Exception as e:
|
||||
log.exception(f"Error calling unified_model.update_status (error case) for {input_path_str}: {e}")
|
||||
|
||||
self._handle_prediction_completion(input_path_str) # Mark as completed (with error)
|
||||
|
||||
# --- Preview Update Method ---
|
||||
def update_preview(self):
|
||||
@@ -1180,6 +1242,7 @@ class MainWindow(QMainWindow):
|
||||
log.warning("get_llm_source_preset_name called before preset_editor_widget was initialized.")
|
||||
return None
|
||||
|
||||
# --- Main Execution ---
|
||||
# --- Main Execution ---
|
||||
def run_gui():
|
||||
"""Initializes and runs the Qt application."""
|
||||
|
||||
@@ -489,3 +489,7 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
self._current_file_list = None
|
||||
self._current_preset_name = None
|
||||
log.info(f"Finished rule-based prediction run for: {input_source_identifier}")
|
||||
def is_running(self) -> bool:
|
||||
"""Returns True if the handler is currently processing a prediction request."""
|
||||
# The _is_running flag is managed by the base class or the run_prediction method
|
||||
return self._is_running
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# gui/unified_view_model.py
|
||||
import logging # Added for debugging
|
||||
log = logging.getLogger(__name__) # Added for debugging
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot # Added Signal and Slot
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot, QMimeData, QByteArray, QDataStream, QIODevice # Added Signal and Slot, QMimeData, QByteArray, QDataStream, QIODevice
|
||||
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
|
||||
@@ -35,6 +35,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# COL_STATUS = 5 # Removed
|
||||
# COL_OUTPUT_PATH = 6 # Removed
|
||||
|
||||
# --- Drag and Drop MIME Type ---
|
||||
MIME_TYPE = "application/x-filerule-index-list" # Custom MIME type
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._source_rules = [] # Now stores a list of SourceRule objects
|
||||
@@ -278,7 +281,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
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 ""
|
||||
elif role == Qt.EditRole:
|
||||
if column == self.COL_ASSET_TYPE:
|
||||
if column == self.COL_NAME:
|
||||
return item.asset_name # Return name for editing
|
||||
elif column == self.COL_ASSET_TYPE:
|
||||
return item.asset_type_override
|
||||
return None
|
||||
|
||||
@@ -330,7 +335,60 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
changed = True
|
||||
|
||||
elif isinstance(item, AssetRule):
|
||||
if column == self.COL_ASSET_TYPE:
|
||||
if column == self.COL_NAME: # Handle Asset Name change
|
||||
new_asset_name = str(value).strip() if value else None
|
||||
if not new_asset_name:
|
||||
log.warning("setData: Asset name cannot be empty.")
|
||||
return False # Don't allow empty names
|
||||
|
||||
if item.asset_name == new_asset_name:
|
||||
return False # No change
|
||||
|
||||
# --- Validation: Check for duplicates within the same SourceRule ---
|
||||
parent_source = getattr(item, 'parent_source', None)
|
||||
if parent_source:
|
||||
for existing_asset in parent_source.assets:
|
||||
if existing_asset.asset_name == new_asset_name and existing_asset is not item:
|
||||
log.warning(f"setData: Duplicate asset name '{new_asset_name}' detected within the same source. Aborting rename.")
|
||||
# Optionally, provide user feedback here via a signal or message box
|
||||
return False
|
||||
else:
|
||||
log.error("setData: Cannot validate asset name, parent SourceRule not found.")
|
||||
# Decide how to handle this - proceed cautiously or abort? Aborting is safer.
|
||||
return False
|
||||
# --- End Validation ---
|
||||
|
||||
log.info(f"setData: Renaming AssetRule from '{item.asset_name}' to '{new_asset_name}'")
|
||||
old_asset_name = item.asset_name
|
||||
item.asset_name = new_asset_name
|
||||
changed = True
|
||||
|
||||
# --- Update Child FileRule Target Asset Overrides ---
|
||||
log.debug(f"setData: Updating FileRule target overrides from '{old_asset_name}' to '{new_asset_name}'")
|
||||
updated_file_indices = []
|
||||
for src_idx, source_rule in enumerate(self._source_rules):
|
||||
source_rule_index = self.createIndex(src_idx, 0, source_rule)
|
||||
for asset_idx, asset_rule in enumerate(source_rule.assets):
|
||||
asset_rule_index = self.createIndex(asset_idx, 0, asset_rule) # This index is relative to source_rule_index
|
||||
for file_idx, file_rule in enumerate(asset_rule.files):
|
||||
if file_rule.target_asset_name_override == old_asset_name:
|
||||
log.debug(f" Updating target for file: {Path(file_rule.file_path).name}")
|
||||
file_rule.target_asset_name_override = new_asset_name
|
||||
# Get the correct index for the file rule to emit dataChanged
|
||||
file_rule_parent_index = self.parent(self.createIndex(file_idx, 0, file_rule)) # Get parent AssetRule index
|
||||
file_rule_index = self.index(file_idx, self.COL_TARGET_ASSET, file_rule_parent_index)
|
||||
if file_rule_index.isValid():
|
||||
updated_file_indices.append(file_rule_index)
|
||||
else:
|
||||
log.warning(f" Could not get valid index for updated file rule target: {Path(file_rule.file_path).name}")
|
||||
|
||||
|
||||
# Emit dataChanged for all updated file rules *after* the loop
|
||||
for file_index in updated_file_indices:
|
||||
self.dataChanged.emit(file_index, file_index, [Qt.DisplayRole, Qt.EditRole])
|
||||
# --- End Child Update ---
|
||||
|
||||
elif 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
|
||||
@@ -416,6 +474,8 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
default_flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
item = index.internalPointer()
|
||||
if not item: # Should not happen for valid index, but safety check
|
||||
return Qt.NoItemFlags
|
||||
column = index.column()
|
||||
|
||||
# Always use detailed mode editability logic
|
||||
@@ -423,15 +483,21 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
if isinstance(item, SourceRule):
|
||||
if column == self.COL_SUPPLIER: can_edit = True
|
||||
elif isinstance(item, AssetRule):
|
||||
if column == self.COL_NAME: can_edit = True # Allow editing name
|
||||
if column == self.COL_ASSET_TYPE: can_edit = True
|
||||
# AssetRule items can accept drops
|
||||
default_flags |= Qt.ItemIsDropEnabled
|
||||
elif isinstance(item, FileRule):
|
||||
if column == self.COL_TARGET_ASSET: can_edit = True
|
||||
if column == self.COL_ITEM_TYPE: can_edit = True
|
||||
# FileRule items can be dragged
|
||||
default_flags |= Qt.ItemIsDragEnabled
|
||||
|
||||
if can_edit:
|
||||
return default_flags | Qt.ItemIsEditable
|
||||
else:
|
||||
return default_flags
|
||||
default_flags |= Qt.ItemIsEditable
|
||||
|
||||
return default_flags
|
||||
# Removed erroneous else block
|
||||
|
||||
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole):
|
||||
"""Returns the data for the given role and section in the header."""
|
||||
@@ -749,18 +815,257 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
log.error(f"removeAssetRule: Could not find parent SourceRule or the AssetRule within its parent's list.")
|
||||
return False
|
||||
|
||||
def get_asset_type_keys(self) -> List[str]:
|
||||
def get_asset_type_keys(self) -> List[str]:
|
||||
"""Returns the cached list of asset type keys."""
|
||||
return self._asset_type_keys
|
||||
|
||||
def get_file_type_keys(self) -> List[str]:
|
||||
"""Returns the cached list of file type keys."""
|
||||
return self._file_type_keys
|
||||
def get_file_type_keys(self) -> List[str]:
|
||||
"""Returns the cached list of file type keys."""
|
||||
return self._file_type_keys
|
||||
|
||||
log.debug(f"Removing empty AssetRule '{asset_rule_to_remove.asset_name}' at row {asset_row_for_removal} under '{Path(source_rule.input_path).name}'")
|
||||
self.beginRemoveRows(grandparent_index, asset_row_for_removal, asset_row_for_removal)
|
||||
source_rule.assets.pop(asset_row_for_removal)
|
||||
self.endRemoveRows()
|
||||
return True
|
||||
def update_status(self, source_path: str, status_text: str):
|
||||
"""
|
||||
Finds the SourceRule node for the given source_path and updates its status.
|
||||
Emits dataChanged for the corresponding row.
|
||||
"""
|
||||
log.debug(f"Attempting to update status for source '{source_path}' to '{status_text}'")
|
||||
found_row = -1
|
||||
found_rule = None
|
||||
for i, rule in enumerate(self._source_rules):
|
||||
if rule.input_path == source_path:
|
||||
found_row = i
|
||||
found_rule = rule
|
||||
break
|
||||
|
||||
if found_rule is not None and found_row != -1:
|
||||
try:
|
||||
# Attempt to set a status attribute (e.g., _status_message)
|
||||
# Note: This attribute isn't formally defined in SourceRule structure yet.
|
||||
setattr(found_rule, '_status_message', status_text)
|
||||
log.info(f"Updated status for SourceRule '{source_path}' (row {found_row}) to '{status_text}'")
|
||||
|
||||
# Emit dataChanged for the entire row to potentially trigger updates
|
||||
# (e.g., delegates, background color based on status if implemented later)
|
||||
start_index = self.createIndex(found_row, 0, found_rule)
|
||||
end_index = self.createIndex(found_row, self.columnCount() - 1, found_rule)
|
||||
self.dataChanged.emit(start_index, end_index, [Qt.DisplayRole]) # Emit for DisplayRole, maybe others needed later
|
||||
|
||||
except Exception as e:
|
||||
log.exception(f"Error setting status attribute or emitting dataChanged for {source_path}: {e}")
|
||||
else:
|
||||
log.warning(f"Could not find SourceRule with path '{source_path}' to update status.")
|
||||
|
||||
# --- Placeholder for node finding method (Original Request - Replaced by direct list search above) ---
|
||||
# Kept for reference, but the logic above directly searches self._source_rules
|
||||
# Kept for reference, but the logic above directly searches self._source_rules
|
||||
|
||||
# --- Drag and Drop Methods ---
|
||||
|
||||
def supportedDropActions(self) -> Qt.DropActions:
|
||||
"""Specifies that only Move actions are supported."""
|
||||
return Qt.MoveAction
|
||||
|
||||
def mimeTypes(self) -> list[str]:
|
||||
"""Returns the list of supported MIME types for dragging."""
|
||||
return [self.MIME_TYPE]
|
||||
|
||||
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData:
|
||||
"""Encodes information about the dragged FileRule items."""
|
||||
mime_data = QMimeData()
|
||||
encoded_data = QByteArray()
|
||||
stream = QDataStream(encoded_data, QIODevice.OpenModeFlag.WriteOnly)
|
||||
|
||||
dragged_file_info = []
|
||||
for index in indexes:
|
||||
if not index.isValid() or index.column() != 0: # Only consider valid indices from the first column
|
||||
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()}")
|
||||
|
||||
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]) # source_row
|
||||
stream.writeInt8(info[1]) # source_parent_row
|
||||
stream.writeInt8(info[2]) # source_grandparent_row
|
||||
|
||||
mime_data.setData(self.MIME_TYPE, encoded_data)
|
||||
log.debug(f"mimeData: Encoded {len(dragged_file_info)} FileRule indices.")
|
||||
return mime_data
|
||||
|
||||
def canDropMimeData(self, data: QMimeData, action: Qt.DropAction, row: int, column: int, parent: QModelIndex) -> bool:
|
||||
"""Checks if the data can be dropped at the specified location."""
|
||||
if action != Qt.MoveAction or not data.hasFormat(self.MIME_TYPE):
|
||||
return False
|
||||
|
||||
# Check if the drop target is a valid AssetRule
|
||||
if not parent.isValid():
|
||||
return False # Cannot drop onto root or SourceRule directly in this implementation
|
||||
|
||||
target_item = parent.internalPointer()
|
||||
if not isinstance(target_item, AssetRule):
|
||||
return False # Can only drop onto AssetRule items
|
||||
|
||||
# Optional: Prevent dropping onto the original parent? (Might be confusing)
|
||||
# For now, allow dropping onto the same parent (moveFileRule handles this)
|
||||
|
||||
return True
|
||||
|
||||
def dropMimeData(self, data: QMimeData, action: Qt.DropAction, row: int, column: int, parent: QModelIndex) -> bool:
|
||||
"""Handles the dropping of FileRule items onto an AssetRule."""
|
||||
if not self.canDropMimeData(data, action, row, column, parent):
|
||||
log.warning("dropMimeData: canDropMimeData check failed.")
|
||||
return False
|
||||
|
||||
target_asset_item = parent.internalPointer()
|
||||
if not isinstance(target_asset_item, AssetRule): # Should be caught by canDrop, but double-check
|
||||
log.error("dropMimeData: Target item is not an AssetRule.")
|
||||
return False
|
||||
|
||||
encoded_data = data.data(self.MIME_TYPE)
|
||||
stream = QDataStream(encoded_data, QIODevice.OpenModeFlag.ReadOnly)
|
||||
|
||||
num_items = stream.readInt8()
|
||||
source_indices_info = []
|
||||
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))
|
||||
|
||||
log.debug(f"dropMimeData: Decoded {len(source_indices_info)} source indices. Target Asset: '{target_asset_item.asset_name}'")
|
||||
|
||||
if not source_indices_info:
|
||||
log.warning("dropMimeData: No valid source index information decoded.")
|
||||
return False
|
||||
|
||||
# Keep track of original parents that might become empty
|
||||
original_parents = set()
|
||||
moved_files_new_indices = {} # Store new index after move for dataChanged emission
|
||||
|
||||
# --- 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)
|
||||
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.")
|
||||
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: # Iterate through the valid indices
|
||||
# Get the file item (already validated during reconstruction)
|
||||
file_item = source_file_index.internalPointer()
|
||||
|
||||
# Track original parent for cleanup (using the valid index)
|
||||
old_parent_index = self.parent(source_file_index) # Get parent from the valid 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: # Handle root case or error
|
||||
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.")
|
||||
else:
|
||||
log.warning(f"Could not get valid parent index for file '{Path(file_item.file_path).name}' during cleanup tracking.")
|
||||
|
||||
|
||||
# Perform the move using the model's method and the valid source_file_index
|
||||
if self.moveFileRule(source_file_index, parent): # 'parent' is the target_parent_asset_index
|
||||
# --- Update Target Asset Override After Successful Move ---
|
||||
# The file_item's parent_asset reference should now be updated by moveFileRule
|
||||
new_parent_asset = getattr(file_item, 'parent_asset', None)
|
||||
if new_parent_asset == target_asset_item: # Check if move was successful internally
|
||||
if file_item.target_asset_name_override != target_asset_item.asset_name:
|
||||
log.debug(f" Updating target override for '{Path(file_item.file_path).name}' to '{target_asset_item.asset_name}'")
|
||||
file_item.target_asset_name_override = target_asset_item.asset_name
|
||||
# Need the *new* index of the moved file to emit dataChanged
|
||||
try:
|
||||
new_row = target_asset_item.files.index(file_item)
|
||||
new_file_index_col0 = self.index(new_row, 0, parent) # Index for col 0 under new parent
|
||||
new_file_index_target_col = self.index(new_row, self.COL_TARGET_ASSET, parent) # Index for target col
|
||||
if new_file_index_target_col.isValid():
|
||||
moved_files_new_indices[file_item.file_path] = new_file_index_target_col # Use hashable file_path as key
|
||||
else:
|
||||
log.warning(f" Could not get valid *new* index for target column of moved file: {Path(file_item.file_path).name}")
|
||||
except ValueError:
|
||||
log.error(f" Could not find moved file '{Path(file_item.file_path).name}' in target parent's list after move.")
|
||||
|
||||
else:
|
||||
log.error(f" Move reported success, but file's parent reference ('{getattr(new_parent_asset, 'asset_name', 'None')}') doesn't match target ('{target_asset_item.asset_name}'). Override not updated.")
|
||||
else:
|
||||
log.error(f"dropMimeData: moveFileRule failed for file '{Path(file_item.file_path).name}'.")
|
||||
# If one move fails, should we stop? For now, continue processing others.
|
||||
continue # Skip override update and cleanup check for this file
|
||||
|
||||
# --- Emit dataChanged for Target Asset column AFTER all moves ---
|
||||
for source_path, new_index in moved_files_new_indices.items(): # Key is now source_path (string)
|
||||
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)}") # Log tuples
|
||||
for gp_row, asset_name in list(original_parents): # Iterate over a copy of tuples
|
||||
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: # Don't remove the target if it was also an original parent
|
||||
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}.")
|
||||
else:
|
||||
log.warning(f"dropMimeData: Cleanup check failed. Invalid grandparent row index {gp_row} found in original_parents set.")
|
||||
except Exception as e:
|
||||
log.exception(f"dropMimeData: Error during cleanup check for parent '{asset_name}' (gp_row {gp_row}): {e}")
|
||||
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user