Major Comment and codebase cleanup
This commit is contained in:
@@ -3,7 +3,7 @@ import logging
|
||||
from PySide6.QtCore import QObject, Slot, QModelIndex
|
||||
from PySide6.QtGui import QColor # Might be needed if copying logic directly, though unlikely now
|
||||
from pathlib import Path
|
||||
from .unified_view_model import UnifiedViewModel # Use relative import
|
||||
from .unified_view_model import UnifiedViewModel
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -25,7 +25,7 @@ class AssetRestructureHandler(QObject):
|
||||
log.debug("AssetRestructureHandler initialized.")
|
||||
|
||||
@Slot(FileRule, str, QModelIndex)
|
||||
def handle_target_asset_override(self, file_rule_item: FileRule, new_target_name: str, index: QModelIndex): # Ensure FileRule is imported
|
||||
def handle_target_asset_override(self, file_rule_item: FileRule, new_target_name: str, index: QModelIndex):
|
||||
"""
|
||||
Slot connected to UnifiedViewModel.targetAssetOverrideChanged.
|
||||
Orchestrates model changes based on the new target asset path.
|
||||
@@ -35,7 +35,7 @@ class AssetRestructureHandler(QObject):
|
||||
new_target_name: The new target asset path (string).
|
||||
index: The QModelIndex of the changed item (passed by the signal).
|
||||
"""
|
||||
if not isinstance(file_rule_item, FileRule): # Check the correct parameter
|
||||
if not isinstance(file_rule_item, FileRule):
|
||||
log.warning(f"Handler received targetAssetOverrideChanged for non-FileRule item: {type(file_rule_item)}. Aborting.")
|
||||
return
|
||||
|
||||
@@ -47,14 +47,12 @@ class AssetRestructureHandler(QObject):
|
||||
if effective_new_target_name == "": effective_new_target_name = None # Treat empty string as None
|
||||
|
||||
# --- Get necessary context ---
|
||||
# Use file_rule_item directly
|
||||
old_parent_asset = getattr(file_rule_item, 'parent_asset', None)
|
||||
if not old_parent_asset:
|
||||
log.error(f"Handler: File item '{Path(file_rule_item.file_path).name}' has no parent asset. Cannot restructure.")
|
||||
# Note: Data change already happened in setData, cannot easily revert here.
|
||||
return
|
||||
|
||||
# Use file_rule_item directly
|
||||
source_rule = getattr(old_parent_asset, 'parent_source', None)
|
||||
if not source_rule:
|
||||
log.error(f"Handler: Could not find SourceRule for parent asset '{old_parent_asset.asset_name}'. Cannot restructure.")
|
||||
@@ -81,7 +79,7 @@ class AssetRestructureHandler(QObject):
|
||||
except ValueError:
|
||||
log.error(f"Handler: Could not find SourceRule index while looking for target parent '{effective_new_target_name}'.")
|
||||
target_parent_asset = None # Reset if index is invalid
|
||||
break # Found the asset
|
||||
break
|
||||
|
||||
# 2. Handle Move or Creation
|
||||
if target_parent_asset: # An existing AssetRule to move to was found
|
||||
@@ -91,7 +89,7 @@ class AssetRestructureHandler(QObject):
|
||||
# The 'index' parameter IS the QModelIndex of the FileRule being changed.
|
||||
# No need to re-fetch or re-validate it if the signal emits it correctly.
|
||||
# The core issue was using a stale index to get the *object*, now we *have* the object.
|
||||
source_file_qmodelindex = index # Use the index passed by the signal
|
||||
source_file_qmodelindex = index
|
||||
|
||||
if not source_file_qmodelindex or not source_file_qmodelindex.isValid(): # Should always be valid if signal emits it
|
||||
log.error(f"Handler: Received invalid QModelIndex for source file '{Path(file_rule_item.file_path).name}'. Cannot move.")
|
||||
@@ -114,7 +112,7 @@ class AssetRestructureHandler(QObject):
|
||||
target_parent_asset = new_asset_qmodelindex.internalPointer() # Get the newly created AssetRule object
|
||||
target_parent_index = new_asset_qmodelindex # The QModelIndex of the new AssetRule
|
||||
|
||||
source_file_qmodelindex = index # Use the index passed by the signal
|
||||
source_file_qmodelindex = index
|
||||
if not source_file_qmodelindex or not source_file_qmodelindex.isValid(): # Should always be valid
|
||||
log.error(f"Handler: Received invalid QModelIndex for source file '{Path(file_rule_item.file_path).name}'. Cannot move to new asset.")
|
||||
self.model.removeAssetRule(target_parent_asset) # Attempt to clean up newly created asset
|
||||
@@ -185,8 +183,8 @@ class AssetRestructureHandler(QObject):
|
||||
return QModelIndex()
|
||||
return QModelIndex()
|
||||
|
||||
@Slot(AssetRule, str, QModelIndex) # Updated signature
|
||||
def handle_asset_name_changed(self, asset_rule_item: AssetRule, new_name: str, index: QModelIndex): # Ensure AssetRule is imported
|
||||
@Slot(AssetRule, str, QModelIndex)
|
||||
def handle_asset_name_changed(self, asset_rule_item: AssetRule, new_name: str, index: QModelIndex):
|
||||
"""
|
||||
Slot connected to UnifiedViewModel.assetNameChanged.
|
||||
Handles logic when an AssetRule's name is changed.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# gui/base_prediction_handler.py
|
||||
import logging
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -16,7 +15,7 @@ except ImportError:
|
||||
class SourceRule: pass
|
||||
|
||||
from abc import ABCMeta
|
||||
from PySide6.QtCore import QObject # Ensure QObject is imported if not already
|
||||
from PySide6.QtCore import QObject
|
||||
|
||||
# Combine metaclasses to avoid conflict between QObject and ABC
|
||||
class QtABCMeta(type(QObject), ABCMeta):
|
||||
@@ -52,7 +51,7 @@ class BasePredictionHandler(QObject, ABC, metaclass=QtABCMeta):
|
||||
super().__init__(parent)
|
||||
self.input_source_identifier = input_source_identifier
|
||||
self._is_running = False
|
||||
self._is_cancelled = False # Added cancellation flag
|
||||
self._is_cancelled = False
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
@@ -65,7 +64,7 @@ class BasePredictionHandler(QObject, ABC, metaclass=QtABCMeta):
|
||||
Main execution slot intended to be connected to QThread.started.
|
||||
Handles the overall process: setup, execution, error handling, signaling.
|
||||
"""
|
||||
log.debug(f"--> Entered BasePredictionHandler.run() for {self.input_source_identifier}") # ADDED DEBUG LOG
|
||||
log.debug(f"--> Entered BasePredictionHandler.run() for {self.input_source_identifier}")
|
||||
if self._is_running:
|
||||
log.warning(f"Handler for '{self.input_source_identifier}' is already running. Aborting.")
|
||||
return
|
||||
@@ -75,7 +74,7 @@ class BasePredictionHandler(QObject, ABC, metaclass=QtABCMeta):
|
||||
return
|
||||
|
||||
self._is_running = True
|
||||
self._is_cancelled = False # Ensure cancel flag is reset at start
|
||||
self._is_cancelled = False
|
||||
thread_id = QThread.currentThread() # Use currentThread() for PySide6
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Starting prediction run for: {self.input_source_identifier}")
|
||||
self.status_update.emit(f"Starting analysis for '{Path(self.input_source_identifier).name}'...")
|
||||
@@ -99,7 +98,7 @@ class BasePredictionHandler(QObject, ABC, metaclass=QtABCMeta):
|
||||
error_msg = f"Error analyzing '{Path(self.input_source_identifier).name}': {e}"
|
||||
self.prediction_error.emit(self.input_source_identifier, error_msg)
|
||||
# Status update might be redundant if error is shown elsewhere, but can be useful
|
||||
# self.status_update.emit(f"Error: {e}")
|
||||
# Status update might be redundant if error is shown elsewhere, but can be useful
|
||||
|
||||
finally:
|
||||
# --- Cleanup ---
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# gui/config_editor_dialog.py
|
||||
|
||||
import json
|
||||
from PySide6.QtWidgets import (
|
||||
@@ -7,14 +6,13 @@ from PySide6.QtWidgets import (
|
||||
QPushButton, QFileDialog, QLabel, QTableWidget,
|
||||
QTableWidgetItem, QDialogButtonBox, QMessageBox, QListWidget,
|
||||
QListWidgetItem, QFormLayout, QGroupBox, QStackedWidget,
|
||||
QHeaderView, QSizePolicy # Added QHeaderView and QSizePolicy
|
||||
QHeaderView, QSizePolicy
|
||||
)
|
||||
from PySide6.QtGui import QColor, QPainter
|
||||
from PySide6.QtCore import Qt, QEvent
|
||||
from PySide6.QtWidgets import QColorDialog, QStyledItemDelegate, QApplication
|
||||
|
||||
# Assuming configuration.py is in the parent directory or accessible
|
||||
# Adjust import path if necessary
|
||||
try:
|
||||
from configuration import load_base_config, save_base_config
|
||||
except ImportError:
|
||||
@@ -30,7 +28,6 @@ class ColorDelegate(QStyledItemDelegate):
|
||||
if isinstance(color_str, str) and color_str.startswith('#'):
|
||||
color = QColor(color_str)
|
||||
if color.isValid():
|
||||
# Fill the background with the color
|
||||
painter.fillRect(option.rect, color)
|
||||
# Optionally draw text (e.g., the hex code) centered
|
||||
# painter.drawText(option.rect, Qt.AlignCenter, color_str)
|
||||
@@ -198,19 +195,19 @@ class ConfigEditorDialog(QDialog):
|
||||
output_dir_layout.addWidget(output_dir_edit)
|
||||
output_dir_layout.addWidget(output_dir_button)
|
||||
form_layout.addRow(output_dir_label, output_dir_layout)
|
||||
self.widgets["OUTPUT_BASE_DIR"] = output_dir_edit # Store reference
|
||||
self.widgets["OUTPUT_BASE_DIR"] = output_dir_edit
|
||||
|
||||
# 2. EXTRA_FILES_SUBDIR: QLineEdit
|
||||
extra_subdir_label = QLabel("Subdirectory for Extra Files:")
|
||||
extra_subdir_edit = QLineEdit()
|
||||
form_layout.addRow(extra_subdir_label, extra_subdir_edit)
|
||||
self.widgets["EXTRA_FILES_SUBDIR"] = extra_subdir_edit # Store reference
|
||||
self.widgets["EXTRA_FILES_SUBDIR"] = extra_subdir_edit
|
||||
|
||||
# 3. METADATA_FILENAME: QLineEdit
|
||||
metadata_label = QLabel("Metadata Filename:")
|
||||
metadata_edit = QLineEdit()
|
||||
form_layout.addRow(metadata_label, metadata_edit)
|
||||
self.widgets["METADATA_FILENAME"] = metadata_edit # Store reference
|
||||
self.widgets["METADATA_FILENAME"] = metadata_edit
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
layout.addStretch() # Keep stretch at the end
|
||||
@@ -245,10 +242,8 @@ class ConfigEditorDialog(QDialog):
|
||||
self.widgets.pop("RESPECT_VARIANT_MAP_TYPES", None)
|
||||
self.widgets.pop("ASPECT_RATIO_DECIMALS", None)
|
||||
|
||||
# Main layout for this tab
|
||||
main_tab_layout = QVBoxLayout()
|
||||
|
||||
# Form layout for simple input fields
|
||||
form_layout = QFormLayout()
|
||||
|
||||
# 1. TARGET_FILENAME_PATTERN: QLineEdit
|
||||
@@ -276,7 +271,6 @@ class ConfigEditorDialog(QDialog):
|
||||
|
||||
main_tab_layout.addLayout(form_layout)
|
||||
|
||||
# Add the main layout to the tab's provided layout
|
||||
layout.addLayout(main_tab_layout)
|
||||
layout.addStretch() # Keep stretch at the end of the tab's main layout
|
||||
|
||||
@@ -315,7 +309,6 @@ class ConfigEditorDialog(QDialog):
|
||||
for key in keys_to_clear:
|
||||
self.widgets.pop(key, None)
|
||||
|
||||
# Main layout for this tab
|
||||
main_tab_layout = QVBoxLayout()
|
||||
|
||||
# --- IMAGE_RESOLUTIONS Section ---
|
||||
@@ -332,7 +325,7 @@ class ConfigEditorDialog(QDialog):
|
||||
# TODO: Implement custom delegate for "Resolution (px)" column
|
||||
# TODO: Connect add/remove buttons signals
|
||||
resolutions_layout.addWidget(resolutions_table)
|
||||
self.widgets["IMAGE_RESOLUTIONS_TABLE"] = resolutions_table # Store table reference
|
||||
self.widgets["IMAGE_RESOLUTIONS_TABLE"] = resolutions_table
|
||||
|
||||
resolutions_button_layout = QHBoxLayout()
|
||||
add_res_button = QPushButton("Add Row")
|
||||
@@ -398,7 +391,6 @@ class ConfigEditorDialog(QDialog):
|
||||
|
||||
main_tab_layout.addLayout(form_layout)
|
||||
|
||||
# Add the main layout to the tab's provided layout
|
||||
layout.addLayout(main_tab_layout)
|
||||
layout.addStretch() # Keep stretch at the end of the tab's main layout
|
||||
|
||||
@@ -436,7 +428,6 @@ class ConfigEditorDialog(QDialog):
|
||||
self.widgets.pop("MAP_BIT_DEPTH_RULES_TABLE", None)
|
||||
|
||||
|
||||
# Overall QVBoxLayout for the "Definitions" tab
|
||||
overall_layout = QVBoxLayout()
|
||||
|
||||
# --- Top Widget: DEFAULT_ASSET_CATEGORY ---
|
||||
@@ -527,7 +518,6 @@ class ConfigEditorDialog(QDialog):
|
||||
file_types_button_layout.addStretch()
|
||||
file_types_layout.addLayout(file_types_button_layout)
|
||||
|
||||
# Add the overall layout to the main tab layout provided
|
||||
layout.addLayout(overall_layout)
|
||||
layout.addStretch() # Keep stretch at the end of the tab's main layout
|
||||
|
||||
@@ -642,7 +632,7 @@ class ConfigEditorDialog(QDialog):
|
||||
nodegroup_layout.addWidget(nodegroup_widget)
|
||||
nodegroup_layout.addWidget(nodegroup_button)
|
||||
form_layout.addRow(nodegroup_label, nodegroup_layout)
|
||||
self.widgets["DEFAULT_NODEGROUP_BLEND_PATH"] = nodegroup_widget # Store reference
|
||||
self.widgets["DEFAULT_NODEGROUP_BLEND_PATH"] = nodegroup_widget
|
||||
|
||||
# 2. DEFAULT_MATERIALS_BLEND_PATH: QLineEdit + QPushButton
|
||||
materials_label = QLabel("Default Materials Library (.blend):")
|
||||
@@ -655,7 +645,7 @@ class ConfigEditorDialog(QDialog):
|
||||
materials_layout.addWidget(materials_widget)
|
||||
materials_layout.addWidget(materials_button)
|
||||
form_layout.addRow(materials_label, materials_layout)
|
||||
self.widgets["DEFAULT_MATERIALS_BLEND_PATH"] = materials_widget # Store reference
|
||||
self.widgets["DEFAULT_MATERIALS_BLEND_PATH"] = materials_widget
|
||||
|
||||
# 3. BLENDER_EXECUTABLE_PATH: QLineEdit + QPushButton
|
||||
blender_label = QLabel("Blender Executable Path:")
|
||||
@@ -668,7 +658,7 @@ class ConfigEditorDialog(QDialog):
|
||||
blender_layout.addWidget(blender_widget)
|
||||
blender_layout.addWidget(blender_button)
|
||||
form_layout.addRow(blender_label, blender_layout)
|
||||
self.widgets["BLENDER_EXECUTABLE_PATH"] = blender_widget # Store reference
|
||||
self.widgets["BLENDER_EXECUTABLE_PATH"] = blender_widget
|
||||
|
||||
layout.addLayout(form_layout)
|
||||
layout.addStretch()
|
||||
@@ -686,7 +676,7 @@ class ConfigEditorDialog(QDialog):
|
||||
# TODO: Implement custom delegate for "Examples" column (QLineEdit)
|
||||
|
||||
layout.addWidget(table)
|
||||
self.widgets["ASSET_TYPE_DEFINITIONS_TABLE"] = table # Store table reference
|
||||
self.widgets["ASSET_TYPE_DEFINITIONS_TABLE"] = table
|
||||
|
||||
def create_file_type_definitions_table_widget(self, layout, definitions_data):
|
||||
"""Creates a QTableWidget for editing file type definitions."""
|
||||
@@ -703,7 +693,7 @@ class ConfigEditorDialog(QDialog):
|
||||
# TODO: Implement custom delegate for "Bit Depth Rule" column (QComboBox)
|
||||
|
||||
layout.addWidget(table)
|
||||
self.widgets["FILE_TYPE_DEFINITIONS_TABLE"] = table # Store table reference
|
||||
self.widgets["FILE_TYPE_DEFINITIONS_TABLE"] = table
|
||||
|
||||
def create_image_resolutions_table_widget(self, layout, resolutions_data):
|
||||
"""Creates a QTableWidget for editing image resolutions."""
|
||||
@@ -717,7 +707,7 @@ class ConfigEditorDialog(QDialog):
|
||||
# TODO: Implement custom delegate for "Resolution (px)" column (e.g., QLineEdit with validation or two SpinBoxes)
|
||||
|
||||
layout.addWidget(table)
|
||||
self.widgets["IMAGE_RESOLUTIONS_TABLE"] = table # Store table reference
|
||||
self.widgets["IMAGE_RESOLUTIONS_TABLE"] = table
|
||||
|
||||
def create_map_bit_depth_rules_table_widget(self, layout, rules_data: dict):
|
||||
"""Creates a QTableWidget for editing map bit depth rules (Map Type -> Rule)."""
|
||||
@@ -731,7 +721,7 @@ class ConfigEditorDialog(QDialog):
|
||||
# TODO: Implement custom delegate for "Rule" column (QComboBox)
|
||||
|
||||
layout.addWidget(table)
|
||||
self.widgets["MAP_BIT_DEPTH_RULES_TABLE"] = table # Store table reference
|
||||
self.widgets["MAP_BIT_DEPTH_RULES_TABLE"] = table
|
||||
|
||||
|
||||
def create_map_merge_rules_widget(self, layout, rules_data):
|
||||
@@ -803,7 +793,7 @@ class ConfigEditorDialog(QDialog):
|
||||
|
||||
group_layout.addWidget(input_table)
|
||||
self.merge_rule_details_layout.addRow(group)
|
||||
self.merge_rule_widgets["inputs_table"] = input_table # Store table reference
|
||||
self.merge_rule_widgets["inputs_table"] = input_table
|
||||
|
||||
|
||||
# defaults: QTableWidget (Fixed Rows: R, G, B, A. Columns: "Channel", "Default Value"). Label: "Channel Defaults (if input missing)".
|
||||
@@ -824,7 +814,7 @@ class ConfigEditorDialog(QDialog):
|
||||
|
||||
group_layout.addWidget(defaults_table)
|
||||
self.merge_rule_details_layout.addRow(group)
|
||||
self.merge_rule_widgets["defaults_table"] = defaults_table # Store table reference
|
||||
self.merge_rule_widgets["defaults_table"] = defaults_table
|
||||
|
||||
|
||||
# output_bit_depth: QComboBox (Options: "respect_inputs", "force_8bit", "force_16bit"). Label: "Output Bit Depth".
|
||||
@@ -1175,18 +1165,6 @@ class ConfigEditorDialog(QDialog):
|
||||
row += 1
|
||||
|
||||
|
||||
# Removed duplicated methods:
|
||||
# - create_map_merge_rules_widget (duplicate of lines 684-689)
|
||||
# - populate_merge_rules_list (duplicate of lines 691-698)
|
||||
# - display_merge_rule_details (duplicate of lines 699-788)
|
||||
# - browse_path (duplicate of lines 790-801)
|
||||
# - pick_color (duplicate of lines 802-807)
|
||||
# - save_settings (duplicate of lines 808-874)
|
||||
# - populate_widgets_from_settings (duplicate of lines 875-933)
|
||||
# - populate_asset_definitions_table (duplicate of lines 935-969)
|
||||
# - populate_file_type_definitions_table (duplicate of lines 971-1005)
|
||||
# - populate_image_resolutions_table (duplicate of lines 1006-1018)
|
||||
# - populate_map_bit_depth_rules_table (duplicate of lines 1020-1027)
|
||||
|
||||
|
||||
# Example usage (for testing the dialog independently)
|
||||
|
||||
@@ -1,41 +1,34 @@
|
||||
from pathlib import Path
|
||||
# gui/delegates.py
|
||||
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
|
||||
from PySide6.QtCore import Qt, QModelIndex
|
||||
# Import Configuration and ConfigurationError
|
||||
from configuration import Configuration, ConfigurationError, load_base_config # Keep load_base_config for SupplierSearchDelegate
|
||||
from PySide6.QtWidgets import QListWidgetItem # Import QListWidgetItem
|
||||
from PySide6.QtWidgets import QListWidgetItem
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os # Added for path manipulation if needed, though json.dump handles creation
|
||||
from PySide6.QtWidgets import QCompleter # Added QCompleter
|
||||
import os
|
||||
from PySide6.QtWidgets import QCompleter
|
||||
|
||||
# Configure logging
|
||||
log = logging.getLogger(__name__)
|
||||
SUPPLIERS_CONFIG_PATH = "config/suppliers.json"
|
||||
|
||||
class LineEditDelegate(QStyledItemDelegate):
|
||||
"""Delegate for editing string values using a QLineEdit."""
|
||||
def createEditor(self, parent, option, index):
|
||||
# Creates the QLineEdit editor widget used for editing.
|
||||
editor = QLineEdit(parent)
|
||||
return editor
|
||||
|
||||
def setEditorData(self, editor: QLineEdit, index: QModelIndex):
|
||||
# Sets the editor's initial data based on the model's data.
|
||||
# Use EditRole to get the raw data suitable for editing.
|
||||
value = index.model().data(index, Qt.EditRole)
|
||||
editor.setText(str(value) if value is not None else "")
|
||||
|
||||
def setModelData(self, editor: QLineEdit, model, index: QModelIndex):
|
||||
# Commits the editor's data back to the model.
|
||||
value = editor.text()
|
||||
# Pass the potentially modified text back to the model's setData.
|
||||
model.setData(index, value, Qt.EditRole)
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
# Ensures the editor widget is placed correctly within the cell.
|
||||
editor.setGeometry(option.rect)
|
||||
|
||||
|
||||
@@ -51,10 +44,9 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
||||
# REMOVED self.main_window store
|
||||
|
||||
def createEditor(self, parent, option, index: QModelIndex):
|
||||
# Creates the QComboBox editor widget.
|
||||
editor = QComboBox(parent)
|
||||
column = index.column()
|
||||
model = index.model() # GET model from index
|
||||
model = index.model()
|
||||
|
||||
# Add a "clear" option first, associating None with it.
|
||||
editor.addItem("---", None) # UserData = None
|
||||
@@ -74,8 +66,6 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
||||
items_keys = model._asset_type_keys # Use cached keys
|
||||
elif column == COL_ITEM_TYPE:
|
||||
items_keys = model._file_type_keys # Use cached keys
|
||||
# else: # Handle other columns if necessary (optional)
|
||||
# log.debug(f"ComboBoxDelegate applied to unexpected column: {column}")
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Error getting keys from UnifiedViewModel in ComboBoxDelegate: {e}")
|
||||
@@ -98,7 +88,6 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
||||
return editor
|
||||
|
||||
def setEditorData(self, editor: QComboBox, index: QModelIndex):
|
||||
# Sets the combo box's current item based on the model's string data.
|
||||
# Get the current string value (or None) from the model via EditRole.
|
||||
value = index.model().data(index, Qt.EditRole) # This should be a string or None
|
||||
|
||||
@@ -115,7 +104,6 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
||||
|
||||
|
||||
def setModelData(self, editor: QComboBox, model, index: QModelIndex):
|
||||
# Commits the selected combo box data (string or None) back to the model.
|
||||
# Get the UserData associated with the currently selected item.
|
||||
# This will be the string value or None (for the "---" option).
|
||||
value = editor.currentData() # This is either the string or None
|
||||
@@ -123,7 +111,6 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
||||
model.setData(index, value, Qt.EditRole)
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
# Ensures the editor widget is placed correctly within the cell.
|
||||
editor.setGeometry(option.rect)
|
||||
|
||||
class SupplierSearchDelegate(QStyledItemDelegate):
|
||||
|
||||
@@ -9,7 +9,7 @@ from PySide6.QtWidgets import (
|
||||
from PySide6.QtCore import Slot as pyqtSlot, Signal as pyqtSignal # Use PySide6 equivalents
|
||||
|
||||
# Assuming configuration module exists and has relevant functions later
|
||||
from configuration import save_llm_config, ConfigurationError # Import necessary items
|
||||
from configuration import save_llm_config, ConfigurationError
|
||||
# For now, define path directly for initial structure
|
||||
LLM_CONFIG_PATH = "config/llm_settings.json"
|
||||
|
||||
@@ -102,10 +102,8 @@ class LLMEditorWidget(QWidget):
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connect signals to slots."""
|
||||
# Save button
|
||||
self.save_button.clicked.connect(self._save_settings)
|
||||
|
||||
# Fields triggering unsaved changes
|
||||
self.prompt_editor.textChanged.connect(self._mark_unsaved)
|
||||
self.endpoint_url_edit.textChanged.connect(self._mark_unsaved)
|
||||
self.api_key_edit.textChanged.connect(self._mark_unsaved)
|
||||
@@ -113,7 +111,6 @@ class LLMEditorWidget(QWidget):
|
||||
self.temperature_spinbox.valueChanged.connect(self._mark_unsaved)
|
||||
self.timeout_spinbox.valueChanged.connect(self._mark_unsaved)
|
||||
|
||||
# Example management buttons and tab close signal
|
||||
self.add_example_button.clicked.connect(self._add_example_tab)
|
||||
self.delete_example_button.clicked.connect(self._delete_current_example_tab)
|
||||
self.examples_tab_widget.tabCloseRequested.connect(self._remove_example_tab)
|
||||
@@ -145,7 +142,7 @@ class LLMEditorWidget(QWidget):
|
||||
example_text = json.dumps(example, indent=4)
|
||||
example_editor = QTextEdit()
|
||||
example_editor.setPlainText(example_text)
|
||||
example_editor.textChanged.connect(self._mark_unsaved) # Connect here
|
||||
example_editor.textChanged.connect(self._mark_unsaved)
|
||||
self.examples_tab_widget.addTab(example_editor, f"Example {i+1}")
|
||||
except TypeError as e:
|
||||
logger.error(f"Error formatting example {i+1}: {e}. Skipping.")
|
||||
@@ -277,7 +274,7 @@ class LLMEditorWidget(QWidget):
|
||||
logger.debug("Adding new example tab.")
|
||||
new_example_editor = QTextEdit()
|
||||
new_example_editor.setPlaceholderText("Enter example JSON here...")
|
||||
new_example_editor.textChanged.connect(self._mark_unsaved) # Connect signal
|
||||
new_example_editor.textChanged.connect(self._mark_unsaved)
|
||||
|
||||
# Determine the next example number
|
||||
next_example_num = self.examples_tab_widget.count() + 1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
import json # Added for direct config loading
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
@@ -8,18 +8,14 @@ from PySide6.QtCore import QObject, Signal, QThread, Slot, QTimer
|
||||
# --- Backend Imports ---
|
||||
# Assuming these might be needed based on MainWindow's usage
|
||||
try:
|
||||
# Removed load_base_config import
|
||||
# Removed Configuration import as we load manually now
|
||||
from configuration import ConfigurationError # Keep error class
|
||||
from .llm_prediction_handler import LLMPredictionHandler # Backend handler
|
||||
from rule_structure import SourceRule # For signal emission type hint
|
||||
except ImportError as e:
|
||||
logging.getLogger(__name__).critical(f"Failed to import backend modules for LLMInteractionHandler: {e}")
|
||||
LLMPredictionHandler = None
|
||||
# load_base_config = None # Removed
|
||||
ConfigurationError = Exception
|
||||
SourceRule = None # Define as None if import fails
|
||||
# Configuration = None # Removed
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
# Define config file paths relative to this handler's location
|
||||
@@ -97,7 +93,7 @@ class LLMInteractionHandler(QObject):
|
||||
def queue_llm_requests_batch(self, requests: list[tuple[str, list | None]]):
|
||||
"""Adds multiple requests to the LLM processing queue."""
|
||||
added_count = 0
|
||||
log.debug(f"Queueing batch. Current queue content: {self.llm_processing_queue}") # ADDED DEBUG LOG
|
||||
log.debug(f"Queueing batch. Current queue content: {self.llm_processing_queue}")
|
||||
for input_path, file_list in requests:
|
||||
is_in_queue = any(item[0] == input_path for item in self.llm_processing_queue)
|
||||
if not is_in_queue:
|
||||
@@ -108,7 +104,6 @@ class LLMInteractionHandler(QObject):
|
||||
|
||||
if added_count > 0:
|
||||
log.info(f"Added {added_count} requests to LLM queue. New size: {len(self.llm_processing_queue)}")
|
||||
# If not currently processing, start the queue
|
||||
if not self._is_processing:
|
||||
QTimer.singleShot(0, self._process_next_llm_item)
|
||||
|
||||
@@ -269,9 +264,9 @@ class LLMInteractionHandler(QObject):
|
||||
# log.debug(f"--> Entered LLMPredictionHandler.run() for {self.input_path}")
|
||||
|
||||
self.llm_prediction_thread.start()
|
||||
log.debug(f"LLM prediction thread start() called for {input_path_str}. Is running: {self.llm_prediction_thread.isRunning()}") # ADDED DEBUG LOG
|
||||
log.debug(f"LLM prediction thread start() called for {input_path_str}. Is running: {self.llm_prediction_thread.isRunning()}")
|
||||
# Log success *after* start() is called successfully
|
||||
log.debug(f"Successfully initiated LLM prediction thread for {input_path_str}.") # MOVED/REWORDED LOG
|
||||
log.debug(f"Successfully initiated LLM prediction thread for {input_path_str}.")
|
||||
|
||||
except Exception as e:
|
||||
# --- Handle errors during setup/start ---
|
||||
@@ -357,8 +352,6 @@ class LLMInteractionHandler(QObject):
|
||||
# Pass the potentially None file_list. _start_llm_prediction handles extraction if needed.
|
||||
self._start_llm_prediction(next_dir, file_list=file_list)
|
||||
# --- DO NOT pop item here. Item is popped in _handle_llm_result or _handle_llm_error ---
|
||||
# Log message moved into the try block of _start_llm_prediction
|
||||
# log.debug(f"Successfully started LLM prediction thread for {next_dir}. Item remains in queue until finished.")
|
||||
except Exception as e:
|
||||
# This block now catches errors from _start_llm_prediction itself
|
||||
log.exception(f"Error occurred *during* _start_llm_prediction call for {next_dir}: {e}")
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import re # Added import for regex
|
||||
import logging # Add logging
|
||||
from pathlib import Path # Add Path for basename
|
||||
from PySide6.QtCore import QObject, Slot # Keep QObject for parent type hint, Slot for cancel if kept separate
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from PySide6.QtCore import QObject, Slot
|
||||
# Removed Signal, QThread as they are handled by BasePredictionHandler or caller
|
||||
from typing import List, Dict, Any
|
||||
|
||||
# Assuming rule_structure defines SourceRule, AssetRule, FileRule etc.
|
||||
# Adjust the import path if necessary based on project structure
|
||||
from rule_structure import SourceRule, AssetRule, FileRule # Ensure AssetRule and FileRule are imported
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
|
||||
# Assuming configuration loads app_settings.json
|
||||
# Adjust the import path if necessary
|
||||
# Removed Configuration import
|
||||
# from configuration import Configuration
|
||||
# from configuration import load_base_config # No longer needed here
|
||||
from .base_prediction_handler import BasePredictionHandler # Import base class
|
||||
from .base_prediction_handler import BasePredictionHandler
|
||||
|
||||
log = logging.getLogger(__name__) # Setup logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class LLMPredictionHandler(BasePredictionHandler):
|
||||
"""
|
||||
@@ -42,8 +40,8 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
"""
|
||||
super().__init__(input_source_identifier, parent)
|
||||
# input_source_identifier is stored by the base class as self.input_source_identifier
|
||||
self.file_list = file_list # Store the provided relative file list
|
||||
self.settings = settings # Store the settings dictionary
|
||||
self.file_list = file_list
|
||||
self.settings = settings
|
||||
# Access LLM settings via self.settings['key']
|
||||
# _is_running and _is_cancelled are handled by the base class
|
||||
|
||||
@@ -68,10 +66,9 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
log.info(f"Performing LLM prediction for: {self.input_source_identifier}")
|
||||
base_name = Path(self.input_source_identifier).name
|
||||
|
||||
# Use the file list passed during initialization
|
||||
if not self.file_list:
|
||||
log.warning(f"No files provided for LLM prediction for {self.input_source_identifier}. Returning empty list.")
|
||||
self.status_update.emit(f"No files found for {base_name}.") # Use base signal
|
||||
self.status_update.emit(f"No files found for {base_name}.")
|
||||
return [] # Return empty list, not an error
|
||||
|
||||
# Check for cancellation before preparing prompt
|
||||
@@ -82,7 +79,6 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
# --- Prepare Prompt ---
|
||||
self.status_update.emit(f"Preparing LLM input for {base_name}...")
|
||||
try:
|
||||
# Pass relative file list
|
||||
prompt = self._prepare_prompt(self.file_list)
|
||||
except Exception as e:
|
||||
log.exception("Error preparing LLM prompt.")
|
||||
@@ -128,13 +124,11 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
"""
|
||||
Prepares the full prompt string to send to the LLM using stored settings.
|
||||
"""
|
||||
# Access settings via the settings dictionary
|
||||
prompt_template = self.settings.get('predictor_prompt')
|
||||
if not prompt_template:
|
||||
raise ValueError("LLM predictor prompt template content is empty or missing in settings.")
|
||||
|
||||
|
||||
# Access definitions and examples directly from the settings dictionary
|
||||
asset_defs = json.dumps(self.settings.get('asset_type_definitions', {}), indent=4)
|
||||
# Combine file type defs and examples (assuming structure from Configuration class)
|
||||
file_type_defs_combined = {}
|
||||
@@ -151,7 +145,6 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
# Format *relative* file list as a single string with newlines
|
||||
file_list_str = "\n".join(relative_file_list)
|
||||
|
||||
# Replace placeholders
|
||||
prompt = prompt_template.replace('{ASSET_TYPE_DEFINITIONS}', asset_defs)
|
||||
prompt = prompt.replace('{FILE_TYPE_DEFINITIONS}', file_defs)
|
||||
prompt = prompt.replace('{EXAMPLE_INPUT_OUTPUT_PAIRS}', examples)
|
||||
@@ -174,51 +167,39 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
ValueError: If the endpoint URL is not configured or the response is invalid.
|
||||
requests.exceptions.RequestException: For other request-related errors.
|
||||
"""
|
||||
endpoint_url = self.settings.get('endpoint_url') # Get from settings dict
|
||||
endpoint_url = self.settings.get('endpoint_url')
|
||||
if not endpoint_url:
|
||||
raise ValueError("LLM endpoint URL is not configured in settings.")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
api_key = self.settings.get('api_key') # Get from settings dict
|
||||
api_key = self.settings.get('api_key')
|
||||
if api_key:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
# Construct payload based on OpenAI Chat Completions format
|
||||
payload = {
|
||||
# Use configured model name from settings dict
|
||||
"model": self.settings.get('model_name', 'local-model'),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
# Use configured temperature from settings dict
|
||||
"temperature": self.settings.get('temperature', 0.5),
|
||||
# Add max_tokens if needed/configurable:
|
||||
# "max_tokens": self.settings.get('max_tokens'), # Example if added to settings
|
||||
# Ensure the LLM is instructed to return JSON in the prompt itself
|
||||
# Some models/endpoints support a specific json mode:
|
||||
# "response_format": { "type": "json_object" } # If supported by endpoint
|
||||
}
|
||||
|
||||
# Status update emitted by _perform_prediction before calling this
|
||||
# self.status_update.emit(f"Sending request to LLM at {endpoint_url}...")
|
||||
print(f"--- Calling LLM API: {endpoint_url} ---")
|
||||
# print(f"--- Payload Preview ---\n{json.dumps(payload, indent=2)[:500]}...\n--- END Payload Preview ---")
|
||||
|
||||
# Note: Exceptions raised here (Timeout, RequestException, ValueError)
|
||||
# will be caught by the _perform_prediction method's handler.
|
||||
|
||||
# Make the POST request with a timeout
|
||||
response = requests.post(
|
||||
endpoint_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=self.settings.get('request_timeout', 120) # Use settings dict (with default)
|
||||
timeout=self.settings.get('request_timeout', 120)
|
||||
)
|
||||
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
|
||||
|
||||
# Parse the JSON response
|
||||
response_data = response.json()
|
||||
# print(f"--- LLM Raw Response ---\n{json.dumps(response_data, indent=2)}\n--- END Raw Response ---") # Debugging
|
||||
|
||||
# Extract content - structure depends on the API (OpenAI format assumed)
|
||||
if "choices" in response_data and len(response_data["choices"]) > 0:
|
||||
@@ -243,10 +224,7 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
# will be caught by the _perform_prediction method's handler.
|
||||
|
||||
# --- Sanitize Input String ---
|
||||
clean_json_str = llm_response_json_str.strip()
|
||||
|
||||
# 1. Remove multi-line /* */ comments
|
||||
clean_json_str = re.sub(r'/\*.*?\*/', '', clean_json_str, flags=re.DOTALL)
|
||||
clean_json_str = re.sub(r'/\*.*?\*/', '', llm_response_json_str.strip(), flags=re.DOTALL)
|
||||
|
||||
# 2. Remove single-line // comments (handle potential URLs carefully)
|
||||
# Only remove // if it's likely a comment (e.g., whitespace before it,
|
||||
@@ -298,14 +276,12 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
# 3. Remove markdown code fences
|
||||
clean_json_str = clean_json_str.strip()
|
||||
if clean_json_str.startswith("```json"):
|
||||
clean_json_str = clean_json_str[7:] # Remove ```json\n
|
||||
clean_json_str = clean_json_str[7:].strip()
|
||||
if clean_json_str.endswith("```"):
|
||||
clean_json_str = clean_json_str[:-3] # Remove ```
|
||||
clean_json_str = clean_json_str.strip() # Remove any extra whitespace
|
||||
clean_json_str = clean_json_str[:-3].strip()
|
||||
|
||||
# 4. Remove <think> tags (just in case)
|
||||
clean_json_str = re.sub(r'<think>.*?</think>', '', clean_json_str, flags=re.DOTALL | re.IGNORECASE)
|
||||
clean_json_str = clean_json_str.strip()
|
||||
clean_json_str = re.sub(r'<think>.*?</think>', '', clean_json_str, flags=re.DOTALL | re.IGNORECASE).strip()
|
||||
|
||||
# --- Parse Sanitized JSON ---
|
||||
try:
|
||||
@@ -327,7 +303,6 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
|
||||
# --- Prepare for Rule Creation ---
|
||||
source_rule = SourceRule(input_path=self.input_source_identifier)
|
||||
# Get valid types directly from the settings dictionary
|
||||
valid_asset_types = list(self.settings.get('asset_type_definitions', {}).keys())
|
||||
valid_file_types = list(self.settings.get('file_type_definitions', {}).keys())
|
||||
asset_rules_map: Dict[str, AssetRule] = {} # Maps group_name to AssetRule
|
||||
@@ -369,17 +344,17 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
# --- Handle Grouping and Asset Type ---
|
||||
if not group_name or not isinstance(group_name, str):
|
||||
log.warning(f"File '{file_path_rel}' has missing, null, or invalid 'proposed_asset_group_name' ({group_name}). Cannot assign to an asset. Skipping file.")
|
||||
continue # Skip files that cannot be grouped
|
||||
continue
|
||||
|
||||
asset_type = response_data["asset_group_classifications"].get(group_name)
|
||||
|
||||
if not asset_type:
|
||||
log.warning(f"No classification found in 'asset_group_classifications' for group '{group_name}' (proposed for file '{file_path_rel}'). Skipping file.")
|
||||
continue # Skip files belonging to unclassified groups
|
||||
continue
|
||||
|
||||
if asset_type not in valid_asset_types:
|
||||
log.warning(f"Invalid asset_type '{asset_type}' found in 'asset_group_classifications' for group '{group_name}'. Skipping file '{file_path_rel}'.")
|
||||
continue # Skip files belonging to groups with invalid types
|
||||
continue
|
||||
|
||||
# --- Construct Absolute Path ---
|
||||
try:
|
||||
@@ -400,14 +375,13 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
asset_rule = AssetRule(asset_name=group_name, asset_type=asset_type)
|
||||
source_rule.assets.append(asset_rule)
|
||||
asset_rules_map[group_name] = asset_rule
|
||||
# else: use existing asset_rule
|
||||
|
||||
# --- Create and Add File Rule ---
|
||||
file_rule = FileRule(
|
||||
file_path=file_path_abs,
|
||||
item_type=file_type,
|
||||
item_type_override=file_type, # Initial override based on LLM
|
||||
target_asset_name_override=group_name, # Use the group name
|
||||
target_asset_name_override=group_name,
|
||||
output_format_override=None,
|
||||
is_gloss_source=False,
|
||||
resolution_override=None,
|
||||
@@ -422,5 +396,3 @@ class LLMPredictionHandler(BasePredictionHandler):
|
||||
log.warning(f"LLM prediction for '{self.input_source_identifier}' resulted in zero valid assets after parsing.")
|
||||
|
||||
return [source_rule] # Return list containing the single SourceRule
|
||||
|
||||
# Removed conceptual example usage comments
|
||||
@@ -1,4 +1,3 @@
|
||||
# gui/log_console_widget.py
|
||||
import logging
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QTextEdit, QLabel, QSizePolicy
|
||||
@@ -18,26 +17,19 @@ class LogConsoleWidget(QWidget):
|
||||
def _init_ui(self):
|
||||
"""Initializes the UI elements for the log console."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 5, 0, 0) # Add some top margin
|
||||
layout.setContentsMargins(0, 5, 0, 0)
|
||||
|
||||
log_console_label = QLabel("Log Console:")
|
||||
self.log_console_output = QTextEdit()
|
||||
self.log_console_output.setReadOnly(True)
|
||||
# self.log_console_output.setMaximumHeight(150) # Let the parent layout control height
|
||||
self.log_console_output.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) # Allow vertical expansion
|
||||
|
||||
layout.addWidget(log_console_label)
|
||||
layout.addWidget(self.log_console_output)
|
||||
|
||||
# Initially hidden, visibility controlled by MainWindow
|
||||
self.setVisible(False)
|
||||
|
||||
@Slot(str)
|
||||
def _append_log_message(self, message):
|
||||
"""Appends a log message to the QTextEdit console."""
|
||||
self.log_console_output.append(message)
|
||||
# Auto-scroll to the bottom
|
||||
self.log_console_output.verticalScrollBar().setValue(self.log_console_output.verticalScrollBar().maximum())
|
||||
|
||||
# Note: Visibility is controlled externally via setVisible(),
|
||||
# so the _toggle_log_console_visibility slot is not needed here.
|
||||
@@ -4,10 +4,9 @@ import json
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
import functools # Ensure functools is imported directly for partial
|
||||
from functools import partial
|
||||
|
||||
from PySide6.QtWidgets import QApplication # Added for processEvents
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QSplitter, QTableView,
|
||||
QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QHeaderView,
|
||||
@@ -16,27 +15,20 @@ from PySide6.QtWidgets import (
|
||||
QFormLayout, QGroupBox, QAbstractItemView, QSizePolicy, QTreeView, QMenu
|
||||
)
|
||||
from PySide6.QtCore import Qt, Signal, Slot, QPoint, QModelIndex, QTimer
|
||||
from PySide6.QtGui import QColor, QAction, QPalette, QClipboard, QGuiApplication # Added QGuiApplication for clipboard
|
||||
from PySide6.QtGui import QColor, QAction, QPalette, QClipboard, QGuiApplication
|
||||
|
||||
# --- Local GUI Imports ---
|
||||
# Import delegates and models needed by the panel
|
||||
from .delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate, ItemTypeSearchDelegate # Added ItemTypeSearchDelegate
|
||||
from .unified_view_model import UnifiedViewModel # Assuming UnifiedViewModel is passed in
|
||||
from .delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate, ItemTypeSearchDelegate
|
||||
from .unified_view_model import UnifiedViewModel
|
||||
|
||||
# --- Backend Imports ---
|
||||
# Import Rule Structures if needed for context menus etc.
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
# Import config loading if defaults are needed directly here (though better passed from MainWindow)
|
||||
# Import configuration directly for PRESETS_DIR access
|
||||
import configuration
|
||||
try:
|
||||
from configuration import ConfigurationError, load_base_config
|
||||
except ImportError:
|
||||
ConfigurationError = Exception
|
||||
load_base_config = None
|
||||
# Define PRESETS_DIR fallback if configuration module fails to load entirely
|
||||
class configuration:
|
||||
PRESETS_DIR = "Presets" # Fallback path
|
||||
PRESETS_DIR = "Presets"
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,27 +41,20 @@ class MainPanelWidget(QWidget):
|
||||
- Processing controls (Start, Cancel, Clear, LLM Re-interpret)
|
||||
"""
|
||||
# --- Signals Emitted by the Panel ---
|
||||
# Request to add new input paths (e.g., from drag/drop handled by MainWindow)
|
||||
# add_paths_requested = Signal(list) # Maybe not needed if MainWindow handles drop directly
|
||||
|
||||
# Request to start the main processing job
|
||||
process_requested = Signal(dict) # Emits dict with settings: output_dir, overwrite, workers, blender_enabled, ng_path, mat_path
|
||||
process_requested = Signal(dict)
|
||||
|
||||
# Request to cancel the ongoing processing job
|
||||
cancel_requested = Signal()
|
||||
|
||||
# Request to clear the current queue/view
|
||||
clear_queue_requested = Signal()
|
||||
|
||||
# Request to re-interpret selected items using LLM
|
||||
llm_reinterpret_requested = Signal(list) # Emits list of source paths
|
||||
preset_reinterpret_requested = Signal(list, str) # Emits list[source_paths], preset_name
|
||||
llm_reinterpret_requested = Signal(list)
|
||||
preset_reinterpret_requested = Signal(list, str)
|
||||
|
||||
# Notify when the output directory changes
|
||||
output_dir_changed = Signal(str)
|
||||
|
||||
# Notify when Blender settings change
|
||||
blender_settings_changed = Signal(bool, str, str) # enabled, ng_path, mat_path
|
||||
blender_settings_changed = Signal(bool, str, str)
|
||||
|
||||
def __init__(self, unified_model: UnifiedViewModel, parent=None, file_type_keys: list[str] | None = None):
|
||||
"""
|
||||
@@ -83,9 +68,8 @@ class MainPanelWidget(QWidget):
|
||||
super().__init__(parent)
|
||||
self.unified_model = unified_model
|
||||
self.file_type_keys = file_type_keys if file_type_keys else []
|
||||
self.llm_processing_active = False # Track if LLM is running (set by MainWindow)
|
||||
self.llm_processing_active = False
|
||||
|
||||
# Get project root for resolving default paths if needed here
|
||||
script_dir = Path(__file__).parent
|
||||
self.project_root = script_dir.parent
|
||||
|
||||
@@ -95,9 +79,8 @@ class MainPanelWidget(QWidget):
|
||||
def _setup_ui(self):
|
||||
"""Sets up the UI elements for the panel."""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(5, 5, 5, 5) # Reduce margins
|
||||
main_layout.setContentsMargins(5, 5, 5, 5)
|
||||
|
||||
# --- Output Directory Selection ---
|
||||
output_layout = QHBoxLayout()
|
||||
self.output_dir_label = QLabel("Output Directory:")
|
||||
self.output_path_edit = QLineEdit()
|
||||
@@ -107,8 +90,6 @@ class MainPanelWidget(QWidget):
|
||||
output_layout.addWidget(self.browse_output_button)
|
||||
main_layout.addLayout(output_layout)
|
||||
|
||||
# --- Set Initial Output Path (Copied from MainWindow) ---
|
||||
# Consider passing this default path from MainWindow instead of reloading config here
|
||||
if load_base_config:
|
||||
try:
|
||||
base_config = load_base_config()
|
||||
@@ -127,36 +108,27 @@ class MainPanelWidget(QWidget):
|
||||
self.output_path_edit.setText("")
|
||||
|
||||
|
||||
# --- Unified View Setup ---
|
||||
self.unified_view = QTreeView()
|
||||
self.unified_view.setModel(self.unified_model) # Set the passed-in model
|
||||
self.unified_view.setModel(self.unified_model)
|
||||
|
||||
# Instantiate Delegates
|
||||
lineEditDelegate = LineEditDelegate(self.unified_view)
|
||||
# ComboBoxDelegate needs access to MainWindow's get_llm_source_preset_name,
|
||||
# which might require passing MainWindow or a callback here.
|
||||
# For now, let's assume it can work without it or we adapt it later.
|
||||
# TODO: Revisit ComboBoxDelegate dependency
|
||||
comboBoxDelegate = ComboBoxDelegate(self) # Pass only parent (self)
|
||||
supplierSearchDelegate = SupplierSearchDelegate(self) # Pass parent
|
||||
# Pass file_type_keys to ItemTypeSearchDelegate
|
||||
comboBoxDelegate = ComboBoxDelegate(self)
|
||||
supplierSearchDelegate = SupplierSearchDelegate(self)
|
||||
itemTypeSearchDelegate = ItemTypeSearchDelegate(self.file_type_keys, self)
|
||||
|
||||
# Set Delegates for Columns
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, supplierSearchDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ASSET_TYPE, comboBoxDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_TARGET_ASSET, lineEditDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, itemTypeSearchDelegate) # Use ItemTypeSearchDelegate
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_NAME, lineEditDelegate) # Assign LineEditDelegate for AssetRule names
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, itemTypeSearchDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_NAME, lineEditDelegate)
|
||||
|
||||
# Configure View Appearance
|
||||
self.unified_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self.unified_view.setAlternatingRowColors(True)
|
||||
self.unified_view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.unified_view.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed)
|
||||
self.unified_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) # Allow multi-select for re-interpret
|
||||
self.unified_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
|
||||
# Configure Header Resize Modes
|
||||
header = self.unified_view.header()
|
||||
header.setStretchLastSection(False)
|
||||
header.setSectionResizeMode(UnifiedViewModel.COL_NAME, QHeaderView.ResizeMode.ResizeToContents)
|
||||
@@ -165,31 +137,23 @@ class MainPanelWidget(QWidget):
|
||||
header.setSectionResizeMode(UnifiedViewModel.COL_ASSET_TYPE, QHeaderView.ResizeMode.ResizeToContents)
|
||||
header.setSectionResizeMode(UnifiedViewModel.COL_ITEM_TYPE, QHeaderView.ResizeMode.ResizeToContents)
|
||||
|
||||
# Enable custom context menu
|
||||
self.unified_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
|
||||
# --- Enable Drag and Drop ---
|
||||
self.unified_view.setDragEnabled(True)
|
||||
self.unified_view.setAcceptDrops(True)
|
||||
self.unified_view.setDropIndicatorShown(True)
|
||||
self.unified_view.setDefaultDropAction(Qt.MoveAction)
|
||||
# Use InternalMove for handling drops within the model itself
|
||||
self.unified_view.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
# Ensure ExtendedSelection is set (already done above, but good practice)
|
||||
self.unified_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
# --- End Drag and Drop ---
|
||||
|
||||
# Add the Unified View to the main layout
|
||||
main_layout.addWidget(self.unified_view, 1) # Give it stretch factor 1
|
||||
main_layout.addWidget(self.unified_view, 1)
|
||||
|
||||
# --- Progress Bar ---
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setTextVisible(True)
|
||||
self.progress_bar.setFormat("Idle") # Initial format
|
||||
self.progress_bar.setFormat("Idle")
|
||||
main_layout.addWidget(self.progress_bar)
|
||||
|
||||
# --- Blender Integration Controls ---
|
||||
blender_group = QGroupBox("Blender Post-Processing")
|
||||
blender_layout = QVBoxLayout(blender_group)
|
||||
|
||||
@@ -197,7 +161,6 @@ class MainPanelWidget(QWidget):
|
||||
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()
|
||||
@@ -207,7 +170,6 @@ class MainPanelWidget(QWidget):
|
||||
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()
|
||||
@@ -217,8 +179,6 @@ class MainPanelWidget(QWidget):
|
||||
materials_layout.addWidget(self.browse_materials_blend_button)
|
||||
blender_layout.addLayout(materials_layout)
|
||||
|
||||
# Initialize paths from config (Copied from MainWindow)
|
||||
# Consider passing these defaults from MainWindow
|
||||
if load_base_config:
|
||||
try:
|
||||
base_config = load_base_config()
|
||||
@@ -234,15 +194,13 @@ class MainPanelWidget(QWidget):
|
||||
log.warning("MainPanelWidget: load_base_config not available to set default Blender paths.")
|
||||
|
||||
|
||||
# Disable Blender controls initially if checkbox is unchecked
|
||||
self.nodegroup_blend_path_input.setEnabled(False)
|
||||
self.browse_nodegroup_blend_button.setEnabled(False)
|
||||
self.materials_blend_path_input.setEnabled(False)
|
||||
self.browse_materials_blend_button.setEnabled(False)
|
||||
|
||||
main_layout.addWidget(blender_group) # Add the group box to the main layout
|
||||
main_layout.addWidget(blender_group)
|
||||
|
||||
# --- 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.")
|
||||
@@ -263,11 +221,6 @@ class MainPanelWidget(QWidget):
|
||||
bottom_controls_layout.addWidget(self.workers_spinbox)
|
||||
bottom_controls_layout.addStretch(1)
|
||||
|
||||
# --- LLM Re-interpret Button (Removed, functionality moved to context menu) ---
|
||||
# self.llm_reinterpret_button = QPushButton("Re-interpret Selected with LLM")
|
||||
# self.llm_reinterpret_button.setToolTip("Re-run LLM interpretation on the selected source items.")
|
||||
# self.llm_reinterpret_button.setEnabled(False) # Initially disabled
|
||||
# bottom_controls_layout.addWidget(self.llm_reinterpret_button)
|
||||
|
||||
self.clear_queue_button = QPushButton("Clear Queue")
|
||||
self.start_button = QPushButton("Start Processing")
|
||||
@@ -281,37 +234,30 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
def _connect_signals(self):
|
||||
"""Connect internal UI signals to slots or emit panel signals."""
|
||||
# Output Directory
|
||||
self.browse_output_button.clicked.connect(self._browse_for_output_directory)
|
||||
self.output_path_edit.editingFinished.connect(self._on_output_path_changed) # Emit signal when user finishes editing
|
||||
self.output_path_edit.editingFinished.connect(self._on_output_path_changed)
|
||||
|
||||
# Unified View
|
||||
self.unified_view.customContextMenuRequested.connect(self._show_unified_view_context_menu)
|
||||
|
||||
# Blender Controls
|
||||
self.blender_integration_checkbox.toggled.connect(self._toggle_blender_controls)
|
||||
self.browse_nodegroup_blend_button.clicked.connect(self._browse_for_nodegroup_blend)
|
||||
self.browse_materials_blend_button.clicked.connect(self._browse_for_materials_blend)
|
||||
# Emit signal when paths change
|
||||
self.nodegroup_blend_path_input.editingFinished.connect(self._emit_blender_settings_changed)
|
||||
self.materials_blend_path_input.editingFinished.connect(self._emit_blender_settings_changed)
|
||||
self.blender_integration_checkbox.toggled.connect(self._emit_blender_settings_changed)
|
||||
|
||||
|
||||
# Bottom Buttons
|
||||
self.clear_queue_button.clicked.connect(self.clear_queue_requested) # Emit signal directly
|
||||
self.start_button.clicked.connect(self._on_start_processing_clicked) # Use slot to gather data
|
||||
self.cancel_button.clicked.connect(self.cancel_requested) # Emit signal directly
|
||||
# self.llm_reinterpret_button.clicked.connect(self._on_llm_reinterpret_clicked) # Removed button connection
|
||||
self.clear_queue_button.clicked.connect(self.clear_queue_requested)
|
||||
self.start_button.clicked.connect(self._on_start_processing_clicked)
|
||||
self.cancel_button.clicked.connect(self.cancel_requested)
|
||||
|
||||
# --- Slots for Internal UI Logic ---
|
||||
|
||||
@Slot()
|
||||
def _browse_for_output_directory(self):
|
||||
"""Opens a dialog to select the output directory."""
|
||||
current_path = self.output_path_edit.text()
|
||||
if not current_path or not Path(current_path).is_dir():
|
||||
current_path = str(self.project_root) # Use project root as fallback
|
||||
current_path = str(self.project_root)
|
||||
|
||||
directory = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
@@ -321,7 +267,7 @@ class MainPanelWidget(QWidget):
|
||||
)
|
||||
if directory:
|
||||
self.output_path_edit.setText(directory)
|
||||
self._on_output_path_changed() # Explicitly call the change handler
|
||||
self._on_output_path_changed()
|
||||
|
||||
@Slot()
|
||||
def _on_output_path_changed(self):
|
||||
@@ -335,7 +281,6 @@ class MainPanelWidget(QWidget):
|
||||
self.browse_nodegroup_blend_button.setEnabled(checked)
|
||||
self.materials_blend_path_input.setEnabled(checked)
|
||||
self.browse_materials_blend_button.setEnabled(checked)
|
||||
# No need to emit here, the checkbox toggle signal is connected separately
|
||||
|
||||
def _browse_for_blend_file(self, line_edit_widget: QLineEdit):
|
||||
"""Opens a dialog to select a .blend file and updates the line edit."""
|
||||
@@ -350,7 +295,7 @@ class MainPanelWidget(QWidget):
|
||||
)
|
||||
if file_path:
|
||||
line_edit_widget.setText(file_path)
|
||||
line_edit_widget.editingFinished.emit() # Trigger editingFinished to emit change signal
|
||||
line_edit_widget.editingFinished.emit()
|
||||
|
||||
@Slot()
|
||||
def _browse_for_nodegroup_blend(self):
|
||||
@@ -376,7 +321,6 @@ class MainPanelWidget(QWidget):
|
||||
QMessageBox.warning(self, "Missing Output Directory", "Please select an output directory.")
|
||||
return
|
||||
|
||||
# Basic validation (MainWindow should do more thorough validation)
|
||||
try:
|
||||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||||
except Exception as e:
|
||||
@@ -393,8 +337,6 @@ class MainPanelWidget(QWidget):
|
||||
}
|
||||
self.process_requested.emit(settings)
|
||||
|
||||
# Removed _update_llm_reinterpret_button_state as the button is removed.
|
||||
# Context menu actions will handle their own enabled state or rely on _on_llm_reinterpret_clicked checks.
|
||||
|
||||
def _get_unique_source_dirs_from_selection(self, selected_indexes: list[QModelIndex]) -> set[str]:
|
||||
"""
|
||||
@@ -407,19 +349,17 @@ class MainPanelWidget(QWidget):
|
||||
log.error("Unified view model not found.")
|
||||
return unique_source_dirs
|
||||
|
||||
processed_source_paths = set() # To avoid processing duplicates if multiple cells of the same source are selected
|
||||
processed_source_paths = set()
|
||||
|
||||
for index in selected_indexes:
|
||||
if not index.isValid():
|
||||
continue
|
||||
|
||||
# Use the model's getItem method for robust node retrieval
|
||||
item_node = model.getItem(index)
|
||||
source_rule_node = None
|
||||
|
||||
# Find the parent SourceRule node by traversing upwards using the index
|
||||
source_rule_node = None
|
||||
current_index = index # Start with the index of the selected item
|
||||
current_index = index
|
||||
while current_index.isValid():
|
||||
current_item = model.getItem(current_index)
|
||||
if isinstance(current_item, SourceRule):
|
||||
@@ -429,11 +369,9 @@ class MainPanelWidget(QWidget):
|
||||
# If loop finishes without break, source_rule_node remains None
|
||||
|
||||
if source_rule_node:
|
||||
# Use input_path attribute as defined in SourceRule
|
||||
source_path = getattr(source_rule_node, 'input_path', None)
|
||||
if source_path and source_path not in processed_source_paths:
|
||||
source_path_obj = Path(source_path)
|
||||
# Check if it's a directory or a zip file (common input types)
|
||||
if source_path_obj.is_dir() or (source_path_obj.is_file() and source_path_obj.suffix.lower() == '.zip'):
|
||||
log.debug(f"Identified source path for re-interpretation: {source_path}")
|
||||
unique_source_dirs.add(source_path)
|
||||
@@ -471,9 +409,7 @@ class MainPanelWidget(QWidget):
|
||||
def _on_reinterpret_preset_selected(self, preset_name: str, index: QModelIndex):
|
||||
"""Handles the selection of a preset from the re-interpret context sub-menu."""
|
||||
log.info(f"Preset re-interpretation requested: Preset='{preset_name}', Index='{index.row()},{index.column()}'")
|
||||
# Reuse logic from _on_llm_reinterpret_clicked to get selected source paths
|
||||
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
||||
# Use the helper method to get all selected source paths, not just the one clicked
|
||||
unique_source_dirs = self._get_unique_source_dirs_from_selection(selected_indexes)
|
||||
|
||||
if not unique_source_dirs:
|
||||
@@ -494,12 +430,11 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
model = self.unified_view.model()
|
||||
if not model: return
|
||||
item_node = model.getItem(index) # Use model's method
|
||||
item_node = model.getItem(index)
|
||||
|
||||
# Find the SourceRule node associated with the clicked index
|
||||
# Find the SourceRule node associated with the clicked index
|
||||
source_rule_node = None
|
||||
current_index = index # Start with the clicked index
|
||||
current_index = index
|
||||
while current_index.isValid():
|
||||
current_item = model.getItem(current_index)
|
||||
if isinstance(current_item, SourceRule):
|
||||
@@ -510,11 +445,9 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
menu = QMenu(self)
|
||||
|
||||
# --- Re-interpret Menu ---
|
||||
if source_rule_node: # Only show if we clicked on or within a SourceRule item
|
||||
reinterpet_menu = menu.addMenu("Re-interpret selected source")
|
||||
|
||||
# Get Preset Names (Option B: Direct File Listing)
|
||||
preset_names = []
|
||||
try:
|
||||
presets_dir = configuration.PRESETS_DIR
|
||||
@@ -523,17 +456,15 @@ class MainPanelWidget(QWidget):
|
||||
if filename.endswith(".json") and filename != "_template.json":
|
||||
preset_name = os.path.splitext(filename)[0]
|
||||
preset_names.append(preset_name)
|
||||
preset_names.sort() # Sort alphabetically
|
||||
preset_names.sort()
|
||||
else:
|
||||
log.warning(f"Presets directory not found or not a directory: {presets_dir}")
|
||||
except Exception as e:
|
||||
log.exception(f"Error listing presets in {configuration.PRESETS_DIR}: {e}")
|
||||
|
||||
# Populate Sub-Menu with Presets
|
||||
if preset_names:
|
||||
for preset_name in preset_names:
|
||||
preset_action = QAction(preset_name, self)
|
||||
# Pass the preset name and the *clicked* index (though the slot will get all selected)
|
||||
preset_action.triggered.connect(functools.partial(self._on_reinterpret_preset_selected, preset_name, index))
|
||||
reinterpet_menu.addAction(preset_action)
|
||||
else:
|
||||
@@ -542,39 +473,31 @@ class MainPanelWidget(QWidget):
|
||||
reinterpet_menu.addAction(no_presets_action)
|
||||
|
||||
|
||||
# Add LLM Option (Static)
|
||||
reinterpet_menu.addSeparator()
|
||||
llm_action = QAction("LLM", self)
|
||||
# Connect to the existing slot that handles LLM re-interpretation requests
|
||||
llm_action.triggered.connect(self._on_llm_reinterpret_clicked)
|
||||
# Disable if LLM is currently processing
|
||||
llm_action.setEnabled(not self.llm_processing_active)
|
||||
reinterpet_menu.addAction(llm_action)
|
||||
|
||||
menu.addSeparator() # Separator before other actions
|
||||
menu.addSeparator()
|
||||
|
||||
# --- Other Actions (like Copy LLM Example) ---
|
||||
if source_rule_node: # Check again if it's a source item for this action
|
||||
copy_llm_example_action = QAction("Copy LLM Example to Clipboard", self)
|
||||
copy_llm_example_action.setToolTip("Copies a JSON structure representing the input files and predicted output, suitable for LLM examples.")
|
||||
# Pass the found source_rule_node
|
||||
copy_llm_example_action.triggered.connect(lambda: self._copy_llm_example_to_clipboard(source_rule_node))
|
||||
menu.addAction(copy_llm_example_action)
|
||||
# menu.addSeparator() # Removed redundant separator
|
||||
|
||||
# Add other general actions here if needed...
|
||||
|
||||
if not menu.isEmpty():
|
||||
menu.exec(self.unified_view.viewport().mapToGlobal(point))
|
||||
|
||||
@Slot(SourceRule) # Accept SourceRule directly
|
||||
@Slot(SourceRule)
|
||||
def _copy_llm_example_to_clipboard(self, source_rule_node: SourceRule | None):
|
||||
"""Copies a JSON structure for the given SourceRule node to the clipboard."""
|
||||
if not source_rule_node:
|
||||
log.warning(f"No SourceRule node provided to copy LLM example.")
|
||||
return
|
||||
|
||||
# We already have the source_rule_node passed in
|
||||
source_rule: SourceRule = source_rule_node
|
||||
log.info(f"Attempting to generate LLM example JSON for source: {source_rule.input_path}")
|
||||
|
||||
@@ -612,11 +535,10 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
try:
|
||||
json_string = json.dumps(llm_example, indent=2)
|
||||
clipboard = QGuiApplication.clipboard() # Use QGuiApplication
|
||||
clipboard = QGuiApplication.clipboard()
|
||||
if clipboard:
|
||||
clipboard.setText(json_string)
|
||||
log.info(f"Copied LLM example JSON to clipboard for source: {source_rule.input_path}")
|
||||
# Cannot show status bar message here
|
||||
else:
|
||||
log.error("Failed to get system clipboard.")
|
||||
except Exception as e:
|
||||
@@ -630,10 +552,10 @@ class MainPanelWidget(QWidget):
|
||||
"""Updates the progress bar display."""
|
||||
if total_count > 0:
|
||||
percentage = int((current_count / total_count) * 100)
|
||||
log.debug(f"Updating progress bar: current={current_count}, total={total_count}, calculated_percentage={percentage}") # DEBUG LOG
|
||||
log.debug(f"Updating progress bar: current={current_count}, total={total_count}, calculated_percentage={percentage}")
|
||||
self.progress_bar.setValue(percentage)
|
||||
self.progress_bar.setFormat(f"%p% ({current_count}/{total_count})")
|
||||
QApplication.processEvents() # Force GUI update
|
||||
QApplication.processEvents()
|
||||
else:
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setFormat("0/0")
|
||||
@@ -642,7 +564,6 @@ class MainPanelWidget(QWidget):
|
||||
def set_progress_bar_text(self, text: str):
|
||||
"""Sets the text format of the progress bar."""
|
||||
self.progress_bar.setFormat(text)
|
||||
# Reset value if setting text like "Idle" or "Waiting..."
|
||||
if not "%" in text:
|
||||
self.progress_bar.setValue(0)
|
||||
|
||||
@@ -650,7 +571,6 @@ class MainPanelWidget(QWidget):
|
||||
@Slot(bool)
|
||||
def set_controls_enabled(self, enabled: bool):
|
||||
"""Enables or disables controls within the panel."""
|
||||
# Enable/disable most controls based on the 'enabled' flag
|
||||
self.output_path_edit.setEnabled(enabled)
|
||||
self.browse_output_button.setEnabled(enabled)
|
||||
self.unified_view.setEnabled(enabled)
|
||||
@@ -670,8 +590,6 @@ class MainPanelWidget(QWidget):
|
||||
self.materials_blend_path_input.setEnabled(blender_paths_enabled)
|
||||
self.browse_materials_blend_button.setEnabled(blender_paths_enabled)
|
||||
|
||||
# LLM button removed, no need to update its state here.
|
||||
# Context menu actions enable/disable themselves based on context (e.g., llm_processing_active).
|
||||
|
||||
|
||||
@Slot(bool)
|
||||
@@ -696,11 +614,11 @@ class MainPanelWidget(QWidget):
|
||||
# No button state to update directly, but context menu will check this flag when built.
|
||||
|
||||
# TODO: Add method to get current output path if needed by MainWindow before processing
|
||||
def get_output_directory(self) -> str:
|
||||
def get_output_directory() -> str:
|
||||
return self.output_path_edit.text().strip()
|
||||
|
||||
# TODO: Add method to get current Blender settings if needed by MainWindow before processing
|
||||
def get_blender_settings(self) -> dict:
|
||||
def get_blender_settings() -> dict:
|
||||
return {
|
||||
"enabled": self.blender_integration_checkbox.isChecked(),
|
||||
"nodegroup_blend_path": self.nodegroup_blend_path_input.text(),
|
||||
@@ -708,16 +626,14 @@ class MainPanelWidget(QWidget):
|
||||
}
|
||||
|
||||
# TODO: Add method to get current worker count if needed by MainWindow before processing
|
||||
def get_worker_count(self) -> int:
|
||||
def get_worker_count() -> int:
|
||||
return self.workers_spinbox.value()
|
||||
|
||||
# TODO: Add method to get current overwrite setting if needed by MainWindow before processing
|
||||
def get_overwrite_setting(self) -> bool:
|
||||
def get_overwrite_setting() -> bool:
|
||||
return self.overwrite_checkbox.isChecked()
|
||||
|
||||
# --- Delegate Dependency ---
|
||||
# This method might be needed by ComboBoxDelegate if it relies on MainWindow's logic
|
||||
def get_llm_source_preset_name(self) -> str | None:
|
||||
def get_llm_source_preset_name() -> str | None:
|
||||
"""
|
||||
Placeholder for providing context to delegates.
|
||||
Ideally, the required info (like last preset name) should be passed
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,12 @@
|
||||
# gui/rule_based_prediction_handler.py
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import time
|
||||
import os
|
||||
import re # Import regex
|
||||
import tempfile # Added for temporary extraction directory
|
||||
import zipfile # Added for zip file handling
|
||||
# import patoolib # Potential import for rar/7z - Add later if zip works
|
||||
from collections import defaultdict, Counter # Added Counter
|
||||
from typing import List, Dict, Any # For type hinting
|
||||
import re
|
||||
import tempfile
|
||||
import zipfile
|
||||
from collections import defaultdict, Counter
|
||||
from typing import List, Dict, Any
|
||||
|
||||
# --- PySide6 Imports ---
|
||||
from PySide6.QtCore import QObject, Slot # Keep QObject for parent type hint, Slot for classify_files if kept as method
|
||||
@@ -22,30 +20,23 @@ if str(project_root) not in sys.path:
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
try:
|
||||
from configuration import Configuration, ConfigurationError # load_base_config might not be needed here
|
||||
from configuration import Configuration, ConfigurationError
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
from .base_prediction_handler import BasePredictionHandler # Import the base class
|
||||
from .base_prediction_handler import BasePredictionHandler
|
||||
BACKEND_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
# Update error message source
|
||||
print(f"ERROR (RuleBasedPredictionHandler): Failed to import backend/config/base modules: {e}")
|
||||
# Define placeholders if imports fail
|
||||
Configuration = None
|
||||
load_base_config = None # Placeholder
|
||||
load_base_config = None
|
||||
ConfigurationError = Exception
|
||||
# AssetProcessingError = Exception
|
||||
SourceRule, AssetRule, FileRule = (None,)*3 # Placeholder for rule structures
|
||||
# Removed: AssetType, ItemType = (None,)*2 # Placeholder for types
|
||||
# Removed: app_config = None # Placeholder for config
|
||||
SourceRule, AssetRule, FileRule = (None,)*3
|
||||
BACKEND_AVAILABLE = False
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
# Basic config if logger hasn't been set up elsewhere
|
||||
if not log.hasHandlers():
|
||||
logging.basicConfig(level=logging.INFO, format='%(levelname)s (RuleBasedPredictHandler): %(message)s')
|
||||
|
||||
|
||||
# Helper function for classification (can be moved outside class if preferred)
|
||||
def classify_files(file_list: List[str], config: Configuration) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""
|
||||
Analyzes a list of files based on configuration rules using a two-pass approach
|
||||
@@ -71,16 +62,15 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
Returns an empty dict if classification fails or no files are provided.
|
||||
"""
|
||||
temp_grouped_files = defaultdict(list)
|
||||
extra_files_to_associate = [] # Store tuples: (file_path_str, filename) for Pass 2 association
|
||||
primary_asset_names = set() # Store asset names derived *only* from primary map files (populated in Pass 1)
|
||||
primary_assignments = set() # Stores tuples: (asset_name, target_type) (populated *only* in Pass 1)
|
||||
processed_in_pass1 = set() # Keep track of files handled in Pass 1
|
||||
extra_files_to_associate = []
|
||||
primary_asset_names = set()
|
||||
primary_assignments = set()
|
||||
processed_in_pass1 = set()
|
||||
|
||||
# --- Validation ---
|
||||
if not file_list or not config:
|
||||
log.warning("Classification skipped: Missing file list or config.")
|
||||
return {}
|
||||
# Access compiled regex directly from the config object
|
||||
if not hasattr(config, 'compiled_map_keyword_regex') or not config.compiled_map_keyword_regex:
|
||||
log.warning("Classification skipped: Missing compiled map keyword regex in config.")
|
||||
if not hasattr(config, 'compiled_extra_regex'):
|
||||
@@ -143,12 +133,10 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
if match:
|
||||
log.debug(f"PASS 1: File '{filename}' matched PRIORITIZED bit depth variant for type '{target_type}'.")
|
||||
matched_item_type = target_type
|
||||
is_gloss_flag = False # Bit depth variants are typically not gloss
|
||||
is_gloss_flag = False
|
||||
|
||||
# Check if primary already assigned (safety for overlapping patterns)
|
||||
if (asset_name, matched_item_type) in primary_assignments:
|
||||
log.warning(f"PASS 1: Primary assignment ({asset_name}, {matched_item_type}) already exists. File '{filename}' will be handled in Pass 2.")
|
||||
# Don't process here, let Pass 2 handle it as a general map or extra
|
||||
else:
|
||||
primary_assignments.add((asset_name, matched_item_type))
|
||||
log.debug(f" PASS 1: Added primary assignment: ({asset_name}, {matched_item_type})")
|
||||
@@ -163,9 +151,6 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
processed_in_pass1.add(file_path_str)
|
||||
processed = True
|
||||
break # Stop checking other variant patterns for this file
|
||||
# Log if not processed in this pass
|
||||
# if not processed:
|
||||
# log.debug(f"PASS 1: File '{filename}' did not match any prioritized variant.")
|
||||
|
||||
log.debug(f"--- Finished Pass 1. Primary assignments made: {primary_assignments} ---")
|
||||
|
||||
@@ -174,7 +159,7 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
for file_path_str in file_list:
|
||||
if file_path_str in processed_in_pass1:
|
||||
log.debug(f"PASS 2: Skipping '{Path(file_path_str).name}' (processed in Pass 1).")
|
||||
continue # Skip files already classified as prioritized variants
|
||||
continue
|
||||
|
||||
file_path = Path(file_path_str)
|
||||
filename = file_path.name
|
||||
@@ -186,20 +171,18 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
for extra_pattern in compiled_extra_regex:
|
||||
if extra_pattern.search(filename):
|
||||
log.debug(f"PASS 2: File '{filename}' matched EXTRA pattern: {extra_pattern.pattern}")
|
||||
# Don't group yet, just collect for later association
|
||||
extra_files_to_associate.append((file_path_str, filename))
|
||||
is_extra = True
|
||||
break
|
||||
|
||||
if is_extra:
|
||||
continue # Move to the next file if it's an extra
|
||||
continue
|
||||
|
||||
# 2. Check for General Map Files in Pass 2
|
||||
for target_type, patterns_list in compiled_map_regex.items():
|
||||
for compiled_regex, original_keyword, rule_index in patterns_list:
|
||||
match = compiled_regex.search(filename)
|
||||
if match:
|
||||
# Access rule details
|
||||
is_gloss_flag = False
|
||||
try:
|
||||
map_type_mapping_list = config.map_type_mapping
|
||||
@@ -213,24 +196,21 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
if (asset_name, target_type) in primary_assignments:
|
||||
log.debug(f"PASS 2: File '{filename}' matched '{original_keyword}' for type '{target_type}', but primary already assigned via Pass 1. Classifying as EXTRA.")
|
||||
matched_item_type = "EXTRA"
|
||||
is_gloss_flag = False # Extras are not gloss sources
|
||||
is_gloss_flag = False
|
||||
else:
|
||||
# No prioritized variant exists, assign the general map type
|
||||
log.debug(f"PASS 2: File '{filename}' matched '{original_keyword}' for item_type '{target_type}'.")
|
||||
matched_item_type = target_type
|
||||
# Do NOT add to primary_assignments here - only Pass 1 does that.
|
||||
# Do NOT add to primary_asset_names here either.
|
||||
|
||||
temp_grouped_files[asset_name].append({
|
||||
'file_path': file_path_str,
|
||||
'item_type': matched_item_type, # Could be target_type or EXTRA
|
||||
'item_type': matched_item_type,
|
||||
'asset_name': asset_name,
|
||||
'is_gloss_source': is_gloss_flag
|
||||
})
|
||||
is_map = True
|
||||
break # Stop checking patterns for this file
|
||||
break
|
||||
if is_map:
|
||||
break # Stop checking target types for this file
|
||||
break
|
||||
|
||||
# 3. Handle Unmatched Files in Pass 2 (Not Extra, Not Map)
|
||||
if not is_extra and not is_map:
|
||||
@@ -246,13 +226,12 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
|
||||
# --- Determine Primary Asset Name for Extra Association (using Pass 1 results) ---
|
||||
final_primary_asset_name = None
|
||||
if primary_asset_names: # Use names derived only from Pass 1 (prioritized variants)
|
||||
# Find the most common name among those derived from primary maps identified in Pass 1
|
||||
if primary_asset_names:
|
||||
primary_map_asset_names_pass1 = [
|
||||
f_info['asset_name']
|
||||
for asset_files in temp_grouped_files.values()
|
||||
for f_info in asset_files
|
||||
if f_info['asset_name'] in primary_asset_names and (f_info['asset_name'], f_info['item_type']) in primary_assignments # Ensure it was a Pass 1 assignment
|
||||
if f_info['asset_name'] in primary_asset_names and (f_info['asset_name'], f_info['item_type']) in primary_assignments
|
||||
]
|
||||
if primary_map_asset_names_pass1:
|
||||
name_counts = Counter(primary_map_asset_names_pass1)
|
||||
@@ -267,7 +246,6 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
log.warning("Primary asset names set (from Pass 1) was populated, but no corresponding groups found. Falling back.")
|
||||
|
||||
if not final_primary_asset_name:
|
||||
# Fallback: No primary maps found in Pass 1. Use the first asset group found overall.
|
||||
if temp_grouped_files and extra_files_to_associate:
|
||||
fallback_name = sorted(temp_grouped_files.keys())[0]
|
||||
final_primary_asset_name = fallback_name
|
||||
@@ -282,7 +260,6 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
if final_primary_asset_name and extra_files_to_associate:
|
||||
log.debug(f"Associating {len(extra_files_to_associate)} extra file(s) with primary asset '{final_primary_asset_name}'")
|
||||
for file_path_str, filename in extra_files_to_associate:
|
||||
# Check if file already exists in the group (e.g., if somehow classified twice)
|
||||
if not any(f['file_path'] == file_path_str for f in temp_grouped_files[final_primary_asset_name]):
|
||||
temp_grouped_files[final_primary_asset_name].append({
|
||||
'file_path': file_path_str,
|
||||
@@ -293,7 +270,6 @@ def classify_files(file_list: List[str], config: Configuration) -> Dict[str, Lis
|
||||
else:
|
||||
log.debug(f"Skipping duplicate association of extra file: {filename}")
|
||||
elif extra_files_to_associate:
|
||||
# Logged warning above if final_primary_asset_name couldn't be determined
|
||||
pass
|
||||
|
||||
|
||||
@@ -321,8 +297,6 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
super().__init__(input_source_identifier, parent)
|
||||
self.original_input_paths = original_input_paths
|
||||
self.preset_name = preset_name
|
||||
# _is_running is handled by the base class
|
||||
# Keep track of the current request being processed by this persistent handler
|
||||
self._current_input_path = None
|
||||
self._current_file_list = None
|
||||
self._current_preset_name = None
|
||||
@@ -341,18 +315,16 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
# Allow re-triggering for the *same* source if needed (e.g., preset changed)
|
||||
if self._is_running and self._current_input_path != input_source_identifier:
|
||||
log.warning(f"RuleBasedPredictionHandler is busy with '{self._current_input_path}'. Ignoring request for '{input_source_identifier}'.")
|
||||
# Optionally emit an error signal specific to this condition
|
||||
# self.prediction_error.emit(input_source_identifier, "Handler busy with another prediction.")
|
||||
return
|
||||
|
||||
self._is_running = True
|
||||
self._is_cancelled = False # Reset cancellation flag for new request
|
||||
self._is_cancelled = False
|
||||
self._current_input_path = input_source_identifier
|
||||
self._current_file_list = original_input_paths
|
||||
self._current_preset_name = preset_name
|
||||
|
||||
log.info(f"Starting rule-based prediction for: {input_source_identifier} using preset: {preset_name}")
|
||||
self.status_update.emit(f"Starting analysis for '{Path(input_source_identifier).name}'...") # Use base signal
|
||||
self.status_update.emit(f"Starting analysis for '{Path(input_source_identifier).name}'...")
|
||||
|
||||
source_rules_list = []
|
||||
try:
|
||||
@@ -362,9 +334,8 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
if not preset_name:
|
||||
log.warning("No preset selected for prediction.")
|
||||
self.status_update.emit("No preset selected.")
|
||||
# Emit empty list for non-critical issues, signal completion
|
||||
self.prediction_ready.emit(input_source_identifier, [])
|
||||
self._is_running = False # Mark as finished
|
||||
self._is_running = False
|
||||
return
|
||||
|
||||
source_path = Path(input_source_identifier)
|
||||
@@ -391,15 +362,13 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
if not classified_assets:
|
||||
log.warning(f"Classification yielded no assets for source '{input_source_identifier}'.")
|
||||
self.status_update.emit("No assets identified from files.")
|
||||
# Emit empty list, signal completion
|
||||
self.prediction_ready.emit(input_source_identifier, [])
|
||||
self._is_running = False # Mark as finished
|
||||
self._is_running = False
|
||||
return
|
||||
|
||||
# --- Build the Hierarchy ---
|
||||
self.status_update.emit(f"Building rule hierarchy for '{source_path.name}'...")
|
||||
try:
|
||||
# (Hierarchy building logic remains the same as before)
|
||||
supplier_identifier = config.supplier_name
|
||||
source_rule = SourceRule(
|
||||
input_path=input_source_identifier,
|
||||
@@ -407,7 +376,6 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
preset_name=preset_name
|
||||
)
|
||||
asset_rules = []
|
||||
# asset_type_definitions = config._core_settings.get('ASSET_TYPE_DEFINITIONS', {}) # Use accessor
|
||||
file_type_definitions = config._core_settings.get('FILE_TYPE_DEFINITIONS', {})
|
||||
|
||||
for asset_name, files_info in classified_assets.items():
|
||||
@@ -415,7 +383,7 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
if not files_info: continue
|
||||
|
||||
asset_category_rules = config.asset_category_rules
|
||||
asset_type_definitions = config.get_asset_type_definitions() # Use new accessor
|
||||
asset_type_definitions = config.get_asset_type_definitions()
|
||||
asset_type_keys = list(asset_type_definitions.keys())
|
||||
|
||||
# Initialize predicted_asset_type using the validated default
|
||||
@@ -427,9 +395,8 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
|
||||
# Check for Model type based on file patterns
|
||||
if "Model" in asset_type_keys:
|
||||
model_patterns_regex = config.compiled_model_regex # Already compiled
|
||||
model_patterns_regex = config.compiled_model_regex
|
||||
for f_info in files_info:
|
||||
# Only consider files not marked as EXTRA or FILE_IGNORE for model classification
|
||||
if f_info['item_type'] in ["EXTRA", "FILE_IGNORE"]:
|
||||
continue
|
||||
file_path_obj = Path(f_info['file_path'])
|
||||
@@ -447,9 +414,9 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
decal_keywords = asset_category_rules.get('decal_keywords', [])
|
||||
for keyword in decal_keywords:
|
||||
# Ensure keyword is a string before trying to escape it
|
||||
if isinstance(keyword, str) and keyword: # Added check for non-empty string
|
||||
if isinstance(keyword, str) and keyword:
|
||||
try:
|
||||
if re.search(r'\b' + re.escape(keyword) + r'\b', asset_name, re.IGNORECASE): # Match whole word
|
||||
if re.search(r'\b' + re.escape(keyword) + r'\b', asset_name, re.IGNORECASE):
|
||||
predicted_asset_type = "Decal"
|
||||
determined_by_rule = True
|
||||
log.debug(f"Asset '{asset_name}' classified as 'Decal' due to keyword '{keyword}'.")
|
||||
@@ -457,7 +424,7 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
except re.error as e_re:
|
||||
log.warning(f"Regex error with decal_keyword '{keyword}': {e_re}")
|
||||
if determined_by_rule:
|
||||
pass # Already logged if Decal
|
||||
pass
|
||||
|
||||
# 2. If not determined by specific rules, check for Surface (if not Model/Decal by rule)
|
||||
if not determined_by_rule and predicted_asset_type == config.default_asset_category and "Surface" in asset_type_keys:
|
||||
@@ -489,13 +456,10 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
log.debug(f"Asset '{asset_name}' classified as 'Surface' due to material indicators.")
|
||||
|
||||
# 3. Final validation: Ensure predicted_asset_type is a valid key.
|
||||
# config.default_asset_category is already validated to be a key.
|
||||
if predicted_asset_type not in asset_type_keys:
|
||||
log.warning(f"Derived AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. "
|
||||
f"Falling back to default: '{config.default_asset_category}'.")
|
||||
predicted_asset_type = config.default_asset_category
|
||||
# This case should ideally not be hit if logic above correctly uses asset_type_keys
|
||||
# and default_asset_category is valid.
|
||||
|
||||
asset_rule = AssetRule(asset_name=asset_name, asset_type=predicted_asset_type)
|
||||
file_rules = []
|
||||
@@ -512,9 +476,6 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
log.warning(f"Predicted ItemType '{base_item_type}' (checked as '{final_item_type}') for file '{file_info['file_path']}' is not in FILE_TYPE_DEFINITIONS. Setting to FILE_IGNORE.")
|
||||
final_item_type = "FILE_IGNORE"
|
||||
|
||||
# standard_map_type is no longer stored on FileRule
|
||||
# It will be looked up from config when needed for naming/output
|
||||
# Remove the logic that determined and assigned it here.
|
||||
|
||||
is_gloss_source_value = file_info.get('is_gloss_source', False)
|
||||
|
||||
@@ -540,22 +501,20 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
|
||||
# --- Emit Success Signal ---
|
||||
log.info(f"Rule-based prediction finished successfully for '{input_source_identifier}'.")
|
||||
self.prediction_ready.emit(input_source_identifier, source_rules_list) # Use base signal
|
||||
self.prediction_ready.emit(input_source_identifier, source_rules_list)
|
||||
|
||||
except Exception as e:
|
||||
# --- Emit Error Signal ---
|
||||
log.exception(f"Error during rule-based prediction for '{input_source_identifier}': {e}")
|
||||
error_msg = f"Error analyzing '{Path(input_source_identifier).name}': {e}"
|
||||
self.prediction_error.emit(input_source_identifier, error_msg) # Use base signal
|
||||
self.prediction_error.emit(input_source_identifier, error_msg)
|
||||
|
||||
finally:
|
||||
# --- Cleanup ---
|
||||
self._is_running = False
|
||||
self._current_input_path = None # Clear current task info
|
||||
self._current_input_path = None
|
||||
self._current_file_list = None
|
||||
self._current_preset_name = None
|
||||
log.info(f"Finished rule-based prediction run for: {input_source_identifier}")
|
||||
def is_running(self) -> bool:
|
||||
"""Returns True if the handler is currently processing a prediction request."""
|
||||
# The _is_running flag is managed by the base class or the run_prediction method
|
||||
return self._is_running
|
||||
|
||||
@@ -18,7 +18,7 @@ from PySide6.QtGui import QAction # Keep QAction if needed for context menus wit
|
||||
# Assuming project root is parent of the directory containing this file
|
||||
script_dir = Path(__file__).parent
|
||||
project_root = script_dir.parent
|
||||
PRESETS_DIR = project_root / "Presets" # Corrected path
|
||||
PRESETS_DIR = project_root / "Presets"
|
||||
TEMPLATE_PATH = PRESETS_DIR / "_template.json"
|
||||
APP_SETTINGS_PATH_LOCAL = project_root / "config" / "app_settings.json"
|
||||
|
||||
@@ -51,10 +51,10 @@ class PresetEditorWidget(QWidget):
|
||||
self._init_ui()
|
||||
|
||||
# --- Initial State ---
|
||||
self._ftd_keys = self._get_file_type_definition_keys() # Load FTD keys
|
||||
self._clear_editor() # Clear/disable editor fields initially
|
||||
self._set_editor_enabled(False) # Disable editor initially
|
||||
self.populate_presets() # Populate preset list
|
||||
self._ftd_keys = self._get_file_type_definition_keys()
|
||||
self._clear_editor()
|
||||
self._set_editor_enabled(False)
|
||||
self.populate_presets()
|
||||
|
||||
# --- Connect Editor Signals ---
|
||||
self._connect_editor_change_signals()
|
||||
@@ -91,7 +91,7 @@ class PresetEditorWidget(QWidget):
|
||||
selector_layout.addWidget(QLabel("Presets:"))
|
||||
self.editor_preset_list = QListWidget()
|
||||
self.editor_preset_list.currentItemChanged.connect(self._load_selected_preset_for_editing)
|
||||
selector_layout.addWidget(self.editor_preset_list) # Corrected: Add to selector_layout
|
||||
selector_layout.addWidget(self.editor_preset_list)
|
||||
|
||||
list_button_layout = QHBoxLayout()
|
||||
self.editor_new_button = QPushButton("New")
|
||||
@@ -101,7 +101,7 @@ class PresetEditorWidget(QWidget):
|
||||
list_button_layout.addWidget(self.editor_new_button)
|
||||
list_button_layout.addWidget(self.editor_delete_button)
|
||||
selector_layout.addLayout(list_button_layout)
|
||||
main_layout.addWidget(self.selector_container) # Add selector container to main layout
|
||||
main_layout.addWidget(self.selector_container)
|
||||
|
||||
# Editor Tabs
|
||||
self.json_editor_container = QWidget()
|
||||
@@ -121,7 +121,7 @@ class PresetEditorWidget(QWidget):
|
||||
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.setEnabled(False)
|
||||
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()
|
||||
@@ -129,7 +129,7 @@ class PresetEditorWidget(QWidget):
|
||||
save_button_layout.addWidget(self.editor_save_as_button)
|
||||
editor_layout.addLayout(save_button_layout)
|
||||
|
||||
main_layout.addWidget(self.json_editor_container) # Add editor container to main layout
|
||||
main_layout.addWidget(self.json_editor_container)
|
||||
|
||||
def _create_editor_general_tab(self):
|
||||
"""Creates the widgets and layout for the 'General & Naming' editor tab."""
|
||||
@@ -206,7 +206,7 @@ class PresetEditorWidget(QWidget):
|
||||
list_widget = QListWidget()
|
||||
list_widget.setAlternatingRowColors(True)
|
||||
list_widget.setEditTriggers(QAbstractItemView.EditTrigger.DoubleClicked | QAbstractItemView.EditTrigger.SelectedClicked | QAbstractItemView.EditTrigger.EditKeyPressed)
|
||||
setattr(self, attribute_name, list_widget) # Store list widget on the instance
|
||||
setattr(self, attribute_name, list_widget)
|
||||
|
||||
add_button = QPushButton("+")
|
||||
remove_button = QPushButton("-")
|
||||
@@ -231,7 +231,7 @@ class PresetEditorWidget(QWidget):
|
||||
# Connections
|
||||
add_button.clicked.connect(partial(self._editor_add_list_item, list_widget))
|
||||
remove_button.clicked.connect(partial(self._editor_remove_list_item, list_widget))
|
||||
list_widget.itemChanged.connect(self._mark_editor_unsaved) # Mark unsaved on item edit
|
||||
list_widget.itemChanged.connect(self._mark_editor_unsaved)
|
||||
|
||||
def _setup_table_widget_with_controls(self, parent_layout, label_text, attribute_name, columns):
|
||||
"""Adds a QTableWidget with Add/Remove buttons to a layout."""
|
||||
@@ -239,7 +239,7 @@ class PresetEditorWidget(QWidget):
|
||||
table_widget.setColumnCount(len(columns))
|
||||
table_widget.setHorizontalHeaderLabels(columns)
|
||||
table_widget.setAlternatingRowColors(True)
|
||||
setattr(self, attribute_name, table_widget) # Store table widget
|
||||
setattr(self, attribute_name, table_widget)
|
||||
|
||||
add_button = QPushButton("+ Row")
|
||||
remove_button = QPushButton("- Row")
|
||||
@@ -259,7 +259,7 @@ class PresetEditorWidget(QWidget):
|
||||
# Connections
|
||||
add_button.clicked.connect(partial(self._editor_add_table_row, table_widget))
|
||||
remove_button.clicked.connect(partial(self._editor_remove_table_row, table_widget))
|
||||
table_widget.itemChanged.connect(self._mark_editor_unsaved) # Mark unsaved on item edit
|
||||
table_widget.itemChanged.connect(self._mark_editor_unsaved)
|
||||
|
||||
# --- Preset Population and Handling ---
|
||||
def populate_presets(self):
|
||||
@@ -271,14 +271,12 @@ class PresetEditorWidget(QWidget):
|
||||
self.editor_preset_list.clear()
|
||||
log.debug("Preset list cleared.")
|
||||
|
||||
# Add the "Select a Preset" placeholder item
|
||||
placeholder_item = QListWidgetItem("--- Select a Preset ---")
|
||||
placeholder_item.setFlags(placeholder_item.flags() & ~Qt.ItemFlag.ItemIsSelectable & ~Qt.ItemFlag.ItemIsEditable)
|
||||
placeholder_item.setData(Qt.ItemDataRole.UserRole, "__PLACEHOLDER__")
|
||||
self.editor_preset_list.addItem(placeholder_item)
|
||||
log.debug("Added '--- Select a Preset ---' placeholder item.")
|
||||
|
||||
# Add LLM Option
|
||||
llm_item = QListWidgetItem("- LLM Interpretation -")
|
||||
llm_item.setData(Qt.ItemDataRole.UserRole, "__LLM__") # Special identifier
|
||||
self.editor_preset_list.addItem(llm_item)
|
||||
@@ -287,7 +285,6 @@ class PresetEditorWidget(QWidget):
|
||||
if not PRESETS_DIR.is_dir():
|
||||
msg = f"Error: Presets directory not found at {PRESETS_DIR}"
|
||||
log.error(msg)
|
||||
# Consider emitting a status signal to MainWindow?
|
||||
return
|
||||
|
||||
presets = sorted([f for f in PRESETS_DIR.glob("*.json") if f.is_file() and not f.name.startswith('_')])
|
||||
@@ -298,13 +295,13 @@ class PresetEditorWidget(QWidget):
|
||||
else:
|
||||
for preset_path in presets:
|
||||
item = QListWidgetItem(preset_path.stem)
|
||||
item.setData(Qt.ItemDataRole.UserRole, preset_path) # Store full path
|
||||
item.setData(Qt.ItemDataRole.UserRole, preset_path)
|
||||
self.editor_preset_list.addItem(item)
|
||||
log.info(f"Loaded {len(presets)} presets into editor list.")
|
||||
|
||||
# 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
|
||||
self.editor_preset_list.setCurrentItem(placeholder_item)
|
||||
|
||||
# --- Preset Editor Methods ---
|
||||
|
||||
@@ -335,7 +332,7 @@ class PresetEditorWidget(QWidget):
|
||||
combo_box.addItems(self._ftd_keys)
|
||||
else:
|
||||
log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping.")
|
||||
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved) # Mark unsaved on change
|
||||
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved)
|
||||
table_widget.setCellWidget(row_count, 0, combo_box)
|
||||
# Column 1: Input Keywords (QTableWidgetItem)
|
||||
table_widget.setItem(row_count, 1, QTableWidgetItem(""))
|
||||
@@ -358,9 +355,6 @@ class PresetEditorWidget(QWidget):
|
||||
if self._is_loading_editor: return
|
||||
self.editor_unsaved_changes = True
|
||||
self.editor_save_button.setEnabled(True)
|
||||
# Update window title (handled by MainWindow) - maybe emit signal?
|
||||
# preset_name = Path(self.current_editing_preset_path).name if self.current_editing_preset_path else 'New Preset'
|
||||
# self.window().setWindowTitle(f"Asset Processor Tool - {preset_name}*") # Access parent window
|
||||
|
||||
def _connect_editor_change_signals(self):
|
||||
"""Connect signals from all editor widgets to mark_editor_unsaved."""
|
||||
@@ -417,7 +411,6 @@ class PresetEditorWidget(QWidget):
|
||||
self.current_editing_preset_path = None
|
||||
self.editor_unsaved_changes = False
|
||||
self.editor_save_button.setEnabled(False)
|
||||
# self.window().setWindowTitle("Asset Processor Tool") # Reset window title (handled by MainWindow)
|
||||
self._set_editor_enabled(False)
|
||||
finally:
|
||||
self._is_loading_editor = False
|
||||
@@ -465,7 +458,7 @@ class PresetEditorWidget(QWidget):
|
||||
else:
|
||||
log.warning("FILE_TYPE_DEFINITIONS keys not available for ComboBox in map_type_mapping during population.")
|
||||
|
||||
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved) # Connect signal
|
||||
combo_box.currentIndexChanged.connect(self._mark_editor_unsaved)
|
||||
self.editor_table_map_type_mapping.setCellWidget(i, 0, combo_box)
|
||||
|
||||
# Column 1: Input Keywords (QTableWidgetItem)
|
||||
@@ -514,7 +507,6 @@ class PresetEditorWidget(QWidget):
|
||||
self.current_editing_preset_path = file_path
|
||||
self.editor_unsaved_changes = False
|
||||
self.editor_save_button.setEnabled(False)
|
||||
# self.window().setWindowTitle(f"Asset Processor Tool - {file_path.name}") # Handled by MainWindow
|
||||
log.info(f"Preset '{file_path.name}' loaded into editor.")
|
||||
except json.JSONDecodeError as json_err:
|
||||
log.error(f"Invalid JSON in {file_path.name}: {json_err}")
|
||||
@@ -562,7 +554,7 @@ class PresetEditorWidget(QWidget):
|
||||
log.debug(f"Loading preset for editing: {current_item.text()}")
|
||||
preset_path = item_data
|
||||
self._load_preset_for_editing(preset_path)
|
||||
self._last_valid_preset_name = preset_path.stem # Store the name
|
||||
self._last_valid_preset_name = preset_path.stem
|
||||
mode = "preset"
|
||||
preset_name = self._last_valid_preset_name
|
||||
else:
|
||||
@@ -656,10 +648,8 @@ class PresetEditorWidget(QWidget):
|
||||
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.window().setWindowTitle(f"Asset Processor Tool - {self.current_editing_preset_path.name}") # Handled by MainWindow
|
||||
self.presets_changed_signal.emit() # Signal that presets changed
|
||||
self.presets_changed_signal.emit()
|
||||
log.info("Preset saved successfully.")
|
||||
# Refresh list within the editor
|
||||
self.populate_presets()
|
||||
# Reselect the saved item
|
||||
items = self.editor_preset_list.findItems(self.current_editing_preset_path.stem, Qt.MatchFlag.MatchExactly)
|
||||
@@ -690,11 +680,10 @@ class PresetEditorWidget(QWidget):
|
||||
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.current_editing_preset_path = save_path
|
||||
self.editor_unsaved_changes = False
|
||||
self.editor_save_button.setEnabled(False)
|
||||
# self.window().setWindowTitle(f"Asset Processor Tool - {save_path.name}") # Handled by MainWindow
|
||||
self.presets_changed_signal.emit() # Signal change
|
||||
self.presets_changed_signal.emit()
|
||||
log.info("Preset saved successfully (Save As).")
|
||||
# Refresh list and select the new item
|
||||
self.populate_presets()
|
||||
@@ -718,18 +707,15 @@ class PresetEditorWidget(QWidget):
|
||||
self._populate_editor_from_data(template_data)
|
||||
# Override specific fields for a new preset
|
||||
self.editor_preset_name.setText("NewPreset")
|
||||
# self.window().setWindowTitle("Asset Processor Tool - New Preset*") # Handled by MainWindow
|
||||
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.window().setWindowTitle("Asset Processor Tool - New Preset*") # Handled by MainWindow
|
||||
self.editor_supplier_name.setText("MySupplier") # Set a default supplier name
|
||||
self.editor_supplier_name.setText("MySupplier")
|
||||
else:
|
||||
log.warning("Presets/_template.json not found. Creating empty preset.")
|
||||
# self.window().setWindowTitle("Asset Processor Tool - New Preset*") # Handled by MainWindow
|
||||
self.editor_preset_name.setText("NewPreset")
|
||||
self.editor_supplier_name.setText("MySupplier") # Set a default supplier name
|
||||
self.editor_supplier_name.setText("MySupplier")
|
||||
self._set_editor_enabled(True)
|
||||
self.editor_unsaved_changes = True
|
||||
self.editor_save_button.setEnabled(True)
|
||||
@@ -761,8 +747,7 @@ class PresetEditorWidget(QWidget):
|
||||
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 list
|
||||
self.presets_changed_signal.emit()
|
||||
self.populate_presets()
|
||||
except Exception as e:
|
||||
log.exception(f"Error deleting preset file {preset_path}: {e}")
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import logging # Import logging
|
||||
import logging
|
||||
import time # For logging timestamps
|
||||
from PySide6.QtCore import QAbstractTableModel, Qt, QModelIndex, QSortFilterProxyModel, QThread # Import QThread
|
||||
from PySide6.QtCore import QAbstractTableModel, Qt, QModelIndex, QSortFilterProxyModel, QThread
|
||||
from PySide6.QtGui import QColor
|
||||
|
||||
log = logging.getLogger(__name__) # Get logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Define colors for alternating asset groups
|
||||
COLOR_ASSET_GROUP_1 = QColor("#292929") # Dark grey 1
|
||||
COLOR_ASSET_GROUP_2 = QColor("#343434") # Dark grey 2
|
||||
|
||||
# Define text colors for statuses
|
||||
class PreviewTableModel(QAbstractTableModel):
|
||||
"""
|
||||
Custom table model for the GUI preview table.
|
||||
@@ -54,11 +53,11 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
self._headers_detailed = ["Status", "Predicted Asset", "Original Path", "Predicted Output", "Details", "Additional Files"] # Added new column header
|
||||
self._sorted_unique_assets = [] # Store sorted unique asset names for coloring
|
||||
self._headers_simple = ["Input Path"]
|
||||
self.set_data(data or []) # Initialize data and simple_data
|
||||
self.set_data(data or [])
|
||||
|
||||
def set_simple_mode(self, enabled: bool):
|
||||
"""Toggles the model between detailed and simple view modes."""
|
||||
thread_id = QThread.currentThread() # Get current thread object
|
||||
thread_id = QThread.currentThread()
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] --> Entered PreviewTableModel.set_simple_mode(enabled={enabled}). Current mode: {self._simple_mode}")
|
||||
if self._simple_mode != enabled:
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Calling beginResetModel()...")
|
||||
@@ -78,7 +77,6 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
if parent.isValid():
|
||||
return 0
|
||||
row_count = len(self._simple_data) if self._simple_mode else len(self._table_rows) # Use _table_rows for detailed mode
|
||||
# log.debug(f"PreviewTableModel.rowCount called. Mode: {self._simple_mode}, Row Count: {row_count}")
|
||||
return row_count
|
||||
|
||||
def columnCount(self, parent=QModelIndex()):
|
||||
@@ -86,7 +84,6 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
if parent.isValid():
|
||||
return 0
|
||||
col_count = len(self._headers_simple) if self._simple_mode else len(self._headers_detailed) # Use updated headers_detailed
|
||||
# log.debug(f"PreviewTableModel.columnCount called. Mode: {self._simple_mode}, Column Count: {col_count}")
|
||||
return col_count
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
|
||||
@@ -100,8 +97,7 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
# --- Simple Mode ---
|
||||
if self._simple_mode:
|
||||
if row >= len(self._simple_data):
|
||||
# log.warning(f"data called with out of bounds row in simple mode: {row}/{len(self._simple_data)}")
|
||||
return None # Bounds check
|
||||
return None
|
||||
source_asset_path = self._simple_data[row]
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if col == self.COL_SIMPLE_PATH:
|
||||
@@ -113,12 +109,10 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
|
||||
# --- Detailed Mode ---
|
||||
if row >= len(self._table_rows): # Use _table_rows
|
||||
# log.warning(f"data called with out of bounds row in detailed mode: {row}/{len(self._table_rows)}")
|
||||
return None # Bounds check
|
||||
return None
|
||||
row_data = self._table_rows[row] # Get data from the structured row
|
||||
|
||||
# --- Handle Custom Internal Roles ---
|
||||
# These roles are now handled by the proxy model based on the structured data
|
||||
if role == self.ROLE_RAW_STATUS:
|
||||
# Return status of the main file if it exists, otherwise a placeholder for additional rows
|
||||
main_file = row_data.get('main_file')
|
||||
@@ -132,7 +126,7 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
main_file = row_data.get('main_file')
|
||||
if main_file:
|
||||
raw_status = main_file.get('status', '[No Status]')
|
||||
details = main_file.get('details', '') # Get details for parsing
|
||||
details = main_file.get('details', '')
|
||||
|
||||
# Implement status text simplification
|
||||
if raw_status == "Unmatched Extra":
|
||||
@@ -268,14 +262,6 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
return None
|
||||
|
||||
|
||||
# --- Handle Text Alignment Role ---
|
||||
if role == Qt.ItemDataRole.TextAlignmentRole:
|
||||
if col == self.COL_ORIGINAL_PATH:
|
||||
return int(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
||||
elif col == self.COL_ADDITIONAL_FILES:
|
||||
return int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
# For other columns, return default alignment (or None)
|
||||
return None
|
||||
|
||||
|
||||
return None
|
||||
@@ -291,7 +277,7 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
def set_data(self, data: list):
|
||||
"""Sets the model's data, extracts simple data, and emits signals."""
|
||||
# Removed diagnostic import here
|
||||
thread_id = QThread.currentThread() # Get current thread object
|
||||
thread_id = QThread.currentThread()
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] --> Entered PreviewTableModel.set_data. Received {len(data)} items.")
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] Calling beginResetModel()...")
|
||||
self.beginResetModel()
|
||||
@@ -357,7 +343,7 @@ class PreviewTableModel(QAbstractTableModel):
|
||||
|
||||
def clear_data(self):
|
||||
"""Clears the model's data."""
|
||||
thread_id = QThread.currentThread() # Get current thread object
|
||||
thread_id = QThread.currentThread()
|
||||
log.info(f"[{time.time():.4f}][T:{thread_id}] PreviewTableModel.clear_data called.")
|
||||
self.set_data([])
|
||||
|
||||
@@ -398,21 +384,18 @@ class PreviewSortFilterProxyModel(QSortFilterProxyModel):
|
||||
"""
|
||||
model = self.sourceModel()
|
||||
if not model:
|
||||
# log.debug("ProxyModel.lessThan: No source model.")
|
||||
return super().lessThan(left, right) # Fallback if no source model
|
||||
|
||||
# If in simple mode, sort by the simple path column
|
||||
if isinstance(model, PreviewTableModel) and model._simple_mode:
|
||||
left_path = model.data(left.siblingAtColumn(model.COL_SIMPLE_PATH), Qt.ItemDataRole.DisplayRole)
|
||||
right_path = model.data(right.siblingAtColumn(model.COL_SIMPLE_PATH), Qt.ItemDataRole.DisplayRole)
|
||||
# log.debug(f"ProxyModel.lessThan (Simple Mode): Comparing '{left_path}' < '{right_path}'")
|
||||
if not left_path: return True
|
||||
if not right_path: return False
|
||||
return left_path < right_path
|
||||
|
||||
|
||||
# --- Detailed Mode Sorting ---
|
||||
# log.debug("ProxyModel.lessThan (Detailed Mode).")
|
||||
# Get the full row data from the source model's _table_rows
|
||||
left_row_data = model._table_rows[left.row()]
|
||||
right_row_data = model._table_rows[right.row()]
|
||||
|
||||
@@ -3,16 +3,12 @@ from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QLine
|
||||
QFormLayout, QComboBox, QCheckBox, QSpinBox, QDoubleSpinBox)
|
||||
from PySide6.QtCore import Signal, Slot, QObject
|
||||
|
||||
# Assuming rule_structure.py is in the parent directory or accessible via PYTHONPATH
|
||||
# from ..rule_structure import SourceRule, AssetRule, FileRule # Adjust import based on actual structure
|
||||
# For now, we'll use placeholder classes or assume rule_structure is directly importable
|
||||
# from rule_structure import SourceRule, AssetRule, FileRule # Assuming direct import is possible
|
||||
|
||||
class RuleEditorWidget(QWidget):
|
||||
"""
|
||||
A widget to display and edit hierarchical processing rules (Source, Asset, File).
|
||||
"""
|
||||
rule_updated = Signal(object) # Signal emitted when a rule is updated
|
||||
rule_updated = Signal(object)
|
||||
|
||||
def __init__(self, asset_types: list[str] | None = None, file_types: list[str] | None = None, parent=None):
|
||||
"""
|
||||
@@ -24,8 +20,8 @@ class RuleEditorWidget(QWidget):
|
||||
parent: The parent widget.
|
||||
"""
|
||||
super().__init__(parent)
|
||||
self.asset_types = asset_types if asset_types else [] # Store asset types
|
||||
self.file_types = file_types if file_types else [] # Store file types
|
||||
self.asset_types = asset_types if asset_types else []
|
||||
self.file_types = file_types if file_types else []
|
||||
self.current_rule_type = None
|
||||
self.current_rule_object = None
|
||||
|
||||
@@ -65,7 +61,6 @@ class RuleEditorWidget(QWidget):
|
||||
editor_widget = self._create_editor_widget(attr_name, attr_value)
|
||||
if editor_widget:
|
||||
self.form_layout.addRow(label, editor_widget)
|
||||
# Connect signal to update rule object
|
||||
self._connect_editor_signal(editor_widget, attr_name)
|
||||
|
||||
def _create_editor_widget(self, attr_name, attr_value):
|
||||
@@ -79,7 +74,6 @@ class RuleEditorWidget(QWidget):
|
||||
# Handle None case for override: if None, don't select anything or select a placeholder
|
||||
if attr_value is None and attr_name == 'asset_type_override':
|
||||
# Optionally add a placeholder like "<None>" or "<Default>"
|
||||
# widget.insertItem(0, "<Default>") # Example placeholder
|
||||
widget.setCurrentIndex(-1) # No selection or placeholder
|
||||
elif attr_value in self.asset_types:
|
||||
widget.setCurrentText(attr_value)
|
||||
@@ -114,12 +108,6 @@ class RuleEditorWidget(QWidget):
|
||||
widget = QLineEdit()
|
||||
widget.setText(str(attr_value) if attr_value is not None else "")
|
||||
return widget
|
||||
# Add more types as needed
|
||||
# elif isinstance(attr_value, list):
|
||||
# # Example for a simple list of strings
|
||||
# widget = QLineEdit()
|
||||
# widget.setText(", ".join(map(str, attr_value)))
|
||||
# return widget
|
||||
else:
|
||||
# For unsupported types, just display the value
|
||||
label = QLabel(str(attr_value))
|
||||
@@ -140,7 +128,6 @@ class RuleEditorWidget(QWidget):
|
||||
elif isinstance(editor_widget, QComboBox):
|
||||
# Use currentTextChanged to get the string value directly
|
||||
editor_widget.currentTextChanged.connect(lambda text: self._update_rule_attribute(attr_name, text))
|
||||
# Add connections for other widget types
|
||||
|
||||
def _update_rule_attribute(self, attr_name, value):
|
||||
"""
|
||||
@@ -162,7 +149,6 @@ class RuleEditorWidget(QWidget):
|
||||
converted_value = value # Fallback for other types
|
||||
setattr(self.current_rule_object, attr_name, converted_value)
|
||||
self.rule_updated.emit(self.current_rule_object)
|
||||
# print(f"Updated {attr_name} to {converted_value} in {self.current_rule_type}") # Debugging
|
||||
except ValueError:
|
||||
# Handle potential conversion errors (e.g., non-numeric input for int/float)
|
||||
print(f"Error converting value '{value}' for attribute '{attr_name}'")
|
||||
@@ -202,8 +188,8 @@ if __name__ == '__main__':
|
||||
file_setting_y: str = "default_file_string"
|
||||
|
||||
# Example usage: Provide asset types during instantiation
|
||||
asset_types_from_config = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"] # Example list
|
||||
file_types_from_config = ["MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP", "MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK", "MAP_IMPERFECTION", "MODEL", "EXTRA", "FILE_IGNORE"] # Example list
|
||||
asset_types_from_config = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"]
|
||||
file_types_from_config = ["MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP", "MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK", "MAP_IMPERFECTION", "MODEL", "EXTRA", "FILE_IGNORE"]
|
||||
editor = RuleEditorWidget(asset_types=asset_types_from_config, file_types=file_types_from_config)
|
||||
|
||||
# Test loading different rule types
|
||||
@@ -212,8 +198,6 @@ if __name__ == '__main__':
|
||||
file_rule = FileRule()
|
||||
|
||||
editor.load_rule(source_rule, "SourceRule")
|
||||
# editor.load_rule(asset_rule, "AssetRule")
|
||||
# editor.load_rule(file_rule, "FileRule")
|
||||
|
||||
|
||||
editor.show()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot
|
||||
from PySide6.QtGui import QIcon # Assuming we might want icons later
|
||||
from rule_structure import SourceRule, AssetRule, FileRule # Import rule structures
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
|
||||
class RuleHierarchyModel(QAbstractItemModel):
|
||||
"""
|
||||
@@ -57,15 +57,6 @@ class RuleHierarchyModel(QAbstractItemModel):
|
||||
else:
|
||||
return None
|
||||
# Add other roles as needed (e.g., Qt.ItemDataRole.DecorationRole for icons)
|
||||
# elif role == Qt.ItemDataRole.DecorationRole:
|
||||
# if isinstance(item, SourceRule):
|
||||
# return QIcon("icons/source.png") # Placeholder icon
|
||||
# elif isinstance(item, AssetRule):
|
||||
# return QIcon("icons/asset.png") # Placeholder icon
|
||||
# elif isinstance(item, FileRule):
|
||||
# return QIcon("icons/file.png") # Placeholder icon
|
||||
# else:
|
||||
# return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# gui/unified_view_model.py
|
||||
import logging # Added for debugging
|
||||
log = logging.getLogger(__name__) # Added for debugging
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot, QMimeData, QByteArray, QDataStream, QIODevice # Added Signal and Slot, QMimeData, QByteArray, QDataStream, QIODevice
|
||||
from PySide6.QtGui import QColor # Added for background role
|
||||
from pathlib import Path # Added for file_name extraction
|
||||
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType import
|
||||
from configuration import load_base_config # Import load_base_config
|
||||
from typing import List # Added for type hinting
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot, QMimeData, QByteArray, QDataStream, QIODevice
|
||||
from PySide6.QtGui import QColor
|
||||
from pathlib import Path
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
from configuration import load_base_config
|
||||
from typing import List
|
||||
|
||||
class CustomRoles:
|
||||
MapTypeRole = Qt.UserRole + 1
|
||||
@@ -15,7 +15,7 @@ class CustomRoles:
|
||||
class UnifiedViewModel(QAbstractItemModel):
|
||||
# --- Color Constants for Row Backgrounds ---
|
||||
# Old colors removed, using config now + fixed source color
|
||||
SOURCE_RULE_COLOR = QColor("#306091") # Fixed color for SourceRule rows
|
||||
SOURCE_RULE_COLOR = QColor("#306091")
|
||||
# -----------------------------------------
|
||||
|
||||
"""
|
||||
@@ -24,7 +24,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
"""
|
||||
# Signal emitted when a FileRule's target asset override changes.
|
||||
# Carries the FileRule object and the new target asset path (or None).
|
||||
targetAssetOverrideChanged = Signal(FileRule, str, QModelIndex) # Emit FileRule object, new value, and index
|
||||
targetAssetOverrideChanged = Signal(FileRule, str, QModelIndex)
|
||||
|
||||
# Signal emitted when an AssetRule's name changes.
|
||||
# Carries the AssetRule object, the new name, and the index.
|
||||
@@ -44,17 +44,17 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# COL_OUTPUT_PATH = 6 # Removed
|
||||
|
||||
# --- Drag and Drop MIME Type ---
|
||||
MIME_TYPE = "application/x-filerule-index-list" # Custom MIME type
|
||||
MIME_TYPE = "application/x-filerule-index-list"
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._source_rules = [] # Now stores a list of SourceRule objects
|
||||
self._source_rules = []
|
||||
# self._display_mode removed
|
||||
self._asset_type_colors = {}
|
||||
self._file_type_colors = {}
|
||||
self._asset_type_keys = [] # Store asset type keys
|
||||
self._file_type_keys = [] # Store file type keys
|
||||
self._load_definitions() # Load colors and keys
|
||||
self._asset_type_keys = []
|
||||
self._file_type_keys = []
|
||||
self._load_definitions()
|
||||
|
||||
def _load_definitions(self):
|
||||
"""Loads configuration and caches colors and type keys."""
|
||||
@@ -91,24 +91,24 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
self._asset_type_keys = []
|
||||
self._file_type_keys = []
|
||||
|
||||
def load_data(self, source_rules_list: list): # Accepts a list
|
||||
def load_data(self, source_rules_list: list):
|
||||
"""Loads or reloads the model with a list of SourceRule objects."""
|
||||
# Consider if color cache needs refreshing if config can change dynamically
|
||||
# self._load_and_cache_colors() # Uncomment if config can change and needs refresh
|
||||
self.beginResetModel()
|
||||
self._source_rules = source_rules_list if source_rules_list else [] # Assign the new list
|
||||
self._source_rules = source_rules_list if source_rules_list else []
|
||||
# Ensure back-references for parent lookup are set on the NEW items
|
||||
for source_rule in self._source_rules:
|
||||
for asset_rule in source_rule.assets:
|
||||
asset_rule.parent_source = source_rule # Set parent SourceRule
|
||||
asset_rule.parent_source = source_rule
|
||||
for file_rule in asset_rule.files:
|
||||
file_rule.parent_asset = asset_rule # Set parent AssetRule
|
||||
file_rule.parent_asset = asset_rule
|
||||
self.endResetModel()
|
||||
|
||||
def clear_data(self):
|
||||
"""Clears the model data."""
|
||||
self.beginResetModel()
|
||||
self._source_rules = [] # Clear the list
|
||||
self._source_rules = []
|
||||
self.endResetModel()
|
||||
|
||||
def get_all_source_rules(self) -> list:
|
||||
@@ -123,14 +123,13 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# Parent is the invisible root. Children are the SourceRules.
|
||||
return len(self._source_rules)
|
||||
|
||||
# Always use detailed logic
|
||||
parent_item = parent.internalPointer()
|
||||
if isinstance(parent_item, SourceRule):
|
||||
return len(parent_item.assets)
|
||||
elif isinstance(parent_item, AssetRule):
|
||||
return len(parent_item.files)
|
||||
elif isinstance(parent_item, FileRule):
|
||||
return 0 # FileRules have no children
|
||||
return 0
|
||||
|
||||
return 0 # Should not happen for valid items
|
||||
|
||||
@@ -165,15 +164,15 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
elif isinstance(child_item, FileRule):
|
||||
# Parent is an AssetRule. Find its row within its parent SourceRule.
|
||||
parent_item = getattr(child_item, 'parent_asset', None) # Get parent AssetRule
|
||||
parent_item = getattr(child_item, 'parent_asset', None)
|
||||
if parent_item:
|
||||
grandparent_item = getattr(parent_item, 'parent_source', None) # Get the SourceRule
|
||||
grandparent_item = getattr(parent_item, 'parent_source', None)
|
||||
if grandparent_item:
|
||||
try:
|
||||
parent_row = grandparent_item.assets.index(parent_item)
|
||||
# We need the index of the grandparent (SourceRule) to create the parent index
|
||||
grandparent_row = self._source_rules.index(grandparent_item)
|
||||
return self.createIndex(parent_row, 0, parent_item) # Create index for the AssetRule parent
|
||||
return self.createIndex(parent_row, 0, parent_item)
|
||||
except ValueError:
|
||||
return QModelIndex() # Parent AssetRule or Grandparent SourceRule not found in respective lists
|
||||
else:
|
||||
@@ -201,7 +200,6 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# Parent is a valid index, get its item
|
||||
parent_item = parent.internalPointer()
|
||||
|
||||
# Always use detailed logic
|
||||
child_item = None
|
||||
if isinstance(parent_item, SourceRule):
|
||||
if row < len(parent_item.assets):
|
||||
@@ -221,7 +219,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
def data(self, index: QModelIndex, role: int = Qt.DisplayRole):
|
||||
"""Returns the data stored under the given role for the item referred to by the index."""
|
||||
if not index.isValid(): # Check only index validity, data list might be empty but valid
|
||||
if not index.isValid():
|
||||
return None
|
||||
|
||||
item = index.internalPointer()
|
||||
@@ -229,15 +227,14 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
# --- Handle Background Role ---
|
||||
if role == Qt.BackgroundRole:
|
||||
# item is already fetched at line 172
|
||||
if isinstance(item, SourceRule):
|
||||
return self.SOURCE_RULE_COLOR # Use the class constant
|
||||
return self.SOURCE_RULE_COLOR
|
||||
elif isinstance(item, AssetRule):
|
||||
# Determine effective asset type
|
||||
asset_type = item.asset_type_override if item.asset_type_override else item.asset_type
|
||||
if asset_type:
|
||||
# Use cached color
|
||||
return self._asset_type_colors.get(asset_type) # Returns None if not found
|
||||
return self._asset_type_colors.get(asset_type)
|
||||
else:
|
||||
return None # Fallback if no asset_type determined
|
||||
elif isinstance(item, FileRule):
|
||||
@@ -257,7 +254,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# Should not happen if structure is correct, but fallback to default
|
||||
return None
|
||||
# --- End New Logic ---
|
||||
else: # Other item types or if item is None
|
||||
else:
|
||||
return None
|
||||
# --- Handle Foreground Role (Text Color) ---
|
||||
elif role == Qt.ForegroundRole:
|
||||
@@ -266,7 +263,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
effective_item_type = item.item_type_override if item.item_type_override is not None else item.item_type
|
||||
if effective_item_type:
|
||||
# Use cached color for text
|
||||
return self._file_type_colors.get(effective_item_type) # Returns None if not found
|
||||
return self._file_type_colors.get(effective_item_type)
|
||||
# For SourceRule and AssetRule, return None to use default text color (usually contrasts well)
|
||||
return None
|
||||
|
||||
@@ -274,9 +271,8 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
if isinstance(item, SourceRule):
|
||||
if role == Qt.DisplayRole or role == Qt.EditRole:
|
||||
if column == self.COL_NAME:
|
||||
# Always display name
|
||||
return Path(item.input_path).name
|
||||
elif column == self.COL_SUPPLIER: # Always handle supplier
|
||||
elif column == self.COL_SUPPLIER:
|
||||
display_value = item.supplier_override if item.supplier_override is not None else item.supplier_identifier
|
||||
return display_value if display_value is not None else ""
|
||||
return None # Other columns/roles are blank for SourceRule
|
||||
@@ -290,7 +286,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
return display_value if display_value else ""
|
||||
elif role == Qt.EditRole:
|
||||
if column == self.COL_NAME:
|
||||
return item.asset_name # Return name for editing
|
||||
return item.asset_name
|
||||
elif column == self.COL_ASSET_TYPE:
|
||||
return item.asset_type_override
|
||||
return None
|
||||
@@ -314,20 +310,20 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
def setData(self, index: QModelIndex, value, role: int = Qt.EditRole) -> bool:
|
||||
"""Sets the role data for the item at index to value."""
|
||||
if not index.isValid() or role != Qt.EditRole: # Check only index and role
|
||||
if not index.isValid() or role != Qt.EditRole:
|
||||
return False
|
||||
|
||||
item = index.internalPointer()
|
||||
if item is None: # Extra check for safety
|
||||
if item is None:
|
||||
return False
|
||||
column = index.column()
|
||||
changed = False
|
||||
|
||||
# --- Handle different item types ---
|
||||
if isinstance(item, SourceRule): # If SourceRule is editable
|
||||
if isinstance(item, SourceRule):
|
||||
if column == self.COL_SUPPLIER:
|
||||
# Get the new value, strip whitespace, treat empty as None
|
||||
log.debug(f"setData COL_SUPPLIER: Index=({index.row()},{column}), Value='{value}', Type={type(value)}") # <-- ADDED LOGGING (Corrected Indentation)
|
||||
log.debug(f"setData COL_SUPPLIER: Index=({index.row()},{column}), Value='{value}', Type={type(value)}")
|
||||
new_value = str(value).strip() if value is not None and str(value).strip() else None
|
||||
|
||||
# Get the original identifier (assuming it exists on SourceRule)
|
||||
@@ -335,7 +331,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
# If the new value is the same as the original, clear the override
|
||||
if new_value == original_identifier:
|
||||
new_value = None # Effectively removes the override
|
||||
new_value = None
|
||||
|
||||
# Update supplier_override only if it's different
|
||||
if item.supplier_override != new_value:
|
||||
@@ -343,14 +339,14 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
changed = True
|
||||
|
||||
elif isinstance(item, AssetRule):
|
||||
if column == self.COL_NAME: # Handle Asset Name change
|
||||
if column == self.COL_NAME:
|
||||
new_asset_name = str(value).strip() if value else None
|
||||
if not new_asset_name:
|
||||
log.warning("setData: Asset name cannot be empty.")
|
||||
return False # Don't allow empty names
|
||||
return False
|
||||
|
||||
if item.asset_name == new_asset_name:
|
||||
return False # No change
|
||||
return False
|
||||
|
||||
# --- Validation: Check for duplicates within the same SourceRule ---
|
||||
parent_source = getattr(item, 'parent_source', None)
|
||||
@@ -379,13 +375,13 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
for src_idx, source_rule in enumerate(self._source_rules):
|
||||
source_rule_index = self.createIndex(src_idx, 0, source_rule)
|
||||
for asset_idx, asset_rule in enumerate(source_rule.assets):
|
||||
asset_rule_index = self.createIndex(asset_idx, 0, asset_rule) # This index is relative to source_rule_index
|
||||
asset_rule_index = self.createIndex(asset_idx, 0, asset_rule)
|
||||
for file_idx, file_rule in enumerate(asset_rule.files):
|
||||
if file_rule.target_asset_name_override == old_asset_name:
|
||||
log.debug(f" Updating target for file: {Path(file_rule.file_path).name}")
|
||||
file_rule.target_asset_name_override = new_asset_name
|
||||
# Get the correct index for the file rule to emit dataChanged
|
||||
file_rule_parent_index = self.parent(self.createIndex(file_idx, 0, file_rule)) # Get parent AssetRule index
|
||||
file_rule_parent_index = self.parent(self.createIndex(file_idx, 0, file_rule))
|
||||
file_rule_index = self.index(file_idx, self.COL_TARGET_ASSET, file_rule_parent_index)
|
||||
if file_rule_index.isValid():
|
||||
updated_file_indices.append(file_rule_index)
|
||||
@@ -401,41 +397,41 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
elif column == self.COL_ASSET_TYPE:
|
||||
# Delegate provides string value (e.g., "Surface", "Model") or None
|
||||
new_value = str(value) if value is not None else None
|
||||
if new_value == "": new_value = None # Treat empty string as None
|
||||
if new_value == "": new_value = None
|
||||
# Update asset_type_override
|
||||
if item.asset_type_override != new_value:
|
||||
item.asset_type_override = new_value
|
||||
changed = True
|
||||
|
||||
elif isinstance(item, FileRule):
|
||||
if column == self.COL_TARGET_ASSET: # Target Asset Name Override
|
||||
if column == self.COL_TARGET_ASSET:
|
||||
# Ensure value is string or None
|
||||
new_value = str(value).strip() if value is not None else None
|
||||
if new_value == "": new_value = None # Treat empty string as None
|
||||
if new_value == "": new_value = None
|
||||
# Update target_asset_name_override
|
||||
if item.target_asset_name_override != new_value:
|
||||
old_value = item.target_asset_name_override # Store old value for potential revert/comparison
|
||||
old_value = item.target_asset_name_override
|
||||
item.target_asset_name_override = new_value
|
||||
changed = True
|
||||
# Emit signal that the override changed, let handler deal with restructuring
|
||||
# Pass the FileRule item itself, the new value, and the index
|
||||
self.targetAssetOverrideChanged.emit(item, new_value, index)
|
||||
elif column == self.COL_ITEM_TYPE: # Item-Type Override
|
||||
elif column == self.COL_ITEM_TYPE:
|
||||
# Delegate provides string value (e.g., "MAP_COL") or None
|
||||
new_value = str(value) if value is not None else None
|
||||
if new_value == "": new_value = None # Treat empty string as None
|
||||
if new_value == "": new_value = None
|
||||
# Update item_type_override
|
||||
if item.item_type_override != new_value:
|
||||
log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Original Override='{item.item_type_override}', New Value='{new_value}'") # DEBUG LOG
|
||||
old_override = item.item_type_override # Store old value for logging
|
||||
log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Original Override='{item.item_type_override}', New Value='{new_value}'")
|
||||
old_override = item.item_type_override
|
||||
item.item_type_override = new_value
|
||||
changed = True
|
||||
|
||||
# standard_map_type is no longer stored on FileRule.
|
||||
# Remove the logic that updated it here.
|
||||
pass # No action needed to update standard_map_type
|
||||
pass
|
||||
|
||||
log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Final Override='{item.item_type_override}'") # DEBUG LOG - Updated
|
||||
log.debug(f"setData COL_ITEM_TYPE: File='{Path(item.file_path).name}', Final Override='{item.item_type_override}'")
|
||||
|
||||
|
||||
if changed:
|
||||
@@ -448,22 +444,21 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
|
||||
"""Returns the item flags for the given index."""
|
||||
if not index.isValid():
|
||||
return Qt.NoItemFlags # No flags for invalid index
|
||||
return Qt.NoItemFlags
|
||||
|
||||
# Start with default flags for a valid item
|
||||
default_flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
item = index.internalPointer()
|
||||
if not item: # Should not happen for valid index, but safety check
|
||||
if not item:
|
||||
return Qt.NoItemFlags
|
||||
column = index.column()
|
||||
|
||||
# Always use detailed mode editability logic
|
||||
can_edit = False
|
||||
if isinstance(item, SourceRule):
|
||||
if column == self.COL_SUPPLIER: can_edit = True
|
||||
elif isinstance(item, AssetRule):
|
||||
if column == self.COL_NAME: can_edit = True # Allow editing name
|
||||
if column == self.COL_NAME: can_edit = True
|
||||
if column == self.COL_ASSET_TYPE: can_edit = True
|
||||
# AssetRule items can accept drops
|
||||
default_flags |= Qt.ItemIsDropEnabled
|
||||
@@ -494,9 +489,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
"""Safely returns the item associated with the index."""
|
||||
if index.isValid():
|
||||
item = index.internalPointer()
|
||||
if item: # Ensure internal pointer is not None
|
||||
if item:
|
||||
return item
|
||||
return None # Return None for invalid index or None pointer
|
||||
return None
|
||||
# --- Method to update model based on prediction results, preserving overrides ---
|
||||
def update_rules_for_sources(self, new_source_rules: List[SourceRule]):
|
||||
"""
|
||||
@@ -539,7 +534,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
self.beginInsertRows(QModelIndex(), insert_row, insert_row)
|
||||
self._source_rules.append(new_source_rule)
|
||||
self.endInsertRows()
|
||||
continue # Process next new_source_rule
|
||||
continue
|
||||
|
||||
# 3. Merge Existing SourceRule
|
||||
log.debug(f"Merging SourceRule for '{source_path}'")
|
||||
@@ -578,7 +573,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
if existing_asset.asset_type != new_asset.asset_type and existing_asset.asset_type_override is None:
|
||||
existing_asset.asset_type = new_asset.asset_type
|
||||
asset_type_col_index = self.createIndex(existing_asset_row, self.COL_ASSET_TYPE, existing_asset)
|
||||
self.dataChanged.emit(asset_type_col_index, asset_type_col_index, [Qt.DisplayRole, Qt.EditRole, Qt.BackgroundRole]) # Include BackgroundRole for color
|
||||
self.dataChanged.emit(asset_type_col_index, asset_type_col_index, [Qt.DisplayRole, Qt.EditRole, Qt.BackgroundRole])
|
||||
|
||||
# --- Merge FileRules within the AssetRule ---
|
||||
self._merge_file_rules(existing_asset, new_asset, existing_asset_index)
|
||||
@@ -586,7 +581,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
else:
|
||||
# --- Add New AssetRule ---
|
||||
log.debug(f" Adding new AssetRule: {asset_name}")
|
||||
new_asset.parent_source = existing_source_rule # Set parent
|
||||
new_asset.parent_source = existing_source_rule
|
||||
# Ensure file parents are set
|
||||
for file_rule in new_asset.files:
|
||||
file_rule.parent_asset = new_asset
|
||||
@@ -601,7 +596,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
assets_to_remove = []
|
||||
for i, existing_asset in reversed(list(enumerate(existing_source_rule.assets))):
|
||||
if existing_asset.asset_name not in processed_asset_names:
|
||||
assets_to_remove.append((i, existing_asset.asset_name)) # Store index and name
|
||||
assets_to_remove.append((i, existing_asset.asset_name))
|
||||
|
||||
for row_index, asset_name_to_remove in assets_to_remove:
|
||||
log.debug(f" Removing old AssetRule: {asset_name_to_remove}")
|
||||
@@ -625,30 +620,30 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# --- Update Existing FileRule ---
|
||||
log.debug(f" Merging FileRule: {Path(file_path).name}")
|
||||
existing_file_row = existing_asset.files.index(existing_file)
|
||||
existing_file_index = self.createIndex(existing_file_row, 0, existing_file) # Index relative to parent_asset_index
|
||||
existing_file_index = self.createIndex(existing_file_row, 0, existing_file)
|
||||
|
||||
# Update non-override fields (item_type, standard_map_type)
|
||||
changed_roles = []
|
||||
if existing_file.item_type != new_file.item_type and existing_file.item_type_override is None:
|
||||
existing_file.item_type = new_file.item_type
|
||||
changed_roles.extend([Qt.DisplayRole, Qt.EditRole, Qt.BackgroundRole]) # Include BackgroundRole for color
|
||||
changed_roles.extend([Qt.DisplayRole, Qt.EditRole, Qt.BackgroundRole])
|
||||
|
||||
# standard_map_type is no longer stored on FileRule.
|
||||
# Remove the logic that updated it during merge.
|
||||
pass # No action needed for standard_map_type during merge
|
||||
pass
|
||||
|
||||
|
||||
# Emit dataChanged only if something actually changed
|
||||
if changed_roles:
|
||||
# Emit for all relevant columns potentially affected by type changes
|
||||
for col in [self.COL_ITEM_TYPE]: # Add other cols if needed
|
||||
for col in [self.COL_ITEM_TYPE]:
|
||||
col_index = self.createIndex(existing_file_row, col, existing_file)
|
||||
self.dataChanged.emit(col_index, col_index, changed_roles)
|
||||
|
||||
else:
|
||||
# --- Add New FileRule ---
|
||||
log.debug(f" Adding new FileRule: {Path(file_path).name}")
|
||||
new_file.parent_asset = existing_asset # Set parent
|
||||
new_file.parent_asset = existing_asset
|
||||
insert_row = len(existing_asset.files)
|
||||
self.beginInsertRows(parent_asset_index, insert_row, insert_row)
|
||||
existing_asset.files.append(new_file)
|
||||
@@ -689,7 +684,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
if old_parent_asset == target_parent_asset:
|
||||
log.debug("moveFileRule: Source and target parent are the same. No move needed.")
|
||||
return True # Technically successful, no change needed
|
||||
return True
|
||||
|
||||
# Get old parent index
|
||||
source_rule = getattr(old_parent_asset, 'parent_source', None)
|
||||
@@ -705,14 +700,14 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
log.error("moveFileRule: Could not find old parent or source file within their respective lists.")
|
||||
return False
|
||||
|
||||
target_row = len(target_parent_asset.files) # Append to the end of the target
|
||||
target_row = len(target_parent_asset.files)
|
||||
|
||||
log.debug(f"Moving file '{Path(file_item.file_path).name}' from '{old_parent_asset.asset_name}' (row {source_row}) to '{target_parent_asset.asset_name}' (row {target_row})")
|
||||
self.beginMoveRows(old_parent_index, source_row, source_row, target_parent_asset_index, target_row)
|
||||
# Restructure internal data
|
||||
old_parent_asset.files.pop(source_row)
|
||||
target_parent_asset.files.append(file_item)
|
||||
file_item.parent_asset = target_parent_asset # Update parent reference
|
||||
file_item.parent_asset = target_parent_asset
|
||||
self.endMoveRows()
|
||||
return True
|
||||
|
||||
@@ -732,11 +727,11 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
return self.createIndex(existing_row, 0, asset)
|
||||
except ValueError:
|
||||
log.error("createAssetRule: Found existing asset but failed to get its index.")
|
||||
return QModelIndex() # Should not happen
|
||||
return QModelIndex()
|
||||
|
||||
log.debug(f"Creating new AssetRule '{new_asset_name}' under '{Path(source_rule.input_path).name}'")
|
||||
new_asset_rule = AssetRule(asset_name=new_asset_name)
|
||||
new_asset_rule.parent_source = source_rule # Set parent reference
|
||||
new_asset_rule.parent_source = source_rule
|
||||
|
||||
# Optionally copy type info from another asset
|
||||
if isinstance(copy_from_asset, AssetRule):
|
||||
@@ -756,7 +751,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
# Emit signals for inserting the new parent row
|
||||
self.beginInsertRows(grandparent_index, new_parent_row, new_parent_row)
|
||||
source_rule.assets.insert(new_parent_row, new_asset_rule) # Insert into data structure
|
||||
source_rule.assets.insert(new_parent_row, new_asset_rule)
|
||||
self.endInsertRows()
|
||||
|
||||
# Return index for the newly created asset
|
||||
@@ -771,7 +766,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
if asset_rule_to_remove.files:
|
||||
log.warning(f"removeAssetRule: Asset '{asset_rule_to_remove.asset_name}' is not empty. Removal aborted.")
|
||||
return False # Do not remove non-empty assets automatically
|
||||
return False
|
||||
|
||||
source_rule = getattr(asset_rule_to_remove, 'parent_source', None)
|
||||
if not source_rule:
|
||||
@@ -811,10 +806,10 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
for sr_row, source_rule in enumerate(self._source_rules):
|
||||
if source_rule is target_item_object:
|
||||
return self.createIndex(sr_row, 0, source_rule) # Top-level item
|
||||
return self.createIndex(sr_row, 0, source_rule)
|
||||
|
||||
parent_source_rule_index = self.createIndex(sr_row, 0, source_rule) # Potential parent for children
|
||||
if not parent_source_rule_index.isValid(): # Should always be valid here
|
||||
parent_source_rule_index = self.createIndex(sr_row, 0, source_rule)
|
||||
if not parent_source_rule_index.isValid():
|
||||
log.error(f"findIndexForItem: Could not create valid index for SourceRule: {source_rule.input_path}")
|
||||
continue
|
||||
|
||||
@@ -826,7 +821,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
parent_asset_rule_index = self.index(ar_row, 0, parent_source_rule_index)
|
||||
if not parent_asset_rule_index.isValid():
|
||||
log.error(f"findIndexForItem: Could not create valid index for AssetRule: {asset_rule.asset_name}")
|
||||
continue # Skip children if parent index is invalid
|
||||
continue
|
||||
|
||||
for fr_row, file_rule in enumerate(asset_rule.files):
|
||||
if file_rule is target_item_object:
|
||||
@@ -850,7 +845,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
self.beginRemoveRows(grandparent_index, asset_row_for_removal, asset_row_for_removal)
|
||||
source_rule.assets.pop(asset_row_for_removal)
|
||||
self.endRemoveRows()
|
||||
return True # This was the original end of removeAssetRule
|
||||
return True
|
||||
|
||||
def update_status(self, source_path: str, status_text: str):
|
||||
"""
|
||||
@@ -877,7 +872,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# (e.g., delegates, background color based on status if implemented later)
|
||||
start_index = self.createIndex(found_row, 0, found_rule)
|
||||
end_index = self.createIndex(found_row, self.columnCount() - 1, found_rule)
|
||||
self.dataChanged.emit(start_index, end_index, [Qt.DisplayRole]) # Emit for DisplayRole, maybe others needed later
|
||||
self.dataChanged.emit(start_index, end_index, [Qt.DisplayRole])
|
||||
|
||||
except Exception as e:
|
||||
log.exception(f"Error setting status attribute or emitting dataChanged for {source_path}: {e}")
|
||||
@@ -905,7 +900,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
dragged_file_info = []
|
||||
for index in indexes:
|
||||
if not index.isValid() or index.column() != 0: # Only consider valid indices from the first column
|
||||
if not index.isValid() or index.column() != 0:
|
||||
continue
|
||||
item = index.internalPointer()
|
||||
if isinstance(item, FileRule):
|
||||
@@ -928,9 +923,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# Write the number of items first, then each tuple
|
||||
stream.writeInt8(len(dragged_file_info))
|
||||
for info in dragged_file_info:
|
||||
stream.writeInt8(info[0]) # source_row
|
||||
stream.writeInt8(info[1]) # source_parent_row
|
||||
stream.writeInt8(info[2]) # source_grandparent_row
|
||||
stream.writeInt8(info[0])
|
||||
stream.writeInt8(info[1])
|
||||
stream.writeInt8(info[2])
|
||||
|
||||
mime_data.setData(self.MIME_TYPE, encoded_data)
|
||||
log.debug(f"mimeData: Encoded {len(dragged_file_info)} FileRule indices.")
|
||||
@@ -943,11 +938,11 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
# Check if the drop target is a valid AssetRule
|
||||
if not parent.isValid():
|
||||
return False # Cannot drop onto root or SourceRule directly in this implementation
|
||||
return False
|
||||
|
||||
target_item = parent.internalPointer()
|
||||
if not isinstance(target_item, AssetRule):
|
||||
return False # Can only drop onto AssetRule items
|
||||
return False
|
||||
|
||||
# Optional: Prevent dropping onto the original parent? (Might be confusing)
|
||||
# For now, allow dropping onto the same parent (moveFileRule handles this)
|
||||
@@ -961,7 +956,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
return False
|
||||
|
||||
target_asset_item = parent.internalPointer()
|
||||
if not isinstance(target_asset_item, AssetRule): # Should be caught by canDrop, but double-check
|
||||
if not isinstance(target_asset_item, AssetRule):
|
||||
log.error("dropMimeData: Target item is not an AssetRule.")
|
||||
return False
|
||||
|
||||
@@ -984,7 +979,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
# Keep track of original parents that might become empty
|
||||
original_parents = set()
|
||||
moved_files_new_indices = {} # Store new index after move for dataChanged emission
|
||||
moved_files_new_indices = {}
|
||||
|
||||
# --- BEGIN FIX: Reconstruct all source indices BEFORE the move loop ---
|
||||
source_indices_to_process = []
|
||||
@@ -1018,12 +1013,12 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
|
||||
# Process moves using the pre-calculated valid indices
|
||||
for source_file_index in source_indices_to_process: # Iterate through the valid indices
|
||||
for source_file_index in source_indices_to_process:
|
||||
# Get the file item (already validated during reconstruction)
|
||||
file_item = source_file_index.internalPointer()
|
||||
|
||||
# Track original parent for cleanup (using the valid index)
|
||||
old_parent_index = self.parent(source_file_index) # Get parent from the valid index
|
||||
old_parent_index = self.parent(source_file_index)
|
||||
if old_parent_index.isValid():
|
||||
old_parent_asset = old_parent_index.internalPointer()
|
||||
if isinstance(old_parent_asset, AssetRule):
|
||||
@@ -1031,7 +1026,7 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
grandparent_index = self.parent(old_parent_index)
|
||||
if grandparent_index.isValid():
|
||||
original_parents.add((grandparent_index.row(), old_parent_asset.asset_name))
|
||||
else: # Handle root case or error
|
||||
else:
|
||||
log.warning(f"Could not get grandparent index for original parent '{old_parent_asset.asset_name}' during cleanup tracking.")
|
||||
else:
|
||||
log.warning(f"Parent of file '{Path(file_item.file_path).name}' is not an AssetRule.")
|
||||
@@ -1040,21 +1035,21 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
|
||||
|
||||
# Perform the move using the model's method and the valid source_file_index
|
||||
if self.moveFileRule(source_file_index, parent): # 'parent' is the target_parent_asset_index
|
||||
if self.moveFileRule(source_file_index, parent):
|
||||
# --- Update Target Asset Override After Successful Move ---
|
||||
# The file_item's parent_asset reference should now be updated by moveFileRule
|
||||
new_parent_asset = getattr(file_item, 'parent_asset', None)
|
||||
if new_parent_asset == target_asset_item: # Check if move was successful internally
|
||||
if new_parent_asset == target_asset_item:
|
||||
if file_item.target_asset_name_override != target_asset_item.asset_name:
|
||||
log.debug(f" Updating target override for '{Path(file_item.file_path).name}' to '{target_asset_item.asset_name}'")
|
||||
file_item.target_asset_name_override = target_asset_item.asset_name
|
||||
# Need the *new* index of the moved file to emit dataChanged
|
||||
try:
|
||||
new_row = target_asset_item.files.index(file_item)
|
||||
new_file_index_col0 = self.index(new_row, 0, parent) # Index for col 0 under new parent
|
||||
new_file_index_target_col = self.index(new_row, self.COL_TARGET_ASSET, parent) # Index for target col
|
||||
new_file_index_col0 = self.index(new_row, 0, parent)
|
||||
new_file_index_target_col = self.index(new_row, self.COL_TARGET_ASSET, parent)
|
||||
if new_file_index_target_col.isValid():
|
||||
moved_files_new_indices[file_item.file_path] = new_file_index_target_col # Use hashable file_path as key
|
||||
moved_files_new_indices[file_item.file_path] = new_file_index_target_col
|
||||
else:
|
||||
log.warning(f" Could not get valid *new* index for target column of moved file: {Path(file_item.file_path).name}")
|
||||
except ValueError:
|
||||
@@ -1065,22 +1060,22 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
else:
|
||||
log.error(f"dropMimeData: moveFileRule failed for file '{Path(file_item.file_path).name}'.")
|
||||
# If one move fails, should we stop? For now, continue processing others.
|
||||
continue # Skip override update and cleanup check for this file
|
||||
continue
|
||||
|
||||
# --- Emit dataChanged for Target Asset column AFTER all moves ---
|
||||
for source_path, new_index in moved_files_new_indices.items(): # Key is now source_path (string)
|
||||
for source_path, new_index in moved_files_new_indices.items():
|
||||
self.dataChanged.emit(new_index, new_index, [Qt.DisplayRole, Qt.EditRole])
|
||||
|
||||
# --- Cleanup: Remove any original parent AssetRules that are now empty ---
|
||||
log.debug(f"dropMimeData: Checking original parents for cleanup: {list(original_parents)}") # Log tuples
|
||||
for gp_row, asset_name in list(original_parents): # Iterate over a copy of tuples
|
||||
log.debug(f"dropMimeData: Checking original parents for cleanup: {list(original_parents)}")
|
||||
for gp_row, asset_name in list(original_parents):
|
||||
try:
|
||||
if 0 <= gp_row < len(self._source_rules):
|
||||
source_rule = self._source_rules[gp_row]
|
||||
# Find the asset rule within the correct source rule
|
||||
asset_rule_to_check = next((asset for asset in source_rule.assets if asset.asset_name == asset_name), None)
|
||||
|
||||
if asset_rule_to_check and not asset_rule_to_check.files and asset_rule_to_check != target_asset_item: # Don't remove the target if it was also an original parent
|
||||
if asset_rule_to_check and not asset_rule_to_check.files and asset_rule_to_check != target_asset_item:
|
||||
log.info(f"dropMimeData: Attempting cleanup of now empty original parent: '{asset_rule_to_check.asset_name}'")
|
||||
if not self.removeAssetRule(asset_rule_to_check):
|
||||
log.warning(f"dropMimeData: Failed to remove empty original parent '{asset_rule_to_check.asset_name}'.")
|
||||
|
||||
Reference in New Issue
Block a user