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): """ Initializes the MainPanelWidget. Args: unified_model: The shared UnifiedViewModel instance. parent: The parent widget. """ super().__init__(parent) self.unified_model = unified_model 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 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, 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