parent
ce26d54a5d
commit
bf79c05bdd
@ -18,13 +18,18 @@ python -m gui.main_window
|
||||
* **Preset List:** Create, delete, load, edit, and save presets. On startup, the "-- Select a Preset --" item is explicitly selected. You must select a specific preset from this list to load it into the editor below, enable the detailed file preview, and enable the "Start Processing" button.
|
||||
* **Preset Editor Tabs:** Edit the details of the selected preset.
|
||||
* **Processing Panel (Right):**
|
||||
* **Preset Selector:** Choose the preset to use for *processing* the current queue. This dropdown now includes a new option: "- LLM Interpretation -". Selecting this option will use the experimental LLM Predictor instead of the traditional rule-based prediction system defined in presets.
|
||||
* **Preset Selector:** Choose the preset to use for *processing* the current queue. (Note: LLM interpretation is now initiated via the right-click context menu in the Preview Table).
|
||||
* **Output Directory:** Set the output path (defaults to `config/app_settings.json`, use "Browse...")
|
||||
* **Drag and Drop Area:** Add asset `.zip`, `.rar`, `.7z` files, or folders by dragging and dropping them here.
|
||||
* **Preview Table:** Shows queued assets in a hierarchical view (Source -> Asset -> File). Assets (files, directories, archives) added via drag-and-drop appear immediately in the table.
|
||||
* If no preset is selected ("-- Select a Preset --"), added items (including files within directories/archives) are displayed with empty prediction fields (Target Asset, Asset Type, Item Type), which can be manually edited.
|
||||
* If a valid preset or LLM mode is selected, the table populates with prediction results as they become available.
|
||||
* The table always displays the detailed view structure with columns: Name, Target Asset, Supplier, Asset Type, Item Type. The "Target Asset" column stretches to fill available space.
|
||||
* **Preview Table:** Shows queued assets in a hierarchical view (Source -> Asset -> File). Assets (files, directories, archives) added via drag-and-drop appear immediately in the table. This table is interactive:
|
||||
* **Editable Fields:** The 'Name' field for Assets and the 'Target Asset', 'Supplier', 'Asset Type', and 'Item Type' fields for all items can be edited directly in the table.
|
||||
* Editing an **Asset Name** automatically updates the 'Target Asset' field for all its child files.
|
||||
* The **Item Type** field is a text input with auto-suggestions based on available types.
|
||||
* **Drag-and-Drop Re-parenting:** File rows can be dragged and dropped onto different Asset rows to change their parent asset association.
|
||||
* **Right-Click Context Menu:** Right-clicking on Source, Asset, or File rows brings up a context menu:
|
||||
* **Re-interpret selected source:** This sub-menu allows re-running the prediction process for the selected source item(s) using either a specific preset or the LLM predictor. The available presets and the "LLM" option are listed dynamically. This replaces the previous standalone "Re-interpret Selected with LLM" button.
|
||||
* **Prediction Population:** If a valid preset is selected in the Preset Selector (or if re-interpretation is triggered), the table populates with prediction results as they become available. If no preset is selected, added items show empty prediction fields.
|
||||
* **Columns:** The table displays columns: Name, Target Asset, Supplier, Asset Type, Item Type. The "Target Asset" column stretches to fill available space.
|
||||
* **Coloring:** The *text color* of file items is determined by their Item Type (colors defined in `config/app_settings.json`). The *background color* of file items is a 30% darker shade of their parent asset's background, helping to visually group files within an asset. Asset rows themselves may use alternating background colors based on the application theme.
|
||||
* **Progress Bar:** Shows overall processing progress.
|
||||
* **Blender Post-Processing:** Checkbox to enable Blender scripts. If enabled, shows fields and browse buttons for target `.blend` files (defaults from `config/app_settings.json`).
|
||||
@ -34,7 +39,6 @@ python -m gui.main_window
|
||||
* `Clear Queue`: Button to clear the queue and preview.
|
||||
* `Start Processing`: Button to start processing the queue. This button is enabled as long as there are items listed in the Preview Table. When clicked, any items that do not have a value assigned in the "Target Asset" column will be automatically ignored for that processing run.
|
||||
* `Cancel`: Button to attempt stopping processing.
|
||||
* **Re-interpret Selected with LLM:** This button appears when the "- LLM Interpretation -" preset is selected. It allows you to re-process only the currently selected items in the Preview Table using the LLM, without affecting other items in the queue. This is useful for refining predictions on specific assets.
|
||||
* **Status Bar:** Displays current status, errors, and completion messages. During LLM processing, the status bar will show messages indicating the progress of the LLM requests.
|
||||
|
||||
## GUI Configuration Editor
|
||||
|
||||
194
ProjectNotes/GUI_User_Friendliness_Enhancement_Plan.md
Normal file
194
ProjectNotes/GUI_User_Friendliness_Enhancement_Plan.md
Normal file
@ -0,0 +1,194 @@
|
||||
# Implementation Plan: GUI User-Friendliness Enhancements
|
||||
|
||||
This document outlines the plan for implementing three key GUI improvements for the Asset Processor Tool, focusing on user-friendliness and workflow efficiency.
|
||||
|
||||
**Target Audience:** Developers implementing these features.
|
||||
**Status:** Planning Phase
|
||||
|
||||
## Feature 1: Editable Asset Name
|
||||
|
||||
**Goal:** Allow users to edit the name of an asset directly in the main view, and automatically update the 'Target Asset' field of all associated child files to reflect the new name.
|
||||
|
||||
**Affected Components:**
|
||||
|
||||
* `gui/unified_view_model.py` (`UnifiedViewModel`)
|
||||
* `gui/delegates.py` (`LineEditDelegate`)
|
||||
* `gui/main_window.py` (or view setup location)
|
||||
* `rule_structure.py` (`AssetRule`, `FileRule`)
|
||||
* Potentially a new handler or modifications to `gui/asset_restructure_handler.py`
|
||||
|
||||
**Implementation Steps:**
|
||||
|
||||
1. **Enable Editing in Model (`UnifiedViewModel`):**
|
||||
* Modify `flags()`: For an index pointing to an `AssetRule`, return `Qt.ItemIsEditable` in addition to default flags when `index.column()` is `COL_NAME`.
|
||||
* Modify `setData()`:
|
||||
* Add logic to handle `isinstance(item, AssetRule)` and `column == self.COL_NAME`.
|
||||
* Get the `new_asset_name` from the `value`.
|
||||
* **Validation:** Before proceeding, check if an `AssetRule` with `new_asset_name` already exists within the same parent `SourceRule`. If so, log a warning and return `False` to prevent duplicate names.
|
||||
* Store the `old_asset_name = item.asset_name`.
|
||||
* If `new_asset_name` is valid and different from `old_asset_name`:
|
||||
* Update `item.asset_name = new_asset_name`.
|
||||
* Set `changed = True`.
|
||||
* **Crucial - Child Update:** Iterate through *all* `SourceRule`s, `AssetRule`s, and `FileRule`s in the model (`self._source_rules`). For each `FileRule` found where `file_rule.target_asset_name_override == old_asset_name`, update `file_rule.target_asset_name_override = new_asset_name`. Emit `dataChanged` for the `COL_TARGET_ASSET` index of each modified `FileRule`. (See Potential Challenges regarding performance).
|
||||
* Emit `dataChanged` for the edited `AssetRule`'s `COL_NAME` index.
|
||||
* Return `changed`.
|
||||
* **(Alternative Signal Approach):** Instead of performing the child update directly in `setData`, emit a new signal like `assetNameChanged = Signal(QModelIndex, str, str)` carrying the `AssetRule` index, old name, and new name. A dedicated handler would connect to this signal to perform the child updates. This improves separation of concerns.
|
||||
|
||||
2. **Assign Delegate (`main_window.py` / View Setup):**
|
||||
* Ensure the `LineEditDelegate` is assigned to the view for the `COL_NAME` using `view.setItemDelegateForColumn(UnifiedViewModel.COL_NAME, line_edit_delegate_instance)`.
|
||||
|
||||
3. **Handling Child Updates (if using Signal Approach):**
|
||||
* Create a new handler class (e.g., `AssetNameChangeHandler`) or add a slot to `AssetRestructureHandler`.
|
||||
* Connect the `UnifiedViewModel.assetNameChanged` signal to this slot.
|
||||
* The slot receives the `AssetRule` index, old name, and new name. It iterates through the model's `FileRule`s, updates their `target_asset_name_override` where it matches the old name, and emits `dataChanged` for those files.
|
||||
|
||||
**Data Model Impact:**
|
||||
|
||||
* `AssetRule.asset_name` becomes directly mutable via the GUI.
|
||||
* The relationship between files and their intended parent asset (represented by `FileRule.target_asset_name_override`) is maintained automatically when the parent asset's name changes.
|
||||
|
||||
**Potential Challenges/Considerations:**
|
||||
|
||||
* **Performance:** The child update logic requires iterating through potentially all files in the model. For very large datasets, this could be slow. Consider optimizing by maintaining an index/lookup map (`Dict[str, List[FileRule]]`) mapping target asset override names to the list of `FileRule`s using them. This map would need careful updating whenever overrides change or files are moved.
|
||||
* **Duplicate Asset Names:** The plan includes basic validation in `setData`. Robust handling (e.g., user feedback, preventing the edit) is needed.
|
||||
* **Undo/Redo:** Reversing an asset name change requires reverting the name *and* reverting all the child `target_asset_name_override` changes, adding complexity.
|
||||
* **Scope of Child Update:** The current plan updates *any* `FileRule` whose override matches the old name. Confirm if this update should be restricted only to files originally under the renamed asset or within the same `SourceRule`. The current approach seems most logical based on how `target_asset_name_override` works.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant View
|
||||
participant LineEditDelegate
|
||||
participant UnifiedViewModel
|
||||
participant AssetNameChangeHandler
|
||||
|
||||
User->>View: Edits AssetRule Name in COL_NAME
|
||||
View->>LineEditDelegate: setModelData(editor, model, index)
|
||||
LineEditDelegate->>UnifiedViewModel: setData(index, new_name, EditRole)
|
||||
UnifiedViewModel->>UnifiedViewModel: Validate new_name (no duplicates)
|
||||
UnifiedViewModel->>UnifiedViewModel: Update AssetRule.asset_name
|
||||
alt Signal Approach
|
||||
UnifiedViewModel->>AssetNameChangeHandler: emit assetNameChanged(index, old_name, new_name)
|
||||
AssetNameChangeHandler->>UnifiedViewModel: Iterate through FileRules
|
||||
loop For each FileRule where target_override == old_name
|
||||
AssetNameChangeHandler->>UnifiedViewModel: Update FileRule.target_asset_name_override = new_name
|
||||
UnifiedViewModel->>View: emit dataChanged(file_rule_target_index)
|
||||
end
|
||||
else Direct Approach in setData
|
||||
UnifiedViewModel->>UnifiedViewModel: Iterate through FileRules
|
||||
loop For each FileRule where target_override == old_name
|
||||
UnifiedViewModel->>UnifiedViewModel: Update FileRule.target_asset_name_override = new_name
|
||||
UnifiedViewModel->>View: emit dataChanged(file_rule_target_index)
|
||||
end
|
||||
end
|
||||
UnifiedViewModel->>View: emit dataChanged(asset_rule_name_index)
|
||||
|
||||
```
|
||||
|
||||
## Feature 2: Item Type Field Conversion
|
||||
|
||||
**Goal:** Replace the `QComboBox` delegate for the "Item Type" column (for `FileRule`s) with a `QLineEdit` that provides auto-suggestions based on defined file types, similar to the existing "Supplier" field.
|
||||
|
||||
**Affected Components:**
|
||||
|
||||
* `gui/main_window.py` (or view setup location)
|
||||
* `gui/delegates.py` (Requires a new delegate)
|
||||
* `gui/unified_view_model.py` (`UnifiedViewModel`)
|
||||
* `config/app_settings.json` (Source of file type definitions)
|
||||
|
||||
**Implementation Steps:**
|
||||
|
||||
1. **Create New Delegate (`delegates.py`):**
|
||||
* Create a new class `ItemTypeSearchDelegate(QStyledItemDelegate)`.
|
||||
* **`createEditor(self, parent, option, index)`:**
|
||||
* Create a `QLineEdit` instance.
|
||||
* Get the list of valid item type keys: `item_keys = index.model()._file_type_keys` (add error handling).
|
||||
* Create a `QCompleter` using `item_keys` and set it on the `QLineEdit` (configure case sensitivity, filter mode, completion mode as in `SupplierSearchDelegate`).
|
||||
* Return the editor.
|
||||
* **`setEditorData(self, editor, index)`:**
|
||||
* Get the current value using `index.model().data(index, Qt.EditRole)`.
|
||||
* Set the editor's text (`editor.setText(str(value) if value is not None else "")`).
|
||||
* **`setModelData(self, editor, model, index)`:**
|
||||
* Get the `final_text = editor.text().strip()`.
|
||||
* Determine the `value_to_set = final_text if final_text else None`.
|
||||
* Call `model.setData(index, value_to_set, Qt.EditRole)`.
|
||||
* **Important:** Unlike `SupplierSearchDelegate`, do *not* add `final_text` to the list of known types or save anything back to config. Suggestions are strictly based on `config/app_settings.json`.
|
||||
* **`updateEditorGeometry(self, editor, option, index)`:**
|
||||
* Standard implementation: `editor.setGeometry(option.rect)`.
|
||||
|
||||
2. **Assign Delegate (`main_window.py` / View Setup):**
|
||||
* Instantiate the new `ItemTypeSearchDelegate`.
|
||||
* Find where delegates are set for the view.
|
||||
* Replace the `ComboBoxDelegate` assignment for `UnifiedViewModel.COL_ITEM_TYPE` with the new `ItemTypeSearchDelegate` instance: `view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, item_type_search_delegate_instance)`.
|
||||
|
||||
**Data Model Impact:**
|
||||
|
||||
* None. The underlying data (`FileRule.item_type_override`) and its handling remain the same. Only the GUI editor changes.
|
||||
|
||||
**Potential Challenges/Considerations:**
|
||||
|
||||
* None significant. This is a relatively straightforward replacement of one delegate type with another, leveraging existing patterns from `SupplierSearchDelegate` and data loading from `UnifiedViewModel`.
|
||||
|
||||
## Feature 3: Drag-and-Drop File Re-parenting
|
||||
|
||||
**Goal:** Enable users to drag one or more `FileRule` rows and drop them onto an `AssetRule` row to change the parent asset of the dragged files.
|
||||
|
||||
**Affected Components:**
|
||||
|
||||
* `gui/main_panel_widget.py` or `gui/main_window.py` (View management)
|
||||
* `gui/unified_view_model.py` (`UnifiedViewModel`)
|
||||
|
||||
**Implementation Steps:**
|
||||
|
||||
1. **Enable Drag/Drop in View (`main_panel_widget.py` / `main_window.py`):**
|
||||
* Get the `QTreeView` instance (`view`).
|
||||
* `view.setSelectionMode(QAbstractItemView.ExtendedSelection)` (Allow selecting multiple files)
|
||||
* `view.setDragEnabled(True)`
|
||||
* `view.setAcceptDrops(True)`
|
||||
* `view.setDropIndicatorShown(True)`
|
||||
* `view.setDefaultDropAction(Qt.MoveAction)`
|
||||
* `view.setDragDropMode(QAbstractItemView.InternalMove)`
|
||||
|
||||
2. **Implement Drag/Drop Support in Model (`UnifiedViewModel`):**
|
||||
* **`flags(self, index)`:**
|
||||
* Modify to include `Qt.ItemIsDragEnabled` if `index.internalPointer()` is a `FileRule`.
|
||||
* Modify to include `Qt.ItemIsDropEnabled` if `index.internalPointer()` is an `AssetRule`.
|
||||
* Return the combined flags.
|
||||
* **`supportedDropActions(self)`:**
|
||||
* Return `Qt.MoveAction`.
|
||||
* **`mimeData(self, indexes)`:**
|
||||
* Create `QMimeData`.
|
||||
* Encode information about the dragged rows (which must be `FileRule`s). Store a list of tuples, each containing `(source_parent_row, source_parent_col, source_row)` for each valid `FileRule` index in `indexes`. Use a custom MIME type (e.g., `"application/x-filerule-index-list"`).
|
||||
* Return the `QMimeData`.
|
||||
* **`canDropMimeData(self, data, action, row, column, parent)`:**
|
||||
* Check if `action == Qt.MoveAction`.
|
||||
* Check if `data.hasFormat("application/x-filerule-index-list")`.
|
||||
* Check if `parent.isValid()` and `parent.internalPointer()` is an `AssetRule`.
|
||||
* Return `True` if all conditions met, `False` otherwise.
|
||||
* **`dropMimeData(self, data, action, row, column, parent)`:**
|
||||
* Check `action` and MIME type again for safety.
|
||||
* Get the target `AssetRule` item: `target_asset = parent.internalPointer()`. If not an `AssetRule`, return `False`.
|
||||
* Decode the `QMimeData` to get the list of source index information.
|
||||
* Create a list `files_to_move = []` containing the actual `QModelIndex` objects for the source `FileRule`s (reconstruct them using the decoded info and `self.index()`).
|
||||
* Iterate through `files_to_move`:
|
||||
* Get the `source_file_index`.
|
||||
* Get the `file_item = source_file_index.internalPointer()`.
|
||||
* Get the `old_parent_asset = getattr(file_item, 'parent_asset', None)`.
|
||||
* If `target_asset != old_parent_asset`:
|
||||
* Call `self.moveFileRule(source_file_index, parent)`. This handles the actual move within the model structure and emits `beginMoveRows`/`endMoveRows`.
|
||||
* **After successful move:** Update the file's override: `file_item.target_asset_name_override = target_asset.asset_name`.
|
||||
* Emit `self.dataChanged.emit(moved_file_index, moved_file_index, [Qt.DisplayRole, Qt.EditRole])` for the `COL_TARGET_ASSET` column of the *now moved* file (get its new index).
|
||||
* **Cleanup:** After the loop, identify any original parent `AssetRule`s that became empty as a result of the moves. Call `self.removeAssetRule(empty_asset_rule)` for each.
|
||||
* Return `True`.
|
||||
|
||||
**Data Model Impact:**
|
||||
|
||||
* Changes the parentage of `FileRule` items within the model's internal structure.
|
||||
* Updates `FileRule.target_asset_name_override` to match the `asset_name` of the new parent `AssetRule`, ensuring consistency between the visual structure and the override field.
|
||||
|
||||
**Potential Challenges/Considerations:**
|
||||
|
||||
* **MIME Data Encoding/Decoding:** Ensure the index information is reliably encoded and decoded, especially handling potential model changes between drag start and drop. Using persistent IDs instead of row/column numbers might be more robust if available.
|
||||
* **Cleanup Logic:** Reliably identifying and removing empty parent assets after potentially moving multiple files from different original parents requires careful tracking.
|
||||
* **Transactionality:** If moving multiple files and one part fails, should the whole operation roll back? The current plan doesn't explicitly handle this; errors are logged, and subsequent steps might proceed.
|
||||
* **Interaction with `AssetRestructureHandler`:** The plan suggests handling the move and override update directly within `dropMimeData`. This means the existing `AssetRestructureHandler` won't be triggered by the override change *during* the drop. Ensure the cleanup logic (removing empty parents) is correctly handled either in `dropMimeData` or by ensuring `moveFileRule` emits signals that the handler *can* use for cleanup.
|
||||
@ -1,5 +1,5 @@
|
||||
[
|
||||
"Dimensiva",
|
||||
"Dinesen",
|
||||
"Poliigon",
|
||||
"poliigon"
|
||||
"Poliigon"
|
||||
]
|
||||
@ -211,3 +211,54 @@ class SupplierSearchDelegate(QStyledItemDelegate):
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
"""Ensures the editor widget is placed correctly."""
|
||||
editor.setGeometry(option.rect)
|
||||
class ItemTypeSearchDelegate(QStyledItemDelegate):
|
||||
"""
|
||||
Delegate for editing item types using a QLineEdit with auto-completion.
|
||||
Loads known item types from the UnifiedViewModel's cached keys.
|
||||
"""
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
# No persistent list needed here, suggestions come from the model
|
||||
|
||||
def createEditor(self, parent, option, index: QModelIndex):
|
||||
"""Creates the QLineEdit editor with a QCompleter."""
|
||||
editor = QLineEdit(parent)
|
||||
model = index.model()
|
||||
item_keys = []
|
||||
|
||||
# Get keys directly from the UnifiedViewModel
|
||||
if hasattr(model, '_file_type_keys'):
|
||||
try:
|
||||
item_keys = model._file_type_keys # Use cached keys
|
||||
except Exception as e:
|
||||
log.error(f"Error getting _file_type_keys from model in ItemTypeSearchDelegate: {e}")
|
||||
item_keys = []
|
||||
else:
|
||||
log.warning("ItemTypeSearchDelegate: Model is missing _file_type_keys attribute. Suggestions will be empty.")
|
||||
|
||||
completer = QCompleter(item_keys, editor)
|
||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
completer.setFilterMode(Qt.MatchContains)
|
||||
completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||
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 item type override
|
||||
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."""
|
||||
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
|
||||
# The model's setData handles updating the override and standard_map_type
|
||||
model.setData(index, value_to_set, Qt.EditRole)
|
||||
# DO NOT add to a persistent list or save back to config
|
||||
|
||||
def updateEditorGeometry(self, editor, option, index):
|
||||
"""Ensures the editor widget is placed correctly."""
|
||||
editor.setGeometry(option.rect)
|
||||
@ -4,6 +4,7 @@ import json
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
import functools # Ensure functools is imported directly for partial
|
||||
from functools import partial
|
||||
|
||||
from PySide6.QtWidgets import QApplication # Added for processEvents
|
||||
@ -19,18 +20,23 @@ from PySide6.QtGui import QColor, QAction, QPalette, QClipboard, QGuiApplication
|
||||
|
||||
# --- Local GUI Imports ---
|
||||
# Import delegates and models needed by the panel
|
||||
from .delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate
|
||||
from .delegates import LineEditDelegate, ComboBoxDelegate, SupplierSearchDelegate, ItemTypeSearchDelegate # Added ItemTypeSearchDelegate
|
||||
from .unified_view_model import UnifiedViewModel # Assuming UnifiedViewModel is passed in
|
||||
|
||||
# --- Backend Imports ---
|
||||
# Import Rule Structures if needed for context menus etc.
|
||||
from rule_structure import SourceRule, AssetRule, FileRule
|
||||
# Import config loading if defaults are needed directly here (though better passed from MainWindow)
|
||||
# Import configuration directly for PRESETS_DIR access
|
||||
import configuration
|
||||
try:
|
||||
from configuration import ConfigurationError, load_base_config
|
||||
except ImportError:
|
||||
ConfigurationError = Exception
|
||||
load_base_config = None
|
||||
# Define PRESETS_DIR fallback if configuration module fails to load entirely
|
||||
class configuration:
|
||||
PRESETS_DIR = "Presets" # Fallback path
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -57,6 +63,7 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
# Request to re-interpret selected items using LLM
|
||||
llm_reinterpret_requested = Signal(list) # Emits list of source paths
|
||||
preset_reinterpret_requested = Signal(list, str) # Emits list[source_paths], preset_name
|
||||
|
||||
# Notify when the output directory changes
|
||||
output_dir_changed = Signal(str)
|
||||
@ -130,12 +137,14 @@ class MainPanelWidget(QWidget):
|
||||
# TODO: Revisit ComboBoxDelegate dependency
|
||||
comboBoxDelegate = ComboBoxDelegate(self) # Pass only parent (self)
|
||||
supplierSearchDelegate = SupplierSearchDelegate(self) # Pass parent
|
||||
itemTypeSearchDelegate = ItemTypeSearchDelegate(self) # Instantiate new delegate
|
||||
|
||||
# Set Delegates for Columns
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_SUPPLIER, supplierSearchDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ASSET_TYPE, comboBoxDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_TARGET_ASSET, lineEditDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, comboBoxDelegate)
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_ITEM_TYPE, itemTypeSearchDelegate) # Use ItemTypeSearchDelegate
|
||||
self.unified_view.setItemDelegateForColumn(UnifiedViewModel.COL_NAME, lineEditDelegate) # Assign LineEditDelegate for AssetRule names
|
||||
|
||||
# Configure View Appearance
|
||||
self.unified_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
@ -156,6 +165,17 @@ class MainPanelWidget(QWidget):
|
||||
# Enable custom context menu
|
||||
self.unified_view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
|
||||
# --- Enable Drag and Drop ---
|
||||
self.unified_view.setDragEnabled(True)
|
||||
self.unified_view.setAcceptDrops(True)
|
||||
self.unified_view.setDropIndicatorShown(True)
|
||||
self.unified_view.setDefaultDropAction(Qt.MoveAction)
|
||||
# Use InternalMove for handling drops within the model itself
|
||||
self.unified_view.setDragDropMode(QAbstractItemView.InternalMove)
|
||||
# Ensure ExtendedSelection is set (already done above, but good practice)
|
||||
self.unified_view.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
# --- End Drag and Drop ---
|
||||
|
||||
# Add the Unified View to the main layout
|
||||
main_layout.addWidget(self.unified_view, 1) # Give it stretch factor 1
|
||||
|
||||
@ -240,11 +260,11 @@ class MainPanelWidget(QWidget):
|
||||
bottom_controls_layout.addWidget(self.workers_spinbox)
|
||||
bottom_controls_layout.addStretch(1)
|
||||
|
||||
# --- LLM Re-interpret Button ---
|
||||
self.llm_reinterpret_button = QPushButton("Re-interpret Selected with LLM")
|
||||
self.llm_reinterpret_button.setToolTip("Re-run LLM interpretation on the selected source items.")
|
||||
self.llm_reinterpret_button.setEnabled(False) # Initially disabled
|
||||
bottom_controls_layout.addWidget(self.llm_reinterpret_button)
|
||||
# --- LLM Re-interpret Button (Removed, functionality moved to context menu) ---
|
||||
# self.llm_reinterpret_button = QPushButton("Re-interpret Selected with LLM")
|
||||
# self.llm_reinterpret_button.setToolTip("Re-run LLM interpretation on the selected source items.")
|
||||
# self.llm_reinterpret_button.setEnabled(False) # Initially disabled
|
||||
# bottom_controls_layout.addWidget(self.llm_reinterpret_button)
|
||||
|
||||
self.clear_queue_button = QPushButton("Clear Queue")
|
||||
self.start_button = QPushButton("Start Processing")
|
||||
@ -263,7 +283,6 @@ class MainPanelWidget(QWidget):
|
||||
self.output_path_edit.editingFinished.connect(self._on_output_path_changed) # Emit signal when user finishes editing
|
||||
|
||||
# Unified View
|
||||
self.unified_view.selectionModel().selectionChanged.connect(self._update_llm_reinterpret_button_state)
|
||||
self.unified_view.customContextMenuRequested.connect(self._show_unified_view_context_menu)
|
||||
|
||||
# Blender Controls
|
||||
@ -280,7 +299,7 @@ class MainPanelWidget(QWidget):
|
||||
self.clear_queue_button.clicked.connect(self.clear_queue_requested) # Emit signal directly
|
||||
self.start_button.clicked.connect(self._on_start_processing_clicked) # Use slot to gather data
|
||||
self.cancel_button.clicked.connect(self.cancel_requested) # Emit signal directly
|
||||
self.llm_reinterpret_button.clicked.connect(self._on_llm_reinterpret_clicked) # Use slot to gather data
|
||||
# self.llm_reinterpret_button.clicked.connect(self._on_llm_reinterpret_clicked) # Removed button connection
|
||||
|
||||
# --- Slots for Internal UI Logic ---
|
||||
|
||||
@ -371,79 +390,98 @@ class MainPanelWidget(QWidget):
|
||||
}
|
||||
self.process_requested.emit(settings)
|
||||
|
||||
@Slot()
|
||||
def _update_llm_reinterpret_button_state(self):
|
||||
"""Enables/disables the LLM re-interpret button based on selection and LLM status."""
|
||||
selection_model = self.unified_view.selectionModel()
|
||||
has_selection = selection_model is not None and selection_model.hasSelection()
|
||||
# Enable only if there's a selection AND LLM is not currently active
|
||||
self.llm_reinterpret_button.setEnabled(has_selection and not self.llm_processing_active)
|
||||
# Removed _update_llm_reinterpret_button_state as the button is removed.
|
||||
# Context menu actions will handle their own enabled state or rely on _on_llm_reinterpret_clicked checks.
|
||||
|
||||
def _get_unique_source_dirs_from_selection(self, selected_indexes: list[QModelIndex]) -> set[str]:
|
||||
"""
|
||||
Extracts unique, valid source directory/zip paths from the selected QModelIndex list.
|
||||
Traverses up the model hierarchy to find the parent SourceRule for each index.
|
||||
"""
|
||||
unique_source_dirs = set()
|
||||
model = self.unified_view.model()
|
||||
if not model:
|
||||
log.error("Unified view model not found.")
|
||||
return unique_source_dirs
|
||||
|
||||
processed_source_paths = set() # To avoid processing duplicates if multiple cells of the same source are selected
|
||||
|
||||
for index in selected_indexes:
|
||||
if not index.isValid():
|
||||
continue
|
||||
|
||||
# Use the model's getItem method for robust node retrieval
|
||||
item_node = model.getItem(index)
|
||||
source_rule_node = None
|
||||
|
||||
# Find the parent SourceRule node by traversing upwards using the index
|
||||
source_rule_node = None
|
||||
current_index = index # Start with the index of the selected item
|
||||
while current_index.isValid():
|
||||
current_item = model.getItem(current_index)
|
||||
if isinstance(current_item, SourceRule):
|
||||
source_rule_node = current_item
|
||||
break
|
||||
current_index = model.parent(current_index) # Move to the parent index
|
||||
# If loop finishes without break, source_rule_node remains None
|
||||
|
||||
if source_rule_node:
|
||||
# Use input_path attribute as defined in SourceRule
|
||||
source_path = getattr(source_rule_node, 'input_path', None)
|
||||
if source_path and source_path not in processed_source_paths:
|
||||
source_path_obj = Path(source_path)
|
||||
# Check if it's a directory or a zip file (common input types)
|
||||
if source_path_obj.is_dir() or (source_path_obj.is_file() and source_path_obj.suffix.lower() == '.zip'):
|
||||
log.debug(f"Identified source path for re-interpretation: {source_path}")
|
||||
unique_source_dirs.add(source_path)
|
||||
processed_source_paths.add(source_path)
|
||||
else:
|
||||
log.warning(f"Selected item's source path is not a directory or zip file: {source_path}")
|
||||
elif not source_path:
|
||||
log.warning(f"Parent SourceRule found for index {index.row()},{index.column()} but has no 'input_path' attribute.")
|
||||
|
||||
else:
|
||||
log.warning(f"Could not find parent SourceRule for selected index: {index.row()},{index.column()} (Node type: {type(item_node).__name__})")
|
||||
|
||||
return unique_source_dirs
|
||||
|
||||
@Slot()
|
||||
def _on_llm_reinterpret_clicked(self):
|
||||
"""Gathers selected source paths and emits the llm_reinterpret_requested signal."""
|
||||
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
||||
if not selected_indexes:
|
||||
return
|
||||
|
||||
"""Gathers selected source paths and emits the llm_reinterpret_requested signal. (Triggered by context menu)"""
|
||||
if self.llm_processing_active:
|
||||
QMessageBox.warning(self, "Busy", "LLM processing is already in progress. Please wait.")
|
||||
return
|
||||
|
||||
unique_source_dirs = set()
|
||||
processed_source_paths = set() # Track processed source paths to avoid duplicates
|
||||
for index in selected_indexes:
|
||||
if not index.isValid(): continue
|
||||
item_node = index.internalPointer()
|
||||
if not item_node: continue
|
||||
|
||||
# Traverse up to find the SourceRule node (Simplified traversal)
|
||||
source_node = None
|
||||
current_node = item_node
|
||||
while current_node is not None:
|
||||
if isinstance(current_node, SourceRule):
|
||||
source_node = current_node
|
||||
break
|
||||
# Simplified parent traversal - adjust if model structure is different
|
||||
parent_attr = getattr(current_node, 'parent', None) # Check for generic 'parent'
|
||||
if callable(parent_attr): # Check if parent is a method (like in QStandardItemModel)
|
||||
current_node = parent_attr()
|
||||
elif parent_attr: # Check if parent is an attribute
|
||||
current_node = parent_attr
|
||||
else: # Try specific parent attributes if generic fails
|
||||
parent_source = getattr(current_node, 'parent_source', None)
|
||||
if parent_source:
|
||||
current_node = parent_source
|
||||
else:
|
||||
parent_asset = getattr(current_node, 'parent_asset', None)
|
||||
if parent_asset:
|
||||
current_node = parent_asset
|
||||
else: # Reached top or unexpected node type
|
||||
current_node = None
|
||||
|
||||
|
||||
if source_node and hasattr(source_node, 'input_path') and source_node.input_path:
|
||||
source_path_str = source_node.input_path
|
||||
if source_path_str in processed_source_paths:
|
||||
continue
|
||||
source_path_obj = Path(source_path_str)
|
||||
if source_path_obj.is_dir() or (source_path_obj.is_file() and source_path_obj.suffix.lower() == '.zip'):
|
||||
unique_source_dirs.add(source_path_str)
|
||||
processed_source_paths.add(source_path_str)
|
||||
else:
|
||||
log.warning(f"Skipping non-directory/zip source for re-interpretation: {source_path_str}")
|
||||
# else: # Reduce log noise
|
||||
# log.warning(f"Could not determine valid SourceRule or input_path for selected index: {index.row()},{index.column()} (Item type: {type(item_node).__name__})")
|
||||
|
||||
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
||||
unique_source_dirs = self._get_unique_source_dirs_from_selection(selected_indexes)
|
||||
|
||||
if not unique_source_dirs:
|
||||
# self.statusBar().showMessage("No valid source directories found for selected items.", 5000) # Status bar is in MainWindow
|
||||
log.warning("No valid source directories found for selected items to re-interpret.")
|
||||
log.warning("No valid source directories found for selected items to re-interpret with LLM.")
|
||||
# Optionally show status bar message via MainWindow reference if available
|
||||
return
|
||||
|
||||
log.info(f"Emitting llm_reinterpret_requested for {len(unique_source_dirs)} paths.")
|
||||
self.llm_reinterpret_requested.emit(list(unique_source_dirs))
|
||||
|
||||
|
||||
@Slot(str, QModelIndex)
|
||||
def _on_reinterpret_preset_selected(self, preset_name: str, index: QModelIndex):
|
||||
"""Handles the selection of a preset from the re-interpret context sub-menu."""
|
||||
log.info(f"Preset re-interpretation requested: Preset='{preset_name}', Index='{index.row()},{index.column()}'")
|
||||
# Reuse logic from _on_llm_reinterpret_clicked to get selected source paths
|
||||
selected_indexes = self.unified_view.selectionModel().selectedIndexes()
|
||||
# Use the helper method to get all selected source paths, not just the one clicked
|
||||
unique_source_dirs = self._get_unique_source_dirs_from_selection(selected_indexes)
|
||||
|
||||
if not unique_source_dirs:
|
||||
log.warning("No valid source directories found for selected items to re-interpret with preset.")
|
||||
# Optionally show status bar message via MainWindow reference if available
|
||||
return
|
||||
|
||||
log.info(f"Emitting preset_reinterpret_requested for {len(unique_source_dirs)} paths with preset '{preset_name}'.")
|
||||
self.preset_reinterpret_requested.emit(list(unique_source_dirs), preset_name)
|
||||
|
||||
|
||||
@Slot(QPoint)
|
||||
def _show_unified_view_context_menu(self, point: QPoint):
|
||||
"""Shows the context menu for the unified view."""
|
||||
@ -451,31 +489,90 @@ class MainPanelWidget(QWidget):
|
||||
if not index.isValid():
|
||||
return
|
||||
|
||||
item_node = index.internalPointer()
|
||||
is_source_item = isinstance(item_node, SourceRule)
|
||||
model = self.unified_view.model()
|
||||
if not model: return
|
||||
item_node = model.getItem(index) # Use model's method
|
||||
|
||||
# Find the SourceRule node associated with the clicked index
|
||||
# Find the SourceRule node associated with the clicked index
|
||||
source_rule_node = None
|
||||
current_index = index # Start with the clicked index
|
||||
while current_index.isValid():
|
||||
current_item = model.getItem(current_index)
|
||||
if isinstance(current_item, SourceRule):
|
||||
source_rule_node = current_item
|
||||
break
|
||||
current_index = model.parent(current_index) # Move to the parent index
|
||||
# If loop finishes without break, source_rule_node remains None
|
||||
|
||||
menu = QMenu(self)
|
||||
|
||||
if is_source_item:
|
||||
# --- Re-interpret Menu ---
|
||||
if source_rule_node: # Only show if we clicked on or within a SourceRule item
|
||||
reinterpet_menu = menu.addMenu("Re-interpret selected source")
|
||||
|
||||
# Get Preset Names (Option B: Direct File Listing)
|
||||
preset_names = []
|
||||
try:
|
||||
presets_dir = configuration.PRESETS_DIR
|
||||
if os.path.isdir(presets_dir):
|
||||
for filename in os.listdir(presets_dir):
|
||||
if filename.endswith(".json") and filename != "_template.json":
|
||||
preset_name = os.path.splitext(filename)[0]
|
||||
preset_names.append(preset_name)
|
||||
preset_names.sort() # Sort alphabetically
|
||||
else:
|
||||
log.warning(f"Presets directory not found or not a directory: {presets_dir}")
|
||||
except Exception as e:
|
||||
log.exception(f"Error listing presets in {configuration.PRESETS_DIR}: {e}")
|
||||
|
||||
# Populate Sub-Menu with Presets
|
||||
if preset_names:
|
||||
for preset_name in preset_names:
|
||||
preset_action = QAction(preset_name, self)
|
||||
# Pass the preset name and the *clicked* index (though the slot will get all selected)
|
||||
preset_action.triggered.connect(functools.partial(self._on_reinterpret_preset_selected, preset_name, index))
|
||||
reinterpet_menu.addAction(preset_action)
|
||||
else:
|
||||
no_presets_action = QAction("No presets found", self)
|
||||
no_presets_action.setEnabled(False)
|
||||
reinterpet_menu.addAction(no_presets_action)
|
||||
|
||||
|
||||
# Add LLM Option (Static)
|
||||
reinterpet_menu.addSeparator()
|
||||
llm_action = QAction("LLM", self)
|
||||
# Connect to the existing slot that handles LLM re-interpretation requests
|
||||
llm_action.triggered.connect(self._on_llm_reinterpret_clicked)
|
||||
# Disable if LLM is currently processing
|
||||
llm_action.setEnabled(not self.llm_processing_active)
|
||||
reinterpet_menu.addAction(llm_action)
|
||||
|
||||
menu.addSeparator() # Separator before other actions
|
||||
|
||||
# --- Other Actions (like Copy LLM Example) ---
|
||||
if source_rule_node: # Check again if it's a source item for this action
|
||||
copy_llm_example_action = QAction("Copy LLM Example to Clipboard", self)
|
||||
copy_llm_example_action.setToolTip("Copies a JSON structure representing the input files and predicted output, suitable for LLM examples.")
|
||||
copy_llm_example_action.triggered.connect(lambda: self._copy_llm_example_to_clipboard(index))
|
||||
# Pass the found source_rule_node
|
||||
copy_llm_example_action.triggered.connect(lambda: self._copy_llm_example_to_clipboard(source_rule_node))
|
||||
menu.addAction(copy_llm_example_action)
|
||||
menu.addSeparator()
|
||||
# menu.addSeparator() # Removed redundant separator
|
||||
|
||||
# Add other actions...
|
||||
# Add other general actions here if needed...
|
||||
|
||||
if not menu.isEmpty():
|
||||
menu.exec(self.unified_view.viewport().mapToGlobal(point))
|
||||
|
||||
@Slot(QModelIndex)
|
||||
def _copy_llm_example_to_clipboard(self, index: QModelIndex):
|
||||
"""Copies a JSON structure for the selected source item to the clipboard."""
|
||||
if not index.isValid(): return
|
||||
item_node = index.internalPointer()
|
||||
if not isinstance(item_node, SourceRule): return
|
||||
@Slot(SourceRule) # Accept SourceRule directly
|
||||
def _copy_llm_example_to_clipboard(self, source_rule_node: SourceRule | None):
|
||||
"""Copies a JSON structure for the given SourceRule node to the clipboard."""
|
||||
if not source_rule_node:
|
||||
log.warning(f"No SourceRule node provided to copy LLM example.")
|
||||
return
|
||||
|
||||
source_rule: SourceRule = item_node
|
||||
# We already have the source_rule_node passed in
|
||||
source_rule: SourceRule = source_rule_node
|
||||
log.info(f"Attempting to generate LLM example JSON for source: {source_rule.input_path}")
|
||||
|
||||
all_file_paths = []
|
||||
@ -570,11 +667,8 @@ class MainPanelWidget(QWidget):
|
||||
self.materials_blend_path_input.setEnabled(blender_paths_enabled)
|
||||
self.browse_materials_blend_button.setEnabled(blender_paths_enabled)
|
||||
|
||||
# Update LLM button state explicitly when controls are enabled/disabled
|
||||
if enabled:
|
||||
self._update_llm_reinterpret_button_state()
|
||||
else:
|
||||
self.llm_reinterpret_button.setEnabled(False)
|
||||
# LLM button removed, no need to update its state here.
|
||||
# Context menu actions enable/disable themselves based on context (e.g., llm_processing_active).
|
||||
|
||||
|
||||
@Slot(bool)
|
||||
@ -594,9 +688,9 @@ class MainPanelWidget(QWidget):
|
||||
|
||||
@Slot(bool)
|
||||
def set_llm_processing_status(self, active: bool):
|
||||
"""Informs the panel whether LLM processing is active."""
|
||||
"""Informs the panel whether LLM processing is active (used for context menu state)."""
|
||||
self.llm_processing_active = active
|
||||
self._update_llm_reinterpret_button_state() # Update button state based on new status
|
||||
# No button state to update directly, but context menu will check this flag when built.
|
||||
|
||||
# TODO: Add method to get current output path if needed by MainWindow before processing
|
||||
def get_output_directory(self) -> str:
|
||||
|
||||
@ -189,6 +189,7 @@ class MainWindow(QMainWindow):
|
||||
self.main_panel_widget.output_dir_changed.connect(self._on_output_dir_changed)
|
||||
self.main_panel_widget.blender_settings_changed.connect(self._on_blender_settings_changed)
|
||||
|
||||
self.main_panel_widget.preset_reinterpret_requested.connect(self._on_preset_reinterpret_requested)
|
||||
# --- Connect Signals from LLMInteractionHandler ---
|
||||
self.llm_interaction_handler.llm_prediction_ready.connect(self._on_llm_prediction_ready_from_handler)
|
||||
self.llm_interaction_handler.llm_prediction_error.connect(self._on_prediction_error) # Use common error slot
|
||||
@ -621,6 +622,67 @@ class MainWindow(QMainWindow):
|
||||
"materials_blend_path": mat_path
|
||||
}
|
||||
log.debug(f"MainWindow stored Blender settings: {self._current_blender_settings}")
|
||||
@Slot(list, str)
|
||||
def _on_preset_reinterpret_requested(self, source_paths: list, preset_name: str):
|
||||
"""Handles the preset_reinterpret_requested signal from MainPanelWidget."""
|
||||
log.info(f"Received preset re-interpret request for {len(source_paths)} paths using preset '{preset_name}'.")
|
||||
|
||||
if not source_paths:
|
||||
self.statusBar().showMessage("No valid source directories selected for preset re-interpretation.", 5000)
|
||||
return
|
||||
|
||||
# Check if rule-based prediction is already running (optional, handler might manage internally)
|
||||
# Note: QueuedConnection on the signal helps, but check anyway for immediate feedback/logging
|
||||
# TODO: Add is_running() method to RuleBasedPredictionHandler if needed for this check
|
||||
if self.prediction_handler and hasattr(self.prediction_handler, 'is_running') and self.prediction_handler.is_running():
|
||||
log.warning("Rule-based prediction is already running. Queuing re-interpretation request.")
|
||||
# Proceed, relying on QueuedConnection
|
||||
|
||||
# Ensure the prediction thread is running
|
||||
if self.prediction_thread and not self.prediction_thread.isRunning():
|
||||
log.debug("Starting prediction thread for preset re-interpretation.")
|
||||
self.prediction_thread.start()
|
||||
elif not self.prediction_thread:
|
||||
log.error("Prediction thread not initialized. Cannot perform preset re-interpretation.")
|
||||
self.statusBar().showMessage("Error: Prediction system not ready.", 5000)
|
||||
return
|
||||
|
||||
|
||||
# Trigger prediction for each path
|
||||
self.statusBar().showMessage(f"Starting re-interpretation for {len(source_paths)} item(s) using preset '{preset_name}'...", 0)
|
||||
for input_path_str in source_paths:
|
||||
# Mark as pending (use existing mechanism)
|
||||
self._pending_predictions.add(input_path_str)
|
||||
self._completed_predictions.discard(input_path_str) # Ensure it's not marked completed
|
||||
|
||||
# Update status in model (Requires update_status method in UnifiedViewModel)
|
||||
try:
|
||||
if hasattr(self.unified_model, 'update_status'):
|
||||
self.unified_model.update_status(input_path_str, "Re-interpreting...")
|
||||
else:
|
||||
log.warning("UnifiedViewModel does not have 'update_status' method. Cannot update status visually.")
|
||||
except Exception as e:
|
||||
log.exception(f"Error calling unified_model.update_status for {input_path_str}: {e}")
|
||||
|
||||
|
||||
# Extract file list (needed by RuleBasedPredictionHandler)
|
||||
file_list = self._extract_file_list(input_path_str)
|
||||
if file_list is not None:
|
||||
log.debug(f"Emitting start_prediction_signal for re-interpretation: Path='{input_path_str}', Preset='{preset_name}'")
|
||||
# Use the existing signal connected to RuleBasedPredictionHandler
|
||||
self.start_prediction_signal.emit(input_path_str, file_list, preset_name)
|
||||
else:
|
||||
log.warning(f"Skipping re-interpretation for {input_path_str} due to extraction error.")
|
||||
# Update status in model to reflect error (Requires update_status method)
|
||||
try:
|
||||
if hasattr(self.unified_model, 'update_status'):
|
||||
self.unified_model.update_status(input_path_str, "Error extracting files")
|
||||
else:
|
||||
log.warning("UnifiedViewModel does not have 'update_status' method. Cannot update error status visually.")
|
||||
except Exception as e:
|
||||
log.exception(f"Error calling unified_model.update_status (error case) for {input_path_str}: {e}")
|
||||
|
||||
self._handle_prediction_completion(input_path_str) # Mark as completed (with error)
|
||||
|
||||
# --- Preview Update Method ---
|
||||
def update_preview(self):
|
||||
@ -1180,6 +1242,7 @@ class MainWindow(QMainWindow):
|
||||
log.warning("get_llm_source_preset_name called before preset_editor_widget was initialized.")
|
||||
return None
|
||||
|
||||
# --- Main Execution ---
|
||||
# --- Main Execution ---
|
||||
def run_gui():
|
||||
"""Initializes and runs the Qt application."""
|
||||
|
||||
@ -489,3 +489,7 @@ class RuleBasedPredictionHandler(BasePredictionHandler):
|
||||
self._current_file_list = None
|
||||
self._current_preset_name = None
|
||||
log.info(f"Finished rule-based prediction run for: {input_source_identifier}")
|
||||
def is_running(self) -> bool:
|
||||
"""Returns True if the handler is currently processing a prediction request."""
|
||||
# The _is_running flag is managed by the base class or the run_prediction method
|
||||
return self._is_running
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# gui/unified_view_model.py
|
||||
import logging # Added for debugging
|
||||
log = logging.getLogger(__name__) # Added for debugging
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot # Added Signal and Slot
|
||||
from PySide6.QtCore import QAbstractItemModel, QModelIndex, Qt, Signal, Slot, QMimeData, QByteArray, QDataStream, QIODevice # Added Signal and Slot, QMimeData, QByteArray, QDataStream, QIODevice
|
||||
from PySide6.QtGui import QColor # Added for background role
|
||||
from pathlib import Path # Added for file_name extraction
|
||||
from rule_structure import SourceRule, AssetRule, FileRule # Removed AssetType, ItemType import
|
||||
@ -35,6 +35,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
# COL_STATUS = 5 # Removed
|
||||
# COL_OUTPUT_PATH = 6 # Removed
|
||||
|
||||
# --- Drag and Drop MIME Type ---
|
||||
MIME_TYPE = "application/x-filerule-index-list" # Custom MIME type
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._source_rules = [] # Now stores a list of SourceRule objects
|
||||
@ -278,7 +281,9 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
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 ""
|
||||
elif role == Qt.EditRole:
|
||||
if column == self.COL_ASSET_TYPE:
|
||||
if column == self.COL_NAME:
|
||||
return item.asset_name # Return name for editing
|
||||
elif column == self.COL_ASSET_TYPE:
|
||||
return item.asset_type_override
|
||||
return None
|
||||
|
||||
@ -330,7 +335,60 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
changed = True
|
||||
|
||||
elif isinstance(item, AssetRule):
|
||||
if column == self.COL_ASSET_TYPE:
|
||||
if column == self.COL_NAME: # Handle Asset Name change
|
||||
new_asset_name = str(value).strip() if value else None
|
||||
if not new_asset_name:
|
||||
log.warning("setData: Asset name cannot be empty.")
|
||||
return False # Don't allow empty names
|
||||
|
||||
if item.asset_name == new_asset_name:
|
||||
return False # No change
|
||||
|
||||
# --- Validation: Check for duplicates within the same SourceRule ---
|
||||
parent_source = getattr(item, 'parent_source', None)
|
||||
if parent_source:
|
||||
for existing_asset in parent_source.assets:
|
||||
if existing_asset.asset_name == new_asset_name and existing_asset is not item:
|
||||
log.warning(f"setData: Duplicate asset name '{new_asset_name}' detected within the same source. Aborting rename.")
|
||||
# Optionally, provide user feedback here via a signal or message box
|
||||
return False
|
||||
else:
|
||||
log.error("setData: Cannot validate asset name, parent SourceRule not found.")
|
||||
# Decide how to handle this - proceed cautiously or abort? Aborting is safer.
|
||||
return False
|
||||
# --- End Validation ---
|
||||
|
||||
log.info(f"setData: Renaming AssetRule from '{item.asset_name}' to '{new_asset_name}'")
|
||||
old_asset_name = item.asset_name
|
||||
item.asset_name = new_asset_name
|
||||
changed = True
|
||||
|
||||
# --- Update Child FileRule Target Asset Overrides ---
|
||||
log.debug(f"setData: Updating FileRule target overrides from '{old_asset_name}' to '{new_asset_name}'")
|
||||
updated_file_indices = []
|
||||
for src_idx, source_rule in enumerate(self._source_rules):
|
||||
source_rule_index = self.createIndex(src_idx, 0, source_rule)
|
||||
for asset_idx, asset_rule in enumerate(source_rule.assets):
|
||||
asset_rule_index = self.createIndex(asset_idx, 0, asset_rule) # This index is relative to source_rule_index
|
||||
for file_idx, file_rule in enumerate(asset_rule.files):
|
||||
if file_rule.target_asset_name_override == old_asset_name:
|
||||
log.debug(f" Updating target for file: {Path(file_rule.file_path).name}")
|
||||
file_rule.target_asset_name_override = new_asset_name
|
||||
# Get the correct index for the file rule to emit dataChanged
|
||||
file_rule_parent_index = self.parent(self.createIndex(file_idx, 0, file_rule)) # Get parent AssetRule index
|
||||
file_rule_index = self.index(file_idx, self.COL_TARGET_ASSET, file_rule_parent_index)
|
||||
if file_rule_index.isValid():
|
||||
updated_file_indices.append(file_rule_index)
|
||||
else:
|
||||
log.warning(f" Could not get valid index for updated file rule target: {Path(file_rule.file_path).name}")
|
||||
|
||||
|
||||
# Emit dataChanged for all updated file rules *after* the loop
|
||||
for file_index in updated_file_indices:
|
||||
self.dataChanged.emit(file_index, file_index, [Qt.DisplayRole, Qt.EditRole])
|
||||
# --- End Child Update ---
|
||||
|
||||
elif column == self.COL_ASSET_TYPE:
|
||||
# Delegate provides string value (e.g., "Surface", "Model") or None
|
||||
new_value = str(value) if value is not None else None
|
||||
if new_value == "": new_value = None # Treat empty string as None
|
||||
@ -416,6 +474,8 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
default_flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||||
|
||||
item = index.internalPointer()
|
||||
if not item: # Should not happen for valid index, but safety check
|
||||
return Qt.NoItemFlags
|
||||
column = index.column()
|
||||
|
||||
# Always use detailed mode editability logic
|
||||
@ -423,15 +483,21 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
if isinstance(item, SourceRule):
|
||||
if column == self.COL_SUPPLIER: can_edit = True
|
||||
elif isinstance(item, AssetRule):
|
||||
if column == self.COL_NAME: can_edit = True # Allow editing name
|
||||
if column == self.COL_ASSET_TYPE: can_edit = True
|
||||
# AssetRule items can accept drops
|
||||
default_flags |= Qt.ItemIsDropEnabled
|
||||
elif isinstance(item, FileRule):
|
||||
if column == self.COL_TARGET_ASSET: can_edit = True
|
||||
if column == self.COL_ITEM_TYPE: can_edit = True
|
||||
# FileRule items can be dragged
|
||||
default_flags |= Qt.ItemIsDragEnabled
|
||||
|
||||
if can_edit:
|
||||
return default_flags | Qt.ItemIsEditable
|
||||
else:
|
||||
default_flags |= Qt.ItemIsEditable
|
||||
|
||||
return default_flags
|
||||
# Removed erroneous else block
|
||||
|
||||
def headerData(self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole):
|
||||
"""Returns the data for the given role and section in the header."""
|
||||
@ -749,18 +815,257 @@ class UnifiedViewModel(QAbstractItemModel):
|
||||
log.error(f"removeAssetRule: Could not find parent SourceRule or the AssetRule within its parent's list.")
|
||||
return False
|
||||
|
||||
def get_asset_type_keys(self) -> List[str]:
|
||||
def get_asset_type_keys(self) -> List[str]:
|
||||
"""Returns the cached list of asset type keys."""
|
||||
return self._asset_type_keys
|
||||
|
||||
def get_file_type_keys(self) -> List[str]:
|
||||
"""Returns the cached list of file type keys."""
|
||||
return self._file_type_keys
|
||||
|
||||
log.debug(f"Removing empty AssetRule '{asset_rule_to_remove.asset_name}' at row {asset_row_for_removal} under '{Path(source_rule.input_path).name}'")
|
||||
self.beginRemoveRows(grandparent_index, asset_row_for_removal, asset_row_for_removal)
|
||||
source_rule.assets.pop(asset_row_for_removal)
|
||||
self.endRemoveRows()
|
||||
return True
|
||||
def update_status(self, source_path: str, status_text: str):
|
||||
"""
|
||||
Finds the SourceRule node for the given source_path and updates its status.
|
||||
Emits dataChanged for the corresponding row.
|
||||
"""
|
||||
log.debug(f"Attempting to update status for source '{source_path}' to '{status_text}'")
|
||||
found_row = -1
|
||||
found_rule = None
|
||||
for i, rule in enumerate(self._source_rules):
|
||||
if rule.input_path == source_path:
|
||||
found_row = i
|
||||
found_rule = rule
|
||||
break
|
||||
|
||||
if found_rule is not None and found_row != -1:
|
||||
try:
|
||||
# Attempt to set a status attribute (e.g., _status_message)
|
||||
# Note: This attribute isn't formally defined in SourceRule structure yet.
|
||||
setattr(found_rule, '_status_message', status_text)
|
||||
log.info(f"Updated status for SourceRule '{source_path}' (row {found_row}) to '{status_text}'")
|
||||
|
||||
# Emit dataChanged for the entire row to potentially trigger updates
|
||||
# (e.g., delegates, background color based on status if implemented later)
|
||||
start_index = self.createIndex(found_row, 0, found_rule)
|
||||
end_index = self.createIndex(found_row, self.columnCount() - 1, found_rule)
|
||||
self.dataChanged.emit(start_index, end_index, [Qt.DisplayRole]) # Emit for DisplayRole, maybe others needed later
|
||||
|
||||
except Exception as e:
|
||||
log.exception(f"Error setting status attribute or emitting dataChanged for {source_path}: {e}")
|
||||
else:
|
||||
log.warning(f"Could not find SourceRule with path '{source_path}' to update status.")
|
||||
|
||||
# --- Placeholder for node finding method (Original Request - Replaced by direct list search above) ---
|
||||
# Kept for reference, but the logic above directly searches self._source_rules
|
||||
|
||||
# --- Drag and Drop Methods ---
|
||||
|
||||
def supportedDropActions(self) -> Qt.DropActions:
|
||||
"""Specifies that only Move actions are supported."""
|
||||
return Qt.MoveAction
|
||||
|
||||
def mimeTypes(self) -> list[str]:
|
||||
"""Returns the list of supported MIME types for dragging."""
|
||||
return [self.MIME_TYPE]
|
||||
|
||||
def mimeData(self, indexes: list[QModelIndex]) -> QMimeData:
|
||||
"""Encodes information about the dragged FileRule items."""
|
||||
mime_data = QMimeData()
|
||||
encoded_data = QByteArray()
|
||||
stream = QDataStream(encoded_data, QIODevice.OpenModeFlag.WriteOnly)
|
||||
|
||||
dragged_file_info = []
|
||||
for index in indexes:
|
||||
if not index.isValid() or index.column() != 0: # Only consider valid indices from the first column
|
||||
continue
|
||||
item = index.internalPointer()
|
||||
if isinstance(item, FileRule):
|
||||
parent_index = self.parent(index)
|
||||
if parent_index.isValid():
|
||||
# Store: source_row, source_parent_row, source_grandparent_row
|
||||
# This allows reconstructing the index later
|
||||
grandparent_index = self.parent(parent_index)
|
||||
# Ensure grandparent_index is valid before accessing its row
|
||||
if grandparent_index.isValid():
|
||||
dragged_file_info.append((index.row(), parent_index.row(), grandparent_index.row()))
|
||||
else:
|
||||
# Handle case where grandparent is the root (shouldn't happen for FileRule, but safety)
|
||||
# Or if parent() failed unexpectedly
|
||||
log.warning(f"mimeData: Could not get valid grandparent index for FileRule at row {index.row()}, parent row {parent_index.row()}")
|
||||
|
||||
else:
|
||||
log.warning(f"mimeData: Could not get parent index for FileRule at row {index.row()}")
|
||||
|
||||
# Write the number of items first, then each tuple
|
||||
stream.writeInt8(len(dragged_file_info))
|
||||
for info in dragged_file_info:
|
||||
stream.writeInt8(info[0]) # source_row
|
||||
stream.writeInt8(info[1]) # source_parent_row
|
||||
stream.writeInt8(info[2]) # source_grandparent_row
|
||||
|
||||
mime_data.setData(self.MIME_TYPE, encoded_data)
|
||||
log.debug(f"mimeData: Encoded {len(dragged_file_info)} FileRule indices.")
|
||||
return mime_data
|
||||
|
||||
def canDropMimeData(self, data: QMimeData, action: Qt.DropAction, row: int, column: int, parent: QModelIndex) -> bool:
|
||||
"""Checks if the data can be dropped at the specified location."""
|
||||
if action != Qt.MoveAction or not data.hasFormat(self.MIME_TYPE):
|
||||
return False
|
||||
|
||||
# Check if the drop target is a valid AssetRule
|
||||
if not parent.isValid():
|
||||
return False # Cannot drop onto root or SourceRule directly in this implementation
|
||||
|
||||
target_item = parent.internalPointer()
|
||||
if not isinstance(target_item, AssetRule):
|
||||
return False # Can only drop onto AssetRule items
|
||||
|
||||
# Optional: Prevent dropping onto the original parent? (Might be confusing)
|
||||
# For now, allow dropping onto the same parent (moveFileRule handles this)
|
||||
|
||||
return True
|
||||
|
||||
def dropMimeData(self, data: QMimeData, action: Qt.DropAction, row: int, column: int, parent: QModelIndex) -> bool:
|
||||
"""Handles the dropping of FileRule items onto an AssetRule."""
|
||||
if not self.canDropMimeData(data, action, row, column, parent):
|
||||
log.warning("dropMimeData: canDropMimeData check failed.")
|
||||
return False
|
||||
|
||||
target_asset_item = parent.internalPointer()
|
||||
if not isinstance(target_asset_item, AssetRule): # Should be caught by canDrop, but double-check
|
||||
log.error("dropMimeData: Target item is not an AssetRule.")
|
||||
return False
|
||||
|
||||
encoded_data = data.data(self.MIME_TYPE)
|
||||
stream = QDataStream(encoded_data, QIODevice.OpenModeFlag.ReadOnly)
|
||||
|
||||
num_items = stream.readInt8()
|
||||
source_indices_info = []
|
||||
for _ in range(num_items):
|
||||
source_row = stream.readInt8()
|
||||
source_parent_row = stream.readInt8()
|
||||
source_grandparent_row = stream.readInt8()
|
||||
source_indices_info.append((source_row, source_parent_row, source_grandparent_row))
|
||||
|
||||
log.debug(f"dropMimeData: Decoded {len(source_indices_info)} source indices. Target Asset: '{target_asset_item.asset_name}'")
|
||||
|
||||
if not source_indices_info:
|
||||
log.warning("dropMimeData: No valid source index information decoded.")
|
||||
return False
|
||||
|
||||
# Keep track of original parents that might become empty
|
||||
original_parents = set()
|
||||
moved_files_new_indices = {} # Store new index after move for dataChanged emission
|
||||
|
||||
# --- BEGIN FIX: Reconstruct all source indices BEFORE the move loop ---
|
||||
source_indices_to_process = []
|
||||
log.debug("Reconstructing initial source indices...")
|
||||
for src_row, src_parent_row, src_grandparent_row in source_indices_info:
|
||||
grandparent_index = self.index(src_grandparent_row, 0, QModelIndex())
|
||||
if not grandparent_index.isValid():
|
||||
log.error(f"dropMimeData: Failed initial reconstruction of grandparent index (row {src_grandparent_row}). Skipping item.")
|
||||
continue
|
||||
old_parent_index = self.index(src_parent_row, 0, grandparent_index)
|
||||
if not old_parent_index.isValid():
|
||||
log.error(f"dropMimeData: Failed initial reconstruction of old parent index (row {src_parent_row}). Skipping item.")
|
||||
continue
|
||||
source_file_index = self.index(src_row, 0, old_parent_index)
|
||||
if not source_file_index.isValid():
|
||||
# Log the specific parent it failed under for better debugging
|
||||
parent_name = getattr(old_parent_index.internalPointer(), 'asset_name', 'Unknown Parent')
|
||||
log.error(f"dropMimeData: Failed initial reconstruction of source file index (original row {src_row}) under parent '{parent_name}'. Skipping item.")
|
||||
continue
|
||||
|
||||
# Check if the reconstructed index actually points to a FileRule
|
||||
item_check = source_file_index.internalPointer()
|
||||
if isinstance(item_check, FileRule):
|
||||
source_indices_to_process.append(source_file_index)
|
||||
log.debug(f" Successfully reconstructed index for file: {Path(item_check.file_path).name}")
|
||||
else:
|
||||
log.warning(f"dropMimeData: Initial reconstructed index (row {src_row}) does not point to a FileRule. Skipping.")
|
||||
|
||||
log.debug(f"Successfully reconstructed {len(source_indices_to_process)} valid source indices.")
|
||||
# --- END FIX ---
|
||||
|
||||
|
||||
# Process moves using the pre-calculated valid indices
|
||||
for source_file_index in source_indices_to_process: # Iterate through the valid indices
|
||||
# Get the file item (already validated during reconstruction)
|
||||
file_item = source_file_index.internalPointer()
|
||||
|
||||
# Track original parent for cleanup (using the valid index)
|
||||
old_parent_index = self.parent(source_file_index) # Get parent from the valid index
|
||||
if old_parent_index.isValid():
|
||||
old_parent_asset = old_parent_index.internalPointer()
|
||||
if isinstance(old_parent_asset, AssetRule):
|
||||
# Need grandparent row for the tuple key
|
||||
grandparent_index = self.parent(old_parent_index)
|
||||
if grandparent_index.isValid():
|
||||
original_parents.add((grandparent_index.row(), old_parent_asset.asset_name))
|
||||
else: # Handle root case or error
|
||||
log.warning(f"Could not get grandparent index for original parent '{old_parent_asset.asset_name}' during cleanup tracking.")
|
||||
else:
|
||||
log.warning(f"Parent of file '{Path(file_item.file_path).name}' is not an AssetRule.")
|
||||
else:
|
||||
log.warning(f"Could not get valid parent index for file '{Path(file_item.file_path).name}' during cleanup tracking.")
|
||||
|
||||
|
||||
# Perform the move using the model's method and the valid source_file_index
|
||||
if self.moveFileRule(source_file_index, parent): # 'parent' is the target_parent_asset_index
|
||||
# --- Update Target Asset Override After Successful Move ---
|
||||
# The file_item's parent_asset reference should now be updated by moveFileRule
|
||||
new_parent_asset = getattr(file_item, 'parent_asset', None)
|
||||
if new_parent_asset == target_asset_item: # Check if move was successful internally
|
||||
if file_item.target_asset_name_override != target_asset_item.asset_name:
|
||||
log.debug(f" Updating target override for '{Path(file_item.file_path).name}' to '{target_asset_item.asset_name}'")
|
||||
file_item.target_asset_name_override = target_asset_item.asset_name
|
||||
# Need the *new* index of the moved file to emit dataChanged
|
||||
try:
|
||||
new_row = target_asset_item.files.index(file_item)
|
||||
new_file_index_col0 = self.index(new_row, 0, parent) # Index for col 0 under new parent
|
||||
new_file_index_target_col = self.index(new_row, self.COL_TARGET_ASSET, parent) # Index for target col
|
||||
if new_file_index_target_col.isValid():
|
||||
moved_files_new_indices[file_item.file_path] = new_file_index_target_col # Use hashable file_path as key
|
||||
else:
|
||||
log.warning(f" Could not get valid *new* index for target column of moved file: {Path(file_item.file_path).name}")
|
||||
except ValueError:
|
||||
log.error(f" Could not find moved file '{Path(file_item.file_path).name}' in target parent's list after move.")
|
||||
|
||||
else:
|
||||
log.error(f" Move reported success, but file's parent reference ('{getattr(new_parent_asset, 'asset_name', 'None')}') doesn't match target ('{target_asset_item.asset_name}'). Override not updated.")
|
||||
else:
|
||||
log.error(f"dropMimeData: moveFileRule failed for file '{Path(file_item.file_path).name}'.")
|
||||
# If one move fails, should we stop? For now, continue processing others.
|
||||
continue # Skip override update and cleanup check for this file
|
||||
|
||||
# --- Emit dataChanged for Target Asset column AFTER all moves ---
|
||||
for source_path, new_index in moved_files_new_indices.items(): # Key is now source_path (string)
|
||||
self.dataChanged.emit(new_index, new_index, [Qt.DisplayRole, Qt.EditRole])
|
||||
|
||||
# --- Cleanup: Remove any original parent AssetRules that are now empty ---
|
||||
log.debug(f"dropMimeData: Checking original parents for cleanup: {list(original_parents)}") # Log tuples
|
||||
for gp_row, asset_name in list(original_parents): # Iterate over a copy of tuples
|
||||
try:
|
||||
if 0 <= gp_row < len(self._source_rules):
|
||||
source_rule = self._source_rules[gp_row]
|
||||
# Find the asset rule within the correct source rule
|
||||
asset_rule_to_check = next((asset for asset in source_rule.assets if asset.asset_name == asset_name), None)
|
||||
|
||||
if asset_rule_to_check and not asset_rule_to_check.files and asset_rule_to_check != target_asset_item: # Don't remove the target if it was also an original parent
|
||||
log.info(f"dropMimeData: Attempting cleanup of now empty original parent: '{asset_rule_to_check.asset_name}'")
|
||||
if not self.removeAssetRule(asset_rule_to_check):
|
||||
log.warning(f"dropMimeData: Failed to remove empty original parent '{asset_rule_to_check.asset_name}'.")
|
||||
elif not asset_rule_to_check:
|
||||
log.warning(f"dropMimeData: Cleanup check failed. Could not find original parent asset '{asset_name}' in source rule at row {gp_row}.")
|
||||
else:
|
||||
log.warning(f"dropMimeData: Cleanup check failed. Invalid grandparent row index {gp_row} found in original_parents set.")
|
||||
except Exception as e:
|
||||
log.exception(f"dropMimeData: Error during cleanup check for parent '{asset_name}' (gp_row {gp_row}): {e}")
|
||||
|
||||
|
||||
return True
|
||||
Loading…
x
Reference in New Issue
Block a user