1777 lines
98 KiB
Python
1777 lines
98 KiB
Python
import sys
|
|
import os
|
|
import json
|
|
import logging
|
|
import time
|
|
from pathlib import Path
|
|
from functools import partial # For connecting signals with arguments
|
|
|
|
log = logging.getLogger(__name__)
|
|
log.info(f"sys.path: {sys.path}")
|
|
|
|
from PySide6.QtWidgets import (
|
|
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, 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
|
|
project_root = script_dir.parent
|
|
if str(project_root) not in sys.path:
|
|
sys.path.insert(0, str(project_root))
|
|
|
|
try:
|
|
from configuration import Configuration, ConfigurationError
|
|
from asset_processor import AssetProcessor, AssetProcessingError
|
|
from gui.processing_handler import ProcessingHandler
|
|
from gui.prediction_handler import PredictionHandler
|
|
import config as core_config # Import the config module
|
|
# PresetEditorDialog is no longer needed
|
|
except ImportError as e:
|
|
print(f"ERROR: Failed to import backend modules: {e}")
|
|
print(f"Ensure GUI is run from project root or backend modules are in PYTHONPATH.")
|
|
Configuration = None
|
|
AssetProcessor = None
|
|
ProcessingHandler = None
|
|
PredictionHandler = None
|
|
ConfigurationError = Exception
|
|
AssetProcessingError = Exception
|
|
|
|
# --- Constants ---
|
|
PRESETS_DIR = project_root / "presets"
|
|
TEMPLATE_PATH = PRESETS_DIR / "_template.json"
|
|
|
|
# Setup basic logging
|
|
log = logging.getLogger(__name__)
|
|
if not log.hasHandlers():
|
|
# Set level back to INFO for normal operation
|
|
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') # Reverted level and format
|
|
|
|
|
|
# --- Custom Log Handler ---
|
|
class QtLogHandler(logging.Handler, QObject):
|
|
"""
|
|
Custom logging handler that emits a Qt signal for each log record.
|
|
Inherits from QObject to support signals.
|
|
"""
|
|
log_record_received = Signal(str) # Signal emitting the formatted log string
|
|
|
|
def __init__(self, parent=None):
|
|
logging.Handler.__init__(self)
|
|
QObject.__init__(self, parent) # Initialize QObject part
|
|
|
|
def emit(self, record):
|
|
"""
|
|
Overrides the default emit method to format the record and emit a signal.
|
|
"""
|
|
try:
|
|
msg = self.format(record)
|
|
self.log_record_received.emit(msg)
|
|
except Exception:
|
|
self.handleError(record)
|
|
|
|
|
|
# --- Helper Functions (from PresetEditorDialog) ---
|
|
# NOTE: Consider moving these to a utils file if reused elsewhere
|
|
|
|
def setup_list_widget_with_controls(parent_layout, label_text, attribute_name, instance):
|
|
"""Adds a QListWidget with Add/Remove buttons to a layout."""
|
|
list_widget = QListWidget()
|
|
list_widget.setAlternatingRowColors(True)
|
|
# Make items editable by default in the editor context
|
|
list_widget.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed)
|
|
instance.__setattr__(attribute_name, list_widget) # Store list widget on the instance
|
|
|
|
add_button = QPushButton("+")
|
|
remove_button = QPushButton("-")
|
|
add_button.setFixedWidth(30)
|
|
remove_button.setFixedWidth(30)
|
|
|
|
button_layout = QVBoxLayout()
|
|
button_layout.addWidget(add_button)
|
|
button_layout.addWidget(remove_button)
|
|
button_layout.addStretch()
|
|
|
|
list_layout = QHBoxLayout()
|
|
list_layout.addWidget(list_widget)
|
|
list_layout.addLayout(button_layout)
|
|
|
|
group_box = QGroupBox(label_text)
|
|
group_box_layout = QVBoxLayout(group_box)
|
|
group_box_layout.addLayout(list_layout)
|
|
|
|
parent_layout.addWidget(group_box)
|
|
|
|
# Connections (use the instance's methods)
|
|
add_button.clicked.connect(partial(instance._editor_add_list_item, list_widget))
|
|
remove_button.clicked.connect(partial(instance._editor_remove_list_item, list_widget))
|
|
list_widget.itemChanged.connect(instance._mark_editor_unsaved) # Mark unsaved on item edit
|
|
|
|
def setup_table_widget_with_controls(parent_layout, label_text, attribute_name, columns, instance):
|
|
"""Adds a QTableWidget with Add/Remove buttons to a layout."""
|
|
table_widget = QTableWidget()
|
|
table_widget.setColumnCount(len(columns))
|
|
table_widget.setHorizontalHeaderLabels(columns)
|
|
table_widget.setAlternatingRowColors(True)
|
|
instance.__setattr__(attribute_name, table_widget) # Store table widget
|
|
|
|
add_button = QPushButton("+ Row")
|
|
remove_button = QPushButton("- Row")
|
|
|
|
button_layout = QHBoxLayout()
|
|
button_layout.addStretch()
|
|
button_layout.addWidget(add_button)
|
|
button_layout.addWidget(remove_button)
|
|
|
|
group_box = QGroupBox(label_text)
|
|
group_box_layout = QVBoxLayout(group_box)
|
|
group_box_layout.addWidget(table_widget)
|
|
group_box_layout.addLayout(button_layout)
|
|
|
|
parent_layout.addWidget(group_box)
|
|
|
|
# Connections (use the instance's methods)
|
|
add_button.clicked.connect(partial(instance._editor_add_table_row, table_widget))
|
|
remove_button.clicked.connect(partial(instance._editor_remove_table_row, table_widget))
|
|
table_widget.itemChanged.connect(instance._mark_editor_unsaved) # Mark unsaved on item edit
|
|
|
|
|
|
# --- Main Window Class ---
|
|
|
|
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__()
|
|
|
|
self.setWindowTitle("Asset Processor Tool")
|
|
self.resize(1200, 700) # Increased default size
|
|
|
|
# --- 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
|
|
self.editor_unsaved_changes = False
|
|
self._is_loading_editor = False # Flag to prevent signals during load
|
|
|
|
# --- Threading Setup ---
|
|
self.processing_thread = None
|
|
self.processing_handler = None
|
|
self.prediction_thread = None
|
|
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)
|
|
|
|
# --- Create Panels ---
|
|
self.editor_panel = QWidget()
|
|
self.main_panel = QWidget()
|
|
|
|
self.splitter.addWidget(self.editor_panel)
|
|
self.splitter.addWidget(self.main_panel)
|
|
|
|
# --- Setup UI Elements for each panel ---
|
|
self.setup_editor_panel_ui()
|
|
self.setup_main_panel_ui()
|
|
self.setup_menu_bar() # Setup menu bar
|
|
|
|
# --- Status Bar ---
|
|
self.statusBar().showMessage("Ready")
|
|
|
|
# --- Initial State ---
|
|
self._clear_editor() # Clear/disable editor fields initially
|
|
self._set_editor_enabled(False) # Disable editor initially
|
|
self.populate_presets() # Populate preset list
|
|
self.setup_logging_handler() # Setup the custom log handler
|
|
|
|
# --- Connect Editor Signals ---
|
|
self._connect_editor_change_signals()
|
|
|
|
# --- Adjust Splitter ---
|
|
self.splitter.setSizes([400, 800]) # Initial size ratio
|
|
|
|
# --- UI Setup Methods ---
|
|
|
|
def setup_editor_panel_ui(self):
|
|
"""Sets up the UI elements for the left preset editor panel."""
|
|
editor_layout = QVBoxLayout(self.editor_panel)
|
|
editor_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins
|
|
|
|
# --- Log Console Output (Initially Hidden) ---
|
|
self.log_console_widget = QWidget()
|
|
log_console_layout = QVBoxLayout(self.log_console_widget)
|
|
log_console_layout.setContentsMargins(0, 0, 0, 5) # Add some bottom margin
|
|
log_console_label = QLabel("Log Console:")
|
|
self.log_console_output = QTextEdit()
|
|
self.log_console_output.setReadOnly(True)
|
|
self.log_console_output.setMaximumHeight(150) # Limit initial height
|
|
self.log_console_output.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Maximum)
|
|
log_console_layout.addWidget(log_console_label)
|
|
log_console_layout.addWidget(self.log_console_output)
|
|
self.log_console_widget.setVisible(False) # Start hidden
|
|
editor_layout.addWidget(self.log_console_widget) # Add it at the top
|
|
|
|
# Preset List and Controls
|
|
list_layout = QVBoxLayout()
|
|
list_layout.addWidget(QLabel("Presets:"))
|
|
self.editor_preset_list = QListWidget()
|
|
self.editor_preset_list.currentItemChanged.connect(self._load_selected_preset_for_editing)
|
|
list_layout.addWidget(self.editor_preset_list)
|
|
|
|
list_button_layout = QHBoxLayout()
|
|
self.editor_new_button = QPushButton("New")
|
|
self.editor_delete_button = QPushButton("Delete")
|
|
self.editor_new_button.clicked.connect(self._new_preset)
|
|
self.editor_delete_button.clicked.connect(self._delete_selected_preset)
|
|
list_button_layout.addWidget(self.editor_new_button)
|
|
list_button_layout.addWidget(self.editor_delete_button)
|
|
list_layout.addLayout(list_button_layout)
|
|
editor_layout.addLayout(list_layout, 1) # Allow list to stretch
|
|
|
|
# Editor Tabs
|
|
self.editor_tab_widget = QTabWidget()
|
|
self.editor_tab_general_naming = QWidget()
|
|
self.editor_tab_mapping_rules = QWidget()
|
|
self.editor_tab_widget.addTab(self.editor_tab_general_naming, "General & Naming")
|
|
self.editor_tab_widget.addTab(self.editor_tab_mapping_rules, "Mapping & Rules")
|
|
self._create_editor_general_tab()
|
|
self._create_editor_mapping_tab()
|
|
editor_layout.addWidget(self.editor_tab_widget, 3) # Allow tabs to stretch more
|
|
|
|
# Save Buttons
|
|
save_button_layout = QHBoxLayout()
|
|
self.editor_save_button = QPushButton("Save")
|
|
self.editor_save_as_button = QPushButton("Save As...")
|
|
self.editor_save_button.setEnabled(False) # Disabled initially
|
|
self.editor_save_button.clicked.connect(self._save_current_preset)
|
|
self.editor_save_as_button.clicked.connect(self._save_preset_as)
|
|
save_button_layout.addStretch()
|
|
save_button_layout.addWidget(self.editor_save_button)
|
|
save_button_layout.addWidget(self.editor_save_as_button)
|
|
editor_layout.addLayout(save_button_layout)
|
|
|
|
def _create_editor_general_tab(self):
|
|
"""Creates the widgets and layout for the 'General & Naming' editor tab."""
|
|
layout = QVBoxLayout(self.editor_tab_general_naming)
|
|
form_layout = QFormLayout()
|
|
form_layout.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.ExpandingFieldsGrow)
|
|
|
|
# Basic Info
|
|
self.editor_preset_name = QLineEdit()
|
|
self.editor_supplier_name = QLineEdit()
|
|
self.editor_notes = QTextEdit()
|
|
self.editor_notes.setAcceptRichText(False)
|
|
self.editor_notes.setFixedHeight(60)
|
|
form_layout.addRow("Preset Name:", self.editor_preset_name)
|
|
form_layout.addRow("Supplier Name:", self.editor_supplier_name)
|
|
form_layout.addRow("Notes:", self.editor_notes)
|
|
layout.addLayout(form_layout)
|
|
|
|
# Source Naming Group
|
|
naming_group = QGroupBox("Source File Naming Rules")
|
|
naming_layout_outer = QVBoxLayout(naming_group)
|
|
naming_layout_form = QFormLayout()
|
|
self.editor_separator = QLineEdit()
|
|
self.editor_separator.setMaxLength(1)
|
|
self.editor_spin_base_name_idx = QSpinBox()
|
|
self.editor_spin_base_name_idx.setMinimum(-1)
|
|
self.editor_spin_map_type_idx = QSpinBox()
|
|
self.editor_spin_map_type_idx.setMinimum(-1)
|
|
naming_layout_form.addRow("Separator:", self.editor_separator)
|
|
naming_layout_form.addRow("Base Name Index:", self.editor_spin_base_name_idx)
|
|
naming_layout_form.addRow("Map Type Index:", self.editor_spin_map_type_idx)
|
|
naming_layout_outer.addLayout(naming_layout_form)
|
|
# Gloss Keywords List
|
|
setup_list_widget_with_controls(naming_layout_outer, "Glossiness Keywords", "editor_list_gloss_keywords", self)
|
|
# Bit Depth Variants Table
|
|
setup_table_widget_with_controls(naming_layout_outer, "16-bit Variant Patterns", "editor_table_bit_depth_variants", ["Map Type", "Pattern"], self)
|
|
self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
|
self.editor_table_bit_depth_variants.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
layout.addWidget(naming_group)
|
|
|
|
# Extra Files Group
|
|
setup_list_widget_with_controls(layout, "Move to 'Extra' Folder Patterns", "editor_list_extra_patterns", self)
|
|
|
|
layout.addStretch(1)
|
|
|
|
def _create_editor_mapping_tab(self):
|
|
"""Creates the widgets and layout for the 'Mapping & Rules' editor tab."""
|
|
layout = QVBoxLayout(self.editor_tab_mapping_rules)
|
|
|
|
# Map Type Mapping Group
|
|
setup_table_widget_with_controls(layout, "Map Type Mapping (Standard Type <- Input Keywords)", "editor_table_map_type_mapping", ["Standard Type", "Input Keywords (comma-sep)"], self)
|
|
self.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
|
self.editor_table_map_type_mapping.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
|
|
# Category Rules Group
|
|
category_group = QGroupBox("Asset Category Rules")
|
|
category_layout = QVBoxLayout(category_group)
|
|
setup_list_widget_with_controls(category_layout, "Model File Patterns", "editor_list_model_patterns", self)
|
|
setup_list_widget_with_controls(category_layout, "Decal Keywords", "editor_list_decal_keywords", self)
|
|
layout.addWidget(category_group)
|
|
|
|
# Archetype Rules Group
|
|
setup_table_widget_with_controls(layout, "Archetype Rules", "editor_table_archetype_rules", ["Archetype Name", "Match Any (comma-sep)", "Match All (comma-sep)"], self)
|
|
self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
|
self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
self.editor_table_archetype_rules.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
|
|
|
|
layout.addStretch(1)
|
|
|
|
def setup_main_panel_ui(self):
|
|
"""Sets up the UI elements for the right main processing panel."""
|
|
main_layout = QVBoxLayout(self.main_panel)
|
|
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()
|
|
# Make read-only for now, user must use Browse
|
|
# self.output_path_edit.setReadOnly(True)
|
|
self.browse_output_button = QPushButton("Browse...")
|
|
self.browse_output_button.clicked.connect(self._browse_for_output_directory)
|
|
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 ---
|
|
try:
|
|
output_base_dir_config = getattr(core_config, 'OUTPUT_BASE_DIR', '../Asset_Processor_Output') # Default if not found
|
|
# Resolve the path relative to the project root
|
|
default_output_dir = (project_root / output_base_dir_config).resolve()
|
|
self.output_path_edit.setText(str(default_output_dir))
|
|
log.info(f"Default output directory set to: {default_output_dir}")
|
|
except Exception as e:
|
|
log.error(f"Error setting default output directory: {e}")
|
|
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)
|
|
self.drag_drop_area.setFrameShadow(QFrame.Shadow.Sunken)
|
|
drag_drop_layout = QVBoxLayout(self.drag_drop_area)
|
|
drag_drop_label = QLabel("Drag and Drop Asset Files/Folders Here")
|
|
drag_drop_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
drag_drop_layout.addWidget(drag_drop_label)
|
|
self.drag_drop_area.setMinimumHeight(100)
|
|
self.setAcceptDrops(True) # Main window handles drops initially
|
|
main_layout.addWidget(self.drag_drop_area)
|
|
self.drag_drop_area.setVisible(False) # Hide the specific visual drag/drop area
|
|
|
|
# --- 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
|
|
|
|
# --- 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)
|
|
|
|
# --- 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
|
|
header = self.preview_table_view.horizontalHeader()
|
|
# Set resize modes for detailed columns
|
|
header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)
|
|
header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents) # Fit
|
|
header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents) # Fit
|
|
header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Fixed width (using ResizeToContents as closest)
|
|
header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Stretch (Fit-If-Possible)
|
|
|
|
# Hide the Predicted Output column
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True)
|
|
|
|
# Set selection behavior and alternating colors
|
|
self.preview_table_view.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
self.preview_table_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
self.preview_table_view.setAlternatingRowColors(False)
|
|
|
|
# Enable sorting via header clicks
|
|
self.preview_table_view.setSortingEnabled(True)
|
|
# Set default sort column (Status) - the proxy model's lessThan handles the custom order
|
|
self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)
|
|
|
|
# Move columns to the desired order: Status, Predicted Asset, Details, Original Path, Additional Files
|
|
# Initial logical order: [0, 1, 2, 3(hidden), 4, 5]
|
|
# Initial visual order: [0, 1, 2, 3, 4, 5] (assuming no initial moves)
|
|
# Desired visual order: [0, 1, 4, 2, 5, 3(hidden)]
|
|
|
|
# Move Predicted Asset (logical 1) to visual index 1 (already there)
|
|
|
|
# Move Details (logical 4) to visual index 2
|
|
header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), 2)
|
|
# Current visual: [0, 1, 4, 2, 3, 5]
|
|
|
|
# Move Original Path (logical 2) to visual index 3
|
|
header.moveSection(header.visualIndex(self.preview_model.COL_ORIGINAL_PATH), 3)
|
|
# Current visual: [0, 1, 4, 2, 3, 5] - Original Path is already at visual index 3 after moving Details
|
|
|
|
# Move Additional Files (logical 5) to visual index 4
|
|
header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), 4)
|
|
# Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.
|
|
|
|
# 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 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
|
|
|
|
# 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
|
|
|
|
# Apply style sheet to remove borders and rounded corners (already done, just referencing)
|
|
# self.preview_table_view.setStyleSheet("QTableView { border: none; }") # Already done
|
|
|
|
# --- 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()
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setTextVisible(True)
|
|
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)
|
|
self.browse_nodegroup_blend_button.clicked.connect(self._browse_for_nodegroup_blend)
|
|
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)
|
|
self.browse_materials_blend_button.clicked.connect(self._browse_for_materials_blend)
|
|
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
|
|
try:
|
|
default_ng_path = getattr(core_config, 'DEFAULT_NODEGROUP_BLEND_PATH', '')
|
|
default_mat_path = getattr(core_config, '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 Exception as e:
|
|
log.error(f"Error reading default Blender paths from config: {e}")
|
|
|
|
# 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)
|
|
self.blender_integration_checkbox.toggled.connect(self._toggle_blender_controls)
|
|
|
|
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.disable_preview_checkbox = QCheckBox("Disable Detailed Preview") # REMOVED - Moved to View Menu
|
|
# self.disable_preview_checkbox.setToolTip("If checked, shows only the list of input assets instead of detailed file predictions.")
|
|
# self.disable_preview_checkbox.setChecked(False) # Default is detailed preview enabled
|
|
# self.disable_preview_checkbox.toggled.connect(self.update_preview) # Update preview when toggled
|
|
# bottom_controls_layout.addWidget(self.disable_preview_checkbox)
|
|
|
|
# bottom_controls_layout.addSpacing(20) # Add some space # REMOVED - No longer needed after checkbox removal
|
|
|
|
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)
|
|
self.clear_queue_button = QPushButton("Clear Queue") # Added Clear button
|
|
self.start_button = QPushButton("Start Processing")
|
|
self.cancel_button = QPushButton("Cancel")
|
|
self.cancel_button.setEnabled(False)
|
|
self.clear_queue_button.clicked.connect(self.clear_queue) # Connect signal
|
|
self.start_button.clicked.connect(self.start_processing)
|
|
self.cancel_button.clicked.connect(self.cancel_processing)
|
|
bottom_controls_layout.addWidget(self.clear_queue_button) # Add button to layout
|
|
bottom_controls_layout.addWidget(self.start_button)
|
|
bottom_controls_layout.addWidget(self.cancel_button)
|
|
main_layout.addLayout(bottom_controls_layout)
|
|
|
|
# --- Preset Population and Handling ---
|
|
|
|
def populate_presets(self):
|
|
"""Scans presets dir and populates BOTH the editor list and processing combo."""
|
|
log.debug("Populating preset list...")
|
|
# Store current list selection
|
|
current_list_item = self.editor_preset_list.currentItem()
|
|
current_list_selection_text = current_list_item.text() if current_list_item else None
|
|
|
|
# Clear list
|
|
self.editor_preset_list.clear()
|
|
log.debug("Preset list cleared.")
|
|
|
|
# Add the "Select a Preset" placeholder item
|
|
placeholder_item = QListWidgetItem("--- Select a Preset ---")
|
|
# Make it non-selectable and non-editable
|
|
placeholder_item.setFlags(placeholder_item.flags() & ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEditable)
|
|
# Set unique data to identify the placeholder
|
|
placeholder_item.setData(Qt.ItemDataRole.UserRole, "__PLACEHOLDER__")
|
|
self.editor_preset_list.addItem(placeholder_item)
|
|
log.debug("Added '--- Select a Preset ---' placeholder item.")
|
|
|
|
if not PRESETS_DIR.is_dir():
|
|
msg = f"Error: Presets directory not found at {PRESETS_DIR}"
|
|
self.statusBar().showMessage(msg)
|
|
log.error(msg)
|
|
return
|
|
|
|
# Exclude template file starting with _
|
|
presets = sorted([f for f in PRESETS_DIR.glob("*.json") if f.is_file() and not f.name.startswith('_')])
|
|
preset_names = [p.stem for p in presets]
|
|
|
|
if not presets:
|
|
msg = "Warning: No presets found in presets directory."
|
|
self.statusBar().showMessage(msg)
|
|
log.warning(msg)
|
|
else:
|
|
# Populate List Widget (for editing)
|
|
for preset_path in presets:
|
|
item = QListWidgetItem(preset_path.stem)
|
|
item.setData(Qt.ItemDataRole.UserRole, preset_path) # Store full path
|
|
self.editor_preset_list.addItem(item)
|
|
|
|
self.statusBar().showMessage(f"Loaded {len(presets)} presets.")
|
|
|
|
# Try to restore list selection
|
|
# combo_index = self.preset_combo.findText(current_combo_selection) # REMOVED
|
|
# if combo_index != -1: # REMOVED
|
|
# self.preset_combo.setCurrentIndex(combo_index) # REMOVED
|
|
|
|
# Do NOT attempt to restore list selection by default
|
|
self.statusBar().showMessage(f"Loaded {len(presets)} presets.")
|
|
|
|
# Select the "Select a Preset" item by default
|
|
log.debug("Preset list populated. Selecting '--- Select a Preset ---' item.")
|
|
self.editor_preset_list.setCurrentItem(placeholder_item) # Select the placeholder item
|
|
|
|
# --- Drag and Drop Event Handlers (Unchanged) ---
|
|
def dragEnterEvent(self, event):
|
|
if event.mimeData().hasUrls(): event.acceptProposedAction()
|
|
else: event.ignore()
|
|
|
|
def dropEvent(self, event):
|
|
if event.mimeData().hasUrls():
|
|
event.acceptProposedAction()
|
|
urls = event.mimeData().urls()
|
|
paths = [url.toLocalFile() for url in urls]
|
|
self.add_input_paths(paths)
|
|
else: event.ignore()
|
|
|
|
def add_input_paths(self, paths):
|
|
if not hasattr(self, 'current_asset_paths'): self.current_asset_paths = set()
|
|
added_count = 0
|
|
newly_added_paths = []
|
|
for p_str in paths:
|
|
p = Path(p_str)
|
|
if p.exists():
|
|
supported_suffixes = ['.zip', '.rar', '.7z']
|
|
if p.is_dir() or (p.is_file() and p.suffix.lower() in supported_suffixes):
|
|
if p_str not in self.current_asset_paths:
|
|
self.current_asset_paths.add(p_str)
|
|
newly_added_paths.append(p_str)
|
|
added_count += 1
|
|
else: log.debug(f"Skipping duplicate asset path: {p_str}") # Changed print to log.debug
|
|
else: self.statusBar().showMessage(f"Invalid input (not dir or supported archive): {p.name}", 5000); log.warning(f"Invalid input: {p_str}") # Changed print to log.warning and updated message
|
|
else: self.statusBar().showMessage(f"Input path not found: {p.name}", 5000); print(f"Input path not found: {p_str}")
|
|
if added_count > 0:
|
|
log.info(f"Added {added_count} new asset paths: {newly_added_paths}")
|
|
self.statusBar().showMessage(f"Added {added_count} asset(s). Updating preview...", 3000)
|
|
|
|
# --- Auto-disable detailed preview if > 10 assets ---
|
|
preview_toggled = False
|
|
if hasattr(self, 'toggle_preview_action') and len(self.current_asset_paths) > 10:
|
|
if not self.toggle_preview_action.isChecked(): # Only check it if it's not already checked
|
|
log.info(f"Asset count ({len(self.current_asset_paths)}) > 10. Forcing simple preview.")
|
|
self.toggle_preview_action.setChecked(True) # This will trigger update_preview via its signal
|
|
preview_toggled = True
|
|
|
|
# Only call update_preview directly if the toggle wasn't triggered
|
|
# If in simple mode, we need to explicitly update the model with the simple list of paths
|
|
if hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked():
|
|
log.debug("Currently in simple preview mode. Updating model with simple paths.")
|
|
self.preview_model.set_data(list(self.current_asset_paths)) # Update model with simple list
|
|
self.statusBar().showMessage(f"Added {added_count} asset(s). Preview updated.", 3000)
|
|
# Only call update_preview if a preset is currently selected in the editor list
|
|
current_editor_item = self.editor_preset_list.currentItem()
|
|
if not preview_toggled and current_editor_item:
|
|
log.debug("Preset selected and not in simple mode. Triggering detailed preview update.")
|
|
self.update_preview()
|
|
elif not current_editor_item:
|
|
log.debug("No preset selected. Not triggering detailed preview update.")
|
|
self.statusBar().showMessage(f"Added {added_count} asset(s). Select a preset to update preview.", 3000)
|
|
|
|
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():
|
|
# Fallback to project root or home directory if current path is invalid
|
|
current_path = str(project_root)
|
|
|
|
directory = QFileDialog.getExistingDirectory(
|
|
self,
|
|
"Select Output Directory",
|
|
current_path,
|
|
QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks
|
|
)
|
|
if directory:
|
|
self.output_path_edit.setText(directory)
|
|
log.info(f"User selected output directory: {directory}")
|
|
|
|
|
|
# --- Processing Action Methods ---
|
|
def start_processing(self):
|
|
if self.processing_handler and self.processing_handler.is_running:
|
|
log.warning("Start clicked, but processing is already running.")
|
|
self.statusBar().showMessage("Processing is already in progress.", 3000)
|
|
return
|
|
if ProcessingHandler is None:
|
|
self.statusBar().showMessage("Error: Processing components not loaded.", 5000)
|
|
return
|
|
if not hasattr(self, 'current_asset_paths') or not self.current_asset_paths:
|
|
self.statusBar().showMessage("No assets added to process.", 3000)
|
|
return
|
|
input_paths = list(self.current_asset_paths)
|
|
if not input_paths:
|
|
self.statusBar().showMessage("No assets added to process.", 3000)
|
|
return
|
|
# --- Get preset from editor list ---
|
|
current_editor_item = self.editor_preset_list.currentItem()
|
|
|
|
# Check if the selected item is the placeholder
|
|
is_placeholder = current_editor_item and current_editor_item.data(Qt.ItemDataRole.UserRole) == "__PLACEHOLDER__"
|
|
|
|
if is_placeholder:
|
|
self.statusBar().showMessage("Please select a valid preset from the list on the left.", 3000)
|
|
log.warning("Start processing failed: Placeholder preset selected.")
|
|
return
|
|
|
|
# Existing logic to get selected_preset text and proceed
|
|
selected_preset = current_editor_item.text() if current_editor_item else None
|
|
overwrite = self.overwrite_checkbox.isChecked()
|
|
num_workers = self.workers_spinbox.value()
|
|
|
|
# --- Get Output Directory from UI and Validate ---
|
|
output_dir_str = self.output_path_edit.text().strip()
|
|
if not output_dir_str:
|
|
self.statusBar().showMessage("Error: Output directory cannot be empty.", 5000)
|
|
log.error("Start processing failed: Output directory field is empty.")
|
|
return
|
|
try:
|
|
output_dir = Path(output_dir_str)
|
|
# Attempt to create the directory if it doesn't exist
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
# Basic writability check (create and delete a temp file)
|
|
# Note: This isn't foolproof due to potential race conditions/permissions issues
|
|
# but provides a basic level of validation.
|
|
temp_file = output_dir / f".writable_check_{time.time()}"
|
|
temp_file.touch()
|
|
temp_file.unlink()
|
|
log.info(f"Using validated output directory: {output_dir_str}")
|
|
except OSError as e:
|
|
error_msg = f"Error creating/accessing output directory '{output_dir_str}': {e}"
|
|
self.statusBar().showMessage(error_msg, 5000)
|
|
log.error(error_msg)
|
|
return
|
|
except Exception as e: # Catch other potential Path errors or unexpected issues
|
|
error_msg = f"Invalid output directory path '{output_dir_str}': {e}"
|
|
self.statusBar().showMessage(error_msg, 5000)
|
|
log.error(error_msg)
|
|
return
|
|
# --- End Output Directory Validation ---
|
|
|
|
log.info(f"Preparing to start processing {len(input_paths)} items to '{output_dir_str}'.")
|
|
self.set_controls_enabled(False)
|
|
self.cancel_button.setEnabled(True)
|
|
self.start_button.setText("Processing...")
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setFormat("%p%")
|
|
self.setup_threads()
|
|
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(),
|
|
# Pass verbose setting
|
|
verbose=self.toggle_verbose_action.isChecked()
|
|
)
|
|
)
|
|
self.processing_thread.start()
|
|
log.info("Processing thread started.")
|
|
self.statusBar().showMessage(f"Processing {len(input_paths)} items...", 0)
|
|
else:
|
|
log.error("Failed to start processing: Thread or handler not initialized.")
|
|
self.statusBar().showMessage("Error: Failed to initialize processing thread.", 5000)
|
|
self.set_controls_enabled(True)
|
|
self.cancel_button.setEnabled(False)
|
|
self.start_button.setText("Start Processing")
|
|
|
|
def cancel_processing(self):
|
|
if self.processing_handler and self.processing_handler.is_running:
|
|
log.info("Cancel button clicked. Requesting cancellation.")
|
|
self.statusBar().showMessage("Requesting cancellation...", 3000)
|
|
self.processing_handler.request_cancel()
|
|
self.cancel_button.setEnabled(False)
|
|
self.start_button.setText("Cancelling...")
|
|
else:
|
|
log.warning("Cancel clicked, but no processing is running.")
|
|
self.statusBar().showMessage("Nothing to cancel.", 3000)
|
|
|
|
def clear_queue(self):
|
|
"""Clears the current asset queue and the preview table."""
|
|
if self.processing_handler and self.processing_handler.is_running:
|
|
self.statusBar().showMessage("Cannot clear queue while processing.", 3000)
|
|
return
|
|
|
|
if hasattr(self, 'current_asset_paths') and self.current_asset_paths:
|
|
log.info(f"Clearing asset queue ({len(self.current_asset_paths)} items).")
|
|
self.current_asset_paths.clear()
|
|
self.preview_model.clear_data() # Clear the model data
|
|
self.statusBar().showMessage("Asset queue cleared.", 3000)
|
|
else:
|
|
self.statusBar().showMessage("Asset queue is already empty.", 3000)
|
|
|
|
|
|
# --- Preview Update Method ---
|
|
def update_preview(self):
|
|
log.info(f"--> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}")
|
|
if hasattr(self, 'toggle_preview_action'):
|
|
log.info(f" Disable Preview Action checked: {self.toggle_preview_action.isChecked()}")
|
|
|
|
# --- Preview Update Method ---
|
|
def update_preview(self):
|
|
thread_id = QThread.currentThread() # Get current thread object
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] --> Entered update_preview. View Action exists: {hasattr(self, 'toggle_preview_action')}")
|
|
if hasattr(self, 'toggle_preview_action'):
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Disable Preview Action checked: {self.toggle_preview_action.isChecked()}")
|
|
|
|
# Determine mode based on menu action
|
|
simple_mode_enabled = hasattr(self, 'toggle_preview_action') and self.toggle_preview_action.isChecked()
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Determined simple_mode_enabled: {simple_mode_enabled}")
|
|
|
|
# --- Cancel Prediction if Running ---
|
|
if self.prediction_handler and self.prediction_handler.is_running:
|
|
log.warning(f"[{time.time():.4f}][T:{thread_id}] Prediction is running. Attempting to call prediction_handler.request_cancel()...")
|
|
try:
|
|
# --- THIS METHOD DOES NOT EXIST IN PredictionHandler ---
|
|
self.prediction_handler.request_cancel()
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Called prediction_handler.request_cancel() (Method might be missing!).")
|
|
except AttributeError as e:
|
|
log.error(f"[{time.time():.4f}][T:{thread_id}] AttributeError calling prediction_handler.request_cancel(): {e}. Prediction cannot be cancelled.")
|
|
except Exception as e:
|
|
log.exception(f"[{time.time():.4f}][T:{thread_id}] Unexpected error calling prediction_handler.request_cancel(): {e}")
|
|
# Note: Cancellation is not immediate even if it existed. The thread would stop when it next checks the flag.
|
|
# We proceed with updating the UI immediately.
|
|
|
|
# Set the model's mode
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Calling preview_model.set_simple_mode({simple_mode_enabled})...")
|
|
self.preview_model.set_simple_mode(simple_mode_enabled)
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Returned from preview_model.set_simple_mode({simple_mode_enabled}).")
|
|
|
|
# Configure the QTableView based on the mode
|
|
header = self.preview_table_view.horizontalHeader()
|
|
if simple_mode_enabled:
|
|
log.info(" Configuring QTableView for SIMPLE mode.")
|
|
# Hide detailed columns, show simple column
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_STATUS, True)
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_ASSET, True)
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_ORIGINAL_PATH, True)
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True) # Already hidden, but good practice
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_DETAILS, True)
|
|
# Ensure the simple path column exists and is visible
|
|
if self.preview_model.columnCount() > self.preview_model.COL_SIMPLE_PATH:
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, False) # Show the simple path column
|
|
# Set resize mode for the single visible column
|
|
header.setSectionResizeMode(self.preview_model.COL_SIMPLE_PATH, QHeaderView.ResizeMode.Stretch)
|
|
else:
|
|
log.error(" Simple path column index out of bounds for model.")
|
|
|
|
|
|
# Disable sorting in simple mode (optional, but makes sense)
|
|
self.preview_table_view.setSortingEnabled(False)
|
|
|
|
# Update status bar
|
|
if hasattr(self, 'current_asset_paths') and self.current_asset_paths:
|
|
self.statusBar().showMessage(f"Preview disabled. Showing {len(self.current_asset_paths)} input assets.", 3000)
|
|
else:
|
|
self.statusBar().showMessage("Preview disabled. No assets added.", 3000)
|
|
|
|
# In simple mode, the model's data is derived from current_asset_paths.
|
|
# We need to ensure the model's simple data is up-to-date.
|
|
# The simplest way is to re-set the data, which will re-extract simple data.
|
|
# This might be slightly inefficient if only the mode changed, but safe.
|
|
# A more optimized approach would be to have a separate method in the model
|
|
# to just update the simple data from a list of paths.
|
|
# For now, let's re-set the data.
|
|
# --- REMOVED REDUNDANT set_data CALL ---
|
|
# The set_simple_mode(True) call above should be sufficient as the model
|
|
# already holds the simple data internally. This extra reset seems to cause instability.
|
|
# log.debug(" Simple mode enabled. Re-setting model data to trigger simple data update.")
|
|
# self.preview_model.set_data(list(self.current_asset_paths)) # Pass the list of paths
|
|
# --- END REMOVAL ---
|
|
|
|
# Stop here, do not run PredictionHandler in simple mode
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] <-- Exiting update_preview (Simple Mode).")
|
|
return
|
|
else:
|
|
# --- Proceed with Detailed Preview ---
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Configuring QTableView for DETAILED mode.")
|
|
# Show detailed columns, hide simple column
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_STATUS, False)
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_ASSET, False)
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_ORIGINAL_PATH, False)
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_PREDICTED_OUTPUT, True) # Keep this hidden
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_DETAILS, False)
|
|
# Ensure the simple path column exists and is hidden
|
|
if self.preview_model.columnCount() > self.preview_model.COL_SIMPLE_PATH:
|
|
self.preview_table_view.setColumnHidden(self.preview_model.COL_SIMPLE_PATH, True) # Hide the simple path column
|
|
else:
|
|
log.warning(" Simple path column index out of bounds for model when hiding.")
|
|
|
|
|
|
# Set resize modes for detailed columns
|
|
header.setSectionResizeMode(self.preview_model.COL_STATUS, QHeaderView.ResizeMode.ResizeToContents)
|
|
header.setSectionResizeMode(self.preview_model.COL_PREDICTED_ASSET, QHeaderView.ResizeMode.ResizeToContents) # Fit
|
|
header.setSectionResizeMode(self.preview_model.COL_DETAILS, QHeaderView.ResizeMode.ResizeToContents) # Fit
|
|
header.setSectionResizeMode(self.preview_model.COL_ORIGINAL_PATH, QHeaderView.ResizeMode.ResizeToContents) # Fixed width (using ResizeToContents as closest)
|
|
header.setSectionResizeMode(self.preview_model.COL_ADDITIONAL_FILES, QHeaderView.ResizeMode.Stretch) # Stretch (Fit-If-Possible)
|
|
|
|
# Move columns to the desired order: Status, Predicted Asset, Details, Original Path, Additional Files
|
|
# Initial logical order: [0, 1, 2, 3(hidden), 4, 5]
|
|
# Initial visual order: [0, 1, 2, 3, 4, 5] (assuming no initial moves)
|
|
# Desired visual order: [0, 1, 4, 2, 5, 3(hidden)]
|
|
|
|
# Move Predicted Asset (logical 1) to visual index 1 (already there)
|
|
|
|
# Move Details (logical 4) to visual index 2
|
|
header.moveSection(header.visualIndex(self.preview_model.COL_DETAILS), 2)
|
|
# Current visual: [0, 1, 4, 2, 3, 5]
|
|
|
|
# Move Original Path (logical 2) to visual index 3
|
|
header.moveSection(header.visualIndex(self.preview_model.COL_ORIGINAL_PATH), 3)
|
|
# Current visual: [0, 1, 4, 2, 3, 5] - Original Path is already at visual index 3 after moving Details
|
|
|
|
# Move Additional Files (logical 5) to visual index 4
|
|
header.moveSection(header.visualIndex(self.preview_model.COL_ADDITIONAL_FILES), 4)
|
|
# Current visual: [0, 1, 4, 2, 5, 3] - This looks correct.
|
|
|
|
# Re-enable sorting for detailed mode
|
|
self.preview_table_view.setSortingEnabled(True)
|
|
# Reset sort order if needed (optional, proxy model handles default)
|
|
# self.preview_table_view.sortByColumn(self.preview_model.COL_STATUS, Qt.SortOrder.AscendingOrder)
|
|
|
|
# --- Trigger Prediction Handler ---
|
|
if self.prediction_handler and self.prediction_handler.is_running:
|
|
log.warning(f"[{time.time():.4f}] Preview update requested, but already running.")
|
|
return
|
|
if PredictionHandler is None:
|
|
self.statusBar().showMessage("Error: Prediction components not loaded.", 5000)
|
|
return
|
|
# Get preset from editor list
|
|
current_editor_item = self.editor_preset_list.currentItem()
|
|
|
|
# Check if the selected item is the placeholder
|
|
is_placeholder = current_editor_item and current_editor_item.data(Qt.ItemDataRole.UserRole) == "__PLACEHOLDER__"
|
|
|
|
if is_placeholder:
|
|
log.debug("Update preview called with placeholder preset selected. Clearing preview.")
|
|
self.preview_model.clear_data() # Clear model if placeholder selected
|
|
self.statusBar().showMessage("Select a preset from the list on the left to update preview.", 3000)
|
|
# Ensure placeholder is visible and table is hidden
|
|
if hasattr(self, 'preview_placeholder_label') and hasattr(self, 'preview_table_view'):
|
|
self.preview_placeholder_label.setVisible(True)
|
|
self.preview_table_view.setVisible(False)
|
|
return # Stop prediction as no valid preset is selected
|
|
|
|
# Existing logic to get selected_preset text and proceed
|
|
selected_preset = current_editor_item.text() if current_editor_item else None
|
|
if not selected_preset:
|
|
log.debug("Update preview called with no preset selected in the editor list.")
|
|
self.preview_model.clear_data() # Clear model if no preset selected
|
|
self.statusBar().showMessage("Select a preset from the list on the left to update preview.", 3000)
|
|
return
|
|
if not hasattr(self, 'current_asset_paths') or not self.current_asset_paths:
|
|
log.debug("Update preview called with no assets tracked.")
|
|
self.preview_model.clear_data() # Clear model if no assets
|
|
return
|
|
input_paths = list(self.current_asset_paths)
|
|
if not input_paths:
|
|
log.debug("Update preview called but no input paths derived.")
|
|
self.preview_model.clear_data() # Clear model if no paths
|
|
return
|
|
|
|
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
|
|
if self.prediction_thread and self.prediction_handler:
|
|
# 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)
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] <-- Exiting update_preview (Detailed Mode).")
|
|
|
|
|
|
# --- Threading and Processing Control ---
|
|
def setup_threads(self):
|
|
# Setup Processing Thread
|
|
if ProcessingHandler and self.processing_thread is None:
|
|
self.processing_thread = QThread(self)
|
|
self.processing_handler = ProcessingHandler()
|
|
self.processing_handler.moveToThread(self.processing_thread)
|
|
self.processing_handler.progress_updated.connect(self.update_progress_bar)
|
|
self.processing_handler.file_status_updated.connect(self.update_file_status)
|
|
self.processing_handler.processing_finished.connect(self.on_processing_finished)
|
|
self.processing_handler.status_message.connect(self.show_status_message)
|
|
self.processing_handler.processing_finished.connect(self.processing_thread.quit)
|
|
self.processing_handler.processing_finished.connect(self.processing_handler.deleteLater)
|
|
self.processing_thread.finished.connect(self.processing_thread.deleteLater)
|
|
self.processing_thread.finished.connect(self._reset_processing_thread_references)
|
|
log.debug("Processing thread and handler set up.")
|
|
elif not ProcessingHandler:
|
|
log.error("ProcessingHandler not available. Cannot set up processing thread.")
|
|
if hasattr(self, 'start_button'):
|
|
self.start_button.setEnabled(False)
|
|
self.start_button.setToolTip("Error: Backend processing components failed to load.")
|
|
|
|
# Setup Prediction Thread
|
|
if PredictionHandler and self.prediction_thread is None:
|
|
self.prediction_thread = QThread(self)
|
|
self.prediction_handler = PredictionHandler()
|
|
self.prediction_handler.moveToThread(self.prediction_thread)
|
|
# 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)
|
|
self.prediction_handler.prediction_finished.connect(self.prediction_handler.deleteLater)
|
|
self.prediction_thread.finished.connect(self.prediction_thread.deleteLater)
|
|
self.prediction_thread.finished.connect(self._reset_prediction_thread_references)
|
|
log.debug("Prediction thread and handler set up.")
|
|
elif not PredictionHandler:
|
|
log.error("PredictionHandler not available. Cannot set up prediction thread.")
|
|
|
|
@Slot()
|
|
def _reset_processing_thread_references(self):
|
|
log.debug("Resetting processing thread and handler references.")
|
|
self.processing_thread = None
|
|
self.processing_handler = None
|
|
|
|
@Slot()
|
|
def _reset_prediction_thread_references(self):
|
|
log.debug("Resetting prediction thread and handler references.")
|
|
self.prediction_thread = None
|
|
self.prediction_handler = None
|
|
|
|
@Slot(int, int)
|
|
def update_progress_bar(self, current_count, total_count):
|
|
if total_count > 0:
|
|
percentage = int((current_count / total_count) * 100)
|
|
self.progress_bar.setValue(percentage)
|
|
self.progress_bar.setFormat(f"%p% ({current_count}/{total_count})")
|
|
else:
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setFormat("0/0")
|
|
|
|
# Slot for prediction results (Updated for new format and coloring)
|
|
@Slot(list)
|
|
def on_prediction_results_ready(self, results: list):
|
|
"""Populates the preview table model with detailed prediction results."""
|
|
thread_id = QThread.currentThread() # Get current thread object
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] --> Entered on_prediction_results_ready. Received {len(results)} file details.")
|
|
# Update the model with the new data
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Calling preview_model.set_data()...")
|
|
self.preview_model.set_data(results)
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] Returned from preview_model.set_data().")
|
|
log.info(f"[{time.time():.4f}][T:{thread_id}] <-- Exiting on_prediction_results_ready.")
|
|
|
|
@Slot()
|
|
def on_prediction_finished(self):
|
|
log.info(f"[{time.time():.4f}] --> Prediction finished signal received.")
|
|
# Optionally update status bar or re-enable controls if needed after prediction finishes
|
|
# (Controls are primarily managed by processing_finished, but prediction is a separate background task)
|
|
self.statusBar().showMessage("Preview updated.", 3000)
|
|
|
|
@Slot(str, str, str)
|
|
def update_file_status(self, input_path_str, status, message):
|
|
# TODO: Update status bar or potentially find rows in table later
|
|
status_text = f"Asset '{Path(input_path_str).name}': {status.upper()}"
|
|
if status == "failed" and message: status_text += f" - Error: {message}"
|
|
self.statusBar().showMessage(status_text, 5000)
|
|
log.debug(f"Received file status update: {input_path_str} - {status}")
|
|
|
|
@Slot(int, int, int)
|
|
def on_processing_finished(self, processed_count, skipped_count, failed_count):
|
|
log.info(f"GUI received processing_finished signal: P={processed_count}, S={skipped_count}, F={failed_count}")
|
|
self.set_controls_enabled(True)
|
|
self.cancel_button.setEnabled(False)
|
|
self.start_button.setText("Start Processing")
|
|
|
|
@Slot(str, int)
|
|
def show_status_message(self, message, timeout_ms):
|
|
if timeout_ms > 0: self.statusBar().showMessage(message, timeout_ms)
|
|
else: self.statusBar().showMessage(message)
|
|
|
|
def set_controls_enabled(self, enabled: bool):
|
|
"""Enables/disables input controls during processing."""
|
|
# Main panel controls
|
|
self.start_button.setEnabled(enabled)
|
|
self.setAcceptDrops(enabled)
|
|
self.drag_drop_area.setEnabled(enabled)
|
|
# self.preview_table.setEnabled(enabled) # This was the old QTableWidget
|
|
self.preview_table_view.setEnabled(enabled) # Enable/disable the QTableView instead
|
|
# Editor panel controls (should generally be enabled unless processing)
|
|
self.editor_panel.setEnabled(enabled) # Enable/disable the whole panel
|
|
# Blender controls
|
|
self.blender_integration_checkbox.setEnabled(enabled)
|
|
# Only enable path inputs if checkbox is checked AND main controls are enabled
|
|
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)
|
|
|
|
|
|
@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)
|
|
|
|
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(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)
|
|
log.info(f"User selected blend file: {file_path}")
|
|
|
|
def _browse_for_nodegroup_blend(self):
|
|
self._browse_for_blend_file(self.nodegroup_blend_path_input)
|
|
|
|
def _browse_for_materials_blend(self):
|
|
self._browse_for_blend_file(self.materials_blend_path_input)
|
|
|
|
# --- Preset Editor Methods (Adapted from PresetEditorDialog) ---
|
|
|
|
def _editor_add_list_item(self, list_widget: QListWidget):
|
|
"""Adds an editable item to the specified list widget in the editor."""
|
|
text, ok = QInputDialog.getText(self, f"Add Item", "Enter value:")
|
|
if ok and text:
|
|
item = QListWidgetItem(text)
|
|
# item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEditable) # Already editable by default
|
|
list_widget.addItem(item)
|
|
self._mark_editor_unsaved()
|
|
|
|
def _editor_remove_list_item(self, list_widget: QListWidget):
|
|
"""Removes the selected item from the specified list widget in the editor."""
|
|
selected_items = list_widget.selectedItems()
|
|
if not selected_items: return
|
|
for item in selected_items: list_widget.takeItem(list_widget.row(item))
|
|
self._mark_editor_unsaved()
|
|
|
|
def _editor_add_table_row(self, table_widget: QTableWidget):
|
|
"""Adds an empty row to the specified table widget in the editor."""
|
|
row_count = table_widget.rowCount()
|
|
table_widget.insertRow(row_count)
|
|
for col in range(table_widget.columnCount()): table_widget.setItem(row_count, col, QTableWidgetItem(""))
|
|
self._mark_editor_unsaved()
|
|
|
|
def _editor_remove_table_row(self, table_widget: QTableWidget):
|
|
"""Removes the selected row(s) from the specified table widget in the editor."""
|
|
selected_rows = sorted(list(set(index.row() for index in table_widget.selectedIndexes())), reverse=True)
|
|
if not selected_rows:
|
|
if table_widget.rowCount() > 0: selected_rows = [table_widget.rowCount() - 1]
|
|
else: return
|
|
for row in selected_rows: table_widget.removeRow(row)
|
|
self._mark_editor_unsaved()
|
|
|
|
def _mark_editor_unsaved(self):
|
|
"""Marks changes in the editor panel as unsaved."""
|
|
if self._is_loading_editor: return
|
|
self.editor_unsaved_changes = True
|
|
self.editor_save_button.setEnabled(True)
|
|
preset_name = Path(self.current_editing_preset_path).name if self.current_editing_preset_path else 'New Preset'
|
|
self.setWindowTitle(f"Asset Processor Tool - {preset_name}*")
|
|
|
|
def _connect_editor_change_signals(self):
|
|
"""Connect signals from all editor widgets to mark_editor_unsaved."""
|
|
self.editor_preset_name.textChanged.connect(self._mark_editor_unsaved)
|
|
self.editor_supplier_name.textChanged.connect(self._mark_editor_unsaved)
|
|
self.editor_notes.textChanged.connect(self._mark_editor_unsaved)
|
|
self.editor_separator.textChanged.connect(self._mark_editor_unsaved)
|
|
self.editor_spin_base_name_idx.valueChanged.connect(self._mark_editor_unsaved)
|
|
self.editor_spin_map_type_idx.valueChanged.connect(self._mark_editor_unsaved)
|
|
# List/Table widgets are connected via helper functions
|
|
|
|
def _check_editor_unsaved_changes(self) -> bool:
|
|
"""Checks for unsaved changes in the editor and prompts the user. Returns True if should cancel action."""
|
|
if not self.editor_unsaved_changes: return False
|
|
reply = QMessageBox.question(self, "Unsaved Preset Changes",
|
|
"You have unsaved changes in the preset editor. Discard them?",
|
|
QMessageBox.StandardButton.Save | QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Cancel,
|
|
QMessageBox.StandardButton.Cancel)
|
|
if reply == QMessageBox.StandardButton.Save: return not self._save_current_preset() # Return True (cancel) if save fails
|
|
elif reply == QMessageBox.StandardButton.Discard: return False # Discarded, proceed
|
|
else: return True # Cancel action
|
|
|
|
def _set_editor_enabled(self, enabled: bool):
|
|
"""Enables or disables all editor widgets."""
|
|
self.editor_tab_widget.setEnabled(enabled)
|
|
# Also enable/disable save buttons based on editor state, not just processing state
|
|
self.editor_save_button.setEnabled(enabled and self.editor_unsaved_changes)
|
|
self.editor_save_as_button.setEnabled(enabled) # Save As is always possible if editor is enabled
|
|
|
|
def _clear_editor(self):
|
|
"""Clears the editor fields and resets state."""
|
|
self._is_loading_editor = True
|
|
self.editor_preset_name.clear()
|
|
self.editor_supplier_name.clear()
|
|
self.editor_notes.clear()
|
|
self.editor_separator.clear()
|
|
self.editor_spin_base_name_idx.setValue(0)
|
|
self.editor_spin_map_type_idx.setValue(1)
|
|
self.editor_list_gloss_keywords.clear()
|
|
self.editor_table_bit_depth_variants.setRowCount(0)
|
|
self.editor_list_extra_patterns.clear()
|
|
self.editor_table_map_type_mapping.setRowCount(0)
|
|
self.editor_list_model_patterns.clear()
|
|
self.editor_list_decal_keywords.clear()
|
|
self.editor_table_archetype_rules.setRowCount(0)
|
|
self.current_editing_preset_path = None
|
|
self.editor_unsaved_changes = False
|
|
self.editor_save_button.setEnabled(False)
|
|
self.setWindowTitle("Asset Processor Tool") # Reset window title
|
|
self._set_editor_enabled(False)
|
|
|
|
# Ensure placeholder is visible and table is hidden when editor is cleared
|
|
if hasattr(self, 'preview_placeholder_label') and hasattr(self, 'preview_table_view'):
|
|
log.debug("Clearing editor. Showing placeholder, hiding table view.")
|
|
self.preview_placeholder_label.setVisible(True)
|
|
self.preview_table_view.setVisible(False)
|
|
|
|
self._is_loading_editor = False
|
|
|
|
def _populate_editor_from_data(self, preset_data: dict):
|
|
"""Helper method to populate editor UI widgets from a preset data dictionary."""
|
|
self._is_loading_editor = True
|
|
try:
|
|
self.editor_preset_name.setText(preset_data.get("preset_name", ""))
|
|
self.editor_supplier_name.setText(preset_data.get("supplier_name", ""))
|
|
self.editor_notes.setText(preset_data.get("notes", ""))
|
|
naming_data = preset_data.get("source_naming", {})
|
|
self.editor_separator.setText(naming_data.get("separator", "_"))
|
|
indices = naming_data.get("part_indices", {})
|
|
self.editor_spin_base_name_idx.setValue(indices.get("base_name", 0))
|
|
self.editor_spin_map_type_idx.setValue(indices.get("map_type", 1))
|
|
self.editor_list_gloss_keywords.clear()
|
|
self.editor_list_gloss_keywords.addItems(naming_data.get("glossiness_keywords", []))
|
|
self.editor_table_bit_depth_variants.setRowCount(0)
|
|
bit_depth_vars = naming_data.get("bit_depth_variants", {})
|
|
for i, (map_type, pattern) in enumerate(bit_depth_vars.items()):
|
|
self.editor_table_bit_depth_variants.insertRow(i)
|
|
self.editor_table_bit_depth_variants.setItem(i, 0, QTableWidgetItem(map_type))
|
|
self.editor_table_bit_depth_variants.setItem(i, 1, QTableWidgetItem(pattern))
|
|
self.editor_list_extra_patterns.clear()
|
|
self.editor_list_extra_patterns.addItems(preset_data.get("move_to_extra_patterns", []))
|
|
self.editor_table_map_type_mapping.setRowCount(0)
|
|
map_mappings = preset_data.get("map_type_mapping", [])
|
|
# --- UPDATED for new dictionary format ---
|
|
for i, mapping_dict in enumerate(map_mappings):
|
|
if isinstance(mapping_dict, dict) and "target_type" in mapping_dict and "keywords" in mapping_dict:
|
|
std_type = mapping_dict["target_type"]
|
|
keywords = mapping_dict["keywords"]
|
|
self.editor_table_map_type_mapping.insertRow(i)
|
|
self.editor_table_map_type_mapping.setItem(i, 0, QTableWidgetItem(std_type))
|
|
# Ensure keywords are strings before joining
|
|
keywords_str = [str(k) for k in keywords if isinstance(k, str)]
|
|
self.editor_table_map_type_mapping.setItem(i, 1, QTableWidgetItem(", ".join(keywords_str)))
|
|
else:
|
|
log.warning(f"Skipping invalid map_type_mapping item during editor population: {mapping_dict}")
|
|
# --- END UPDATE ---
|
|
category_rules = preset_data.get("asset_category_rules", {})
|
|
self.editor_list_model_patterns.clear()
|
|
self.editor_list_model_patterns.addItems(category_rules.get("model_patterns", []))
|
|
self.editor_list_decal_keywords.clear()
|
|
self.editor_list_decal_keywords.addItems(category_rules.get("decal_keywords", []))
|
|
self.editor_table_archetype_rules.setRowCount(0)
|
|
arch_rules = preset_data.get("archetype_rules", [])
|
|
for i, rule in enumerate(arch_rules):
|
|
if isinstance(rule, (list, tuple)) and len(rule) == 2:
|
|
arch_name, conditions = rule
|
|
match_any = ", ".join(conditions.get("match_any", []))
|
|
match_all = ", ".join(conditions.get("match_all", []))
|
|
self.editor_table_archetype_rules.insertRow(i)
|
|
self.editor_table_archetype_rules.setItem(i, 0, QTableWidgetItem(arch_name))
|
|
self.editor_table_archetype_rules.setItem(i, 1, QTableWidgetItem(match_any))
|
|
self.editor_table_archetype_rules.setItem(i, 2, QTableWidgetItem(match_all))
|
|
finally:
|
|
self._is_loading_editor = False
|
|
|
|
def _load_preset_for_editing(self, file_path: Path):
|
|
"""Loads the content of the selected preset file into the editor widgets."""
|
|
if not file_path or not file_path.is_file():
|
|
self._clear_editor()
|
|
return
|
|
log.info(f"Loading preset into editor: {file_path.name}")
|
|
log.info(f"Loading preset into editor: {file_path.name}")
|
|
try:
|
|
with open(file_path, 'r', encoding='utf-8') as f: preset_data = json.load(f)
|
|
self._populate_editor_from_data(preset_data)
|
|
self._set_editor_enabled(True)
|
|
self.current_editing_preset_path = file_path
|
|
self.editor_unsaved_changes = False
|
|
self.editor_save_button.setEnabled(False)
|
|
self.setWindowTitle(f"Asset Processor Tool - {file_path.name}")
|
|
log.info(f"Preset '{file_path.name}' loaded into editor.")
|
|
log.debug("Preset loaded. Checking visibility states.")
|
|
log.debug(f"preview_placeholder_label visible: {self.preview_placeholder_label.isVisible()}")
|
|
log.debug(f"preview_table_view visible: {self.preview_table_view.isVisible()}")
|
|
except json.JSONDecodeError as json_err:
|
|
log.error(f"Invalid JSON in {file_path.name}: {json_err}")
|
|
QMessageBox.warning(self, "Load Error", f"Failed to load preset '{file_path.name}'.\nInvalid JSON structure:\n{json_err}")
|
|
self._clear_editor()
|
|
except Exception as e:
|
|
log.exception(f"Error loading preset file {file_path}: {e}")
|
|
QMessageBox.critical(self, "Error", f"Could not load preset file:\n{file_path}\n\nError: {e}")
|
|
self._clear_editor()
|
|
|
|
def _load_selected_preset_for_editing(self, current_item: QListWidgetItem, previous_item: QListWidgetItem):
|
|
"""Loads the preset currently selected in the editor list."""
|
|
log.debug(f"currentItemChanged signal triggered. current_item: {current_item.text() if current_item else 'None'}, previous_item: {previous_item.text() if previous_item else 'None'}")
|
|
|
|
# Check if the selected item is the placeholder
|
|
is_placeholder = current_item and current_item.data(Qt.ItemDataRole.UserRole) == "__PLACEHOLDER__"
|
|
|
|
if self._check_editor_unsaved_changes():
|
|
# If user cancels, revert selection
|
|
if previous_item:
|
|
log.debug("Unsaved changes check cancelled. Reverting selection.")
|
|
self.editor_preset_list.blockSignals(True)
|
|
self.editor_preset_list.setCurrentItem(previous_item)
|
|
self.editor_preset_list.blockSignals(False)
|
|
return
|
|
|
|
if is_placeholder:
|
|
log.debug("Placeholder item selected. Clearing editor and preview.")
|
|
self._clear_editor() # This also hides the table and shows the placeholder label
|
|
self.preview_model.clear_data() # Ensure the model is empty
|
|
# Visibility is handled by _clear_editor, but explicitly set here for clarity
|
|
self.preview_placeholder_label.setVisible(True)
|
|
self.preview_table_view.setVisible(False)
|
|
self.start_button.setEnabled(False) # Disable start button
|
|
return # Stop processing as no real preset is selected
|
|
|
|
# Existing logic for handling real preset items starts here
|
|
if current_item:
|
|
log.debug(f"Loading preset for editing: {current_item.text()}")
|
|
preset_path = current_item.data(Qt.ItemDataRole.UserRole)
|
|
self._load_preset_for_editing(preset_path)
|
|
self.start_button.setEnabled(True) # Enable start button
|
|
# --- Trigger preview update after loading editor ---
|
|
self.update_preview()
|
|
# --- End Trigger ---
|
|
|
|
# Hide placeholder and show table view
|
|
log.debug("Real preset selected. Hiding placeholder, showing table view.")
|
|
self.preview_placeholder_label.setVisible(False)
|
|
self.preview_table_view.setVisible(True)
|
|
else:
|
|
# This case should ideally not be reached if the placeholder is always present
|
|
log.debug("No preset selected (unexpected state if placeholder is present). Clearing editor.")
|
|
self._clear_editor() # Clear editor if selection is cleared
|
|
# Ensure placeholder is visible if no preset is selected
|
|
log.debug("No preset selected. Showing placeholder, hiding table view.")
|
|
self.preview_placeholder_label.setVisible(True)
|
|
self.preview_table_view.setVisible(False)
|
|
|
|
def _gather_editor_data(self) -> dict:
|
|
"""Gathers data from all editor UI widgets and returns a dictionary."""
|
|
preset_data = {}
|
|
preset_data["preset_name"] = self.editor_preset_name.text().strip()
|
|
preset_data["supplier_name"] = self.editor_supplier_name.text().strip()
|
|
preset_data["notes"] = self.editor_notes.toPlainText().strip()
|
|
naming_data = {}
|
|
naming_data["separator"] = self.editor_separator.text()
|
|
naming_data["part_indices"] = { "base_name": self.editor_spin_base_name_idx.value(), "map_type": self.editor_spin_map_type_idx.value() }
|
|
naming_data["glossiness_keywords"] = [self.editor_list_gloss_keywords.item(i).text() for i in range(self.editor_list_gloss_keywords.count())]
|
|
naming_data["bit_depth_variants"] = {self.editor_table_bit_depth_variants.item(r, 0).text(): self.editor_table_bit_depth_variants.item(r, 1).text()
|
|
for r in range(self.editor_table_bit_depth_variants.rowCount()) if self.editor_table_bit_depth_variants.item(r, 0) and self.editor_table_bit_depth_variants.item(r, 1)}
|
|
preset_data["source_naming"] = naming_data
|
|
preset_data["move_to_extra_patterns"] = [self.editor_list_extra_patterns.item(i).text() for i in range(self.editor_list_extra_patterns.count())]
|
|
# --- UPDATED for new dictionary format ---
|
|
map_mappings = []
|
|
for r in range(self.editor_table_map_type_mapping.rowCount()):
|
|
type_item = self.editor_table_map_type_mapping.item(r, 0)
|
|
keywords_item = self.editor_table_map_type_mapping.item(r, 1)
|
|
# Ensure both items exist and have text before processing
|
|
if type_item and type_item.text() and keywords_item and keywords_item.text():
|
|
target_type = type_item.text().strip()
|
|
keywords = [k.strip() for k in keywords_item.text().split(',') if k.strip()]
|
|
if target_type and keywords: # Only add if both parts are valid
|
|
map_mappings.append({"target_type": target_type, "keywords": keywords})
|
|
else:
|
|
log.warning(f"Skipping row {r} in map type mapping table due to empty target type or keywords.")
|
|
else:
|
|
log.warning(f"Skipping row {r} in map type mapping table due to missing items.")
|
|
preset_data["map_type_mapping"] = map_mappings
|
|
# --- END UPDATE ---
|
|
category_rules = {}
|
|
category_rules["model_patterns"] = [self.editor_list_model_patterns.item(i).text() for i in range(self.editor_list_model_patterns.count())]
|
|
category_rules["decal_keywords"] = [self.editor_list_decal_keywords.item(i).text() for i in range(self.editor_list_decal_keywords.count())]
|
|
preset_data["asset_category_rules"] = category_rules
|
|
arch_rules = []
|
|
for r in range(self.editor_table_archetype_rules.rowCount()):
|
|
name_item = self.editor_table_archetype_rules.item(r, 0)
|
|
any_item = self.editor_table_archetype_rules.item(r, 1)
|
|
all_item = self.editor_table_archetype_rules.item(r, 2)
|
|
if name_item and any_item and all_item:
|
|
match_any = [k.strip() for k in any_item.text().split(',') if k.strip()]
|
|
match_all = [k.strip() for k in all_item.text().split(',') if k.strip()]
|
|
arch_rules.append([name_item.text().strip(), {"match_any": match_any, "match_all": match_all}])
|
|
preset_data["archetype_rules"] = arch_rules
|
|
return preset_data
|
|
|
|
def _save_current_preset(self) -> bool:
|
|
"""Saves the current editor content to the currently loaded file path."""
|
|
if not self.current_editing_preset_path: return self._save_preset_as()
|
|
log.info(f"Saving preset: {self.current_editing_preset_path.name}")
|
|
try:
|
|
preset_data = self._gather_editor_data()
|
|
if not preset_data.get("preset_name"): QMessageBox.warning(self, "Save Error", "Preset Name cannot be empty."); return False
|
|
if not preset_data.get("supplier_name"): QMessageBox.warning(self, "Save Error", "Supplier Name cannot be empty."); return False
|
|
content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False)
|
|
with open(self.current_editing_preset_path, 'w', encoding='utf-8') as f: f.write(content_to_save)
|
|
self.editor_unsaved_changes = False
|
|
self.editor_save_button.setEnabled(False)
|
|
self.setWindowTitle(f"Asset Processor Tool - {self.current_editing_preset_path.name}")
|
|
self.presets_changed_signal.emit() # Signal that presets changed
|
|
log.info("Preset saved successfully.")
|
|
# Refresh lists after save
|
|
self.populate_presets()
|
|
return True
|
|
except Exception as e:
|
|
log.exception(f"Error saving preset file {self.current_editing_preset_path}: {e}")
|
|
QMessageBox.critical(self, "Save Error", f"Could not save preset file:\n{self.current_editing_preset_path}\n\nError: {e}")
|
|
return False
|
|
|
|
def _save_preset_as(self) -> bool:
|
|
"""Saves the current editor content to a new file chosen by the user."""
|
|
log.debug("Save As action triggered.")
|
|
try:
|
|
preset_data = self._gather_editor_data()
|
|
new_preset_name = preset_data.get("preset_name")
|
|
if not new_preset_name: QMessageBox.warning(self, "Save As Error", "Preset Name cannot be empty."); return False
|
|
if not preset_data.get("supplier_name"): QMessageBox.warning(self, "Save As Error", "Supplier Name cannot be empty."); return False
|
|
content_to_save = json.dumps(preset_data, indent=4, ensure_ascii=False)
|
|
suggested_name = f"{new_preset_name}.json"
|
|
default_path = PRESETS_DIR / suggested_name
|
|
file_path_str, _ = QFileDialog.getSaveFileName(self, "Save Preset As", str(default_path), "JSON Files (*.json);;All Files (*)")
|
|
if not file_path_str: log.debug("Save As cancelled by user."); return False
|
|
save_path = Path(file_path_str)
|
|
if save_path.suffix.lower() != ".json": save_path = save_path.with_suffix(".json")
|
|
if save_path.exists() and save_path != self.current_editing_preset_path:
|
|
reply = QMessageBox.warning(self, "Confirm Overwrite", f"Preset '{save_path.name}' already exists. Overwrite?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
|
if reply == QMessageBox.StandardButton.No: log.debug("Save As overwrite cancelled."); return False
|
|
log.info(f"Saving preset as: {save_path.name}")
|
|
with open(save_path, 'w', encoding='utf-8') as f: f.write(content_to_save)
|
|
self.current_editing_preset_path = save_path # Update current path
|
|
self.editor_unsaved_changes = False
|
|
self.editor_save_button.setEnabled(False)
|
|
self.setWindowTitle(f"Asset Processor Tool - {save_path.name}")
|
|
self.presets_changed_signal.emit() # Signal change
|
|
log.info("Preset saved successfully (Save As).")
|
|
# Refresh lists and select the new item
|
|
self.populate_presets()
|
|
return True
|
|
except Exception as e:
|
|
log.exception(f"Error saving preset file (Save As): {e}")
|
|
QMessageBox.critical(self, "Save Error", f"Could not save preset file.\n\nError: {e}")
|
|
return False
|
|
|
|
def _new_preset(self):
|
|
"""Clears the editor and loads data from _template.json."""
|
|
log.debug("New Preset action triggered.")
|
|
if self._check_editor_unsaved_changes(): return
|
|
self._clear_editor()
|
|
if TEMPLATE_PATH.is_file():
|
|
log.info("Loading new preset from _template.json")
|
|
try:
|
|
with open(TEMPLATE_PATH, 'r', encoding='utf-8') as f: template_data = json.load(f)
|
|
self._populate_editor_from_data(template_data)
|
|
# Override specific fields for a new preset
|
|
self.editor_preset_name.setText("NewPreset")
|
|
self.setWindowTitle("Asset Processor Tool - New Preset*")
|
|
except Exception as e:
|
|
log.exception(f"Error loading template preset file {TEMPLATE_PATH}: {e}")
|
|
QMessageBox.critical(self, "Error", f"Could not load template preset file:\n{TEMPLATE_PATH}\n\nError: {e}")
|
|
self._clear_editor()
|
|
self.setWindowTitle("Asset Processor Tool - New Preset*")
|
|
else:
|
|
log.warning("Presets/_template.json not found. Creating empty preset.")
|
|
self.setWindowTitle("Asset Processor Tool - New Preset*")
|
|
self.editor_preset_name.setText("NewPreset")
|
|
self.editor_supplier_name.setText("MySupplier")
|
|
self._set_editor_enabled(True)
|
|
self.editor_unsaved_changes = True
|
|
self.editor_save_button.setEnabled(True)
|
|
|
|
def _delete_selected_preset(self):
|
|
"""Deletes the currently selected preset file from the editor list after confirmation."""
|
|
current_item = self.editor_preset_list.currentItem()
|
|
if not current_item: QMessageBox.information(self, "Delete Preset", "Please select a preset from the list to delete."); return
|
|
preset_path = current_item.data(Qt.ItemDataRole.UserRole)
|
|
preset_name = preset_path.stem
|
|
reply = QMessageBox.warning(self, "Confirm Delete", f"Are you sure you want to permanently delete the preset '{preset_name}'?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
log.info(f"Deleting preset: {preset_path.name}")
|
|
try:
|
|
preset_path.unlink()
|
|
log.info("Preset deleted successfully.")
|
|
if self.current_editing_preset_path == preset_path: self._clear_editor()
|
|
self.presets_changed_signal.emit() # Signal change
|
|
# Refresh lists
|
|
self.populate_presets()
|
|
except Exception as e:
|
|
log.exception(f"Error deleting preset file {preset_path}: {e}")
|
|
QMessageBox.critical(self, "Delete Error", f"Could not delete preset file:\n{preset_path}\n\nError: {e}")
|
|
|
|
# --- Menu Bar Setup ---
|
|
def setup_menu_bar(self):
|
|
"""Creates the main menu bar and View menu."""
|
|
self.menu_bar = self.menuBar()
|
|
view_menu = self.menu_bar.addMenu("&View")
|
|
|
|
# Log Console Action
|
|
self.toggle_log_action = QAction("Show Log Console", self, checkable=True)
|
|
self.toggle_log_action.setChecked(False) # Start hidden
|
|
self.toggle_log_action.toggled.connect(self._toggle_log_console_visibility)
|
|
view_menu.addAction(self.toggle_log_action)
|
|
|
|
# Detailed Preview Action
|
|
self.toggle_preview_action = QAction("Disable Detailed Preview", self, checkable=True)
|
|
self.toggle_preview_action.setChecked(False) # Start enabled (detailed view)
|
|
# Connect to update_preview, which now checks this action's state
|
|
self.toggle_preview_action.toggled.connect(self.update_preview)
|
|
view_menu.addAction(self.toggle_preview_action)
|
|
|
|
# Verbose Logging Action
|
|
self.toggle_verbose_action = QAction("Verbose Logging (DEBUG)", self, checkable=True)
|
|
self.toggle_verbose_action.setChecked(False) # Start disabled (INFO level)
|
|
self.toggle_verbose_action.toggled.connect(self._toggle_verbose_logging)
|
|
view_menu.addAction(self.toggle_verbose_action)
|
|
|
|
# --- Logging Handler Setup ---
|
|
def setup_logging_handler(self):
|
|
"""Creates and configures the custom QtLogHandler."""
|
|
self.log_handler = QtLogHandler(self)
|
|
# Set the formatter to match the basicConfig format
|
|
log_format = '%(levelname)s: %(message)s' # Simpler format for UI console
|
|
formatter = logging.Formatter(log_format)
|
|
self.log_handler.setFormatter(formatter)
|
|
# Set level (e.g., INFO to capture standard messages)
|
|
self.log_handler.setLevel(logging.INFO)
|
|
# Add handler to the root logger to capture logs from all modules
|
|
logging.getLogger().addHandler(self.log_handler)
|
|
# Connect the signal to the slot
|
|
self.log_handler.log_record_received.connect(self._append_log_message)
|
|
log.info("UI Log Handler Initialized.") # Log that the handler is ready
|
|
|
|
# --- Slots for Menu Actions and Logging ---
|
|
@Slot(bool)
|
|
def _toggle_log_console_visibility(self, checked):
|
|
"""Shows or hides the log console widget based on menu action."""
|
|
if hasattr(self, 'log_console_widget'):
|
|
self.log_console_widget.setVisible(checked)
|
|
log.debug(f"Log console visibility set to: {checked}")
|
|
|
|
@Slot(bool)
|
|
def _toggle_verbose_logging(self, checked):
|
|
"""Sets the logging level for the root logger and the GUI handler."""
|
|
if not hasattr(self, 'log_handler'):
|
|
log.error("Log handler not initialized, cannot change level.")
|
|
return
|
|
|
|
new_level = logging.DEBUG if checked else logging.INFO
|
|
root_logger = logging.getLogger() # Get the root logger
|
|
root_logger.setLevel(new_level)
|
|
self.log_handler.setLevel(new_level)
|
|
log.info(f"Root and GUI logging level set to: {logging.getLevelName(new_level)}")
|
|
# Update status bar or log console to indicate change
|
|
self.statusBar().showMessage(f"Logging level set to {logging.getLevelName(new_level)}", 3000)
|
|
|
|
@Slot(str)
|
|
def _append_log_message(self, message):
|
|
"""Appends a log message to the QTextEdit console."""
|
|
if hasattr(self, 'log_console_output'):
|
|
# Optional: Add basic coloring (can be expanded)
|
|
# if message.startswith("ERROR"):
|
|
# message = f"<font color='red'>{message}</font>"
|
|
# elif message.startswith("WARNING"):
|
|
# message = f"<font color='orange'>{message}</font>"
|
|
|
|
self.log_console_output.append(message) # Use append for plain text
|
|
# Optional: Limit history size
|
|
# MAX_LINES = 500
|
|
# if self.log_console_output.document().blockCount() > MAX_LINES:
|
|
# cursor = self.log_console_output.textCursor()
|
|
# cursor.movePosition(QTextCursor.MoveOperation.Start)
|
|
# cursor.select(QTextCursor.SelectionType.BlockUnderCursor)
|
|
# cursor.removeSelectedText()
|
|
# cursor.deletePreviousChar() # Remove the newline potentially left behind
|
|
# Ensure the view scrolls to the bottom
|
|
self.log_console_output.verticalScrollBar().setValue(self.log_console_output.verticalScrollBar().maximum())
|
|
|
|
|
|
# --- Overridden Close Event ---
|
|
def closeEvent(self, event):
|
|
"""Overrides close event to check for unsaved changes in the editor."""
|
|
if self._check_editor_unsaved_changes():
|
|
event.ignore() # Ignore close event if user cancels
|
|
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."""
|
|
app = QApplication(sys.argv)
|
|
app.setStyle('Fusion')
|
|
|
|
# Set a custom palette to override default Fusion colors
|
|
palette = app.palette()
|
|
grey_color = QColor("#3a3a3a")
|
|
palette.setColor(QPalette.ColorRole.Base, grey_color)
|
|
palette.setColor(QPalette.ColorRole.AlternateBase, grey_color.lighter(110)) # Use a slightly lighter shade for alternate rows if needed
|
|
# You might need to experiment with other roles depending on which widgets are affected
|
|
# palette.setColor(QPalette.ColorRole.Window, grey_color)
|
|
# palette.setColor(QPalette.ColorRole.WindowText, Qt.GlobalColor.white) # Example: set text color to white
|
|
|
|
app.setPalette(palette)
|
|
|
|
window = MainWindow()
|
|
window.show()
|
|
sys.exit(app.exec())
|
|
|
|
if __name__ == "__main__":
|
|
run_gui() |