parent
26e1a769ce
commit
a5be50b587
2
.gitignore
vendored
2
.gitignore
vendored
@ -27,3 +27,5 @@ build/
|
|||||||
|
|
||||||
# Ignore Windows thumbnail cache
|
# Ignore Windows thumbnail cache
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
gui/__pycache__
|
||||||
|
__pycache__
|
||||||
|
|||||||
@ -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:
|
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.
|
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:
|
3. **Multiple Interfaces:** Provides different ways to interact with the tool:
|
||||||
* Graphical User Interface (GUI)
|
* Graphical User Interface (GUI)
|
||||||
* Command-Line Interface (CLI)
|
* Command-Line Interface (CLI)
|
||||||
* Directory Monitor for automated processing.
|
* 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.
|
4. **Optional Integration:** Includes scripts and logic for integrating with external software, specifically Blender, to automate material and node group creation.
|
||||||
|
|
||||||
## Hierarchical Rule System
|
## Hierarchical Rule System
|
||||||
@ -27,14 +27,15 @@ This hierarchy allows for fine-grained control over processing parameters. The G
|
|||||||
|
|
||||||
## Core Components
|
## 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.
|
* `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.
|
* `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.
|
* `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`).
|
* `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 and handling inline editing.
|
* `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.
|
* `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 based on input files and the selected preset.
|
* `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.
|
* `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.
|
* `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`.
|
* `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`.
|
||||||
|
|||||||
@ -5,7 +5,9 @@ This document outlines the key files and directories within the Asset Processor
|
|||||||
```
|
```
|
||||||
Asset_processor_tool/
|
Asset_processor_tool/
|
||||||
├── asset_processor.py # Older core class, kept for reference (not used in main flow)
|
├── 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)
|
├── configuration.py # Class for loading and accessing configuration (merges config.py and presets)
|
||||||
├── detailed_documentation_plan.md # (Existing file, potentially outdated)
|
├── detailed_documentation_plan.md # (Existing file, potentially outdated)
|
||||||
├── Dockerfile # Instructions for building the Docker container image
|
├── Dockerfile # Instructions for building the Docker container image
|
||||||
@ -50,7 +52,9 @@ Asset_processor_tool/
|
|||||||
**Key Files and Directories:**
|
**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.
|
* `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`.
|
* `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.
|
* `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.
|
* `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.
|
||||||
|
|||||||
@ -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.
|
* `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.
|
* `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`)
|
## `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:
|
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).
|
* 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.
|
* Hold the `SourceRule` data that is the single source of truth for the GUI's processing rules.
|
||||||
|
|
||||||
## `Delegates` (`gui/delegates.py`)
|
## `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:
|
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`).
|
* `ComboBoxDelegate`: For selecting from predefined lists of allowed asset and file types, sourced from `config.py`.
|
||||||
* `QLineEdit`: For free-form text editing (e.g., target asset name override, supplier identifier override).
|
* `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.
|
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:
|
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.
|
* 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., asset type, item type, target asset name).
|
* 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 the complete `SourceRule` hierarchy based on these predictions.
|
* Constructs a `SourceRule` hierarchy for the single input source.
|
||||||
* Emits a signal (`rule_hierarchy_ready`) with the generated `List[SourceRule]` to the `MainWindow` to populate the Unified Hierarchical View.
|
* 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`)
|
## `ZipHandler` (`monitor.py`)
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,25 @@ This document provides technical details about the configuration system and the
|
|||||||
|
|
||||||
The tool utilizes a two-tiered configuration system:
|
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.
|
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`)
|
## `Configuration` Class (`configuration.py`)
|
||||||
|
|
||||||
The `Configuration` class is responsible for loading, merging, and preparing the configuration settings for use by the `AssetProcessor`.
|
The `Configuration` class is responsible for loading, merging, and preparing the configuration settings for use by the `AssetProcessor`.
|
||||||
|
|||||||
@ -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.
|
* 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`.
|
* 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.
|
* 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`.
|
* 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`.
|
* 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.
|
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.
|
* **`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):**
|
**Data Flow Diagram (GUI Rule Management):**
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
63
config.py
63
config.py
@ -2,13 +2,62 @@
|
|||||||
# Core settings defining the pipeline standards and output format.
|
# Core settings defining the pipeline standards and output format.
|
||||||
|
|
||||||
# --- Core Definitions ---
|
# --- Core Definitions ---
|
||||||
ALLOWED_ASSET_TYPES = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"]
|
# Old definitions (commented out)
|
||||||
ALLOWED_FILE_TYPES = [
|
# ALLOWED_ASSET_TYPES = ["Surface", "Model", "Decal", "Atlas", "UtilityMap"]
|
||||||
"MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP",
|
# ALLOWED_FILE_TYPES = [
|
||||||
"MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK",
|
# "MAP_COL", "MAP_NRM", "MAP_METAL", "MAP_ROUGH", "MAP_AO", "MAP_DISP",
|
||||||
"MAP_IMPERFECTION", # Added for imperfection maps
|
# "MAP_REFL", "MAP_SSS", "MAP_FUZZ", "MAP_IDMAP", "MAP_MASK",
|
||||||
"MODEL", "EXTRA", "FILE_IGNORE"
|
# "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 Output Standards ---
|
||||||
TARGET_FILENAME_PATTERN = "{base_name}_{map_type}_{resolution}.{ext}"
|
TARGET_FILENAME_PATTERN = "{base_name}_{map_type}_{resolution}.{ext}"
|
||||||
STANDARD_MAP_TYPES = [
|
STANDARD_MAP_TYPES = [
|
||||||
|
|||||||
5
config/suppliers.json
Normal file
5
config/suppliers.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
"Dinesen",
|
||||||
|
"Poliigon",
|
||||||
|
"poliigon"
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
115
gui/delegates.py
115
gui/delegates.py
@ -1,7 +1,8 @@
|
|||||||
# gui/delegates.py
|
# gui/delegates.py
|
||||||
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
|
from PySide6.QtWidgets import QStyledItemDelegate, QLineEdit, QComboBox
|
||||||
from PySide6.QtCore import Qt, QModelIndex
|
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):
|
class LineEditDelegate(QStyledItemDelegate):
|
||||||
"""Delegate for editing string values using a QLineEdit."""
|
"""Delegate for editing string values using a QLineEdit."""
|
||||||
@ -41,17 +42,17 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
|||||||
# Add a "clear" option first, associating None with it.
|
# Add a "clear" option first, associating None with it.
|
||||||
editor.addItem("---", None) # UserData = None
|
editor.addItem("---", None) # UserData = None
|
||||||
|
|
||||||
# Populate based on column using lists from config
|
# Populate based on column using keys from config dictionaries
|
||||||
items_list = None
|
items_keys = None
|
||||||
if column == 2: # Asset-Type Override (AssetRule)
|
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)
|
elif column == 4: # Item-Type Override (FileRule)
|
||||||
items_list = ALLOWED_FILE_TYPES
|
items_keys = list(FILE_TYPE_DEFINITIONS.keys())
|
||||||
|
|
||||||
if items_list:
|
if items_keys:
|
||||||
for item_str in items_list:
|
for item_key in sorted(items_keys): # Sort keys alphabetically for consistency
|
||||||
# Add item with the string itself as text and UserData
|
# Add item with the key string itself as text and UserData
|
||||||
editor.addItem(item_str, item_str)
|
editor.addItem(item_key, item_key)
|
||||||
else:
|
else:
|
||||||
# If the delegate is incorrectly applied to another column,
|
# If the delegate is incorrectly applied to another column,
|
||||||
# it will just have the "---" option.
|
# it will just have the "---" option.
|
||||||
@ -87,3 +88,99 @@ class ComboBoxDelegate(QStyledItemDelegate):
|
|||||||
def updateEditorGeometry(self, editor, option, index):
|
def updateEditorGeometry(self, editor, option, index):
|
||||||
# Ensures the editor widget is placed correctly within the cell.
|
# Ensures the editor widget is placed correctly within the cell.
|
||||||
editor.setGeometry(option.rect)
|
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)
|
||||||
@ -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.preview_table_model import PreviewTableModel, PreviewSortFilterProxyModel
|
||||||
# Removed: from gui.rule_hierarchy_model import RuleHierarchyModel
|
# Removed: from gui.rule_hierarchy_model import RuleHierarchyModel
|
||||||
from gui.unified_view_model import UnifiedViewModel # Import the new unified model
|
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
|
from gui.delegates import LineEditDelegate, ComboBoxDelegate # Import delegates
|
||||||
|
|
||||||
# --- Backend Imports ---
|
# --- Backend Imports ---
|
||||||
@ -171,6 +172,9 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
# --- Internal State ---
|
# --- Internal State ---
|
||||||
self.current_asset_paths = set() # Store unique paths of assets added
|
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.rule_hierarchy_model = RuleHierarchyModel()
|
||||||
# Removed: self._current_source_rule = None # The new model will hold the data
|
# Removed: self._current_source_rule = None # The new model will hold the data
|
||||||
|
|
||||||
@ -218,7 +222,7 @@ class MainWindow(QMainWindow):
|
|||||||
# --- Connect Editor Signals ---
|
# --- Connect Editor Signals ---
|
||||||
self._connect_editor_change_signals()
|
self._connect_editor_change_signals()
|
||||||
|
|
||||||
# --- Adjust Splitter ---
|
# --- Adjust Splitter ---
|
||||||
self.splitter.setSizes([400, 800]) # Initial size ratio
|
self.splitter.setSizes([400, 800]) # Initial size ratio
|
||||||
|
|
||||||
# --- UI Setup Methods ---
|
# --- UI Setup Methods ---
|
||||||
@ -402,10 +406,11 @@ class MainWindow(QMainWindow):
|
|||||||
# Instantiate Delegates
|
# Instantiate Delegates
|
||||||
lineEditDelegate = LineEditDelegate(self.unified_view)
|
lineEditDelegate = LineEditDelegate(self.unified_view)
|
||||||
comboBoxDelegate = ComboBoxDelegate(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)
|
# Set Delegates for Columns (adjust column indices as per UnifiedViewModel)
|
||||||
# Assuming columns are: Name (0), Supplier (1), AssetType (2), TargetAsset (3), ItemType (4)
|
# 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_ASSET_TYPE, comboBoxDelegate)
|
||||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_TARGET_ASSET, lineEditDelegate)
|
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_TARGET_ASSET, lineEditDelegate)
|
||||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, comboBoxDelegate)
|
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)
|
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.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
|
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)
|
self.start_prediction_signal.emit(input_path_str, file_list, selected_preset)
|
||||||
else:
|
else:
|
||||||
log.warning(f"Skipping prediction for {input_path_str} due to extraction error.")
|
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.current_asset_paths.clear()
|
||||||
# self.preview_model.clear_data() # Old model removed
|
# self.preview_model.clear_data() # Old model removed
|
||||||
self.unified_model.clear_data() # Clear the new model data
|
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:
|
else:
|
||||||
self.statusBar().showMessage("Asset queue is already empty.", 3000)
|
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}'")
|
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)
|
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
|
# Clearing is handled by model's set_data now, no need to clear table view directly
|
||||||
if self.prediction_thread and self.prediction_handler:
|
if self.prediction_thread and self.prediction_handler:
|
||||||
# REMOVED Placeholder SourceRule creation
|
# REMOVED Placeholder SourceRule creation
|
||||||
@ -981,8 +1004,10 @@ class MainWindow(QMainWindow):
|
|||||||
# Connect the new signal to the handler's run_prediction slot using QueuedConnection
|
# Connect the new signal to the handler's run_prediction slot using QueuedConnection
|
||||||
self.start_prediction_signal.connect(self.prediction_handler.run_prediction, Qt.ConnectionType.QueuedConnection)
|
self.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
|
# 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
|
# Assume PredictionHandler.rule_hierarchy_ready signal is changed to Signal(str, list) -> input_path, rules_list
|
||||||
self.prediction_handler.prediction_finished.connect(self.on_prediction_finished)
|
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)
|
self.prediction_handler.status_message.connect(self.show_status_message)
|
||||||
# --- REMOVED connections causing thread/handler cleanup ---
|
# --- REMOVED connections causing thread/handler cleanup ---
|
||||||
# self.prediction_handler.prediction_finished.connect(self.prediction_thread.quit)
|
# 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.
|
# # This is no longer needed as _on_rule_hierarchy_ready handles data loading for the new model.
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
@Slot()
|
# Slot signature assumes prediction_finished signal is updated to emit input_path: Signal(str)
|
||||||
def on_prediction_finished(self):
|
# Slot signature assumes prediction_finished signal is updated to emit input_path: Signal(str)
|
||||||
log.info(f"[{time.time():.4f}] --> Prediction finished signal received.")
|
@Slot(str)
|
||||||
# Optionally update status bar or re-enable controls if needed after prediction finishes
|
def on_prediction_finished(self, input_path: str):
|
||||||
# (Controls are primarily managed by processing_finished, but prediction is a separate background task)
|
"""Handles the completion (potentially failure) of a single prediction task."""
|
||||||
self.statusBar().showMessage("Preview updated.", 3000)
|
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)
|
@Slot(str, str, str)
|
||||||
def update_file_status(self, input_path_str, status, message):
|
def update_file_status(self, input_path_str, status, message):
|
||||||
@ -1580,18 +1623,74 @@ class MainWindow(QMainWindow):
|
|||||||
# @Slot(object)
|
# @Slot(object)
|
||||||
# def _on_rule_updated(self, rule_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):
|
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 prediction results (a list containing one SourceRule) for a single input path,
|
||||||
"""Receives the generated list of SourceRule hierarchies and updates the unified view model."""
|
accumulates them, and updates the model when all are ready."""
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Load the LIST of data into the new UnifiedViewModel
|
# --- Extract input_path from the received rule ---
|
||||||
self.unified_model.load_data(source_rules_list) # Pass the list
|
input_path = None
|
||||||
log.debug("Unified view model updated with new list of SourceRules.")
|
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
|
# Resize columns to fit content after loading data
|
||||||
for col in range(self.unified_model.columnCount()):
|
for col in range(self.unified_model.columnCount()):
|
||||||
@ -1599,6 +1698,8 @@ class MainWindow(QMainWindow):
|
|||||||
log.debug("Unified view columns resized to contents.")
|
log.debug("Unified view columns resized to contents.")
|
||||||
self.unified_view.expandToDepth(1) # Expand Source -> Asset level
|
self.unified_view.expandToDepth(1) # Expand Source -> Asset level
|
||||||
|
|
||||||
|
self.statusBar().showMessage(f"Preview complete for {len(final_rules)} asset(s).", 5000)
|
||||||
|
|
||||||
|
|
||||||
# --- Main Execution ---
|
# --- Main Execution ---
|
||||||
def run_gui():
|
def run_gui():
|
||||||
|
|||||||
@ -26,8 +26,8 @@ try:
|
|||||||
# from asset_processor import AssetProcessor, AssetProcessingError
|
# from asset_processor import AssetProcessor, AssetProcessingError
|
||||||
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType
|
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType
|
||||||
import config as app_config # Import project's config module
|
import config as app_config # Import project's config module
|
||||||
# Import the lists directly for easier access
|
# Import the new dictionaries directly for easier access
|
||||||
from config import ALLOWED_ASSET_TYPES, ALLOWED_FILE_TYPES
|
from config import ASSET_TYPE_DEFINITIONS, FILE_TYPE_DEFINITIONS
|
||||||
BACKEND_AVAILABLE = True
|
BACKEND_AVAILABLE = True
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}")
|
print(f"ERROR (PredictionHandler): Failed to import backend/config modules: {e}")
|
||||||
@ -209,8 +209,8 @@ class PredictionHandler(QObject):
|
|||||||
# --- Signals ---
|
# --- Signals ---
|
||||||
# Emitted when the hierarchical rule structure is ready for a single source
|
# Emitted when the hierarchical rule structure is ready for a single source
|
||||||
rule_hierarchy_ready = Signal(list) # Emits a LIST containing ONE SourceRule object
|
rule_hierarchy_ready = Signal(list) # Emits a LIST containing ONE SourceRule object
|
||||||
# Emitted when prediction/hierarchy generation for a source is done
|
# Emitted when prediction/hierarchy generation for a source is done (emits the input_source_identifier)
|
||||||
prediction_finished = Signal()
|
prediction_finished = Signal(str)
|
||||||
# Emitted for status updates
|
# Emitted for status updates
|
||||||
status_message = Signal(str, int)
|
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.")
|
log.warning(f"Input source path does not exist: '{input_source_identifier}'. Skipping prediction.")
|
||||||
self.status_message.emit("Input path not found.", 3000)
|
self.status_message.emit("Input path not found.", 3000)
|
||||||
self.rule_hierarchy_ready.emit([])
|
self.rule_hierarchy_ready.emit([])
|
||||||
self.prediction_finished.emit()
|
self.prediction_finished.emit(input_source_identifier)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
@ -269,30 +269,30 @@ class PredictionHandler(QObject):
|
|||||||
self.status_message.emit(f"Analyzing '{source_path.name}'...", 0)
|
self.status_message.emit(f"Analyzing '{source_path.name}'...", 0)
|
||||||
|
|
||||||
config: Configuration | None = None
|
config: Configuration | None = None
|
||||||
allowed_asset_types: List[str] = []
|
asset_type_definitions: Dict[str, Dict] = {}
|
||||||
allowed_file_types: List[str] = [] # These are ItemType names
|
file_type_definitions: Dict[str, Dict] = {} # These are ItemType names
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = Configuration(preset_name)
|
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:
|
if app_config:
|
||||||
allowed_asset_types = getattr(app_config, 'ALLOWED_ASSET_TYPES', [])
|
asset_type_definitions = getattr(app_config, 'ASSET_TYPE_DEFINITIONS', {})
|
||||||
allowed_file_types = getattr(app_config, 'ALLOWED_FILE_TYPES', [])
|
file_type_definitions = getattr(app_config, 'FILE_TYPE_DEFINITIONS', {})
|
||||||
log.debug(f"Loaded allowed AssetTypes: {allowed_asset_types}")
|
log.debug(f"Loaded AssetType Definitions: {list(asset_type_definitions.keys())}")
|
||||||
log.debug(f"Loaded allowed FileTypes (ItemTypes): {allowed_file_types}")
|
log.debug(f"Loaded FileType Definitions (ItemTypes): {list(file_type_definitions.keys())}")
|
||||||
else:
|
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:
|
except ConfigurationError as e:
|
||||||
log.error(f"Failed to load configuration for preset '{preset_name}': {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.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
|
self._is_running = False
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Unexpected error loading configuration or allowed types for preset '{preset_name}': {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.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
|
self._is_running = False
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -303,7 +303,7 @@ class PredictionHandler(QObject):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(f"Error during file classification for source '{input_source_identifier}': {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.status_message.emit(f"Error classifying files: {e}", 5000)
|
||||||
self.prediction_finished.emit()
|
self.prediction_finished.emit(input_source_identifier)
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -311,7 +311,7 @@ class PredictionHandler(QObject):
|
|||||||
log.warning(f"Classification yielded no assets for source '{input_source_identifier}'.")
|
log.warning(f"Classification yielded no assets for source '{input_source_identifier}'.")
|
||||||
self.status_message.emit("No assets identified from files.", 3000)
|
self.status_message.emit("No assets identified from files.", 3000)
|
||||||
self.rule_hierarchy_ready.emit([]) # Emit empty list
|
self.rule_hierarchy_ready.emit([]) # Emit empty list
|
||||||
self.prediction_finished.emit()
|
self.prediction_finished.emit(input_source_identifier)
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -348,16 +348,16 @@ class PredictionHandler(QObject):
|
|||||||
|
|
||||||
# Ensure the predicted type is allowed, fallback if necessary
|
# Ensure the predicted type is allowed, fallback if necessary
|
||||||
# Now predicted_asset_type is already a string
|
# Now predicted_asset_type is already a string
|
||||||
if allowed_asset_types and predicted_asset_type not in allowed_asset_types:
|
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 ALLOWED_ASSET_TYPES. Falling back.")
|
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
|
# Fallback logic: use the default from config if allowed, else first allowed type
|
||||||
default_type = getattr(app_config, 'DEFAULT_ASSET_CATEGORY', 'Surface')
|
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
|
predicted_asset_type = default_type
|
||||||
elif allowed_asset_types:
|
elif asset_type_definitions:
|
||||||
predicted_asset_type = allowed_asset_types[0]
|
predicted_asset_type = list(asset_type_definitions.keys())[0] # Use first key
|
||||||
else:
|
else:
|
||||||
pass # Keep the original prediction if allowed list is empty
|
pass # Keep the original prediction if definitions are empty
|
||||||
|
|
||||||
|
|
||||||
asset_rule = AssetRule(
|
asset_rule = AssetRule(
|
||||||
@ -370,35 +370,42 @@ class PredictionHandler(QObject):
|
|||||||
file_rules = []
|
file_rules = []
|
||||||
for file_info in files_info:
|
for file_info in files_info:
|
||||||
# Determine FileRule level overrides/defaults
|
# 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
|
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
|
# Determine the final item_type string (prefix maps, check if allowed)
|
||||||
# Only prefix if it's a map type that doesn't already have the prefix
|
final_item_type = base_item_type # Start with the base type
|
||||||
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
|
if not base_item_type.startswith("MAP_") and base_item_type not in ["FILE_IGNORE", "EXTRA", "MODEL"]:
|
||||||
# Check if the (potentially prefixed) type is allowed, but only if it's not supposed to be ignored or extra
|
# Prefix map types that don't already have it
|
||||||
if allowed_file_types and prefixed_item_type not in allowed_file_types and item_type_override not in ["FILE_IGNORE", "EXTRA"]:
|
final_item_type = f"MAP_{base_item_type}"
|
||||||
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
|
# 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 is determined by the engine, not predicted here. Leave as None.
|
||||||
output_format_override = None
|
output_format_override = None
|
||||||
|
# User override for item type starts as None
|
||||||
|
item_type_override = None
|
||||||
|
|
||||||
# --- DEBUG LOG: Inspect data before FileRule creation ---
|
# --- DEBUG LOG: Inspect data before FileRule creation ---
|
||||||
log.debug(f" Creating FileRule for: {file_info['file_path']}")
|
log.debug(f" Creating FileRule for: {file_info['file_path']}")
|
||||||
log.debug(f" Using item_type_override: {item_type_override}")
|
log.debug(f" Base Item Type (from classification): {base_item_type}")
|
||||||
log.debug(f" Using target_asset_name_override: {target_asset_name_override}")
|
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
|
# 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'
|
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}")
|
log.debug(f" Value for 'is_gloss_source' from file_info: {is_gloss_source_value}")
|
||||||
# --- End DEBUG LOG ---
|
# --- 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
|
# Pass the retrieved flag value to the constructor
|
||||||
file_rule = FileRule(
|
file_rule = FileRule(
|
||||||
file_path=file_info['file_path'], # This is static info based on input
|
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 ---
|
# --- 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,
|
target_asset_name_override=target_asset_name_override,
|
||||||
output_format_override=output_format_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
|
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}")
|
log.exception(f"Error building rule hierarchy for source '{input_source_identifier}': {e}")
|
||||||
self.status_message.emit(f"Error building rules: {e}", 5000)
|
self.status_message.emit(f"Error building rules: {e}", 5000)
|
||||||
# Don't emit hierarchy, just finish
|
# Don't emit hierarchy, just finish
|
||||||
self.prediction_finished.emit()
|
self.prediction_finished.emit(input_source_identifier)
|
||||||
self._is_running = False
|
self._is_running = False
|
||||||
# Removed erroneous temp_dir_obj cleanup
|
# Removed erroneous temp_dir_obj cleanup
|
||||||
return
|
return
|
||||||
@ -439,7 +446,7 @@ class PredictionHandler(QObject):
|
|||||||
# Removed prediction_results_ready signal emission
|
# Removed prediction_results_ready signal emission
|
||||||
|
|
||||||
self.status_message.emit(f"Analysis complete for '{input_source_identifier}'.", 3000)
|
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
|
self._is_running = False
|
||||||
# Removed temp_dir_obj cleanup - not relevant here
|
# Removed temp_dir_obj cleanup - not relevant here
|
||||||
log.info(f"[{time.time():.4f}][T:{thread_id}] <-- Exiting PredictionHandler.run_prediction.")
|
log.info(f"[{time.time():.4f}][T:{thread_id}] <-- Exiting PredictionHandler.run_prediction.")
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
# gui/unified_view_model.py
|
# 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 pathlib import Path # Added for file_name extraction
|
||||||
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType import
|
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):
|
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
|
A QAbstractItemModel for displaying and editing the hierarchical structure
|
||||||
of SourceRule -> AssetRule -> FileRule.
|
of SourceRule -> AssetRule -> FileRule.
|
||||||
"""
|
"""
|
||||||
Columns = [
|
Columns = [
|
||||||
"Name", "Supplier Override", "Asset-Type Override",
|
"Name", "Supplier", "Asset-Type Override", # Renamed "Supplier Override"
|
||||||
"Target Asset Name Override", "Item-Type Override",
|
"Target Asset Name Override", "Item-Type Override",
|
||||||
"Status", "Output Path"
|
"Status", "Output Path"
|
||||||
]
|
]
|
||||||
@ -165,55 +172,101 @@ class UnifiedViewModel(QAbstractItemModel):
|
|||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
column = index.column()
|
column = index.column()
|
||||||
|
|
||||||
# --- Handle different item types ---
|
# --- Handle Background Role ---
|
||||||
if isinstance(item, SourceRule): # This might only be relevant if SourceRule is displayed
|
if role == Qt.BackgroundRole:
|
||||||
if role == Qt.DisplayRole:
|
# item is already fetched at line 172
|
||||||
if column == 0: return item.input_path
|
if isinstance(item, SourceRule):
|
||||||
# Use supplier_override if set, otherwise empty string
|
return self.SOURCE_RULE_COLOR # Use the class constant
|
||||||
if column == self.COL_SUPPLIER: return item.supplier_override if item.supplier_override is not None else ""
|
elif isinstance(item, AssetRule):
|
||||||
# Other columns return None or "" for SourceRule
|
# Determine effective asset type
|
||||||
elif role == Qt.EditRole:
|
asset_type = item.asset_type_override if item.asset_type_override else item.asset_type
|
||||||
# Return supplier_override for editing
|
if asset_type:
|
||||||
if column == self.COL_SUPPLIER: return item.supplier_override if item.supplier_override is not None else ""
|
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
|
return None # Default for SourceRule for other roles/columns
|
||||||
|
|
||||||
elif isinstance(item, AssetRule):
|
elif isinstance(item, AssetRule):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
if column == self.COL_NAME: return item.asset_name
|
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:
|
if column == self.COL_ASSET_TYPE:
|
||||||
display_value = item.asset_type_override if item.asset_type_override is not None else item.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 ""
|
return display_value if display_value else ""
|
||||||
# Placeholder columns
|
|
||||||
if column == self.COL_STATUS: return "" # Status (Not handled yet)
|
if column == self.COL_STATUS: return "" # Status (Not handled yet)
|
||||||
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
|
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
|
||||||
elif role == Qt.EditRole:
|
elif role == Qt.EditRole:
|
||||||
# Return asset_type_override for editing (delegate expects string or None)
|
|
||||||
if column == self.COL_ASSET_TYPE:
|
if column == self.COL_ASSET_TYPE:
|
||||||
return item.asset_type_override # Return string or None
|
return item.asset_type_override # Return string or None
|
||||||
return None # Default for AssetRule
|
return None # Default for AssetRule
|
||||||
|
|
||||||
|
|
||||||
elif isinstance(item, FileRule):
|
elif isinstance(item, FileRule):
|
||||||
if role == Qt.DisplayRole:
|
if role == Qt.DisplayRole:
|
||||||
if column == self.COL_NAME: return Path(item.file_path).name # Display only filename
|
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:
|
if column == self.COL_TARGET_ASSET:
|
||||||
return item.target_asset_name_override if item.target_asset_name_override is not None else ""
|
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:
|
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 ""
|
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_STATUS: return "" # Status (Not handled yet)
|
||||||
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
|
if column == self.COL_OUTPUT_PATH: return "" # Output Path (Not handled yet)
|
||||||
elif role == Qt.EditRole:
|
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 ""
|
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
|
if column == self.COL_ITEM_TYPE: return item.item_type_override # Return string or None
|
||||||
return None # Default for FileRule
|
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:
|
def setData(self, index: QModelIndex, value, role: int = Qt.EditRole) -> bool:
|
||||||
"""Sets the role data for the item at index to value."""
|
"""Sets the role data for the item at index to value."""
|
||||||
@ -229,10 +282,17 @@ class UnifiedViewModel(QAbstractItemModel):
|
|||||||
# --- Handle different item types ---
|
# --- Handle different item types ---
|
||||||
if isinstance(item, SourceRule): # If SourceRule is editable
|
if isinstance(item, SourceRule): # If SourceRule is editable
|
||||||
if column == self.COL_SUPPLIER:
|
if column == self.COL_SUPPLIER:
|
||||||
# Ensure value is string or None
|
# Get the new value, strip whitespace, treat empty as None
|
||||||
new_value = str(value).strip() if value is not None else None
|
new_value = str(value).strip() if value is not None and str(value).strip() else None
|
||||||
if new_value == "": new_value = None # Treat empty string as None
|
|
||||||
# Update supplier_override
|
# 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:
|
if item.supplier_override != new_value:
|
||||||
item.supplier_override = new_value
|
item.supplier_override = new_value
|
||||||
changed = True
|
changed = True
|
||||||
@ -254,8 +314,122 @@ class UnifiedViewModel(QAbstractItemModel):
|
|||||||
if new_value == "": new_value = None # Treat empty string as None
|
if new_value == "": new_value = None # Treat empty string as None
|
||||||
# Update target_asset_name_override
|
# Update target_asset_name_override
|
||||||
if item.target_asset_name_override != new_value:
|
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
|
item.target_asset_name_override = new_value
|
||||||
changed = True
|
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
|
elif column == self.COL_ITEM_TYPE: # Item-Type Override
|
||||||
# Delegate provides string value (e.g., "MAP_COL") or None
|
# Delegate provides string value (e.g., "MAP_COL") or None
|
||||||
new_value = str(value) if value is not None else None
|
new_value = str(value) if value is not None else None
|
||||||
|
|||||||
Binary file not shown.
@ -4,6 +4,7 @@ from typing import List, Dict, Any, Tuple
|
|||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class FileRule:
|
class FileRule:
|
||||||
file_path: str = None
|
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
|
item_type_override: str = None # Renamed from map_type_override
|
||||||
target_asset_name_override: str = None # Added override field
|
target_asset_name_override: str = None # Added override field
|
||||||
resolution_override: Tuple[int, int] = None
|
resolution_override: Tuple[int, int] = None
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user