Various Issue Completions

#10
#9
#8
#7
#6
#5
This commit is contained in:
Rusfort 2025-05-01 15:44:40 +02:00
parent 26e1a769ce
commit a5be50b587
31 changed files with 589 additions and 123 deletions

2
.gitignore vendored
View File

@ -27,3 +27,5 @@ build/
# Ignore Windows thumbnail cache
Thumbs.db
gui/__pycache__
__pycache__

View File

@ -7,12 +7,12 @@ This document provides a high-level overview of the Asset Processor Tool's archi
The Asset Processor Tool is designed to process 3D asset source files into a standardized library format. Its high-level architecture consists of:
1. **Core Processing Engine (`processing_engine.py`):** The primary component responsible for executing the asset processing pipeline for a single input asset based on a provided `SourceRule` object and static configuration. The older `asset_processor.py` remains in the codebase for reference but is no longer used in the main processing flow.
2. **Configuration System (`Configuration`):** Handles loading core settings and merging them with supplier-specific rules defined in JSON presets.
2. **Configuration System (`Configuration`):** Handles loading core settings (including centralized type definitions) and merging them with supplier-specific rules defined in JSON presets and the persistent `config/suppliers.json` file.
3. **Multiple Interfaces:** Provides different ways to interact with the tool:
* Graphical User Interface (GUI)
* Command-Line Interface (CLI)
* Directory Monitor for automated processing.
The GUI now acts as the primary source of truth for processing rules, generating and managing the `SourceRule` hierarchy before sending it to the processing engine. The CLI and Monitor interfaces can also generate `SourceRule` objects to bypass the GUI for automated workflows.
The GUI now acts as the primary source of truth for processing rules, generating and managing the `SourceRule` hierarchy before sending it to the processing engine. It also accumulates prediction results from multiple input sources before updating the view. The CLI and Monitor interfaces can also generate `SourceRule` objects to bypass the GUI for automated workflows.
4. **Optional Integration:** Includes scripts and logic for integrating with external software, specifically Blender, to automate material and node group creation.
## Hierarchical Rule System
@ -27,14 +27,15 @@ This hierarchy allows for fine-grained control over processing parameters. The G
## Core Components
* `config.py`: Defines core, global settings, constants, and centralized lists of allowed asset and file types.
* `config.py`: Defines core, global settings, constants, and centralized definitions for allowed asset and file types (`ASSET_TYPE_DEFINITIONS`, `FILE_TYPE_DEFINITIONS`), including metadata like colors and descriptions.
* `config/suppliers.json`: A persistent JSON file storing known supplier names for GUI auto-completion.
* `Presets/*.json`: Supplier-specific JSON files defining rules for file interpretation and initial prediction.
* `configuration.py` (`Configuration` class): Loads `config.py` settings and merges them with a selected preset, pre-compiling regex patterns for efficiency. This static configuration is used by the processing engine.
* `rule_structure.py`: Defines the `SourceRule`, `AssetRule`, and `FileRule` dataclasses used to represent the hierarchical processing rules.
* `gui/`: Directory containing modules for the Graphical User Interface (GUI), built with PySide6. The GUI is responsible for generating and managing the `SourceRule` hierarchy via the Unified View and interacting with background handlers (`ProcessingHandler`, `PredictionHandler`).
* `unified_view_model.py`: Implements the `QAbstractItemModel` for the Unified Hierarchical View, holding the `SourceRule` data and handling inline editing.
* `delegates.py`: Contains custom `QStyledItemDelegate` implementations for inline editing in the Unified View.
* `prediction_handler.py`: Generates the initial `SourceRule` hierarchy with predicted values based on input files and the selected preset.
* `gui/`: Directory containing modules for the Graphical User Interface (GUI), built with PySide6. The GUI is responsible for generating and managing the `SourceRule` hierarchy via the Unified View, accumulating prediction results, and interacting with background handlers (`ProcessingHandler`, `PredictionHandler`).
* `unified_view_model.py`: Implements the `QAbstractItemModel` for the Unified Hierarchical View, holding the `SourceRule` data, handling inline editing (including direct model restructuring for `target_asset_name_override`), and managing row coloring based on config definitions.
* `delegates.py`: Contains custom `QStyledItemDelegate` implementations for inline editing in the Unified View, including the new `SupplierSearchDelegate` for supplier name auto-completion and management.
* `prediction_handler.py`: Generates the initial `SourceRule` hierarchy with predicted values for a single input source based on its files and the selected preset.
* `processing_engine.py` (`ProcessingEngine` class): The new core component that executes the processing pipeline for a single `SourceRule` object using the static `Configuration`. It contains no internal prediction or fallback logic.
* `asset_processor.py` (`AssetProcessor` class): The older processing engine, kept for reference but not used in the main processing flow.
* `main.py`: The entry point for the Command-Line Interface (CLI). It handles argument parsing, logging, parallel processing orchestration, and triggering Blender scripts. It now orchestrates processing by passing `SourceRule` objects to the `ProcessingEngine`.

View File

@ -5,7 +5,9 @@ This document outlines the key files and directories within the Asset Processor
```
Asset_processor_tool/
├── asset_processor.py # Older core class, kept for reference (not used in main flow)
├── config.py # Core settings, constants, and allowed types
├── config.py # Core settings, constants, and definitions for allowed asset/file types
├── config/ # Directory for configuration files
│ └── suppliers.json # Persistent list of known supplier names for GUI auto-completion
├── configuration.py # Class for loading and accessing configuration (merges config.py and presets)
├── detailed_documentation_plan.md # (Existing file, potentially outdated)
├── Dockerfile # Instructions for building the Docker container image
@ -50,7 +52,9 @@ Asset_processor_tool/
**Key Files and Directories:**
* `asset_processor.py`: Contains the older `AssetProcessor` class. It is kept for reference but is no longer used in the main processing flow orchestrated by `main.py` or the GUI.
* `config.py`: Stores global default settings, constants, core rules, and centralized lists of `ALLOWED_ASSET_TYPES` and `ALLOWED_FILE_TYPES` used for validation and GUI dropdowns.
* `config.py`: Stores global default settings, constants, core rules, and centralized definitions for allowed asset and file types (`ASSET_TYPE_DEFINITIONS`, `FILE_TYPE_DEFINITIONS`) used for validation, GUI dropdowns, and coloring.
* `config/`: Directory containing configuration files, such as `suppliers.json`.
* `config/suppliers.json`: A JSON file storing a persistent list of known supplier names, used by the GUI's `SupplierSearchDelegate` for auto-completion.
* `configuration.py`: Defines the `Configuration` class. Responsible for loading core settings from `config.py` and merging them with a specified preset JSON file (`Presets/*.json`). Pre-compiles regex patterns from presets for efficiency. An instance of this class is passed to the `ProcessingEngine`.
* `rule_structure.py`: Defines the `SourceRule`, `AssetRule`, and `FileRule` dataclasses. These structures represent the hierarchical processing rules and are the primary data contract passed from the GUI/prediction layer to the processing engine.
* `processing_engine.py`: Defines the new `ProcessingEngine` class. This is the core component that executes the processing pipeline for a single asset based *solely* on a provided `SourceRule` object and the static `Configuration`. It contains no internal prediction or fallback logic.

View File

@ -26,7 +26,7 @@ This module defines the data structures used to represent the hierarchical proce
* `AssetRule`: A dataclass representing rules applied at the asset level. It contains nested `FileRule` objects.
* `FileRule`: A dataclass representing rules applied at the file level.
These classes hold specific rule parameters (e.g., `supplier_identifier`, `asset_type`, `map_type_override`). Attributes like `asset_type` and `item_type_override` now use string types, which are validated against centralized lists in `config.py`. These structures support serialization (Pickle, JSON) to allow them to be passed between different parts of the application, including across process boundaries.
These classes hold specific rule parameters (e.g., `supplier_identifier`, `asset_type`, `asset_type_override`, `item_type`, `item_type_override`, `target_asset_name_override`). Attributes like `asset_type` and `item_type_override` now use string types, which are validated against centralized lists in `config.py`. These structures support serialization (Pickle, JSON) to allow them to be passed between different parts of the application, including across process boundaries.
## `Configuration` (`configuration.py`)
@ -58,15 +58,18 @@ The `MainWindow` class is the main application window for the Graphical User Int
The `UnifiedViewModel` implements a `QAbstractItemModel` for use with Qt's model-view architecture. It is specifically designed to:
* Wrap a list of `SourceRule` objects and expose their hierarchical structure (Source -> Asset -> File) to a `QTreeView` (the Unified Hierarchical View).
* Provide methods (`data`, `index`, `parent`, `rowCount`, `columnCount`, `flags`, `setData`) required by `QAbstractItemModel` to allow the `QTreeView` to display the rule hierarchy and support inline editing of specific attributes (e.g., asset type, item type override, target asset name override).
* Provide methods (`data`, `index`, `parent`, `rowCount`, `columnCount`, `flags`, `setData`) required by `QAbstractItemModel` to allow the `QTreeView` to display the rule hierarchy and support inline editing of specific attributes (e.g., `supplier_override`, `asset_type_override`, `item_type_override`, `target_asset_name_override`).
* Handle the direct restructuring of the underlying `SourceRule` hierarchy when `target_asset_name_override` is edited, including moving `FileRule`s and managing `AssetRule` creation/deletion.
* Determine row background colors based on the `asset_type` and `item_type`/`item_type_override` using color metadata from `config.py`.
* Hold the `SourceRule` data that is the single source of truth for the GUI's processing rules.
## `Delegates` (`gui/delegates.py`)
This module contains custom `QStyledItemDelegate` implementations used by the Unified Hierarchical View (`QTreeView`) to provide inline editors for specific data types or rule attributes. Examples include delegates for:
* `QComboBox`: For selecting from a predefined list of options (e.g., allowed asset types, allowed file types sourced from `config.py`).
* `QLineEdit`: For free-form text editing (e.g., target asset name override, supplier identifier override).
* `ComboBoxDelegate`: For selecting from predefined lists of allowed asset and file types, sourced from `config.py`.
* `LineEditDelegate`: For free-form text editing, such as the `target_asset_name_override`.
* `SupplierSearchDelegate`: A new delegate for the "Supplier" column. It provides a `QLineEdit` with auto-completion suggestions loaded from `config/suppliers.json` and handles adding/saving new suppliers.
These delegates handle the presentation and editing of data within the tree view cells, interacting with the `UnifiedViewModel` to get and set data.
@ -84,10 +87,10 @@ The `ProcessingHandler` class is designed to run in a separate `QThread` within
The `PredictionHandler` class also runs in a separate `QThread` in the GUI. It is responsible for generating the initial `SourceRule` hierarchy with predicted values based on the input files and the selected preset. It:
* Takes a list of input files and the selected preset name as input.
* Uses logic (including accessing preset rules and `config.py`'s allowed types) to analyze files and predict initial values for overridable fields in the `SourceRule`, `AssetRule`, and `FileRule` objects (e.g., asset type, item type, target asset name).
* Constructs the complete `SourceRule` hierarchy based on these predictions.
* Emits a signal (`rule_hierarchy_ready`) with the generated `List[SourceRule]` to the `MainWindow` to populate the Unified Hierarchical View.
* Takes an input source identifier (path), a list of files within that source, and the selected preset name as input.
* Uses logic (including accessing preset rules and `config.py`'s allowed types) to analyze files and predict initial values for overridable fields in the `SourceRule`, `AssetRule`, and `FileRule` objects (e.g., `supplier_identifier`, `asset_type`, `item_type`, `target_asset_name_override`).
* Constructs a `SourceRule` hierarchy for the single input source.
* Emits a signal (`rule_hierarchy_ready`) with the input source identifier and the generated `SourceRule` object (within a list) to the `MainWindow` for accumulation and eventual population of the `UnifiedViewModel`.
## `ZipHandler` (`monitor.py`)

View File

@ -6,9 +6,25 @@ This document provides technical details about the configuration system and the
The tool utilizes a two-tiered configuration system:
1. **Core Settings (`config.py`):** This Python module defines global default settings, constants, and core rules that apply generally across different asset sources. Examples include default output paths, standard image resolutions, map merge rules, output format rules, Blender executable paths, and default map types.
1. **Core Settings (`config.py`):** This Python module defines global default settings, constants, and core rules that apply generally across different asset sources. Examples include default output paths, standard image resolutions, map merge rules, output format rules, Blender executable paths, and default map types. It also now centrally defines metadata for allowed asset and file types.
2. **Preset Files (`Presets/*.json`):** These JSON files define supplier-specific rules and overrides. They contain patterns (often regular expressions) to interpret filenames, classify map types, handle variants, define naming conventions, and specify other source-specific behaviors.
## Core Definitions in `config.py`
The `config.py` file now uses dictionary structures to define allowed asset and file types and their associated metadata:
* **`ASSET_TYPE_DEFINITIONS`:** A dictionary where keys are the standard asset type names (e.g., `"Surface"`, `"Model"`, `"Decal"`) and values are dictionaries containing metadata such as `description` and `color` (used for GUI coloring).
* **`FILE_TYPE_DEFINITIONS`:** A dictionary where keys are the standard file/item type names (e.g., `"MAP_COL"`, `"MAP_NRM"`, `"MODEL"`, `"EXTRA"`) and values are dictionaries containing metadata such as `description`, `color` (used for GUI coloring), and `examples` (example filename patterns).
These dictionaries serve as the central source of truth for valid types and their associated display information throughout the application, particularly in the GUI for dropdowns and coloring.
## Supplier Management (`config/suppliers.json`)
A new file, `config/suppliers.json`, is used to store a persistent list of known supplier names. This file is a simple JSON array of strings.
* **Purpose:** Provides a list of suggestions for the "Supplier" field in the GUI's Unified View, enabling auto-completion.
* **Management:** The GUI's `SupplierSearchDelegate` is responsible for loading this list on startup, adding new, unique supplier names entered by the user, and saving the updated list back to the file.
## `Configuration` Class (`configuration.py`)
The `Configuration` class is responsible for loading, merging, and preparing the configuration settings for use by the `AssetProcessor`.

View File

@ -16,6 +16,7 @@ The `MainWindow` class is the central component of the GUI application. It is re
* Connecting user interactions (button clicks, drag-and-drop events, edits in the Unified View) to corresponding methods (slots) within the `MainWindow` or other handler classes.
* Managing the display of application logs in the UI console using a custom `QtLogHandler`.
* Interacting with background handlers (`ProcessingHandler`, `PredictionHandler`) via Qt signals and slots to ensure thread-safe updates to the UI during long-running operations.
* Accumulating prediction results from the `PredictionHandler` for multiple input sources before updating the `UnifiedViewModel`.
* Receiving the initial `SourceRule` hierarchy from the `PredictionHandler` and populating the `UnifiedViewModel`.
* Sending the final, potentially user-modified, `SourceRule` list to `main.py` to initiate processing via the `ProcessingEngine`.
@ -42,9 +43,14 @@ The GUI includes an integrated preset editor panel. This allows users to interac
The core of the GUI's rule editing interface is the Unified Hierarchical View, implemented using a `QTreeView` with a custom model and delegates.
* **`Unified View Model` (`gui/unified_view_model.py`):** This class implements a `QAbstractItemModel` to expose the structure of a list of `SourceRule` objects (Source -> Asset -> File) to the `QTreeView`. It holds the `SourceRule` data that is the single source of truth for the GUI's processing rules. It provides data and flags for display in multiple columns and supports inline editing of specific rule attributes (e.g., asset type, item type override, target asset name override) by interacting with delegates.
* **`Delegates` (`gui/delegates.py`):** This module contains custom `QStyledItemDelegate` implementations used by the `QTreeView` to provide inline editors for specific data types or rule attributes. Examples include delegates for `QComboBox` (for selecting from allowed types sourced from `config.py`) and `QLineEdit` (for free-form text editing). These delegates handle the presentation and editing of data within the tree view cells, interacting with the `UnifiedViewModel` to get and set data.
* **Direct Model Restructuring:** The `setData` method now includes logic to directly restructure the underlying `SourceRule` hierarchy when the `target_asset_name_override` field of a `FileRule` is edited. This involves moving the `FileRule` to a different `AssetRule` (creating a new one if necessary) and removing the old `AssetRule` if it becomes empty. This replaces the previous mechanism of re-running prediction after an edit.
* **Row Coloring:** Row background colors are dynamically determined based on the `asset_type` (for `AssetRule`s) and `item_type` or `item_type_override` (for `FileRule`s), using the color metadata defined in the `ASSET_TYPE_DEFINITIONS` and `FILE_TYPE_DEFINITIONS` dictionaries in `config.py`. `SourceRule` rows have a fixed color.
* **`Delegates` (`gui/delegates.py`):** This module contains custom `QStyledItemDelegate` implementations used by the `QTreeView` to provide inline editors for specific data types or rule attributes.
* **`ComboBoxDelegate`:** Used for selecting from predefined lists (e.g., allowed asset types, allowed file types sourced from `config.py`).
* **`LineEditDelegate`:** Used for free-form text editing (e.g., target asset name override).
* **`SupplierSearchDelegate`:** A new delegate used for the "Supplier" column. It provides a `QLineEdit` with auto-completion suggestions loaded from `config/suppliers.json`. It also handles adding new, unique supplier names entered by the user to the list and saving the updated list back to the JSON file.
The `PredictionHandler` generates the initial `SourceRule` hierarchy, which is then set on the `UnifiedViewModel`. The `QTreeView` displays this model, allowing users to navigate the hierarchy and make inline edits to the rule attributes. Edits made in the view directly modify the attributes of the underlying rule objects in the `SourceRule` hierarchy held by the model.
The `PredictionHandler` generates the initial `SourceRule` hierarchy, which is then set on the `UnifiedViewModel`. The `QTreeView` displays this model, allowing users to navigate the hierarchy and make inline edits to the rule attributes. Edits made in the view directly modify the attributes of the underlying rule objects in the `SourceRule` hierarchy held by the model, with the `UnifiedViewModel` handling the necessary model restructuring and signal emission for view updates.
**Data Flow Diagram (GUI Rule Management):**

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -2,13 +2,62 @@
# Core settings defining the pipeline standards and output format.
# --- Core Definitions ---
ALLOWED_ASSET_TYPES = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"]
ALLOWED_FILE_TYPES = [
"MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP",
"MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK",
"MAP_IMPERFECTION", # Added for imperfection maps
"MODEL", "EXTRA", "FILE_IGNORE"
]
# Old definitions (commented out)
# ALLOWED_ASSET_TYPES = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"]
# ALLOWED_FILE_TYPES = [
# "MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP",
# "MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK",
# "MAP_IMPERFECTION", # Added for imperfection maps
# "MODEL", "EXTRA", "FILE_IGNORE"
# ]
# New definitions using dictionaries
ASSET_TYPE_DEFINITIONS = {
"Surface": {
"description": "Standard PBR material set for a surface.",
"color": "#87CEEB", # Light Blue
"examples": ["WoodFloor01", "MetalPlate05"]
},
"Model": {
"description": "A 3D model file.",
"color": "#FFA500", # Orange
"examples": ["Chair.fbx", "Character.obj"]
},
"Decal": {
"description": "A texture designed to be projected onto surfaces.",
"color": "#90EE90", # Light Green
"examples": ["Graffiti01", "LeakStain03"]
},
"Atlas": {
"description": "A texture sheet containing multiple smaller textures.",
"color": "#FFC0CB", # Pink
"examples": ["FoliageAtlas", "UITextureSheet"]
},
"UtilityMap": {
"description": "A map used for specific technical purposes (e.g., flow map).",
"color": "#D3D3D3", # Light Grey
"examples": ["FlowMap", "CurvatureMap"]
}
}
FILE_TYPE_DEFINITIONS = {
"MAP_COL": {"description": "Color/Albedo Map", "color": "#FFFFE0", "examples": ["_col.", "_basecolor."]},
"MAP_NRM": {"description": "Normal Map", "color": "#E6E6FA", "examples": ["_nrm.", "_normal."]},
"MAP_METAL": {"description": "Metalness Map", "color": "#C0C0C0", "examples": ["_metal.", "_met."]},
"MAP_ROUGH": {"description": "Roughness Map", "color": "#A0522D", "examples": ["_rough.", "_rgh."]},
"MAP_AO": {"description": "Ambient Occlusion Map", "color": "#A9A9A9", "examples": ["_ao.", "_ambientocclusion."]},
"MAP_DISP": {"description": "Displacement/Height Map", "color": "#FFB6C1", "examples": ["_disp.", "_height."]},
"MAP_REFL": {"description": "Reflection/Specular Map", "color": "#E0FFFF", "examples": ["_refl.", "_specular."]},
"MAP_SSS": {"description": "Subsurface Scattering Map", "color": "#FFDAB9", "examples": ["_sss.", "_subsurface."]},
"MAP_FUZZ": {"description": "Fuzz/Sheen Map", "color": "#FFA07A", "examples": ["_fuzz.", "_sheen."]},
"MAP_IDMAP": {"description": "ID Map (for masking)", "color": "#F08080", "examples": ["_id.", "_matid."]},
"MAP_MASK": {"description": "Generic Mask Map", "color": "#FFFFFF", "examples": ["_mask."]},
"MAP_IMPERFECTION": {"description": "Imperfection Map (scratches, dust)", "color": "#F0E68C", "examples": ["_imp.", "_imperfection."]},
"MODEL": {"description": "3D Model File", "color": "#FFA500", "examples": [".fbx", ".obj"]},
"EXTRA": {"description": "Non-standard/Unclassified File", "color": "#778899", "examples": [".txt", ".zip"]},
"FILE_IGNORE": {"description": "File to be ignored", "color": "#2F4F4F", "examples": ["Thumbs.db", ".DS_Store"]}
}
# --- Target Output Standards ---
TARGET_FILENAME_PATTERN = "{base_name}_{map_type}_{resolution}.{ext}"
STANDARD_MAP_TYPES = [

5
config/suppliers.json Normal file
View File

@ -0,0 +1,5 @@
[
"Dinesen",
"Poliigon",
"poliigon"
]

View File

@ -1,7 +1,8 @@
# gui/delegates.py
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
from PySide6.QtCore import Qt, QModelIndex
from config import ALLOWED_ASSET_TYPES, ALLOWED_FILE_TYPES # Import config lists
# Import the new config dictionaries
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
class LineEditDelegate(QStyledItemDelegate):
"""Delegate for editing string values using a QLineEdit."""
@ -41,17 +42,17 @@ class ComboBoxDelegate(QStyledItemDelegate):
# Add a "clear" option first, associating None with it.
editor.addItem("---", None) # UserData = None
# Populate based on column using lists from config
items_list = None
# Populate based on column using keys from config dictionaries
items_keys = None
if column == 2: # Asset-Type Override (AssetRule)
items_list = ALLOWED_ASSET_TYPES
items_keys = list(ASSET_TYPE_DEFINITIONS.keys())
elif column == 4: # Item-Type Override (FileRule)
items_list = ALLOWED_FILE_TYPES
items_keys = list(FILE_TYPE_DEFINITIONS.keys())
if items_list:
for item_str in items_list:
# Add item with the string itself as text and UserData
editor.addItem(item_str, item_str)
if items_keys:
for item_key in sorted(items_keys): # Sort keys alphabetically for consistency
# Add item with the key string itself as text and UserData
editor.addItem(item_key, item_key)
else:
# If the delegate is incorrectly applied to another column,
# it will just have the "---" option.
@ -87,3 +88,99 @@ class ComboBoxDelegate(QStyledItemDelegate):
def updateEditorGeometry(self, editor, option, index):
# Ensures the editor widget is placed correctly within the cell.
editor.setGeometry(option.rect)
# gui/delegates.py - New content to insert
import json
import logging
import os # Added for path manipulation if needed, though json.dump handles creation
from PySide6.QtWidgets import QCompleter # Added QCompleter
# Configure logging
log = logging.getLogger(__name__)
SUPPLIERS_CONFIG_PATH = "config/suppliers.json"
class SupplierSearchDelegate(QStyledItemDelegate):
"""
Delegate for editing supplier names using a QLineEdit with auto-completion.
Loads known suppliers from config/suppliers.json and allows adding new ones.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.known_suppliers = self._load_suppliers()
def _load_suppliers(self):
"""Loads the list of known suppliers from the JSON config file."""
try:
with open(SUPPLIERS_CONFIG_PATH, 'r') as f:
suppliers = json.load(f)
if isinstance(suppliers, list):
# Ensure all items are strings
return sorted([str(s) for s in suppliers if isinstance(s, str)])
else:
log.warning(f"'{SUPPLIERS_CONFIG_PATH}' does not contain a valid list. Starting fresh.")
return []
except FileNotFoundError:
log.info(f"'{SUPPLIERS_CONFIG_PATH}' not found. Starting with an empty supplier list.")
return []
except json.JSONDecodeError:
log.error(f"Error decoding JSON from '{SUPPLIERS_CONFIG_PATH}'. Starting fresh.", exc_info=True)
return []
except Exception as e:
log.error(f"An unexpected error occurred loading '{SUPPLIERS_CONFIG_PATH}': {e}", exc_info=True)
return []
def _save_suppliers(self):
"""Saves the current list of known suppliers back to the JSON config file."""
try:
# Ensure the directory exists (though write_to_file handled initial creation)
os.makedirs(os.path.dirname(SUPPLIERS_CONFIG_PATH), exist_ok=True)
with open(SUPPLIERS_CONFIG_PATH, 'w') as f:
json.dump(self.known_suppliers, f, indent=4) # Save sorted list with indentation
log.debug(f"Successfully saved updated supplier list to '{SUPPLIERS_CONFIG_PATH}'.")
except IOError as e:
log.error(f"Could not write to '{SUPPLIERS_CONFIG_PATH}': {e}", exc_info=True)
except Exception as e:
log.error(f"An unexpected error occurred saving '{SUPPLIERS_CONFIG_PATH}': {e}", exc_info=True)
def createEditor(self, parent, option, index):
"""Creates the QLineEdit editor with a QCompleter."""
editor = QLineEdit(parent)
completer = QCompleter(self.known_suppliers, editor)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains) # More flexible matching
completer.setCompletionMode(QCompleter.PopupCompletion) # Standard popup
editor.setCompleter(completer)
return editor
def setEditorData(self, editor: QLineEdit, index: QModelIndex):
"""Sets the editor's initial data from the model."""
# Use EditRole as defined in the model's data() method for supplier
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 and handles new suppliers."""
final_text = editor.text().strip()
value_to_set = final_text if final_text else None # Set None if empty after stripping
# Set data in the model first
model.setData(index, value_to_set, Qt.EditRole)
# Add new supplier if necessary
if final_text and final_text not in self.known_suppliers:
log.info(f"Adding new supplier '{final_text}' to known list.")
self.known_suppliers.append(final_text)
self.known_suppliers.sort() # Keep the list sorted
# Update the completer's model immediately
completer = editor.completer()
if completer:
completer.model().setStringList(self.known_suppliers)
# Save the updated list back to the file
self._save_suppliers()
def updateEditorGeometry(self, editor, option, index):
"""Ensures the editor widget is placed correctly."""
editor.setGeometry(option.rect)

View File

@ -29,6 +29,7 @@ from rule_structure import SourceRule, AssetRule, FileRule # Import Rule Structu
# Removed: from gui.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel
# Removed: from gui.rule_hierarchy_model import RuleHierarchyModel
from gui.unified_view_model import UnifiedViewModel # Import the new unified model
from gui.delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate # Import delegates
from gui.delegates import LineEditDelegate, ComboBoxDelegate # Import delegates
# --- Backend Imports ---
@ -171,6 +172,9 @@ class MainWindow(QMainWindow):
# --- Internal State ---
self.current_asset_paths = set() # Store unique paths of assets added
self._pending_predictions = set() # Track input paths awaiting prediction results
self._accumulated_rules = {} # Store {input_path: SourceRule} as results arrive
self._source_file_lists = {} # Store {input_path: [file_list]} for context
# Removed: self.rule_hierarchy_model = RuleHierarchyModel()
# Removed: self._current_source_rule = None # The new model will hold the data
@ -402,10 +406,11 @@ class MainWindow(QMainWindow):
# Instantiate Delegates
lineEditDelegate = LineEditDelegate(self.unified_view)
comboBoxDelegate = ComboBoxDelegate(self.unified_view)
supplierSearchDelegate = SupplierSearchDelegate(self.unified_view) # Instantiate the new delegate
# Set Delegates for Columns (adjust column indices as per UnifiedViewModel)
# Assuming columns are: Name (0), Supplier (1), AssetType (2), TargetAsset (3), ItemType (4)
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, lineEditDelegate)
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, supplierSearchDelegate) # Use the new delegate for Supplier
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, comboBoxDelegate)
@ -667,6 +672,10 @@ class MainWindow(QMainWindow):
if file_list is not None: # Check if extraction was successful (not None)
log.debug(f"Extracted {len(file_list)} files for {input_path_str}. Emitting signal.")
log.info(f"VERIFY: Extracted file list for '{input_path_str}'. Count: {len(file_list)}. Emitting prediction signal.") # DEBUG Verify
# Store file list and mark as pending before emitting
self._source_file_lists[input_path_str] = file_list
self._pending_predictions.add(input_path_str)
log.debug(f"Added '{input_path_str}' to pending predictions. Current pending: {self._pending_predictions}")
self.start_prediction_signal.emit(input_path_str, file_list, selected_preset)
else:
log.warning(f"Skipping prediction for {input_path_str} due to extraction error.")
@ -844,7 +853,12 @@ class MainWindow(QMainWindow):
self.current_asset_paths.clear()
# self.preview_model.clear_data() # Old model removed
self.unified_model.clear_data() # Clear the new model data
self.statusBar().showMessage("Asset queue cleared.", 3000)
# Clear accumulation state
self._pending_predictions.clear()
self._accumulated_rules.clear()
self._source_file_lists.clear()
log.info("Cleared accumulation state (_pending_predictions, _accumulated_rules, _source_file_lists).")
self.statusBar().showMessage("Asset queue and prediction state cleared.", 3000)
else:
self.statusBar().showMessage("Asset queue is already empty.", 3000)
@ -924,6 +938,15 @@ class MainWindow(QMainWindow):
log.info(f"[{time.time():.4f}] Requesting background preview update for {len(input_paths)} items using Preset='{selected_preset}'")
self.statusBar().showMessage(f"Updating preview for '{selected_preset}'...", 0)
# --- Reset Accumulation State for this batch ---
log.debug("Clearing accumulated rules for new preview batch.")
self._accumulated_rules.clear()
# Reset pending predictions to only include paths in this update request
self._pending_predictions = set(input_paths)
log.debug(f"Reset pending predictions for batch: {self._pending_predictions}")
# Keep _source_file_lists, it might contain lists for paths already processed
# Clearing is handled by model's set_data now, no need to clear table view directly
if self.prediction_thread and self.prediction_handler:
# REMOVED Placeholder SourceRule creation
@ -981,8 +1004,10 @@ class MainWindow(QMainWindow):
# Connect the new signal to the handler's run_prediction slot using QueuedConnection
self.start_prediction_signal.connect(self.prediction_handler.run_prediction, Qt.ConnectionType.QueuedConnection)
# Removed: self.prediction_handler.prediction_results_ready.connect(self.on_prediction_results_ready) # Old signal
self.prediction_handler.rule_hierarchy_ready.connect(self._on_rule_hierarchy_ready) # Connect the LIST signal
self.prediction_handler.prediction_finished.connect(self.on_prediction_finished)
# Assume PredictionHandler.rule_hierarchy_ready signal is changed to Signal(str, list) -> input_path, rules_list
self.prediction_handler.rule_hierarchy_ready.connect(self._on_rule_hierarchy_ready) # Connect the LIST signal (now with input_path)
# Assume PredictionHandler.prediction_finished signal is changed to Signal(str) -> input_path
self.prediction_handler.prediction_finished.connect(self.on_prediction_finished) # Connect finish signal (now with input_path)
self.prediction_handler.status_message.connect(self.show_status_message)
# --- REMOVED connections causing thread/handler cleanup ---
# self.prediction_handler.prediction_finished.connect(self.prediction_thread.quit)
@ -1027,12 +1052,30 @@ class MainWindow(QMainWindow):
# # This is no longer needed as _on_rule_hierarchy_ready handles data loading for the new model.
# pass
@Slot()
def on_prediction_finished(self):
log.info(f"[{time.time():.4f}] --> Prediction finished signal received.")
# Optionally update status bar or re-enable controls if needed after prediction finishes
# (Controls are primarily managed by processing_finished, but prediction is a separate background task)
self.statusBar().showMessage("Preview updated.", 3000)
# Slot signature assumes prediction_finished signal is updated to emit input_path: Signal(str)
# Slot signature assumes prediction_finished signal is updated to emit input_path: Signal(str)
@Slot(str)
def on_prediction_finished(self, input_path: str):
"""Handles the completion (potentially failure) of a single prediction task."""
log.info(f"[{time.time():.4f}] --> Prediction finished signal received for: {input_path}")
# Ensure path is removed from pending even if rule_hierarchy_ready wasn't emitted (e.g., critical error)
if input_path in self._pending_predictions:
log.warning(f"Prediction finished for '{input_path}', but it was still marked as pending. Removing.")
self._pending_predictions.discard(input_path)
# Check if this was the last pending item after an error
if not self._pending_predictions:
log.info("Prediction finished, and no more predictions are pending (potentially due to error). Finalizing model update.")
self._finalize_model_update()
else:
# Update status about remaining items
remaining_count = len(self._pending_predictions)
self.statusBar().showMessage(f"Prediction failed/finished for {Path(input_path).name}. Waiting for {remaining_count} more...", 5000)
else:
log.debug(f"Prediction finished for '{input_path}', which was already processed.")
# Original status message might be misleading now, handled by accumulation logic.
# self.statusBar().showMessage("Preview updated.", 3000) # Removed
@Slot(str, str, str)
def update_file_status(self, input_path_str, status, message):
@ -1580,18 +1623,74 @@ class MainWindow(QMainWindow):
# @Slot(object)
# def _on_rule_updated(self, rule_object): ...
@Slot(list) # Changed signature to accept list
# Slot signature assumes rule_hierarchy_ready signal is updated to emit input_path: Signal(str, list)
# Slot signature matches rule_hierarchy_ready = Signal(list)
@Slot(list)
def _on_rule_hierarchy_ready(self, source_rules_list: list):
log.debug(f"--> Entered _on_rule_hierarchy_ready with {len(source_rules_list)} SourceRule(s)")
"""Receives the generated list of SourceRule hierarchies and updates the unified view model."""
# Removed: log.info(f"Received rule hierarchy ready signal for input: {source_rule.input_path}")
# Removed: self._current_source_rule = source_rule # This concept might need rethinking if processing needs a specific rule
# Removed: self.rule_hierarchy_model.set_root_rule(source_rule)
# Removed: self.hierarchy_tree_view.expandToDepth(0)
"""Receives prediction results (a list containing one SourceRule) for a single input path,
accumulates them, and updates the model when all are ready."""
# Load the LIST of data into the new UnifiedViewModel
self.unified_model.load_data(source_rules_list) # Pass the list
log.debug("Unified view model updated with new list of SourceRules.")
# --- Extract input_path from the received rule ---
input_path = None
source_rule = None
if source_rules_list and isinstance(source_rules_list[0], SourceRule):
source_rule = source_rules_list[0]
input_path = source_rule.input_path
log.debug(f"--> Entered _on_rule_hierarchy_ready for '{input_path}' with {len(source_rules_list)} SourceRule(s)")
elif source_rules_list:
log.error(f"Received non-SourceRule object in list: {type(source_rules_list[0])}. Cannot process.")
# Attempt to find which pending prediction this might correspond to? Difficult.
# For now, we can't reliably remove from pending without the path.
return
else:
# This case might happen if prediction failed critically before creating a rule.
# The prediction_finished signal (which now includes input_path) should handle removing from pending.
log.warning("Received empty source_rules_list in _on_rule_hierarchy_ready. Prediction likely failed.")
return # Nothing to accumulate
if input_path is None:
log.error("Could not determine input_path from received source_rules_list. Aborting accumulation.")
return
if input_path not in self._pending_predictions:
log.warning(f"Received rule hierarchy for '{input_path}', but it was not in the pending set. Ignoring stale result? Pending: {self._pending_predictions}")
return # Ignore if not expected
# --- Accumulate Result ---
if source_rule: # Check if we successfully got the rule object
self._accumulated_rules[input_path] = source_rule
log.debug(f"Accumulated rule for '{input_path}'. Total accumulated: {len(self._accumulated_rules)}")
else:
# This path is already handled by the initial checks, but log just in case.
log.warning(f"No valid SourceRule found for '{input_path}' to accumulate.")
# --- Mark as Completed ---
self._pending_predictions.discard(input_path)
log.debug(f"Removed '{input_path}' from pending predictions. Remaining: {self._pending_predictions}")
# --- Check for Completion ---
if not self._pending_predictions:
log.info("All pending predictions received. Finalizing model update.")
self._finalize_model_update()
else:
# Update status bar with progress
completed_count = len(self._accumulated_rules)
pending_count = len(self._pending_predictions)
total_count = completed_count + pending_count # This might be slightly off if some failed without rules
status_msg = f"Preview updated for {Path(input_path).name}. Waiting for {pending_count} more ({completed_count}/{total_count} requested)..."
self.statusBar().showMessage(status_msg, 5000)
log.debug(status_msg)
def _finalize_model_update(self):
"""Combines accumulated rules and updates the UI model and view."""
log.debug("Entering _finalize_model_update")
final_rules = list(self._accumulated_rules.values())
log.info(f"Finalizing model with {len(final_rules)} accumulated SourceRule(s).")
# Load the FINAL LIST of data into the UnifiedViewModel
self.unified_model.load_data(final_rules)
log.debug("Unified view model updated with final list of SourceRules.")
# Resize columns to fit content after loading data
for col in range(self.unified_model.columnCount()):
@ -1599,6 +1698,8 @@ class MainWindow(QMainWindow):
log.debug("Unified view columns resized to contents.")
self.unified_view.expandToDepth(1) # Expand Source -> Asset level
self.statusBar().showMessage(f"Preview complete for {len(final_rules)} asset(s).", 5000)
# --- Main Execution ---
def run_gui():

View File

@ -26,8 +26,8 @@ try:
# from asset_processor import AssetProcessor, AssetProcessingError
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType
import config as app_config # Import project's config module
# Import the lists directly for easier access
from config import ALLOWED_ASSET_TYPES, ALLOWED_FILE_TYPES
# Import the new dictionaries directly for easier access
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
BACKEND_AVAILABLE = True
except ImportError as e:
print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}")
@ -209,8 +209,8 @@ class PredictionHandler(QObject):
# --- Signals ---
# Emitted when the hierarchical rule structure is ready for a single source
rule_hierarchy_ready = Signal(list) # Emits a LIST containing ONE SourceRule object
# Emitted when prediction/hierarchy generation for a source is done
prediction_finished = Signal()
# Emitted when prediction/hierarchy generation for a source is done (emits the input_source_identifier)
prediction_finished = Signal(str)
# Emitted for status updates
status_message = Signal(str, int)
@ -261,7 +261,7 @@ class PredictionHandler(QObject):
log.warning(f"Input source path does not exist: '{input_source_identifier}'. Skipping prediction.")
self.status_message.emit("Input path not found.", 3000)
self.rule_hierarchy_ready.emit([])
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
return
@ -269,30 +269,30 @@ class PredictionHandler(QObject):
self.status_message.emit(f"Analyzing '{source_path.name}'...", 0)
config: Configuration | None = None
allowed_asset_types: List[str] = []
allowed_file_types: List[str] = [] # These are ItemType names
asset_type_definitions: Dict[str, Dict] = {}
file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
try:
config = Configuration(preset_name)
# Load allowed types from the project's config module
# Load allowed types from the project's config module (now dictionaries)
if app_config:
allowed_asset_types = getattr(app_config, 'ALLOWED_ASSET_TYPES', [])
allowed_file_types = getattr(app_config, 'ALLOWED_FILE_TYPES', [])
log.debug(f"Loaded allowed AssetTypes: {allowed_asset_types}")
log.debug(f"Loaded allowed FileTypes (ItemTypes): {allowed_file_types}")
asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
else:
log.warning("Project config module not loaded. Cannot get allowed types.")
log.warning("Project config module not loaded. Cannot get type definitions.")
except ConfigurationError as e:
log.error(f"Failed to load configuration for preset '{preset_name}': {e}")
self.status_message.emit(f"Error loading preset '{preset_name}': {e}", 5000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
except Exception as e:
log.exception(f"Unexpected error loading configuration or allowed types for preset '{preset_name}': {e}")
self.status_message.emit(f"Unexpected error loading preset '{preset_name}'.", 5000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
@ -303,7 +303,7 @@ class PredictionHandler(QObject):
except Exception as e:
log.exception(f"Error during file classification for source '{input_source_identifier}': {e}")
self.status_message.emit(f"Error classifying files: {e}", 5000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
@ -311,7 +311,7 @@ class PredictionHandler(QObject):
log.warning(f"Classification yielded no assets for source '{input_source_identifier}'.")
self.status_message.emit("No assets identified from files.", 3000)
self.rule_hierarchy_ready.emit([]) # Emit empty list
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
return
@ -348,16 +348,16 @@ class PredictionHandler(QObject):
# Ensure the predicted type is allowed, fallback if necessary
# Now predicted_asset_type is already a string
if allowed_asset_types and predicted_asset_type not in allowed_asset_types:
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ALLOWED_ASSET_TYPES. Falling back.")
if asset_type_definitions and predicted_asset_type not in asset_type_definitions:
log.warning(f"Predicted AssetType '{predicted_asset_type}' for asset '{asset_name}' is not in ASSET_TYPE_DEFINITIONS. Falling back.")
# Fallback logic: use the default from config if allowed, else first allowed type
default_type = getattr(app_config, 'DEFAULT_ASSET_CATEGORY', 'Surface')
if default_type in allowed_asset_types:
if default_type in asset_type_definitions:
predicted_asset_type = default_type
elif allowed_asset_types:
predicted_asset_type = allowed_asset_types[0]
elif asset_type_definitions:
predicted_asset_type = list(asset_type_definitions.keys())[0] # Use first key
else:
pass # Keep the original prediction if allowed list is empty
pass # Keep the original prediction if definitions are empty
asset_rule = AssetRule(
@ -370,35 +370,42 @@ class PredictionHandler(QObject):
file_rules = []
for file_info in files_info:
# Determine FileRule level overrides/defaults
item_type_override = file_info['item_type'] # From classification
base_item_type = file_info['item_type'] # Type from classification (e.g., COL, NRM, EXTRA)
target_asset_name_override = file_info['asset_name'] # From classification
# Ensure the predicted item type is allowed (check against prefixed version), skipping EXTRA and FILE_IGNORE
# Only prefix if it's a map type that doesn't already have the prefix
prefixed_item_type = f"MAP_{item_type_override}" if not item_type_override.startswith("MAP_") and item_type_override not in ["FILE_IGNORE", "EXTRA", "MODEL"] else item_type_override
# Check if the (potentially prefixed) type is allowed, but only if it's not supposed to be ignored or extra
if allowed_file_types and prefixed_item_type not in allowed_file_types and item_type_override not in ["FILE_IGNORE", "EXTRA"]:
log.warning(f"Predicted ItemType '{item_type_override}' (checked as '{prefixed_item_type}') for file '{file_info['file_path']}' is not in ALLOWED_FILE_TYPES. Setting to FILE_IGNORE.")
item_type_override = "FILE_IGNORE" # Fallback to FILE_IGNORE string
# Determine the final item_type string (prefix maps, check if allowed)
final_item_type = base_item_type # Start with the base type
if not base_item_type.startswith("MAP_") and base_item_type not in ["FILE_IGNORE", "EXTRA", "MODEL"]:
# Prefix map types that don't already have it
final_item_type = f"MAP_{base_item_type}"
# Check if the final type is allowed (exists as a key in config)
if file_type_definitions and final_item_type not in file_type_definitions and base_item_type not in ["FILE_IGNORE", "EXTRA"]:
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 base type to FILE_IGNORE.")
final_item_type = "FILE_IGNORE" # Fallback base type to FILE_IGNORE string
# Output format is determined by the engine, not predicted here. Leave as None.
output_format_override = None
# User override for item type starts as None
item_type_override = None
# --- DEBUG LOG: Inspect data before FileRule creation ---
log.debug(f" Creating FileRule for: {file_info['file_path']}")
log.debug(f" Using item_type_override: {item_type_override}")
log.debug(f" Using target_asset_name_override: {target_asset_name_override}")
log.debug(f" Base Item Type (from classification): {base_item_type}")
log.debug(f" Final Item Type (for model): {final_item_type}")
log.debug(f" Target Asset Name Override: {target_asset_name_override}")
# Explicitly check and log the flag value from file_info
is_gloss_source_value = file_info.get('is_gloss_source', 'MISSING') # Get value or 'MISSING'
log.debug(f" Value for 'is_gloss_source' from file_info: {is_gloss_source_value}")
# --- End DEBUG LOG ---
# TODO: Need to verify FileRule constructor accepts is_gloss_source
# and pass is_gloss_source_value if it does.
# Pass the retrieved flag value to the constructor
file_rule = FileRule(
file_path=file_info['file_path'], # This is static info based on input
item_type=final_item_type, # Set the new base item_type field
# --- Populate ONLY Overridable Fields ---
item_type_override=item_type_override,
# Initialize override with the classified type for display
item_type_override=final_item_type,
target_asset_name_override=target_asset_name_override,
output_format_override=output_format_override,
is_gloss_source=is_gloss_source_value if isinstance(is_gloss_source_value, bool) else False, # Pass the flag, ensure boolean
@ -421,7 +428,7 @@ class PredictionHandler(QObject):
log.exception(f"Error building rule hierarchy for source '{input_source_identifier}': {e}")
self.status_message.emit(f"Error building rules: {e}", 5000)
# Don't emit hierarchy, just finish
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
# Removed erroneous temp_dir_obj cleanup
return
@ -439,7 +446,7 @@ class PredictionHandler(QObject):
# Removed prediction_results_ready signal emission
self.status_message.emit(f"Analysis complete for '{input_source_identifier}'.", 3000)
self.prediction_finished.emit()
self.prediction_finished.emit(input_source_identifier)
self._is_running = False
# Removed temp_dir_obj cleanup - not relevant here
log.info(f"[{time.time():.4f}][T:{thread_id}] <-- Exiting PredictionHandler.run_prediction.")

View File

@ -1,15 +1,22 @@
# gui/unified_view_model.py
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal # Added Signal
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 config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS # Added for coloring
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
# -----------------------------------------
"""
A QAbstractItemModel for displaying and editing the hierarchical structure
of SourceRule -> AssetRule -> FileRule.
"""
Columns = [
"Name", "Supplier Override", "Asset-Type Override",
"Name", "Supplier", "Asset-Type Override", # Renamed "Supplier Override"
"Target Asset Name Override", "Item-Type Override",
"Status", "Output Path"
]
@ -165,55 +172,101 @@ class UnifiedViewModel(QAbstractItemModel):
item = index.internalPointer()
column = index.column()
# --- Handle different item types ---
if isinstance(item, SourceRule): # This might only be relevant if SourceRule is displayed
if role == Qt.DisplayRole:
if column == 0: return item.input_path
# Use supplier_override if set, otherwise empty string
if column == self.COL_SUPPLIER: return item.supplier_override if item.supplier_override is not None else ""
# Other columns return None or "" for SourceRule
elif role == Qt.EditRole:
# Return supplier_override for editing
if column == self.COL_SUPPLIER: return item.supplier_override if item.supplier_override is not None else ""
# --- 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
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:
type_info = ASSET_TYPE_DEFINITIONS.get(asset_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for asset type '{asset_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for asset type '{asset_type}' in config.")
return None # Fallback if color key missing
else:
# Optional: Add logging for missing asset type definition
# print(f"Warning: Asset type '{asset_type}' not found in ASSET_TYPE_DEFINITIONS.")
return None # Fallback if type not in config
else:
return None # Fallback if no asset_type determined
elif isinstance(item, FileRule):
# Determine effective item type: Prioritize override, then use base type
effective_item_type = item.item_type_override if item.item_type_override is not None else item.item_type
if effective_item_type:
type_info = FILE_TYPE_DEFINITIONS.get(effective_item_type)
if type_info:
hex_color = type_info.get("color")
if hex_color:
try:
return QColor(hex_color)
except ValueError:
# Optional: Add logging for invalid hex color
# print(f"Warning: Invalid hex color '{hex_color}' for file type '{item_type}' in config.")
return None # Fallback for invalid hex
else:
# Optional: Add logging for missing color key
# print(f"Warning: No color defined for file type '{item_type}' in config.")
return None # Fallback if color key missing
else:
# File types often don't have specific colors, so no warning needed unless debugging
return None # Fallback if type not in config
else:
return None # Fallback if no item_type determined
else: # Other item types or if item is None
return None
# --- Handle other roles (Display, Edit, etc.) ---
if isinstance(item, SourceRule):
if role == Qt.DisplayRole or role == Qt.EditRole: # Combine Display and Edit logic
if column == self.COL_NAME:
return Path(item.input_path).name # Display only basename for SourceRule
elif column == self.COL_SUPPLIER:
# Return override if set, otherwise the original identifier, else empty string
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 ""
# Other columns return None or "" for SourceRule in Display/Edit roles
return None # Default for SourceRule for other roles/columns
elif isinstance(item, AssetRule):
if role == Qt.DisplayRole:
if column == self.COL_NAME: return item.asset_name
# Use asset_type_override if set, otherwise fall back to predicted asset_type
if column == self.COL_ASSET_TYPE:
display_value = item.asset_type_override if item.asset_type_override is not None else item.asset_type
return display_value if display_value else ""
# Placeholder columns
if column == self.COL_STATUS: return "" # Status (Not handled yet)
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
elif role == Qt.EditRole:
# Return asset_type_override for editing (delegate expects string or None)
if column == self.COL_ASSET_TYPE:
return item.asset_type_override # Return string or None
return None # Default for AssetRule
elif isinstance(item, FileRule):
if role == Qt.DisplayRole:
if column == self.COL_NAME: return Path(item.file_path).name # Display only filename
# Use target_asset_name_override if set, otherwise empty string
if column == self.COL_TARGET_ASSET:
return item.target_asset_name_override if item.target_asset_name_override is not None else ""
# Use item_type_override if set, otherwise empty string (assuming predicted isn't stored directly)
if column == self.COL_ITEM_TYPE:
# Assuming item_type_override stores the string name of the ItemType enum
return item.item_type_override if item.item_type_override else ""
if column == self.COL_STATUS: return "" # Status (Not handled yet)
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
elif role == Qt.EditRole:
# Return target_asset_name_override for editing
if column == self.COL_TARGET_ASSET: return item.target_asset_name_override if item.target_asset_name_override is not None else ""
# Return item_type_override for editing (delegate expects string or None)
if column == self.COL_ITEM_TYPE: return item.item_type_override # Return string or None
return None # Default for FileRule
return None # Should not be reached if item is one of the known types
return None # Default return if role/item combination not handled
def setData(self, index: QModelIndex, value, role: int = Qt.EditRole) -> bool:
"""Sets the role data for the item at index to value."""
@ -229,10 +282,17 @@ class UnifiedViewModel(QAbstractItemModel):
# --- Handle different item types ---
if isinstance(item, SourceRule): # If SourceRule is editable
if column == self.COL_SUPPLIER:
# 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
# Update supplier_override
# Get the new value, strip whitespace, treat empty as None
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)
original_identifier = getattr(item, 'supplier_identifier', None)
# 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
# Update supplier_override only if it's different
if item.supplier_override != new_value:
item.supplier_override = new_value
changed = True
@ -254,8 +314,122 @@ class UnifiedViewModel(QAbstractItemModel):
if new_value == "": new_value = None # Treat empty string as 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
item.target_asset_name_override = new_value
changed = True
# --- Start: New Direct Model Restructuring Logic ---
old_parent_asset = getattr(item, 'parent_asset', None)
if old_parent_asset: # Ensure we have the old parent
source_rule = getattr(old_parent_asset, 'parent_source', None)
if source_rule: # Ensure we have the grandparent
new_target_name = new_value # Can be None or a string
# Get old parent index and source row
try:
grandparent_row = self._source_rules.index(source_rule)
old_parent_row = source_rule.assets.index(old_parent_asset)
source_row = old_parent_asset.files.index(item)
old_parent_index = self.createIndex(old_parent_row, 0, old_parent_asset)
grandparent_index = self.createIndex(grandparent_row, 0, source_rule) # Needed for insert/remove parent
except ValueError:
print("Error: Could not find item, parent, or grandparent in model structure during setData.")
item.target_asset_name_override = old_value # Revert data change
return False # Indicate failure
target_parent_asset = None
target_parent_index = QModelIndex()
target_parent_row = -1 # Row within source_rule.assets
target_row = -1 # Row within target_parent_asset.files
move_occurred = False # Flag to track if a move happened
# 1. Find existing target parent
if new_target_name: # Only search if a specific target is given
for i, asset in enumerate(source_rule.assets):
if asset.asset_name == new_target_name:
target_parent_asset = asset
target_parent_row = i
target_parent_index = self.createIndex(target_parent_row, 0, target_parent_asset)
break
# 2. Handle Move/Creation
if target_parent_asset:
# --- Move to Existing Parent ---
if target_parent_asset != old_parent_asset: # Don't move if target is the same as old parent
target_row = len(target_parent_asset.files) # Append to the end
# print(f"DEBUG: Moving {Path(item.file_path).name} from {old_parent_asset.asset_name} ({source_row}) to {target_parent_asset.asset_name} ({target_row})")
self.beginMoveRows(old_parent_index, source_row, source_row, target_parent_index, target_row)
# Restructure internal data
old_parent_asset.files.pop(source_row)
target_parent_asset.files.append(item)
item.parent_asset = target_parent_asset # Update parent reference
self.endMoveRows()
move_occurred = True
else:
# Target is the same as the old parent. No move needed.
pass
elif new_target_name: # Only create if a *new* specific target name was given
# --- Create New Parent and Move ---
# print(f"DEBUG: Creating new parent '{new_target_name}' and moving {Path(item.file_path).name}")
# Create new AssetRule
new_asset_rule = AssetRule(asset_name=new_target_name)
new_asset_rule.asset_type = old_parent_asset.asset_type # Copy type from old parent
new_asset_rule.asset_type_override = old_parent_asset.asset_type_override # Copy override too
new_asset_rule.parent_source = source_rule # Set parent reference
# Determine insertion row for the new parent (e.g., append)
new_parent_row = len(source_rule.assets)
# print(f"DEBUG: Inserting new parent at row {new_parent_row} under {Path(source_rule.input_path).name}")
# 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
self.endInsertRows()
# Get index for the newly inserted parent
target_parent_index = self.createIndex(new_parent_row, 0, new_asset_rule)
target_row = 0 # Insert file at the beginning of the new parent (for signal)
# Emit signals for moving the file row
# print(f"DEBUG: Moving {Path(item.file_path).name} from {old_parent_asset.asset_name} ({source_row}) to new {new_asset_rule.asset_name} ({target_row})")
self.beginMoveRows(old_parent_index, source_row, source_row, target_parent_index, target_row)
# Restructure internal data
old_parent_asset.files.pop(source_row)
new_asset_rule.files.append(item) # Append is fine, target_row=0 was for signal
item.parent_asset = new_asset_rule # Update parent reference
self.endMoveRows()
move_occurred = True
# Update target_parent_asset for potential cleanup check later
target_parent_asset = new_asset_rule
else: # new_target_name is None or empty
# No move happens when the override is simply cleared.
pass
# 3. Cleanup Empty Old Parent (only if a move occurred and old parent is empty)
if move_occurred and not old_parent_asset.files:
# print(f"DEBUG: Removing empty old parent {old_parent_asset.asset_name}")
try:
# Find the row of the old parent again, as it might have shifted
old_parent_row_for_removal = source_rule.assets.index(old_parent_asset)
# print(f"DEBUG: Removing parent at row {old_parent_row_for_removal} under {Path(source_rule.input_path).name}")
self.beginRemoveRows(grandparent_index, old_parent_row_for_removal, old_parent_row_for_removal)
source_rule.assets.pop(old_parent_row_for_removal)
self.endRemoveRows()
except ValueError:
print(f"Error: Could not find old parent '{old_parent_asset.asset_name}' for removal.")
# Log error, but continue
else:
print("Error: Could not find grandparent SourceRule during setData restructuring.")
item.target_asset_name_override = old_value # Revert
return False
else:
print("Error: Could not find parent AssetRule during setData restructuring.")
item.target_asset_name_override = old_value # Revert
return False
# --- End: New Direct Model Restructuring Logic ---
elif column == self.COL_ITEM_TYPE: # Item-Type Override
# Delegate provides string value (e.g., "MAP_COL") or None
new_value = str(value) if value is not None else None

View File

@ -4,6 +4,7 @@ from typing import List, Dict, Any, Tuple
@dataclasses.dataclass
class FileRule:
file_path: str = None
item_type: str = None # Base type determined by classification (e.g., MAP_COL, EXTRA)
item_type_override: str = None # Renamed from map_type_override
target_asset_name_override: str = None # Added override field
resolution_override: Tuple[int, int] = None