730 lines
36 KiB
Python
730 lines
36 KiB
Python
import sys
|
|
import os
|
|
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
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QTableView,
|
|
QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,
|
|
QProgressBar, QLabel, QFrame, QCheckBox, QSpinBox, QListWidget, QTextEdit,
|
|
QLineEdit, QMessageBox, QFileDialog, QInputDialog, QListWidgetItem, QTabWidget,
|
|
QFormLayout, QGroupBox, QAbstractItemView, QSizePolicy, QTreeView, QMenu
|
|
)
|
|
from PySide6.QtCore import Qt, Signal, Slot, QPoint, QModelIndex, QTimer
|
|
from PySide6.QtGui import QColor, QAction, QPalette, QClipboard, QGuiApplication # Added QGuiApplication for clipboard
|
|
|
|
# --- Local GUI Imports ---
|
|
# Import delegates and models needed by the panel
|
|
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__)
|
|
|
|
class MainPanelWidget(QWidget):
|
|
"""
|
|
Widget handling the main interaction panel:
|
|
- Output directory selection
|
|
- Asset preview/editing view (Unified View)
|
|
- Blender post-processing options
|
|
- Processing controls (Start, Cancel, Clear, LLM Re-interpret)
|
|
"""
|
|
# --- Signals Emitted by the Panel ---
|
|
# Request to add new input paths (e.g., from drag/drop handled by MainWindow)
|
|
# add_paths_requested = Signal(list) # Maybe not needed if MainWindow handles drop directly
|
|
|
|
# Request to start the main processing job
|
|
process_requested = Signal(dict) # Emits dict with settings: output_dir, overwrite, workers, blender_enabled, ng_path, mat_path
|
|
|
|
# Request to cancel the ongoing processing job
|
|
cancel_requested = Signal()
|
|
|
|
# Request to clear the current queue/view
|
|
clear_queue_requested = Signal()
|
|
|
|
# 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)
|
|
|
|
# Notify when Blender settings change
|
|
blender_settings_changed = Signal(bool, str, str) # enabled, ng_path, mat_path
|
|
|
|
def __init__(self, unified_model: UnifiedViewModel, parent=None, file_type_keys: list[str] | None = None):
|
|
"""
|
|
Initializes the MainPanelWidget.
|
|
|
|
Args:
|
|
unified_model: The shared UnifiedViewModel instance.
|
|
parent: The parent widget.
|
|
file_type_keys: A list of available file type names (keys from FILE_TYPE_DEFINITIONS).
|
|
"""
|
|
super().__init__(parent)
|
|
self.unified_model = unified_model
|
|
self.file_type_keys = file_type_keys if file_type_keys else []
|
|
self.llm_processing_active = False # Track if LLM is running (set by MainWindow)
|
|
|
|
# Get project root for resolving default paths if needed here
|
|
script_dir = Path(__file__).parent
|
|
self.project_root = script_dir.parent
|
|
|
|
self._setup_ui()
|
|
self._connect_signals()
|
|
|
|
def _setup_ui(self):
|
|
"""Sets up the UI elements for the panel."""
|
|
main_layout = QVBoxLayout(self)
|
|
main_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins
|
|
|
|
# --- Output Directory Selection ---
|
|
output_layout = QHBoxLayout()
|
|
self.output_dir_label = QLabel("Output Directory:")
|
|
self.output_path_edit = QLineEdit()
|
|
self.browse_output_button = QPushButton("Browse...")
|
|
output_layout.addWidget(self.output_dir_label)
|
|
output_layout.addWidget(self.output_path_edit, 1)
|
|
output_layout.addWidget(self.browse_output_button)
|
|
main_layout.addLayout(output_layout)
|
|
|
|
# --- Set Initial Output Path (Copied from MainWindow) ---
|
|
# Consider passing this default path from MainWindow instead of reloading config here
|
|
if load_base_config:
|
|
try:
|
|
base_config = load_base_config()
|
|
output_base_dir_config = base_config.get('OUTPUT_BASE_DIR', '../Asset_Processor_Output')
|
|
default_output_dir = (self.project_root / output_base_dir_config).resolve()
|
|
self.output_path_edit.setText(str(default_output_dir))
|
|
log.info(f"MainPanelWidget: Default output directory set to: {default_output_dir}")
|
|
except ConfigurationError as e:
|
|
log.error(f"MainPanelWidget: Error reading base configuration for default output directory: {e}")
|
|
self.output_path_edit.setText("")
|
|
except Exception as e:
|
|
log.exception(f"MainPanelWidget: Error setting default output directory: {e}")
|
|
self.output_path_edit.setText("")
|
|
else:
|
|
log.warning("MainPanelWidget: load_base_config not available to set default output path.")
|
|
self.output_path_edit.setText("")
|
|
|
|
|
|
# --- Unified View Setup ---
|
|
self.unified_view = QTreeView()
|
|
self.unified_view.setModel(self.unified_model) # Set the passed-in model
|
|
|
|
# Instantiate Delegates
|
|
lineEditDelegate = LineEditDelegate(self.unified_view)
|
|
# ComboBoxDelegate needs access to MainWindow's get_llm_source_preset_name,
|
|
# which might require passing MainWindow or a callback here.
|
|
# For now, let's assume it can work without it or we adapt it later.
|
|
# TODO: Revisit ComboBoxDelegate dependency
|
|
comboBoxDelegate = ComboBoxDelegate(self) # Pass only parent (self)
|
|
supplierSearchDelegate = SupplierSearchDelegate(self) # Pass parent
|
|
# Pass file_type_keys to ItemTypeSearchDelegate
|
|
itemTypeSearchDelegate = ItemTypeSearchDelegate(self.file_type_keys, self)
|
|
|
|
# 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, 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)
|
|
self.unified_view.setAlternatingRowColors(True)
|
|
self.unified_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
self.unified_view.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed)
|
|
self.unified_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # Allow multi-select for re-interpret
|
|
|
|
# Configure Header Resize Modes
|
|
header = self.unified_view.header()
|
|
header.setStretchLastSection(False)
|
|
header.setSectionResizeMode(UnifiedViewModel.COL_NAME, QHeaderView.ResizeMode.ResizeToContents)
|
|
header.setSectionResizeMode(UnifiedViewModel.COL_TARGET_ASSET, QHeaderView.ResizeMode.Stretch)
|
|
header.setSectionResizeMode(UnifiedViewModel.COL_SUPPLIER, QHeaderView.ResizeMode.ResizeToContents)
|
|
header.setSectionResizeMode(UnifiedViewModel.COL_ASSET_TYPE, QHeaderView.ResizeMode.ResizeToContents)
|
|
header.setSectionResizeMode(UnifiedViewModel.COL_ITEM_TYPE, QHeaderView.ResizeMode.ResizeToContents)
|
|
|
|
# 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
|
|
|
|
# --- Progress Bar ---
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setTextVisible(True)
|
|
self.progress_bar.setFormat("Idle") # Initial format
|
|
main_layout.addWidget(self.progress_bar)
|
|
|
|
# --- Blender Integration Controls ---
|
|
blender_group = QGroupBox("Blender Post-Processing")
|
|
blender_layout = QVBoxLayout(blender_group)
|
|
|
|
self.blender_integration_checkbox = QCheckBox("Run Blender Scripts After Processing")
|
|
self.blender_integration_checkbox.setToolTip("If checked, attempts to run create_nodegroups.py and create_materials.py in Blender.")
|
|
blender_layout.addWidget(self.blender_integration_checkbox)
|
|
|
|
# Nodegroup Blend Path
|
|
nodegroup_layout = QHBoxLayout()
|
|
nodegroup_layout.addWidget(QLabel("Nodegroup .blend:"))
|
|
self.nodegroup_blend_path_input = QLineEdit()
|
|
self.browse_nodegroup_blend_button = QPushButton("...")
|
|
self.browse_nodegroup_blend_button.setFixedWidth(30)
|
|
nodegroup_layout.addWidget(self.nodegroup_blend_path_input)
|
|
nodegroup_layout.addWidget(self.browse_nodegroup_blend_button)
|
|
blender_layout.addLayout(nodegroup_layout)
|
|
|
|
# Materials Blend Path
|
|
materials_layout = QHBoxLayout()
|
|
materials_layout.addWidget(QLabel("Materials .blend:"))
|
|
self.materials_blend_path_input = QLineEdit()
|
|
self.browse_materials_blend_button = QPushButton("...")
|
|
self.browse_materials_blend_button.setFixedWidth(30)
|
|
materials_layout.addWidget(self.materials_blend_path_input)
|
|
materials_layout.addWidget(self.browse_materials_blend_button)
|
|
blender_layout.addLayout(materials_layout)
|
|
|
|
# Initialize paths from config (Copied from MainWindow)
|
|
# Consider passing these defaults from MainWindow
|
|
if load_base_config:
|
|
try:
|
|
base_config = load_base_config()
|
|
default_ng_path = base_config.get('DEFAULT_NODEGROUP_BLEND_PATH', '')
|
|
default_mat_path = base_config.get('DEFAULT_MATERIALS_BLEND_PATH', '')
|
|
self.nodegroup_blend_path_input.setText(default_ng_path if default_ng_path else "")
|
|
self.materials_blend_path_input.setText(default_mat_path if default_mat_path else "")
|
|
except ConfigurationError as e:
|
|
log.error(f"MainPanelWidget: Error reading base configuration for default Blender paths: {e}")
|
|
except Exception as e:
|
|
log.error(f"MainPanelWidget: Error reading default Blender paths from config: {e}")
|
|
else:
|
|
log.warning("MainPanelWidget: load_base_config not available to set default Blender paths.")
|
|
|
|
|
|
# Disable Blender controls initially if checkbox is unchecked
|
|
self.nodegroup_blend_path_input.setEnabled(False)
|
|
self.browse_nodegroup_blend_button.setEnabled(False)
|
|
self.materials_blend_path_input.setEnabled(False)
|
|
self.browse_materials_blend_button.setEnabled(False)
|
|
|
|
main_layout.addWidget(blender_group) # Add the group box to the main layout
|
|
|
|
# --- Bottom Controls ---
|
|
bottom_controls_layout = QHBoxLayout()
|
|
self.overwrite_checkbox = QCheckBox("Overwrite Existing")
|
|
self.overwrite_checkbox.setToolTip("If checked, existing output folders for processed assets will be deleted and replaced.")
|
|
bottom_controls_layout.addWidget(self.overwrite_checkbox)
|
|
|
|
self.workers_label = QLabel("Workers:")
|
|
self.workers_spinbox = QSpinBox()
|
|
default_workers = 1
|
|
try:
|
|
cores = os.cpu_count()
|
|
if cores: default_workers = max(1, cores // 2)
|
|
except NotImplementedError: pass
|
|
self.workers_spinbox.setMinimum(1)
|
|
self.workers_spinbox.setMaximum(os.cpu_count() or 32)
|
|
self.workers_spinbox.setValue(default_workers)
|
|
self.workers_spinbox.setToolTip("Number of assets to process concurrently.")
|
|
bottom_controls_layout.addWidget(self.workers_label)
|
|
bottom_controls_layout.addWidget(self.workers_spinbox)
|
|
bottom_controls_layout.addStretch(1)
|
|
|
|
# --- 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")
|
|
self.cancel_button = QPushButton("Cancel")
|
|
self.cancel_button.setEnabled(False)
|
|
|
|
bottom_controls_layout.addWidget(self.clear_queue_button)
|
|
bottom_controls_layout.addWidget(self.start_button)
|
|
bottom_controls_layout.addWidget(self.cancel_button)
|
|
main_layout.addLayout(bottom_controls_layout)
|
|
|
|
def _connect_signals(self):
|
|
"""Connect internal UI signals to slots or emit panel signals."""
|
|
# Output Directory
|
|
self.browse_output_button.clicked.connect(self._browse_for_output_directory)
|
|
self.output_path_edit.editingFinished.connect(self._on_output_path_changed) # Emit signal when user finishes editing
|
|
|
|
# Unified View
|
|
self.unified_view.customContextMenuRequested.connect(self._show_unified_view_context_menu)
|
|
|
|
# Blender Controls
|
|
self.blender_integration_checkbox.toggled.connect(self._toggle_blender_controls)
|
|
self.browse_nodegroup_blend_button.clicked.connect(self._browse_for_nodegroup_blend)
|
|
self.browse_materials_blend_button.clicked.connect(self._browse_for_materials_blend)
|
|
# Emit signal when paths change
|
|
self.nodegroup_blend_path_input.editingFinished.connect(self._emit_blender_settings_changed)
|
|
self.materials_blend_path_input.editingFinished.connect(self._emit_blender_settings_changed)
|
|
self.blender_integration_checkbox.toggled.connect(self._emit_blender_settings_changed)
|
|
|
|
|
|
# Bottom Buttons
|
|
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) # Removed button connection
|
|
|
|
# --- Slots for Internal UI Logic ---
|
|
|
|
@Slot()
|
|
def _browse_for_output_directory(self):
|
|
"""Opens a dialog to select the output directory."""
|
|
current_path = self.output_path_edit.text()
|
|
if not current_path or not Path(current_path).is_dir():
|
|
current_path = str(self.project_root) # Use project root as fallback
|
|
|
|
directory = QFileDialog.getExistingDirectory(
|
|
self,
|
|
"Select Output Directory",
|
|
current_path,
|
|
QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks
|
|
)
|
|
if directory:
|
|
self.output_path_edit.setText(directory)
|
|
self._on_output_path_changed() # Explicitly call the change handler
|
|
|
|
@Slot()
|
|
def _on_output_path_changed(self):
|
|
"""Emits the output_dir_changed signal."""
|
|
self.output_dir_changed.emit(self.output_path_edit.text())
|
|
|
|
@Slot(bool)
|
|
def _toggle_blender_controls(self, checked):
|
|
"""Enable/disable Blender path inputs based on the checkbox state."""
|
|
self.nodegroup_blend_path_input.setEnabled(checked)
|
|
self.browse_nodegroup_blend_button.setEnabled(checked)
|
|
self.materials_blend_path_input.setEnabled(checked)
|
|
self.browse_materials_blend_button.setEnabled(checked)
|
|
# No need to emit here, the checkbox toggle signal is connected separately
|
|
|
|
def _browse_for_blend_file(self, line_edit_widget: QLineEdit):
|
|
"""Opens a dialog to select a .blend file and updates the line edit."""
|
|
current_path = line_edit_widget.text()
|
|
start_dir = str(Path(current_path).parent) if current_path and Path(current_path).exists() else str(self.project_root)
|
|
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self,
|
|
"Select Blender File",
|
|
start_dir,
|
|
"Blender Files (*.blend);;All Files (*)"
|
|
)
|
|
if file_path:
|
|
line_edit_widget.setText(file_path)
|
|
line_edit_widget.editingFinished.emit() # Trigger editingFinished to emit change signal
|
|
|
|
@Slot()
|
|
def _browse_for_nodegroup_blend(self):
|
|
self._browse_for_blend_file(self.nodegroup_blend_path_input)
|
|
|
|
@Slot()
|
|
def _browse_for_materials_blend(self):
|
|
self._browse_for_blend_file(self.materials_blend_path_input)
|
|
|
|
@Slot()
|
|
def _emit_blender_settings_changed(self):
|
|
"""Gathers current Blender settings and emits the blender_settings_changed signal."""
|
|
enabled = self.blender_integration_checkbox.isChecked()
|
|
ng_path = self.nodegroup_blend_path_input.text()
|
|
mat_path = self.materials_blend_path_input.text()
|
|
self.blender_settings_changed.emit(enabled, ng_path, mat_path)
|
|
|
|
@Slot()
|
|
def _on_start_processing_clicked(self):
|
|
"""Gathers settings and emits the process_requested signal."""
|
|
output_dir = self.output_path_edit.text().strip()
|
|
if not output_dir:
|
|
QMessageBox.warning(self, "Missing Output Directory", "Please select an output directory.")
|
|
return
|
|
|
|
# Basic validation (MainWindow should do more thorough validation)
|
|
try:
|
|
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Invalid Output Directory", f"Cannot use output directory:\n{output_dir}\n\nError: {e}")
|
|
return
|
|
|
|
settings = {
|
|
"output_dir": output_dir,
|
|
"overwrite": self.overwrite_checkbox.isChecked(),
|
|
"workers": self.workers_spinbox.value(),
|
|
"blender_enabled": self.blender_integration_checkbox.isChecked(),
|
|
"nodegroup_blend_path": self.nodegroup_blend_path_input.text(),
|
|
"materials_blend_path": self.materials_blend_path_input.text()
|
|
}
|
|
self.process_requested.emit(settings)
|
|
|
|
# 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. (Triggered by context menu)"""
|
|
if self.llm_processing_active:
|
|
QMessageBox.warning(self, "Busy", "LLM processing is already in progress. Please wait.")
|
|
return
|
|
|
|
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
|
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 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."""
|
|
index = self.unified_view.indexAt(point)
|
|
if not index.isValid():
|
|
return
|
|
|
|
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)
|
|
|
|
# --- 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.")
|
|
# 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() # Removed redundant separator
|
|
|
|
# Add other general actions here if needed...
|
|
|
|
if not menu.isEmpty():
|
|
menu.exec(self.unified_view.viewport().mapToGlobal(point))
|
|
|
|
@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
|
|
|
|
# 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 = []
|
|
predicted_assets_data = []
|
|
|
|
for asset_rule in source_rule.assets:
|
|
asset_files_data = []
|
|
for file_rule in asset_rule.files:
|
|
if file_rule.file_path:
|
|
all_file_paths.append(file_rule.file_path)
|
|
asset_files_data.append({
|
|
"file_path": file_rule.file_path,
|
|
"predicted_file_type": file_rule.item_type or "UNKNOWN"
|
|
})
|
|
asset_files_data.sort(key=lambda x: x['file_path'])
|
|
predicted_assets_data.append({
|
|
"suggested_asset_name": asset_rule.asset_name or "UnnamedAsset",
|
|
"predicted_asset_type": asset_rule.asset_type or "UNKNOWN",
|
|
"files": asset_files_data
|
|
})
|
|
|
|
predicted_assets_data.sort(key=lambda x: x['suggested_asset_name'])
|
|
all_file_paths.sort()
|
|
|
|
if not all_file_paths:
|
|
log.warning(f"No file paths found for source: {source_rule.input_path}. Cannot generate example.")
|
|
# Cannot show status bar message here
|
|
return
|
|
|
|
llm_example = {
|
|
"input": "\n".join(all_file_paths),
|
|
"output": {"predicted_assets": predicted_assets_data}
|
|
}
|
|
|
|
try:
|
|
json_string = json.dumps(llm_example, indent=2)
|
|
clipboard = QGuiApplication.clipboard() # Use QGuiApplication
|
|
if clipboard:
|
|
clipboard.setText(json_string)
|
|
log.info(f"Copied LLM example JSON to clipboard for source: {source_rule.input_path}")
|
|
# Cannot show status bar message here
|
|
else:
|
|
log.error("Failed to get system clipboard.")
|
|
except Exception as e:
|
|
log.exception(f"Error copying LLM example JSON to clipboard: {e}")
|
|
|
|
|
|
# --- Public Slots for MainWindow to Call ---
|
|
|
|
@Slot(int, int)
|
|
def update_progress_bar(self, current_count, total_count):
|
|
"""Updates the progress bar display."""
|
|
if total_count > 0:
|
|
percentage = int((current_count / total_count) * 100)
|
|
log.debug(f"Updating progress bar: current={current_count}, total={total_count}, calculated_percentage={percentage}") # DEBUG LOG
|
|
self.progress_bar.setValue(percentage)
|
|
self.progress_bar.setFormat(f"%p% ({current_count}/{total_count})")
|
|
QApplication.processEvents() # Force GUI update
|
|
else:
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setFormat("0/0")
|
|
|
|
@Slot(str)
|
|
def set_progress_bar_text(self, text: str):
|
|
"""Sets the text format of the progress bar."""
|
|
self.progress_bar.setFormat(text)
|
|
# Reset value if setting text like "Idle" or "Waiting..."
|
|
if not "%" in text:
|
|
self.progress_bar.setValue(0)
|
|
|
|
|
|
@Slot(bool)
|
|
def set_controls_enabled(self, enabled: bool):
|
|
"""Enables or disables controls within the panel."""
|
|
# Enable/disable most controls based on the 'enabled' flag
|
|
self.output_path_edit.setEnabled(enabled)
|
|
self.browse_output_button.setEnabled(enabled)
|
|
self.unified_view.setEnabled(enabled)
|
|
self.overwrite_checkbox.setEnabled(enabled)
|
|
self.workers_spinbox.setEnabled(enabled)
|
|
self.clear_queue_button.setEnabled(enabled)
|
|
self.blender_integration_checkbox.setEnabled(enabled)
|
|
|
|
# Start button is enabled only if controls are generally enabled AND preset mode is active (handled by MainWindow)
|
|
# Cancel button is enabled only when processing is active (handled by MainWindow)
|
|
# LLM button state depends on selection and LLM status (handled by _update_llm_reinterpret_button_state)
|
|
|
|
# Blender path inputs depend on both 'enabled' and the checkbox state
|
|
blender_paths_enabled = enabled and self.blender_integration_checkbox.isChecked()
|
|
self.nodegroup_blend_path_input.setEnabled(blender_paths_enabled)
|
|
self.browse_nodegroup_blend_button.setEnabled(blender_paths_enabled)
|
|
self.materials_blend_path_input.setEnabled(blender_paths_enabled)
|
|
self.browse_materials_blend_button.setEnabled(blender_paths_enabled)
|
|
|
|
# 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)
|
|
def set_start_button_enabled(self, enabled: bool):
|
|
"""Sets the enabled state of the Start Processing button."""
|
|
self.start_button.setEnabled(enabled)
|
|
|
|
@Slot(str)
|
|
def set_start_button_text(self, text: str):
|
|
"""Sets the text of the Start Processing button."""
|
|
self.start_button.setText(text)
|
|
|
|
@Slot(bool)
|
|
def set_cancel_button_enabled(self, enabled: bool):
|
|
"""Sets the enabled state of the Cancel button."""
|
|
self.cancel_button.setEnabled(enabled)
|
|
|
|
@Slot(bool)
|
|
def set_llm_processing_status(self, active: bool):
|
|
"""Informs the panel whether LLM processing is active (used for context menu state)."""
|
|
self.llm_processing_active = active
|
|
# 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:
|
|
return self.output_path_edit.text().strip()
|
|
|
|
# TODO: Add method to get current Blender settings if needed by MainWindow before processing
|
|
def get_blender_settings(self) -> dict:
|
|
return {
|
|
"enabled": self.blender_integration_checkbox.isChecked(),
|
|
"nodegroup_blend_path": self.nodegroup_blend_path_input.text(),
|
|
"materials_blend_path": self.materials_blend_path_input.text()
|
|
}
|
|
|
|
# TODO: Add method to get current worker count if needed by MainWindow before processing
|
|
def get_worker_count(self) -> int:
|
|
return self.workers_spinbox.value()
|
|
|
|
# TODO: Add method to get current overwrite setting if needed by MainWindow before processing
|
|
def get_overwrite_setting(self) -> bool:
|
|
return self.overwrite_checkbox.isChecked()
|
|
|
|
# --- Delegate Dependency ---
|
|
# This method might be needed by ComboBoxDelegate if it relies on MainWindow's logic
|
|
def get_llm_source_preset_name(self) -> str | None:
|
|
"""
|
|
Placeholder for providing context to delegates.
|
|
Ideally, the required info (like last preset name) should be passed
|
|
from MainWindow when the delegate needs it, or the delegate's dependency
|
|
should be refactored.
|
|
"""
|
|
log.warning("MainPanelWidget.get_llm_source_preset_name called - needs proper implementation or refactoring.")
|
|
# This needs to get the info from MainWindow, perhaps via a signal/slot or passed reference.
|
|
# Returning None for now.
|
|
return None |