Initial Work on data-transfer refactor
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -10,18 +10,23 @@ log = logging.getLogger(__name__)
|
||||
log.info(f"sys.path: {sys.path}")
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, # Added QSplitter
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QTableView, # Added QSplitter, QTableView
|
||||
QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
QProgressBar, QLabel, QFrame, QCheckBox, QSpinBox, QListWidget, QTextEdit, # Added QListWidget, QTextEdit
|
||||
QLineEdit, QMessageBox, QFileDialog, QInputDialog, QListWidgetItem, QTabWidget, # Added more widgets
|
||||
QFormLayout, QGroupBox, QAbstractItemView, QSizePolicy, # Added more layout/widget items
|
||||
QMenuBar, QMenu # Added for menu
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Slot, Signal, QObject # Added Signal, QObject
|
||||
from PySide6.QtCore import Qt, QThread, Slot, Signal, QObject, QModelIndex # Added Signal, QObject, QModelIndex
|
||||
from PySide6.QtGui import QColor, QAction, QPalette # Add QColor import, QAction, QPalette
|
||||
|
||||
# --- Backend Imports for Data Structures ---
|
||||
from rule_structure import SourceRule, AssetRule, FileRule # Import Rule Structures
|
||||
from gui.rule_editor_widget import RuleEditorWidget # Import the new rule editor widget
|
||||
|
||||
# --- GUI Model Imports ---
|
||||
from gui.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel
|
||||
from gui.rule_hierarchy_model import RuleHierarchyModel # Import the new hierarchy model
|
||||
|
||||
# --- Backend Imports ---
|
||||
script_dir = Path(__file__).parent
|
||||
@@ -150,6 +155,8 @@ def setup_table_widget_with_controls(parent_layout, label_text, attribute_name,
|
||||
class MainWindow(QMainWindow):
|
||||
# Signal emitted when presets change in the editor panel
|
||||
presets_changed_signal = Signal()
|
||||
# Signal to trigger prediction handler in its thread
|
||||
start_prediction_signal = Signal(list, str, object) # input_paths, preset_name, rules
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -159,6 +166,8 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# --- Internal State ---
|
||||
self.current_asset_paths = set() # Store unique paths of assets added
|
||||
self.rule_hierarchy_model = RuleHierarchyModel() # Instantiate the hierarchy model
|
||||
self._current_source_rule = None # Store the current SourceRule object
|
||||
|
||||
# --- Editor State ---
|
||||
self.current_editing_preset_path = None
|
||||
@@ -172,6 +181,27 @@ class MainWindow(QMainWindow):
|
||||
self.prediction_handler = None
|
||||
self.setup_threads()
|
||||
|
||||
# --- Preview Area (Table) Setup ---
|
||||
# Initialize models
|
||||
self.preview_model = PreviewTableModel()
|
||||
self.preview_proxy_model = PreviewSortFilterProxyModel()
|
||||
self.preview_proxy_model.setSourceModel(self.preview_model)
|
||||
|
||||
# Initialize table view and placeholder
|
||||
self.preview_table_view = QTableView()
|
||||
self.preview_table_view.setModel(self.preview_proxy_model)
|
||||
|
||||
self.preview_placeholder_label = QLabel("Please select a preset to view file predictions")
|
||||
self.preview_placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.preview_placeholder_label.setStyleSheet("QLabel { font-size: 16px; color: grey; }")
|
||||
|
||||
# Initially hide the table view and show the placeholder
|
||||
self.preview_table_view.setVisible(False)
|
||||
self.preview_placeholder_label.setVisible(True)
|
||||
|
||||
# Apply style sheet to remove borders and rounded corners
|
||||
self.preview_table_view.setStyleSheet("QTableView { border: none; }")
|
||||
|
||||
# --- Main Layout with Splitter ---
|
||||
self.splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
self.setCentralWidget(self.splitter)
|
||||
@@ -361,7 +391,6 @@ class MainWindow(QMainWindow):
|
||||
self.output_path_edit.setText("") # Clear on error
|
||||
self.statusBar().showMessage(f"Error setting default output path: {e}", 5000)
|
||||
|
||||
|
||||
# --- Drag and Drop Area ---
|
||||
self.drag_drop_area = QFrame()
|
||||
self.drag_drop_area.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
@@ -375,27 +404,31 @@ class MainWindow(QMainWindow):
|
||||
main_layout.addWidget(self.drag_drop_area)
|
||||
self.drag_drop_area.setVisible(False) # Hide the specific visual drag/drop area
|
||||
|
||||
# --- Preview Area (Table) ---
|
||||
self.preview_label = QLabel("File Preview:") # Updated Label
|
||||
self.preview_table = QTableWidget() # Keep QTableWidget for now, will replace with QTableView later
|
||||
# Initialize models
|
||||
self.preview_model = PreviewTableModel()
|
||||
self.preview_proxy_model = PreviewSortFilterProxyModel()
|
||||
self.preview_proxy_model.setSourceModel(self.preview_model)
|
||||
# --- Hierarchy and Rule Editor Splitter ---
|
||||
self.hierarchy_rule_splitter = QSplitter(Qt.Orientation.Vertical)
|
||||
main_layout.addWidget(self.hierarchy_rule_splitter, 1) # Give it stretch factor
|
||||
|
||||
# Use the proxy model for the table view
|
||||
# NOTE: QTableWidget is simpler but less flexible with models.
|
||||
# For full model/view benefits (like multi-column sorting via proxy),
|
||||
# we should ideally switch to QTableView. Sticking with QTableWidget for minimal change first.
|
||||
# However, QTableWidget doesn't fully support QSortFilterProxyModel for sorting.
|
||||
# Let's switch to QTableView now for proper model/proxy integration.
|
||||
from PySide6.QtWidgets import QTableView # Import QTableView
|
||||
# --- Hierarchy Tree View ---
|
||||
from PySide6.QtWidgets import QTreeView # Import QTreeView
|
||||
self.hierarchy_tree_view = QTreeView()
|
||||
self.hierarchy_tree_view.setHeaderHidden(True) # Hide header for simple hierarchy display
|
||||
self.hierarchy_tree_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) # Make items non-editable
|
||||
self.hierarchy_tree_view.setModel(self.rule_hierarchy_model) # Set the hierarchy model
|
||||
self.hierarchy_tree_view.clicked.connect(self._on_hierarchy_item_clicked) # Connect click signal
|
||||
self.hierarchy_rule_splitter.addWidget(self.hierarchy_tree_view)
|
||||
|
||||
self.preview_table_view = QTableView() # Use QTableView instead of QTableWidget
|
||||
self.preview_table_view.setModel(self.preview_proxy_model) # Set the proxy model
|
||||
# --- Rule Editor Widget ---
|
||||
self.rule_editor_widget = RuleEditorWidget()
|
||||
self.rule_editor_widget.rule_updated.connect(self._on_rule_updated) # Connect rule updated signal
|
||||
self.hierarchy_rule_splitter.addWidget(self.rule_editor_widget)
|
||||
|
||||
# Set initial sizes for the splitter
|
||||
self.hierarchy_rule_splitter.setSizes([200, 400]) # Adjust sizes as needed
|
||||
|
||||
# --- Preview Area (Table) - Moved into the splitter ---
|
||||
# The preview table view will now be used to display files for the selected asset/source
|
||||
|
||||
# Set headers and resize modes using the model's headerData
|
||||
# The model defines the columns and headers
|
||||
header = self.preview_table_view.horizontalHeader()
|
||||
# Set resize modes for detailed columns
|
||||
header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)
|
||||
@@ -436,24 +469,29 @@ class MainWindow(QMainWindow):
|
||||
header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), 4)
|
||||
# Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.
|
||||
|
||||
main_layout.addWidget(self.preview_label)
|
||||
# Add placeholder label for the preview area (already done, just referencing)
|
||||
# self.preview_placeholder_label = QLabel("Please select a preset to view file predictions") # Already initialized in __init__
|
||||
# self.preview_placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Already done
|
||||
# self.preview_placeholder_label.setStyleSheet("QLabel { font-size: 16px; color: grey; }") # Optional styling # Already done
|
||||
|
||||
# Add placeholder label for the preview area
|
||||
self.preview_placeholder_label = QLabel("Please select a preset to view file predictions")
|
||||
self.preview_placeholder_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.preview_placeholder_label.setStyleSheet("QLabel { font-size: 16px; color: grey; }") # Optional styling
|
||||
# Add both the table view and the placeholder label to the layout (already done, just referencing)
|
||||
# We will manage their visibility later (already done, just referencing)
|
||||
# main_layout.addWidget(self.preview_placeholder_label, 1) # Give it stretch factor # REMOVED - Now managed by splitter
|
||||
# main_layout.addWidget(self.preview_table_view, 1) # Give it stretch factor # REMOVED - Now managed by splitter
|
||||
|
||||
# Add both the table view and the placeholder label to the layout
|
||||
# We will manage their visibility later
|
||||
main_layout.addWidget(self.preview_placeholder_label, 1) # Give it stretch factor
|
||||
main_layout.addWidget(self.preview_table_view, 1) # Give it stretch factor
|
||||
# Initially hide the table view and show the placeholder (already done, just referencing)
|
||||
# self.preview_table_view.setVisible(False) # Already done
|
||||
# self.preview_placeholder_label.setVisible(True) # Already done
|
||||
|
||||
# Initially hide the table view and show the placeholder
|
||||
self.preview_table_view.setVisible(False)
|
||||
self.preview_placeholder_label.setVisible(True)
|
||||
# Apply style sheet to remove borders and rounded corners (already done, just referencing)
|
||||
# self.preview_table_view.setStyleSheet("QTableView { border: none; }") # Already done
|
||||
|
||||
# Apply style sheet to remove borders and rounded corners
|
||||
self.preview_table_view.setStyleSheet("QTableView { border: none; }")
|
||||
# --- Add Preview Table View to Splitter ---
|
||||
# The preview table view will now be placed below the hierarchy tree view in the splitter
|
||||
# It will display the files associated with the selected item in the hierarchy
|
||||
self.hierarchy_rule_splitter.addWidget(self.preview_table_view)
|
||||
# Set initial sizes for the splitter (adjusting to include the table view)
|
||||
self.hierarchy_rule_splitter.setSizes([200, 200, 400]) # Hierarchy, Rule Editor, File Preview
|
||||
|
||||
# --- Progress Bar ---
|
||||
self.progress_bar = QProgressBar()
|
||||
@@ -751,10 +789,22 @@ class MainWindow(QMainWindow):
|
||||
if self.processing_thread and self.processing_handler:
|
||||
try: self.processing_thread.started.disconnect()
|
||||
except RuntimeError: pass
|
||||
# Use the current SourceRule from the hierarchy model
|
||||
if self._current_source_rule is None:
|
||||
log.error("Cannot start processing: No rule hierarchy available.")
|
||||
self.statusBar().showMessage("Error: No rule hierarchy available. Run preview first.", 5000)
|
||||
self.set_controls_enabled(True)
|
||||
self.cancel_button.setEnabled(False)
|
||||
self.start_button.setText("Start Processing")
|
||||
return
|
||||
|
||||
log.debug(f"Using SourceRule '{self._current_source_rule.name}' for processing.")
|
||||
|
||||
self.processing_thread.started.connect(
|
||||
lambda: self.processing_handler.run_processing(
|
||||
input_paths, selected_preset, output_dir_str, overwrite, num_workers,
|
||||
# Pass Blender integration settings
|
||||
rules=self._current_source_rule, # Pass the current SourceRule
|
||||
run_blender=self.blender_integration_checkbox.isChecked(),
|
||||
nodegroup_blend_path=self.nodegroup_blend_path_input.text(),
|
||||
materials_blend_path=self.materials_blend_path_input.text(),
|
||||
@@ -969,17 +1019,25 @@ class MainWindow(QMainWindow):
|
||||
log.info(f"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items, Preset='{selected_preset}'")
|
||||
self.statusBar().showMessage(f"Updating preview for '{selected_preset}'...", 0)
|
||||
# Clearing is handled by model's set_data now, no need to clear table view directly
|
||||
self.setup_threads() # Ensure threads are ready
|
||||
if self.prediction_thread and self.prediction_handler:
|
||||
try: self.prediction_thread.started.disconnect() # Disconnect previous lambda if any
|
||||
except RuntimeError: pass
|
||||
# Connect the lambda to start the prediction
|
||||
self.prediction_thread.started.connect(
|
||||
lambda: self.prediction_handler.run_prediction(input_paths, selected_preset)
|
||||
)
|
||||
# Create a placeholder SourceRule instance (replace with actual rule loading later)
|
||||
placeholder_rules = SourceRule() # Temporary rule for passing data
|
||||
log.debug(f"Created placeholder SourceRule for prediction.")
|
||||
|
||||
# Create a placeholder SourceRule instance (replace with actual rule loading later)
|
||||
placeholder_rules = SourceRule() # Temporary rule for passing data
|
||||
log.debug(f"Created placeholder SourceRule for prediction.")
|
||||
|
||||
# Start the prediction thread
|
||||
log.debug(f"[{time.time():.4f}] Starting prediction thread...")
|
||||
self.prediction_thread.start()
|
||||
log.debug(f"[{time.time():.4f}] Prediction thread start requested.")
|
||||
|
||||
# Emit the signal to trigger run_prediction in the prediction thread
|
||||
log.debug(f"[{time.time():.4f}] Emitting start_prediction_signal...")
|
||||
self.start_prediction_signal.emit(input_paths, selected_preset, placeholder_rules)
|
||||
log.debug(f"[{time.time():.4f}] start_prediction_signal emitted.")
|
||||
|
||||
else:
|
||||
log.error(f"[{time.time():.4f}][T:{thread_id}] Failed to start prediction: Thread or handler not initialized.")
|
||||
self.statusBar().showMessage("Error: Failed to initialize prediction thread.", 5000)
|
||||
@@ -1013,7 +1071,10 @@ class MainWindow(QMainWindow):
|
||||
self.prediction_thread = QThread(self)
|
||||
self.prediction_handler = PredictionHandler()
|
||||
self.prediction_handler.moveToThread(self.prediction_thread)
|
||||
self.prediction_handler.prediction_results_ready.connect(self.on_prediction_results_ready) # Updated slot below
|
||||
# Connect the new signal to the handler's run_prediction slot using QueuedConnection
|
||||
self.start_prediction_signal.connect(self.prediction_handler.run_prediction, Qt.ConnectionType.QueuedConnection)
|
||||
self.prediction_handler.prediction_results_ready.connect(self.on_prediction_results_ready) # Connect the file list signal
|
||||
self.prediction_handler.rule_hierarchy_ready.connect(self._on_rule_hierarchy_ready) # Connect the new hierarchy signal
|
||||
self.prediction_handler.prediction_finished.connect(self.on_prediction_finished)
|
||||
self.prediction_handler.status_message.connect(self.show_status_message)
|
||||
self.prediction_handler.prediction_finished.connect(self.prediction_thread.quit)
|
||||
@@ -1613,6 +1674,84 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
event.accept() # Accept close event
|
||||
|
||||
# --- Slots for Hierarchy and Rule Editor ---
|
||||
|
||||
@Slot(QModelIndex)
|
||||
def _on_hierarchy_item_clicked(self, index: QModelIndex):
|
||||
"""Loads the selected rule item into the rule editor and filters the preview table."""
|
||||
if index.isValid():
|
||||
rule_item = self.rule_hierarchy_model.get_item_from_index(index)
|
||||
if rule_item:
|
||||
rule_type_name = type(rule_item).__name__
|
||||
log.debug(f"Hierarchy item clicked: {rule_type_name} - {getattr(rule_item, 'name', 'N/A')}")
|
||||
self.rule_editor_widget.load_rule(rule_item, rule_type_name)
|
||||
|
||||
# Filter the preview table based on the selected item
|
||||
if isinstance(rule_item, SourceRule):
|
||||
# Show all files for the source
|
||||
self.preview_proxy_model.setFilterRegularExpression("") # Clear filter
|
||||
self.preview_proxy_model.setFilterKeyColumn(-1) # Apply to all columns (effectively no column filter)
|
||||
log.debug("Filtering preview table: Showing all files for Source.")
|
||||
elif isinstance(rule_item, AssetRule):
|
||||
# Show files belonging to this asset
|
||||
# Filter by the 'source_asset' column (which stores the asset name/path)
|
||||
# Need to escape potential regex special characters in the asset name/path
|
||||
filter_string = "^" + rule_item.asset_name.replace('\\', '\\\\').replace('.', '\\.') + "$"
|
||||
self.preview_proxy_model.setFilterRegularExpression(filter_string)
|
||||
self.preview_proxy_model.setFilterKeyColumn(self.preview_model.ROLE_SOURCE_ASSET) # Filter by source_asset column
|
||||
log.debug(f"Filtering preview table: Showing files for Asset '{rule_item.name}'. Filter: '{filter_string}' on column {self.preview_model.COL_SOURCE_ASSET}")
|
||||
elif isinstance(rule_item, FileRule):
|
||||
# Show only this specific file
|
||||
# Filter by the 'original_path' column
|
||||
filter_string = "^" + rule_item.file_path.replace('\\', '\\\\').replace('.', '\\.') + "$"
|
||||
self.preview_proxy_model.setFilterRegularExpression(filter_string)
|
||||
self.preview_proxy_model.setFilterKeyColumn(self.preview_model.COL_ORIGINAL_PATH) # Filter by original_path column
|
||||
log.debug(f"Filtering preview table: Showing file '{rule_item.file_path}'. Filter: '{filter_string}' on column {self.preview_model.COL_ORIGINAL_PATH}")
|
||||
else:
|
||||
# Clear filter for unknown types
|
||||
self.preview_proxy_model.setFilterRegularExpression("")
|
||||
self.preview_proxy_model.setFilterKeyColumn(-1)
|
||||
log.warning(f"Clicked item has unknown type {type(rule_item)}. Clearing preview filter.")
|
||||
|
||||
else:
|
||||
log.warning("Clicked item has no associated rule object. Clearing editor and preview filter.")
|
||||
self.rule_editor_widget.clear_editor()
|
||||
self.preview_proxy_model.setFilterRegularExpression("") # Clear filter
|
||||
self.preview_proxy_model.setFilterKeyColumn(-1)
|
||||
|
||||
else:
|
||||
log.debug("Clicked item index is invalid. Clearing editor and preview filter.")
|
||||
self.rule_editor_widget.clear_editor()
|
||||
self.preview_proxy_model.setFilterRegularExpression("") # Clear filter
|
||||
self.preview_proxy_model.setFilterKeyColumn(-1)
|
||||
|
||||
|
||||
@Slot(object)
|
||||
def _on_rule_updated(self, rule_object):
|
||||
"""Handles the signal when a rule is updated in the editor."""
|
||||
# This slot is called when an attribute is changed in the RuleEditorWidget.
|
||||
# The rule_object passed is the actual object instance from the hierarchy model.
|
||||
# Since the RuleEditorWidget modifies the object in place, we don't need to
|
||||
# explicitly update the model's data structure here.
|
||||
# However, if the change affects the display in the hierarchy tree or preview table,
|
||||
# we might need to emit dataChanged signals or trigger updates.
|
||||
# For now, just log the update.
|
||||
log.debug(f"Rule object updated in editor: {type(rule_object).__name__} - {getattr(rule_object, 'name', 'N/A')}")
|
||||
# TODO: Consider if any UI updates are needed based on the rule change.
|
||||
# E.g., if a rule name changes, the hierarchy tree might need a dataChanged signal.
|
||||
# If a rule affects file output names, the preview table might need updating.
|
||||
# This is complex and depends on which rule attributes are editable and their impact.
|
||||
|
||||
@Slot(object)
|
||||
def _on_rule_hierarchy_ready(self, source_rule: SourceRule):
|
||||
"""Receives the generated SourceRule hierarchy and updates the tree view model."""
|
||||
log.info(f"Received rule hierarchy ready signal for input: {source_rule.input_path}")
|
||||
self._current_source_rule = source_rule # Store the generated rule hierarchy
|
||||
self.rule_hierarchy_model.set_root_rule(source_rule) # Update the tree view model
|
||||
self.hierarchy_tree_view.expandToDepth(0) # Expand the first level (Source and Assets)
|
||||
log.debug("Rule hierarchy model updated and tree view expanded.")
|
||||
|
||||
|
||||
# --- Main Execution ---
|
||||
def run_gui():
|
||||
"""Initializes and runs the Qt application."""
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
# gui/prediction_handler.py
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import time # For potential delays if needed
|
||||
import os # For cpu_count
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed # For parallel prediction
|
||||
from collections import defaultdict
|
||||
|
||||
# --- PySide6 Imports ---
|
||||
from PySide6.QtCore import QObject, Signal, QThread # Import QThread
|
||||
from PySide6.QtCore import QObject, Signal, QThread, Slot # Import QThread and Slot
|
||||
|
||||
# --- Backend Imports ---
|
||||
# Adjust path to ensure modules can be found relative to this file's location
|
||||
@@ -43,6 +45,8 @@ class PredictionHandler(QObject):
|
||||
# Emits a list of dictionaries, each representing a file row for the table
|
||||
# Dict format: {'original_path': str, 'predicted_asset_name': str | None, 'predicted_output_name': str | None, 'status': str, 'details': str | None, 'source_asset': str}
|
||||
prediction_results_ready = Signal(list)
|
||||
# Emitted when the hierarchical rule structure is ready
|
||||
rule_hierarchy_ready = Signal(object) # Emits a SourceRule object
|
||||
# Emitted when all predictions for a batch are done
|
||||
prediction_finished = Signal()
|
||||
# Emitted for status updates
|
||||
@@ -57,78 +61,72 @@ class PredictionHandler(QObject):
|
||||
def is_running(self):
|
||||
return self._is_running
|
||||
|
||||
def _predict_single_asset(self, input_path_str: str, config: Configuration) -> list[dict]:
|
||||
def _predict_single_asset(self, input_path_str: str, config: Configuration, rules: SourceRule) -> list[dict] | dict:
|
||||
"""
|
||||
Helper method to predict a single asset. Runs within the ThreadPoolExecutor.
|
||||
Returns a list of prediction dictionaries for the asset, or a single error dict.
|
||||
Helper method to run detailed file prediction for a single input path.
|
||||
Runs within the ThreadPoolExecutor.
|
||||
Returns a list of file prediction dictionaries for the input, or a dictionary representing an error.
|
||||
"""
|
||||
input_path = Path(input_path_str)
|
||||
source_asset_name = input_path.name # For reference in the results
|
||||
asset_results = []
|
||||
source_asset_name = input_path.name # For reference in error reporting
|
||||
|
||||
try:
|
||||
# Create AssetProcessor instance (needs dummy output path)
|
||||
# Ensure AssetProcessor is thread-safe or create a new instance per thread.
|
||||
# Based on its structure (using temp dirs), creating new instances should be safe.
|
||||
# Create AssetProcessor instance (needs dummy output path for prediction)
|
||||
# The detailed prediction method handles its own workspace setup/cleanup
|
||||
processor = AssetProcessor(input_path, config, Path(".")) # Dummy output path
|
||||
|
||||
# Get detailed file predictions
|
||||
detailed_predictions = processor.get_detailed_file_predictions()
|
||||
# Get the detailed file predictions
|
||||
# This method returns a list of dictionaries
|
||||
detailed_predictions = processor.get_detailed_file_predictions(rules)
|
||||
|
||||
if detailed_predictions is None:
|
||||
log.error(f"Detailed prediction failed critically for {input_path_str}. Adding asset-level error.")
|
||||
# Add a single error entry for the whole asset if the method returns None
|
||||
asset_results.append({
|
||||
'original_path': source_asset_name, # Use asset name as placeholder
|
||||
'predicted_asset_name': None, # New key
|
||||
'predicted_output_name': None, # New key
|
||||
'status': 'Error',
|
||||
'details': 'Critical prediction failure (check logs)',
|
||||
'source_asset': source_asset_name
|
||||
})
|
||||
else:
|
||||
log.debug(f"Received {len(detailed_predictions)} detailed predictions for {input_path_str}.")
|
||||
# Add source_asset key and ensure correct keys exist
|
||||
for prediction_dict in detailed_predictions:
|
||||
# Ensure all expected keys are present, even if None
|
||||
result_entry = {
|
||||
'original_path': prediction_dict.get('original_path', '[Missing Path]'),
|
||||
'predicted_asset_name': prediction_dict.get('predicted_asset_name'), # New key
|
||||
'predicted_output_name': prediction_dict.get('predicted_output_name'), # New key
|
||||
'status': prediction_dict.get('status', 'Error'),
|
||||
'details': prediction_dict.get('details', '[Missing Details]'),
|
||||
'source_asset': source_asset_name # Add the source asset identifier
|
||||
}
|
||||
asset_results.append(result_entry)
|
||||
log.error(f"AssetProcessor.get_detailed_file_predictions returned None for {input_path_str}.")
|
||||
# Return a list containing a single error entry for consistency
|
||||
return [{
|
||||
'original_path': source_asset_name,
|
||||
'predicted_asset_name': None,
|
||||
'predicted_output_name': None,
|
||||
'status': 'Error',
|
||||
'details': 'Prediction returned no results',
|
||||
'source_asset': source_asset_name
|
||||
}]
|
||||
|
||||
except AssetProcessingError as e: # Catch errors during processor instantiation or prediction setup
|
||||
log.error(f"Asset processing error during prediction setup for {input_path_str}: {e}")
|
||||
asset_results.append({
|
||||
# Add the source_asset name to each prediction result for grouping later
|
||||
for prediction in detailed_predictions:
|
||||
prediction['source_asset'] = source_asset_name
|
||||
|
||||
log.debug(f"Generated {len(detailed_predictions)} detailed predictions for {input_path_str}.")
|
||||
return detailed_predictions # Return the list of dictionaries
|
||||
|
||||
except AssetProcessingError as e:
|
||||
log.error(f"Asset processing error during prediction for {input_path_str}: {e}")
|
||||
# Return a list containing a single error entry for consistency
|
||||
return [{
|
||||
'original_path': source_asset_name,
|
||||
'predicted_asset_name': None,
|
||||
'predicted_output_name': None,
|
||||
'status': 'Error',
|
||||
'details': f'Asset Error: {e}',
|
||||
'source_asset': source_asset_name
|
||||
})
|
||||
except Exception as e: # Catch unexpected errors
|
||||
}]
|
||||
except Exception as e:
|
||||
log.exception(f"Unexpected error during prediction for {input_path_str}: {e}")
|
||||
asset_results.append({
|
||||
# Return a list containing a single error entry for consistency
|
||||
return [{
|
||||
'original_path': source_asset_name,
|
||||
'predicted_asset_name': None,
|
||||
'predicted_output_name': None,
|
||||
'status': 'Error',
|
||||
'details': f'Unexpected Error: {e}',
|
||||
'source_asset': source_asset_name
|
||||
})
|
||||
finally:
|
||||
# Cleanup for the single asset prediction if needed (AssetProcessor handles its own temp dir)
|
||||
pass
|
||||
return asset_results
|
||||
}]
|
||||
|
||||
|
||||
def run_prediction(self, input_paths: list[str], preset_name: str):
|
||||
@Slot()
|
||||
def run_prediction(self, input_paths: list[str], preset_name: str, rules: SourceRule):
|
||||
"""
|
||||
Runs the prediction logic for the given paths and preset using a ThreadPoolExecutor.
|
||||
Generates the hierarchical rule structure and detailed file predictions.
|
||||
This method is intended to be run in a separate QThread.
|
||||
"""
|
||||
if self._is_running:
|
||||
@@ -156,7 +154,6 @@ class PredictionHandler(QObject):
|
||||
except ConfigurationError as e:
|
||||
log.error(f"Failed to load configuration for preset '{preset_name}': {e}")
|
||||
self.status_message.emit(f"Error loading preset '{preset_name}': {e}", 5000)
|
||||
# Emit error for all items? Or just finish? Finish for now.
|
||||
self.prediction_finished.emit()
|
||||
self._is_running = False
|
||||
return
|
||||
@@ -166,9 +163,15 @@ class PredictionHandler(QObject):
|
||||
self.prediction_finished.emit()
|
||||
return
|
||||
|
||||
all_file_results = [] # Accumulate results here
|
||||
# Create the root SourceRule object
|
||||
# For now, use a generic name. Later, this might be derived from input paths.
|
||||
source_rule = SourceRule()
|
||||
log.debug(f"Created root SourceRule object.")
|
||||
|
||||
# Collect all detailed file prediction results from completed futures
|
||||
all_file_prediction_results = []
|
||||
|
||||
futures = []
|
||||
# Determine number of workers - use half the cores, minimum 1, max 8?
|
||||
max_workers = min(max(1, (os.cpu_count() or 1) // 2), 8)
|
||||
log.info(f"Using ThreadPoolExecutor with max_workers={max_workers} for prediction.")
|
||||
|
||||
@@ -176,22 +179,34 @@ class PredictionHandler(QObject):
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
# Submit tasks for each input path
|
||||
for input_path_str in input_paths:
|
||||
future = executor.submit(self._predict_single_asset, input_path_str, config)
|
||||
# _predict_single_asset now returns a list of file prediction dicts or an error dict list
|
||||
future = executor.submit(self._predict_single_asset, input_path_str, config, rules)
|
||||
futures.append(future)
|
||||
|
||||
# Process results as they complete
|
||||
for future in as_completed(futures):
|
||||
try:
|
||||
# Result is a list of dicts for one asset
|
||||
asset_result_list = future.result()
|
||||
if asset_result_list: # Check if list is not empty
|
||||
all_file_results.extend(asset_result_list)
|
||||
result = future.result()
|
||||
if isinstance(result, list):
|
||||
# Extend the main list with results from this asset
|
||||
all_file_prediction_results.extend(result)
|
||||
elif isinstance(result, dict) and result.get('status') == 'Error':
|
||||
# Handle error dictionaries returned by _predict_single_asset (should be in a list now, but handle single dict for safety)
|
||||
all_file_prediction_results.append(result)
|
||||
else:
|
||||
log.error(f'Prediction task returned unexpected result type: {type(result)}')
|
||||
all_file_prediction_results.append({
|
||||
'original_path': '[Unknown Asset - Unexpected Result]',
|
||||
'predicted_asset_name': None,
|
||||
'predicted_output_name': None,
|
||||
'status': 'Error',
|
||||
'details': f'Unexpected result type: {type(result)}',
|
||||
'source_asset': '[Unknown]'
|
||||
})
|
||||
|
||||
except Exception as exc:
|
||||
# This catches errors within the future execution itself if not handled by _predict_single_asset
|
||||
log.error(f'Prediction task generated an exception: {exc}', exc_info=True)
|
||||
# We might not know which input path failed here easily without more mapping
|
||||
# Add a generic error?
|
||||
all_file_results.append({
|
||||
all_file_prediction_results.append({
|
||||
'original_path': '[Unknown Asset - Executor Error]',
|
||||
'predicted_asset_name': None,
|
||||
'predicted_output_name': None,
|
||||
@@ -203,8 +218,7 @@ class PredictionHandler(QObject):
|
||||
except Exception as pool_exc:
|
||||
log.exception(f"An error occurred with the prediction ThreadPoolExecutor: {pool_exc}")
|
||||
self.status_message.emit(f"Error during prediction setup: {pool_exc}", 5000)
|
||||
# Add a generic error if the pool fails
|
||||
all_file_results.append({
|
||||
all_file_prediction_results.append({
|
||||
'original_path': '[Prediction Pool Error]',
|
||||
'predicted_asset_name': None,
|
||||
'predicted_output_name': None,
|
||||
@@ -213,19 +227,61 @@ class PredictionHandler(QObject):
|
||||
'source_asset': '[System]'
|
||||
})
|
||||
|
||||
# Emit the combined list of detailed file results at the end
|
||||
# Note: thread_id was already defined earlier in this function
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Parallel prediction run finished. Preparing to emit {len(all_file_results)} file results.")
|
||||
# <<< Add logging before emit >>>
|
||||
log.debug(f"[{time.time():.4f}][T:{thread_id}] Type of all_file_results before emit: {type(all_file_results)}")
|
||||
|
||||
# --- Build the hierarchical rule structure (SourceRule -> AssetRule -> FileRule) ---
|
||||
# Group file prediction results by predicted_asset_name
|
||||
grouped_by_asset = defaultdict(list)
|
||||
for file_pred in all_file_prediction_results:
|
||||
# Group by predicted_asset_name, handle None or errors
|
||||
asset_name = file_pred.get('predicted_asset_name')
|
||||
if asset_name is None:
|
||||
# Group files without a predicted asset name under a special key or ignore for hierarchy?
|
||||
# Let's group them under their source_asset name for now, but mark them clearly.
|
||||
asset_name = f"[{file_pred.get('source_asset', 'UnknownSource')}]" # Use source asset name as a fallback identifier
|
||||
log.debug(f"File '{file_pred.get('original_path', 'UnknownPath')}' has no predicted asset name, grouping under '{asset_name}' for hierarchy.")
|
||||
grouped_by_asset[asset_name].append(file_pred)
|
||||
|
||||
# Create AssetRule objects from the grouped results
|
||||
asset_rules = []
|
||||
for asset_name, file_preds in grouped_by_asset.items():
|
||||
# Determine the source_path for the AssetRule (use the source_asset from the first file in the group)
|
||||
source_asset_path = file_preds[0].get('source_asset', asset_name) # Fallback to asset_name if source_asset is missing
|
||||
asset_rule = AssetRule(asset_name=asset_name)
|
||||
|
||||
# Create FileRule objects from the file prediction dictionaries
|
||||
for file_pred in file_preds:
|
||||
file_rule = FileRule(
|
||||
file_path=file_pred.get('original_path', 'UnknownPath'),
|
||||
map_type_override=None, # Assuming these are not predicted here
|
||||
resolution_override=None, # Assuming these are not predicted here
|
||||
channel_merge_instructions={}, # Assuming these are not predicted here
|
||||
output_format_override=None # Assuming these are not predicted here
|
||||
)
|
||||
asset_rule.files.append(file_rule)
|
||||
|
||||
asset_rules.append(asset_rule)
|
||||
|
||||
# Populate the SourceRule with the collected AssetRules
|
||||
source_rule.assets = asset_rules
|
||||
log.debug(f"Built SourceRule with {len(asset_rules)} AssetRule(s).")
|
||||
|
||||
|
||||
# Emit the hierarchical rule structure
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Parallel prediction run finished. Preparing to emit rule hierarchy.")
|
||||
self.rule_hierarchy_ready.emit(source_rule)
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Emitted rule_hierarchy_ready signal.")
|
||||
|
||||
# Emit the combined list of detailed file results for the table view
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Preparing to emit {len(all_file_prediction_results)} file results for table view.")
|
||||
log.debug(f"[{time.time():.4f}][T:{thread_id}] Type of all_file_prediction_results before emit: {type(all_file_prediction_results)}")
|
||||
try:
|
||||
log.debug(f"[{time.time():.4f}][T:{thread_id}] Content of all_file_results (first 5) before emit: {all_file_results[:5]}")
|
||||
log.debug(f"[{time.time():.4f}][T:{thread_id}] Content of all_file_prediction_results (first 5) before emit: {all_file_prediction_results[:5]}")
|
||||
except Exception as e:
|
||||
log.error(f"[{time.time():.4f}][T:{thread_id}] Error logging all_file_results content: {e}")
|
||||
# <<< End added logging >>>
|
||||
log.error(f"[{time.time():.4f}][T:{thread_id}] Error logging all_file_prediction_results content: {e}")
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Emitting prediction_results_ready signal...")
|
||||
self.prediction_results_ready.emit(all_file_results)
|
||||
self.prediction_results_ready.emit(all_file_prediction_results)
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Emitted prediction_results_ready signal.")
|
||||
|
||||
self.status_message.emit("Preview update complete.", 3000)
|
||||
self.prediction_finished.emit()
|
||||
self._is_running = False
|
||||
|
||||
@@ -7,6 +7,7 @@ import time # For potential delays if needed
|
||||
import subprocess # <<< ADDED IMPORT
|
||||
import shutil # <<< ADDED IMPORT
|
||||
from typing import Optional # <<< ADDED IMPORT
|
||||
from rule_structure import SourceRule # Import SourceRule
|
||||
|
||||
# --- PySide6 Imports ---
|
||||
# Inherit from QObject to support signals/slots for thread communication
|
||||
@@ -70,7 +71,7 @@ class ProcessingHandler(QObject):
|
||||
return self._is_running
|
||||
|
||||
def run_processing(self, input_paths: list[str], preset_name: str, output_dir_str: str, overwrite: bool, num_workers: int,
|
||||
run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str, verbose: bool): # <<< ADDED verbose PARAM
|
||||
run_blender: bool, nodegroup_blend_path: str, materials_blend_path: str, verbose: bool, rules: SourceRule): # <<< ADDED verbose PARAM
|
||||
"""
|
||||
Starts the asset processing task and optionally runs Blender scripts afterwards.
|
||||
This method should be called when the handler is moved to a separate thread.
|
||||
@@ -107,7 +108,7 @@ class ProcessingHandler(QObject):
|
||||
for input_path in input_paths:
|
||||
if self._cancel_requested: break # Check before submitting more
|
||||
log.debug(f"Submitting task for: {input_path}")
|
||||
future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=verbose) # Pass verbose flag from GUI
|
||||
future = executor.submit(process_single_asset_wrapper, input_path, preset_name, output_dir_str, overwrite, verbose=verbose, rules=rules) # Pass verbose flag from GUI and rules
|
||||
self._futures[future] = input_path # Map future back to input path
|
||||
# Optionally emit "processing" status here
|
||||
self.file_status_updated.emit(input_path, "processing", "")
|
||||
|
||||
180
gui/rule_editor_widget.py
Normal file
180
gui/rule_editor_widget.py
Normal file
@@ -0,0 +1,180 @@
|
||||
import sys
|
||||
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QLineEdit,
|
||||
QFormLayout, QComboBox, QCheckBox, QSpinBox, QDoubleSpinBox)
|
||||
from PySide6.QtCore import Signal, Slot, QObject
|
||||
|
||||
# Assuming rule_structure.py is in the parent directory or accessible via PYTHONPATH
|
||||
# from ..rule_structure import SourceRule, AssetRule, FileRule # Adjust import based on actual structure
|
||||
# For now, we'll use placeholder classes or assume rule_structure is directly importable
|
||||
# from rule_structure import SourceRule, AssetRule, FileRule # Assuming direct import is possible
|
||||
|
||||
class RuleEditorWidget(QWidget):
|
||||
"""
|
||||
A widget to display and edit hierarchical processing rules (Source, Asset, File).
|
||||
"""
|
||||
rule_updated = Signal(object) # Signal emitted when a rule is updated
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.current_rule_type = None
|
||||
self.current_rule_object = None
|
||||
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.rule_type_label = QLabel("Select an item in the hierarchy to view/edit rules.")
|
||||
self.layout.addWidget(self.rule_type_label)
|
||||
|
||||
self.form_layout = QFormLayout()
|
||||
self.layout.addLayout(self.form_layout)
|
||||
|
||||
self.layout.addStretch() # Add stretch to push content to the top
|
||||
|
||||
self.setLayout(self.layout)
|
||||
self.clear_editor()
|
||||
|
||||
@Slot(object, str)
|
||||
def load_rule(self, rule_object, rule_type_name):
|
||||
"""
|
||||
Loads a rule object into the editor.
|
||||
|
||||
Args:
|
||||
rule_object: The SourceRule, AssetRule, or FileRule object.
|
||||
rule_type_name: The name of the rule type ('SourceRule', 'AssetRule', 'FileRule').
|
||||
"""
|
||||
self.clear_editor()
|
||||
self.current_rule_object = rule_object
|
||||
self.current_rule_type = rule_type_name
|
||||
self.rule_type_label.setText(f"Editing: {rule_type_name}")
|
||||
|
||||
if rule_object:
|
||||
# Dynamically create form fields based on rule object attributes
|
||||
for attr_name, attr_value in vars(rule_object).items():
|
||||
if attr_name.startswith('_'): # Skip private attributes
|
||||
continue
|
||||
|
||||
label = QLabel(attr_name.replace('_', ' ').title() + ":")
|
||||
editor_widget = self._create_editor_widget(attr_name, attr_value)
|
||||
if editor_widget:
|
||||
self.form_layout.addRow(label, editor_widget)
|
||||
# Connect signal to update rule object
|
||||
self._connect_editor_signal(editor_widget, attr_name)
|
||||
|
||||
def _create_editor_widget(self, attr_name, attr_value):
|
||||
"""
|
||||
Creates an appropriate editor widget based on the attribute type.
|
||||
"""
|
||||
if isinstance(attr_value, bool):
|
||||
widget = QCheckBox()
|
||||
widget.setChecked(attr_value)
|
||||
return widget
|
||||
elif isinstance(attr_value, int):
|
||||
widget = QSpinBox()
|
||||
widget.setRange(-2147483648, 2147483647) # Default integer range
|
||||
widget.setValue(attr_value)
|
||||
return widget
|
||||
elif isinstance(attr_value, float):
|
||||
widget = QDoubleSpinBox()
|
||||
widget.setRange(-sys.float_info.max, sys.float_info.max) # Default float range
|
||||
widget.setValue(attr_value)
|
||||
return widget
|
||||
elif isinstance(attr_value, (str, type(None))): # Handle None for strings
|
||||
widget = QLineEdit()
|
||||
widget.setText(str(attr_value) if attr_value is not None else "")
|
||||
return widget
|
||||
# Add more types as needed (e.g., dropdowns for enums/choices)
|
||||
# elif isinstance(attr_value, list):
|
||||
# # Example for a simple list of strings
|
||||
# widget = QLineEdit()
|
||||
# widget.setText(", ".join(map(str, attr_value)))
|
||||
# return widget
|
||||
else:
|
||||
# For unsupported types, just display the value
|
||||
label = QLabel(str(attr_value))
|
||||
return label
|
||||
|
||||
def _connect_editor_signal(self, editor_widget, attr_name):
|
||||
"""
|
||||
Connects the appropriate signal of the editor widget to the update logic.
|
||||
"""
|
||||
if isinstance(editor_widget, QLineEdit):
|
||||
editor_widget.textChanged.connect(lambda text: self._update_rule_attribute(attr_name, text))
|
||||
elif isinstance(editor_widget, QCheckBox):
|
||||
editor_widget.toggled.connect(lambda checked: self._update_rule_attribute(attr_name, checked))
|
||||
elif isinstance(editor_widget, QSpinBox):
|
||||
editor_widget.valueChanged.connect(lambda value: self._update_rule_attribute(attr_name, value))
|
||||
elif isinstance(editor_widget, QDoubleSpinBox):
|
||||
editor_widget.valueChanged.connect(lambda value: self._update_rule_attribute(attr_name, value))
|
||||
# Add connections for other widget types
|
||||
|
||||
def _update_rule_attribute(self, attr_name, value):
|
||||
"""
|
||||
Updates the attribute of the current rule object and emits the signal.
|
||||
"""
|
||||
if self.current_rule_object:
|
||||
# Basic type conversion based on the original attribute type
|
||||
original_value = getattr(self.current_rule_object, attr_name)
|
||||
try:
|
||||
if isinstance(original_value, bool):
|
||||
converted_value = bool(value)
|
||||
elif isinstance(original_value, int):
|
||||
converted_value = int(value)
|
||||
elif isinstance(original_value, float):
|
||||
converted_value = float(value)
|
||||
elif isinstance(original_value, (str, type(None))):
|
||||
converted_value = str(value) if value != "" else None # Convert empty string to None for original None types
|
||||
else:
|
||||
converted_value = value # Fallback for other types
|
||||
setattr(self.current_rule_object, attr_name, converted_value)
|
||||
self.rule_updated.emit(self.current_rule_object)
|
||||
# print(f"Updated {attr_name} to {converted_value} in {self.current_rule_type}") # Debugging
|
||||
except ValueError:
|
||||
# Handle potential conversion errors (e.g., non-numeric input for int/float)
|
||||
print(f"Error converting value '{value}' for attribute '{attr_name}'")
|
||||
# Optionally, revert the editor widget to the original value or show an error indicator
|
||||
|
||||
def clear_editor(self):
|
||||
"""
|
||||
Clears the form layout.
|
||||
"""
|
||||
self.current_rule_object = None
|
||||
self.current_rule_type = None
|
||||
self.rule_type_label.setText("Select an item in the hierarchy to view/edit rules.")
|
||||
while self.form_layout.rowCount() > 0:
|
||||
self.form_layout.removeRow(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Placeholder Rule Classes for testing
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
@dataclass
|
||||
class SourceRule:
|
||||
source_setting_1: str = "default_source_string"
|
||||
source_setting_2: int = 123
|
||||
source_setting_3: bool = True
|
||||
|
||||
@dataclass
|
||||
class AssetRule:
|
||||
asset_setting_a: float = 4.56
|
||||
asset_setting_b: str = None
|
||||
asset_setting_c: bool = False
|
||||
|
||||
@dataclass
|
||||
class FileRule:
|
||||
file_setting_x: int = 789
|
||||
file_setting_y: str = "default_file_string"
|
||||
|
||||
editor = RuleEditorWidget()
|
||||
|
||||
# Test loading different rule types
|
||||
source_rule = SourceRule()
|
||||
asset_rule = AssetRule()
|
||||
file_rule = FileRule()
|
||||
|
||||
editor.load_rule(source_rule, "SourceRule")
|
||||
# editor.load_rule(asset_rule, "AssetRule")
|
||||
# editor.load_rule(file_rule, "FileRule")
|
||||
|
||||
|
||||
editor.show()
|
||||
sys.exit(app.exec())
|
||||
184
gui/rule_hierarchy_model.py
Normal file
184
gui/rule_hierarchy_model.py
Normal file
@@ -0,0 +1,184 @@
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot
|
||||
from PySide6.QtGui import QIcon # Assuming we might want icons later
|
||||
from rule_structure import SourceRule, AssetRule, FileRule # Import rule structures
|
||||
|
||||
class RuleHierarchyModel(QAbstractItemModel):
|
||||
"""
|
||||
A custom model for displaying the hierarchical structure of SourceRule,
|
||||
AssetRule, and FileRule objects in a QTreeView.
|
||||
"""
|
||||
def __init__(self, root_rule: SourceRule = None, parent=None):
|
||||
super().__init__(parent)
|
||||
self._root_rule = root_rule
|
||||
|
||||
def set_root_rule(self, root_rule: SourceRule):
|
||||
"""Sets the root SourceRule for the model and resets the model."""
|
||||
self.beginResetModel()
|
||||
self._root_rule = root_rule
|
||||
self.endResetModel()
|
||||
|
||||
def rowCount(self, parent: QModelIndex = QModelIndex()):
|
||||
"""Returns the number of rows (children) for the given parent index."""
|
||||
if not parent.isValid():
|
||||
# Root item (SourceRule)
|
||||
return 1 if self._root_rule else 0
|
||||
else:
|
||||
parent_item = parent.internalPointer()
|
||||
if isinstance(parent_item, SourceRule):
|
||||
# Children of SourceRule are AssetRules
|
||||
return len(parent_item.assets)
|
||||
elif isinstance(parent_item, AssetRule):
|
||||
# Children of AssetRule are FileRules
|
||||
return len(parent_item.files)
|
||||
elif isinstance(parent_item, FileRule):
|
||||
# FileRules have no children
|
||||
return 0
|
||||
else:
|
||||
return 0
|
||||
|
||||
def columnCount(self, parent: QModelIndex = QModelIndex()):
|
||||
"""Returns the number of columns."""
|
||||
return 1 # We only need one column for the hierarchy name
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
||||
"""Returns the data for the given index and role."""
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
item = index.internalPointer()
|
||||
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if isinstance(item, SourceRule):
|
||||
return f"Source: {item.input_path}" # Or some other identifier
|
||||
elif isinstance(item, AssetRule):
|
||||
return f"Asset: {item.asset_name}" # Or some other identifier
|
||||
elif isinstance(item, FileRule):
|
||||
return f"File: {item.file_path}" # Or some other identifier
|
||||
else:
|
||||
return None
|
||||
# Add other roles as needed (e.g., Qt.ItemDataRole.DecorationRole for icons)
|
||||
# elif role == Qt.ItemDataRole.DecorationRole:
|
||||
# if isinstance(item, SourceRule):
|
||||
# return QIcon("icons/source.png") # Placeholder icon
|
||||
# elif isinstance(item, AssetRule):
|
||||
# return QIcon("icons/asset.png") # Placeholder icon
|
||||
# elif isinstance(item, FileRule):
|
||||
# return QIcon("icons/file.png") # Placeholder icon
|
||||
# else:
|
||||
# return None
|
||||
|
||||
return None
|
||||
|
||||
def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()):
|
||||
"""Returns the model index for the given row, column, and parent index."""
|
||||
if not self.hasIndex(row, column, parent):
|
||||
return QModelIndex()
|
||||
|
||||
if not parent.isValid():
|
||||
# Requesting index for the root item (SourceRule)
|
||||
if self._root_rule and row == 0:
|
||||
return self.createIndex(row, column, self._root_rule)
|
||||
else:
|
||||
return QModelIndex()
|
||||
else:
|
||||
parent_item = parent.internalPointer()
|
||||
if isinstance(parent_item, SourceRule):
|
||||
# Children are AssetRules
|
||||
if 0 <= row < len(parent_item.assets):
|
||||
child_item = parent_item.assets[row]
|
||||
return self.createIndex(row, column, child_item)
|
||||
else:
|
||||
return QModelIndex()
|
||||
elif isinstance(parent_item, AssetRule):
|
||||
# Children are FileRules
|
||||
if 0 <= row < len(parent_item.files):
|
||||
child_item = parent_item.files[row]
|
||||
return self.createIndex(row, column, child_item)
|
||||
else:
|
||||
return QModelIndex()
|
||||
else:
|
||||
return QModelIndex() # Should not happen for FileRule parents
|
||||
|
||||
def parent(self, index: QModelIndex):
|
||||
"""Returns the parent index for the given index."""
|
||||
if not index.isValid():
|
||||
return QModelIndex()
|
||||
|
||||
child_item = index.internalPointer()
|
||||
|
||||
if isinstance(child_item, SourceRule):
|
||||
# SourceRule is the root, has no parent in the model hierarchy
|
||||
return QModelIndex()
|
||||
elif isinstance(child_item, AssetRule):
|
||||
# Find the SourceRule that contains this AssetRule
|
||||
if self._root_rule and child_item in self._root_rule.assets:
|
||||
# The row of the SourceRule is always 0 in this model
|
||||
return self.createIndex(0, 0, self._root_rule)
|
||||
else:
|
||||
return QModelIndex() # Should not happen if data is consistent
|
||||
elif isinstance(child_item, FileRule):
|
||||
# Find the AssetRule that contains this FileRule
|
||||
if self._root_rule:
|
||||
for asset_row, asset_rule in enumerate(self._root_rule.assets):
|
||||
if child_item in asset_rule.files:
|
||||
# The row of the parent AssetRule within the SourceRule's children
|
||||
return self.createIndex(asset_row, 0, asset_rule)
|
||||
return QModelIndex() # Should not happen if data is consistent
|
||||
else:
|
||||
return QModelIndex() # Unknown item type
|
||||
|
||||
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.ItemDataRole.DisplayRole):
|
||||
"""Returns the data for the header."""
|
||||
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
||||
if section == 0:
|
||||
return "Hierarchy"
|
||||
return None
|
||||
|
||||
def get_item_from_index(self, index: QModelIndex):
|
||||
"""Helper to get the underlying rule object from a model index."""
|
||||
if index.isValid():
|
||||
return index.internalPointer()
|
||||
return None
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Example Usage (for testing the model)
|
||||
from PySide6.QtWidgets import QApplication, QTreeView
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
# Define placeholder rule structures if not imported
|
||||
@dataclass
|
||||
class FileRule:
|
||||
name: str = "file"
|
||||
setting_f1: str = "value1"
|
||||
setting_f2: int = 10
|
||||
|
||||
@dataclass
|
||||
class AssetRule:
|
||||
name: str = "asset"
|
||||
files: list[FileRule] = field(default_factory=list)
|
||||
setting_a1: bool = True
|
||||
setting_a2: float = 3.14
|
||||
|
||||
@dataclass
|
||||
class SourceRule:
|
||||
name: str = "source"
|
||||
assets: list[AssetRule] = field(default_factory=list)
|
||||
setting_s1: str = "hello"
|
||||
|
||||
# Create a sample hierarchical structure
|
||||
file1 = FileRule(name="texture_diffuse.png")
|
||||
file2 = FileRule(name="texture_normal.png")
|
||||
file3 = FileRule(name="model.obj")
|
||||
|
||||
asset1 = AssetRule(name="Material_01", files=[file1, file2])
|
||||
asset2 = AssetRule(name="Model_01", files=[file3])
|
||||
|
||||
source_rule_instance = SourceRule(name="Input_Archive", assets=[asset1, asset2])
|
||||
|
||||
app = QApplication([])
|
||||
tree_view = QTreeView()
|
||||
model = RuleHierarchyModel(source_rule_instance)
|
||||
tree_view.setModel(model)
|
||||
tree_view.setWindowTitle("Rule Hierarchy Example")
|
||||
tree_view.show()
|
||||
app.exec()
|
||||
Reference in New Issue
Block a user