New Definitions editor

This commit is contained in:
Rusfort 2025-05-13 13:08:52 +02:00
parent 344ae078a8
commit 87673507d8
4 changed files with 481 additions and 192 deletions

View File

@ -0,0 +1,62 @@
# Issue: List item selection not working in Definitions Editor
**Date:** 2025-05-13
**Affected File:** [`gui/definitions_editor_dialog.py`](gui/definitions_editor_dialog.py)
**Problem Description:**
User mouse clicks on items within the `QListWidget` instances (for Asset Types, File Types, and Suppliers) in the Definitions Editor dialog do not trigger item selection or the `currentItemChanged` signal. The first item is selected by default and its details are displayed correctly. Programmatic selection of items (e.g., via a diagnostic button) *does* correctly trigger the `currentItemChanged` signal and updates the UI detail views. The issue is specific to user-initiated mouse clicks for selection after the initial load.
**Debugging Steps Taken & Findings:**
1. **Initial Analysis:**
* Reviewed GUI internals documentation ([`Documentation/02_Developer_Guide/06_GUI_Internals.md`](Documentation/02_Developer_Guide/06_GUI_Internals.md)) and [`gui/definitions_editor_dialog.py`](gui/definitions_editor_dialog.py) source code.
* Confirmed signal connections (`currentItemChanged` to display slots) are made.
2. **Logging in Display Slots (`_display_*_details`):**
* Added logging to display slots. Confirmed they are called for the initial (default) item selection.
* No further calls to these slots occur on user clicks, indicating `currentItemChanged` is not firing.
3. **Color Swatch Palette Role:**
* Investigated and corrected `QPalette.ColorRole` for color swatches (reverted from `Background` to `Window`). This fixed an `AttributeError` but did not resolve the selection issue.
4. **Robust Error Handling in Display Slots:**
* Wrapped display slot logic in `try...finally` blocks with detailed logging. Confirmed slots complete without error for initial selection and signals for detail widgets are reconnected.
5. **Diagnostic Lambda for `currentItemChanged`:**
* Added a lambda logger to `currentItemChanged` alongside the main display slot.
* Confirmed both lambda and display slot fire for initial programmatic selection.
* Neither fires for subsequent user clicks. This proved the `QListWidget` itself was not emitting the signal.
6. **Explicit `setEnabled` and `setSelectionMode` on `QListWidget`:**
* Explicitly set these properties. No change in behavior.
7. **Explicit `setEnabled` and `setFocusPolicy(Qt.ClickFocus)` on `tab_page` (parent of `QListWidget` layout):**
* This change **allowed programmatic selection via a diagnostic button to correctly fire `currentItemChanged` and update the UI**.
* However, user mouse clicks still did not work and did not fire the signal.
8. **Event Filter Investigation:**
* **Filter on `QListWidget`:** Did NOT receive mouse press/release events from user clicks.
* **Filter on `tab_page` (parent of `QListWidget`'s layout):** Did NOT receive mouse press/release events.
* **Filter on `self.tab_widget` (QTabWidget):** DID receive mouse press/release events.
* Modified `self.tab_widget`'s event filter to return `False` for events over the current page, attempting to ensure propagation.
* **Result:** With the modified `tab_widget` filter, an event filter re-added to `asset_type_list_widget` *did* start receiving mouse press/release events. **However, `asset_type_list_widget` still did not emit `currentItemChanged` from these user clicks.**
9. **`DebugListWidget` (Subclassing `QListWidget`):**
* Created `DebugListWidget` overriding `mousePressEvent` with logging.
* Used `DebugListWidget` for `asset_type_list_widget`.
* **Initial user report indicated that `DebugListWidget.mousePressEvent` logs were NOT appearing for user clicks.** This means that even with the `QTabWidget` event filter attempting to propagate events, and the `asset_type_list_widget`'s filter (from step 8) confirming it received them, the `mousePressEvent` of the `QListWidget` itself was not being triggered by those propagated events. This is the current mystery.
**Current Status:**
- Programmatic selection works and fires signals.
- User clicks are received by an event filter on `asset_type_list_widget` (after `QTabWidget` filter modification) but do not result in `mousePressEvent` being called on the `QListWidget` (or `DebugListWidget`) itself, and thus no `currentItemChanged` signal is emitted.
- The issue seems to be a very low-level event processing problem specifically for user mouse clicks within the `QListWidget` instances when they are children of the `QTabWidget` pages, even when events appear to reach the list widget via an event filter.
**Next Steps (When Resuming):**
1. Re-verify the logs from the `DebugListWidget.mousePressEvent` test. If it's truly not being called despite its event filter seeing events, this is extremely unusual.
2. Simplify the `_create_tab_pane` method drastically for one tab:
* Remove the right-hand pane.
* Add the `DebugListWidget` directly to the `tab_page`'s layout without the intermediate `left_pane_layout`.
3. Consider if any styles applied to `QListWidget` or its parents via stylesheets could be interfering with hit testing or event processing (unlikely for this specific symptom, but possible).
4. Explore alternative ways to populate/manage the `QListWidget` or its items if a subtle corruption is occurring.
5. If all else fails, consider replacing the `QListWidget` with a `QListView` and a `QStringListModel` as a more fundamental change to see if the issue is specific to `QListWidget` in this context.

View File

@ -1,39 +1,39 @@
{ {
"ASSET_TYPE_DEFINITIONS": { "ASSET_TYPE_DEFINITIONS": {
"Surface": { "Surface": {
"description": "A single Standard PBR material set for a surface.",
"color": "#1f3e5d", "color": "#1f3e5d",
"description": "A single Standard PBR material set for a surface.",
"examples": [ "examples": [
"Set: Wood01_COL + Wood01_NRM + WOOD01_ROUGH", "Set: Wood01_COL + Wood01_NRM + WOOD01_ROUGH",
"Set: Dif_Concrete + Normal_Concrete + Refl_Concrete" "Set: Dif_Concrete + Normal_Concrete + Refl_Concrete"
] ]
}, },
"Model": { "Model": {
"description": "A set that contains models, can include PBR textureset",
"color": "#b67300", "color": "#b67300",
"description": "A set that contains models, can include PBR textureset",
"examples": [ "examples": [
"Single = Chair.fbx", "Single = Chair.fbx",
"Set = Plant02.fbx + Plant02_col + Plant02_SSS" "Set = Plant02.fbx + Plant02_col + Plant02_SSS"
] ]
}, },
"Decal": { "Decal": {
"description": "A alphamasked textureset",
"color": "#68ac68", "color": "#68ac68",
"description": "A alphamasked textureset",
"examples": [ "examples": [
"Set = DecalGraffiti01_Col + DecalGraffiti01_Alpha", "Set = DecalGraffiti01_Col + DecalGraffiti01_Alpha",
"Single = DecalLeakStain03" "Single = DecalLeakStain03"
] ]
}, },
"Atlas": { "Atlas": {
"description": "A texture, name usually hints that it's an atlas",
"color": "#955b8b", "color": "#955b8b",
"description": "A texture, name usually hints that it's an atlas",
"examples": [ "examples": [
"Set = FoliageAtlas01_col + FoliageAtlas01_nrm" "Set = FoliageAtlas01_col + FoliageAtlas01_nrm"
] ]
}, },
"UtilityMap": { "UtilityMap": {
"description": "A useful image-asset consisting of only a single texture. Therefor each Utilitymap can only contain a single item.",
"color": "#706b87", "color": "#706b87",
"description": "A useful image-asset consisting of only a single texture. Therefor each Utilitymap can only contain a single item.",
"examples": [ "examples": [
"Single = imperfection.png", "Single = imperfection.png",
"Single = smudges.png", "Single = smudges.png",

View File

@ -1,149 +1,155 @@
{ {
"FILE_TYPE_DEFINITIONS": { "FILE_TYPE_DEFINITIONS": {
"MAP_COL": { "MAP_COL": {
"description": "Color/Albedo Map", "bit_depth_rule": "force_8bit",
"color": "#ffaa00", "color": "#ffaa00",
"description": "Color/Albedo Map",
"examples": [ "examples": [
"_col.", "_col.",
"_basecolor.", "_basecolor.",
"albedo", "albedo",
"diffuse" "diffuse"
], ],
"standard_type": "COL",
"bit_depth_rule": "force_8bit",
"is_grayscale": false, "is_grayscale": false,
"keybind": "C" "keybind": "C",
"standard_type": "COL"
}, },
"MAP_NRM": { "MAP_NRM": {
"description": "Normal Map", "bit_depth_rule": "respect",
"color": "#cca2f1", "color": "#cca2f1",
"description": "Normal Map",
"examples": [ "examples": [
"_nrm.", "_nrm.",
"_normal." "_normal."
], ],
"standard_type": "NRM",
"bit_depth_rule": "respect",
"is_grayscale": false, "is_grayscale": false,
"keybind": "N" "keybind": "N",
"standard_type": "NRM"
}, },
"MAP_METAL": { "MAP_METAL": {
"description": "Metalness Map", "bit_depth_rule": "force_8bit",
"color": "#dcf4f2", "color": "#dcf4f2",
"description": "Metalness Map",
"examples": [ "examples": [
"_metal.", "_metal.",
"_met." "_met."
], ],
"standard_type": "METAL",
"bit_depth_rule": "force_8bit",
"is_grayscale": true, "is_grayscale": true,
"keybind": "M" "keybind": "M",
"standard_type": "METAL"
}, },
"MAP_ROUGH": { "MAP_ROUGH": {
"description": "Roughness Map", "bit_depth_rule": "force_8bit",
"color": "#bfd6bf", "color": "#bfd6bf",
"description": "Roughness Map",
"examples": [ "examples": [
"_rough.", "_rough.",
"_rgh.", "_rgh.",
"_gloss" "_gloss"
], ],
"standard_type": "ROUGH",
"bit_depth_rule": "force_8bit",
"is_grayscale": true, "is_grayscale": true,
"keybind": "R" "keybind": "R",
"standard_type": "ROUGH"
}, },
"MAP_GLOSS": { "MAP_GLOSS": {
"description": "Glossiness Map", "bit_depth_rule": "force_8bit",
"color": "#d6bfd6", "color": "#d6bfd6",
"description": "Glossiness Map",
"examples": [ "examples": [
"_gloss.", "_gloss.",
"_gls." "_gls."
], ],
"standard_type": "GLOSS",
"bit_depth_rule": "force_8bit",
"is_grayscale": true, "is_grayscale": true,
"keybind": "R" "keybind": "R",
"standard_type": "GLOSS"
}, },
"MAP_AO": { "MAP_AO": {
"description": "Ambient Occlusion Map", "bit_depth_rule": "force_8bit",
"color": "#e3c7c7", "color": "#e3c7c7",
"description": "Ambient Occlusion Map",
"examples": [ "examples": [
"_ao.", "_ao.",
"_ambientocclusion." "_ambientocclusion."
], ],
"standard_type": "AO", "is_grayscale": true,
"bit_depth_rule": "force_8bit", "keybind": "",
"is_grayscale": true "standard_type": "AO"
}, },
"MAP_DISP": { "MAP_DISP": {
"description": "Displacement/Height Map", "bit_depth_rule": "respect",
"color": "#c6ddd5", "color": "#c6ddd5",
"description": "Displacement/Height Map",
"examples": [ "examples": [
"_disp.", "_disp.",
"_height." "_height."
], ],
"standard_type": "DISP",
"bit_depth_rule": "respect",
"is_grayscale": true, "is_grayscale": true,
"keybind": "D" "keybind": "D",
"standard_type": "DISP"
}, },
"MAP_REFL": { "MAP_REFL": {
"description": "Reflection/Specular Map", "bit_depth_rule": "force_8bit",
"color": "#c2c2b9", "color": "#c2c2b9",
"description": "Reflection/Specular Map",
"examples": [ "examples": [
"_refl.", "_refl.",
"_specular." "_specular."
], ],
"standard_type": "REFL",
"bit_depth_rule": "force_8bit",
"is_grayscale": true, "is_grayscale": true,
"keybind": "M" "keybind": "M",
"standard_type": "REFL"
}, },
"MAP_SSS": { "MAP_SSS": {
"description": "Subsurface Scattering Map", "bit_depth_rule": "respect",
"color": "#a0d394", "color": "#a0d394",
"description": "Subsurface Scattering Map",
"examples": [ "examples": [
"_sss.", "_sss.",
"_subsurface." "_subsurface."
], ],
"standard_type": "SSS", "is_grayscale": true,
"bit_depth_rule": "respect", "keybind": "",
"is_grayscale": true "standard_type": "SSS"
}, },
"MAP_FUZZ": { "MAP_FUZZ": {
"description": "Fuzz/Sheen Map", "bit_depth_rule": "force_8bit",
"color": "#a2d1da", "color": "#a2d1da",
"description": "Fuzz/Sheen Map",
"examples": [ "examples": [
"_fuzz.", "_fuzz.",
"_sheen." "_sheen."
], ],
"standard_type": "FUZZ", "is_grayscale": true,
"bit_depth_rule": "force_8bit", "keybind": "",
"is_grayscale": true "standard_type": "FUZZ"
}, },
"MAP_IDMAP": { "MAP_IDMAP": {
"description": "ID Map (for masking)", "bit_depth_rule": "force_8bit",
"color": "#ca8fb4", "color": "#ca8fb4",
"description": "ID Map (for masking)",
"examples": [ "examples": [
"_id.", "_id.",
"_matid." "_matid."
], ],
"standard_type": "IDMAP", "is_grayscale": false,
"bit_depth_rule": "force_8bit", "keybind": "",
"is_grayscale": false "standard_type": "IDMAP"
}, },
"MAP_MASK": { "MAP_MASK": {
"description": "Generic Mask Map", "bit_depth_rule": "force_8bit",
"color": "#c6e2bf", "color": "#c6e2bf",
"description": "Generic Mask Map",
"examples": [ "examples": [
"_mask." "_mask."
], ],
"standard_type": "MASK", "is_grayscale": true,
"bit_depth_rule": "force_8bit", "keybind": "",
"is_grayscale": true "standard_type": "MASK"
}, },
"MAP_IMPERFECTION": { "MAP_IMPERFECTION": {
"description": "Imperfection Map (scratches, dust)", "bit_depth_rule": "force_8bit",
"color": "#e6d1a6", "color": "#e6d1a6",
"description": "Imperfection Map (scratches, dust)",
"examples": [ "examples": [
"_imp.", "_imp.",
"_imperfection.", "_imperfection.",
@ -153,24 +159,26 @@
"hairs", "hairs",
"fingerprints" "fingerprints"
], ],
"standard_type": "IMPERFECTION", "is_grayscale": true,
"bit_depth_rule": "force_8bit", "keybind": "",
"is_grayscale": true "standard_type": "IMPERFECTION"
}, },
"MODEL": { "MODEL": {
"description": "3D Model File", "bit_depth_rule": "",
"color": "#3db2bd", "color": "#3db2bd",
"description": "3D Model File",
"examples": [ "examples": [
".fbx", ".fbx",
".obj" ".obj"
], ],
"standard_type": "", "is_grayscale": false,
"bit_depth_rule": "", "keybind": "",
"is_grayscale": false "standard_type": ""
}, },
"EXTRA": { "EXTRA": {
"description": "asset previews or metadata", "bit_depth_rule": "",
"color": "#8c8c8c", "color": "#8c8c8c",
"description": "asset previews or metadata",
"examples": [ "examples": [
".txt", ".txt",
".zip", ".zip",
@ -180,22 +188,21 @@
"_Cube.", "_Cube.",
"thumb" "thumb"
], ],
"standard_type": "",
"bit_depth_rule": "",
"is_grayscale": false, "is_grayscale": false,
"keybind": "E" "keybind": "E",
"standard_type": ""
}, },
"FILE_IGNORE": { "FILE_IGNORE": {
"description": "File to be ignored", "bit_depth_rule": "",
"color": "#673d35", "color": "#673d35",
"description": "File to be ignored",
"examples": [ "examples": [
"Thumbs.db", "Thumbs.db",
".DS_Store" ".DS_Store"
], ],
"standard_type": "",
"bit_depth_rule": "",
"is_grayscale": false, "is_grayscale": false,
"keybind": "X" "keybind": "X",
"standard_type": ""
} }
} }
} }

View File

@ -1,12 +1,12 @@
import logging import logging
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QTabWidget, QWidget, QListWidget, QPushButton, QDialog, QVBoxLayout, QTabWidget, QWidget, QListWidget, QListWidgetItem, QPushButton,
QHBoxLayout, QLabel, QGroupBox, QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QGroupBox, QDialogButtonBox, QFormLayout,
QTextEdit, QColorDialog, QInputDialog, QMessageBox, QFrame, QComboBox, QTextEdit, QColorDialog, QInputDialog, QMessageBox, QFrame, QComboBox,
QLineEdit, QCheckBox QLineEdit, QCheckBox, QAbstractItemView
) )
from PySide6.QtGui import QColor, QPalette from PySide6.QtGui import QColor, QPalette, QMouseEvent # Added QMouseEvent
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, QEvent
# Assuming load_asset_definitions, load_file_type_definitions, load_supplier_settings # Assuming load_asset_definitions, load_file_type_definitions, load_supplier_settings
# are in configuration.py at the root level. # are in configuration.py at the root level.
@ -38,6 +38,17 @@ except ImportError as e:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DebugListWidget(QListWidget):
def mousePressEvent(self, event: QMouseEvent): # QMouseEvent needs to be imported from PySide6.QtGui
logger.info(f"DebugListWidget.mousePressEvent: pos={event.pos()}")
item = self.itemAt(event.pos())
if item:
logger.info(f"DebugListWidget.mousePressEvent: Item under cursor: {item.text()}")
else:
logger.info("DebugListWidget.mousePressEvent: No item under cursor.")
super().mousePressEvent(event)
logger.info("DebugListWidget.mousePressEvent: super call finished.")
class DefinitionsEditorDialog(QDialog): class DefinitionsEditorDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -48,6 +59,7 @@ class DefinitionsEditorDialog(QDialog):
self.file_type_data = {} self.file_type_data = {}
self.supplier_data = {} self.supplier_data = {}
self.unsaved_changes = False # For unsaved changes tracking self.unsaved_changes = False # For unsaved changes tracking
self.asset_types_tab_page_for_filtering = None # For event filtering
self._load_all_definitions() self._load_all_definitions()
@ -64,6 +76,8 @@ class DefinitionsEditorDialog(QDialog):
main_layout.addWidget(self.button_box) main_layout.addWidget(self.button_box)
self.setLayout(main_layout) self.setLayout(main_layout)
# self.tab_widget.installEventFilter(self) # Temporarily disable event filter on tab_widget for this test
# logger.info(f"Event filter on self.tab_widget ({self.tab_widget}) TEMPORARILY DISABLED for DebugListWidget test.")
def _load_all_definitions(self): def _load_all_definitions(self):
logger.info("Loading all definitions...") logger.info("Loading all definitions...")
@ -95,8 +109,45 @@ class DefinitionsEditorDialog(QDialog):
self.tab_widget.addTab(self._create_file_types_tab(), "File Type Definitions") self.tab_widget.addTab(self._create_file_types_tab(), "File Type Definitions")
self.tab_widget.addTab(self._create_suppliers_tab(), "Supplier Settings") self.tab_widget.addTab(self._create_suppliers_tab(), "Supplier Settings")
# Add a diagnostic button
self.diag_button = QPushButton("Test Select Item 2 (Asset)")
self.diag_button.clicked.connect(self._run_diag_selection)
# Assuming main_layout is accessible here or passed if _create_ui is part of __init__
# If main_layout is self.layout() established in __init__
if self.layout(): # Check if layout exists
self.layout().addWidget(self.diag_button)
else:
logger.error("Main layout not found for diagnostic button in _create_ui. Button not added.")
def _run_diag_selection(self):
logger.info("Diagnostic button clicked. Attempting to select second item in asset_type_list_widget.")
if hasattr(self, 'asset_type_list_widget') and self.asset_type_list_widget.count() > 1:
logger.info(f"Asset type list widget isEnabled: {self.asset_type_list_widget.isEnabled()}") # Check if enabled
logger.info(f"Asset type list widget signalsBlocked: {self.asset_type_list_widget.signalsBlocked()}")
self.asset_type_list_widget.setFocus() # Explicitly set focus
logger.info(f"Attempted to set focus to asset_type_list_widget. Has focus: {self.asset_type_list_widget.hasFocus()}")
item_to_select = self.asset_type_list_widget.item(1) # Select the second item (index 1)
if item_to_select:
logger.info(f"Programmatically selecting: {item_to_select.text()}")
self.asset_type_list_widget.setCurrentItem(item_to_select)
# Check if it's actually selected
if self.asset_type_list_widget.currentItem() == item_to_select:
logger.info(f"Programmatic selection successful. Current item is now: {self.asset_type_list_widget.currentItem().text()}")
else:
logger.warning("Programmatic selection FAILED. Current item did not change as expected.")
else:
logger.warning("Second item not found in asset_type_list_widget.")
elif hasattr(self, 'asset_type_list_widget'):
logger.warning("asset_type_list_widget has less than 2 items for diagnostic selection.")
else:
logger.warning("asset_type_list_widget not found for diagnostic selection.")
def _create_tab_pane(self, title_singular, data_dict, list_widget_name): def _create_tab_pane(self, title_singular, data_dict, list_widget_name):
tab_page = QWidget() tab_page = QWidget()
tab_page.setFocusPolicy(Qt.FocusPolicy.ClickFocus)
tab_layout = QHBoxLayout(tab_page) tab_layout = QHBoxLayout(tab_page)
# Left Pane # Left Pane
@ -105,12 +156,32 @@ class DefinitionsEditorDialog(QDialog):
lbl_list_title = QLabel(f"{title_singular}s:") lbl_list_title = QLabel(f"{title_singular}s:")
left_pane_layout.addWidget(lbl_list_title) left_pane_layout.addWidget(lbl_list_title)
list_widget = QListWidget() if list_widget_name == "asset_type_list_widget":
logger.info(f"Creating DebugListWidget for {list_widget_name}")
list_widget = DebugListWidget(self) # Pass parent
else:
list_widget = QListWidget(self) # Pass parent
from PySide6.QtWidgets import QAbstractItemView
list_widget.setSelectionMode(QAbstractItemView.SingleSelection)
list_widget.setEnabled(True)
logger.info(f"For {list_widget_name}, SelectionMode set to SingleSelection, Enabled set to True.")
setattr(self, list_widget_name, list_widget) # e.g., self.asset_type_list_widget = list_widget setattr(self, list_widget_name, list_widget) # e.g., self.asset_type_list_widget = list_widget
logger.info(f"Creating tab pane for {title_singular}, list_widget_name: {list_widget_name}")
logger.info(f"List widget instance for {list_widget_name}: {list_widget}")
# Ensure no other event filters are active on the list_widget for this specific test
if list_widget_name == "asset_type_list_widget":
# If an event filter was installed on list_widget by a previous debug step via self.installEventFilter(list_widget),
# it would need to be removed here, or the logic installing it should be conditional.
# For now, we assume no other filter is on list_widget itself.
logger.info(f"Ensuring no stray event filter on DebugListWidget instance for {list_widget_name}.")
if isinstance(data_dict, dict): if isinstance(data_dict, dict):
for key, value_dict in data_dict.items(): # Iterate over items for UserRole data for key, value_dict in data_dict.items(): # Iterate over items for UserRole data
item = QListWidgetItem(key) item = QListWidgetItem(key)
item.setData(Qt.UserRole, value_dict) # Store the whole dict item.setData(Qt.UserRole, value_dict) # Store the whole dict
item.setFlags(item.flags() | Qt.ItemIsSelectable | Qt.ItemIsEnabled) # Explicitly set flags
list_widget.addItem(item) list_widget.addItem(item)
else: else:
logger.warning(f"Data for {title_singular} is not a dictionary, cannot populate list.") logger.warning(f"Data for {title_singular} is not a dictionary, cannot populate list.")
@ -125,15 +196,37 @@ class DefinitionsEditorDialog(QDialog):
if list_widget_name == "asset_type_list_widget": if list_widget_name == "asset_type_list_widget":
btn_add.clicked.connect(self._add_asset_type) btn_add.clicked.connect(self._add_asset_type)
btn_remove.clicked.connect(self._remove_asset_type) btn_remove.clicked.connect(self._remove_asset_type)
# The event filter on asset_type_list_widget should be disabled for this test.
# Assuming the Debug mode task that set it up can be told to disable/remove it,
# or we ensure it's not re-added here if it was part of this method.
# For now, we just connect currentItemChanged directly.
list_widget.currentItemChanged.connect(
lambda current, previous, name=list_widget_name:
logger.info(f"LAMBDA: currentItemChanged for {name}. Current: {current.text() if current else 'None'}")
)
list_widget.currentItemChanged.connect(self._display_asset_type_details) list_widget.currentItemChanged.connect(self._display_asset_type_details)
logger.info(f"Connected currentItemChanged for {list_widget_name} to _display_asset_type_details AND diagnostic lambda.")
elif list_widget_name == "file_type_list_widget": elif list_widget_name == "file_type_list_widget":
# For other list widgets, keep the previous event filter setup if it was specific,
# or remove if it was generic and now we only want DebugListWidget for assets.
# For this step, we are only changing asset_type_list_widget.
btn_add.clicked.connect(self._add_file_type) btn_add.clicked.connect(self._add_file_type)
btn_remove.clicked.connect(self._remove_file_type) btn_remove.clicked.connect(self._remove_file_type)
list_widget.currentItemChanged.connect(
lambda current, previous, name=list_widget_name:
logger.info(f"LAMBDA: currentItemChanged for {name}. Current: {current.text() if current else 'None'}")
)
list_widget.currentItemChanged.connect(self._display_file_type_details) list_widget.currentItemChanged.connect(self._display_file_type_details)
logger.info(f"Connected currentItemChanged for {list_widget_name} to _display_file_type_details AND diagnostic lambda.")
elif list_widget_name == "supplier_list_widget": # Connections for Supplier tab elif list_widget_name == "supplier_list_widget": # Connections for Supplier tab
btn_add.clicked.connect(self._add_supplier) btn_add.clicked.connect(self._add_supplier)
btn_remove.clicked.connect(self._remove_supplier) btn_remove.clicked.connect(self._remove_supplier)
list_widget.currentItemChanged.connect(
lambda current, previous, name=list_widget_name:
logger.info(f"LAMBDA: currentItemChanged for {name}. Current: {current.text() if current else 'None'}")
)
list_widget.currentItemChanged.connect(self._display_supplier_details) list_widget.currentItemChanged.connect(self._display_supplier_details)
logger.info(f"Connected currentItemChanged for {list_widget_name} to _display_supplier_details AND diagnostic lambda.")
buttons_layout.addWidget(btn_add) buttons_layout.addWidget(btn_add)
buttons_layout.addWidget(btn_remove) buttons_layout.addWidget(btn_remove)
@ -145,11 +238,16 @@ class DefinitionsEditorDialog(QDialog):
right_pane_widget = QWidget() # Create a generic widget to be returned right_pane_widget = QWidget() # Create a generic widget to be returned
tab_layout.addWidget(right_pane_widget, 2) # 2 parts for right pane tab_layout.addWidget(right_pane_widget, 2) # 2 parts for right pane
tab_page.setEnabled(True) # Explicitly enable the tab page widget
logger.info(f"Tab page for {title_singular} explicitly enabled.")
tab_page.setLayout(tab_layout) tab_page.setLayout(tab_layout)
return tab_page, right_pane_widget # Return the pane for customization return tab_page, right_pane_widget # Return the pane for customization
def _create_asset_types_tab(self): def _create_asset_types_tab(self):
tab_page, right_pane_container = self._create_tab_pane("Asset Type", self.asset_type_data, "asset_type_list_widget") tab_page, right_pane_container = self._create_tab_pane("Asset Type", self.asset_type_data, "asset_type_list_widget")
self.asset_types_tab_page_for_filtering = tab_page # Store reference for event filter
# Ensure event filter on tab_page is also disabled if it was installed
# logger.info(f"Event filter on asset_types_tab_page ({tab_page}) should be disabled for DebugListWidget test.")
# Customize the right pane for Asset Types # Customize the right pane for Asset Types
right_pane_groupbox = QGroupBox("Details for Selected Asset Type") right_pane_groupbox = QGroupBox("Details for Selected Asset Type")
@ -229,41 +327,61 @@ class DefinitionsEditorDialog(QDialog):
self.asset_type_list_widget.addItem(item) self.asset_type_list_widget.addItem(item)
def _display_asset_type_details(self, current_item, previous_item=None): def _display_asset_type_details(self, current_item, previous_item=None):
# Disconnect signals temporarily to prevent feedback loops during population logger.info(f"_display_asset_type_details called. Current: {current_item.text() if current_item else 'None'}, Previous: {previous_item.text() if previous_item else 'None'}")
if hasattr(self, 'asset_description_edit'):
try:
self.asset_description_edit.textChanged.disconnect(self._on_asset_detail_changed)
except TypeError: # Signal not connected
pass
if current_item: if current_item:
asset_data = current_item.data(Qt.UserRole) logger.info(f"Current item text: {current_item.text()}")
if not isinstance(asset_data, dict): # Should not happen if _populate is correct logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}")
logger.error(f"Invalid data for item {current_item.text()}. Expected dict, got {type(asset_data)}")
asset_data = {"description": "Error: Invalid data", "color": "#ff0000", "examples": []}
self.asset_description_edit.setText(asset_data.get('description', ''))
color_hex = asset_data.get('color', '#ffffff')
self._update_color_swatch(color_hex)
self.asset_examples_list_widget.clear()
for example in asset_data.get('examples', []):
self.asset_examples_list_widget.addItem(example)
else: else:
# Clear details if no item is selected logger.info("Current item is None for asset_type_details.")
self.asset_description_edit.clear()
self._update_color_swatch("#ffffff")
self.asset_examples_list_widget.clear()
# Reconnect signals try:
if hasattr(self, 'asset_description_edit'): # Disconnect signals temporarily to prevent feedback loops during population
self.asset_description_edit.textChanged.connect(self._on_asset_detail_changed) if hasattr(self, 'asset_description_edit'):
try:
self.asset_description_edit.textChanged.disconnect(self._on_asset_detail_changed)
logger.debug("Disconnected asset_description_edit.textChanged")
except TypeError: # Signal not connected
logger.debug("asset_description_edit.textChanged was not connected or already disconnected.")
pass
if current_item:
asset_data = current_item.data(Qt.UserRole)
if not isinstance(asset_data, dict): # Should not happen if _populate is correct
logger.error(f"Invalid data for item {current_item.text()}. Expected dict, got {type(asset_data)}")
asset_data = {"description": "Error: Invalid data", "color": "#ff0000", "examples": []}
self.asset_description_edit.setText(asset_data.get('description', ''))
color_hex = asset_data.get('color', '#ffffff')
self._update_color_swatch(color_hex)
self.asset_examples_list_widget.clear()
for example in asset_data.get('examples', []):
self.asset_examples_list_widget.addItem(example)
logger.debug(f"Populated details for {current_item.text()}")
else:
# Clear details if no item is selected
self.asset_description_edit.clear()
self._update_color_swatch("#ffffff")
self.asset_examples_list_widget.clear()
logger.debug("Cleared asset type details as no item is selected.")
except Exception as e:
logger.error(f"Error in _display_asset_type_details: {e}", exc_info=True)
finally:
# Reconnect signals
if hasattr(self, 'asset_description_edit'):
try:
self.asset_description_edit.textChanged.connect(self._on_asset_detail_changed)
logger.debug("Reconnected asset_description_edit.textChanged")
except Exception as e:
logger.error(f"Failed to reconnect asset_description_edit.textChanged: {e}", exc_info=True)
logger.info("_display_asset_type_details finished.")
def _update_color_swatch(self, color_hex): def _update_color_swatch(self, color_hex):
if hasattr(self, 'asset_color_swatch_label'): if hasattr(self, 'asset_color_swatch_label'):
palette = self.asset_color_swatch_label.palette() palette = self.asset_color_swatch_label.palette()
palette.setColor(QPalette.Background, QColor(color_hex)) palette.setColor(QPalette.Window, QColor(color_hex))
self.asset_color_swatch_label.setPalette(palette) self.asset_color_swatch_label.setPalette(palette)
def _choose_asset_color(self): def _choose_asset_color(self):
@ -420,9 +538,9 @@ class DefinitionsEditorDialog(QDialog):
def _update_file_type_color_swatch(self, color_hex, swatch_label): def _update_file_type_color_swatch(self, color_hex, swatch_label):
if hasattr(self, swatch_label_name): # Check if the specific swatch label exists if hasattr(self, swatch_label): # Check if the specific swatch label exists
palette = swatch_label.palette() palette = swatch_label.palette()
palette.setColor(QPalette.Background, QColor(color_hex)) palette.setColor(QPalette.Window, QColor(color_hex))
swatch_label.setPalette(palette) swatch_label.setPalette(palette)
def _create_file_types_tab(self): def _create_file_types_tab(self):
@ -526,69 +644,88 @@ class DefinitionsEditorDialog(QDialog):
self.file_type_list_widget.addItem(item) self.file_type_list_widget.addItem(item)
def _display_file_type_details(self, current_item, previous_item=None): def _display_file_type_details(self, current_item, previous_item=None):
# Disconnect signals temporarily logger.info(f"_display_file_type_details called. Current: {current_item.text() if current_item else 'None'}, Previous: {previous_item.text() if previous_item else 'None'}")
try: self.ft_description_edit.textChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
try: self.ft_standard_type_edit.textChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
try: self.ft_bit_depth_combo.currentIndexChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
try: self.ft_is_grayscale_check.stateChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
try: self.ft_keybind_edit.textChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
# Color and examples are handled by their own buttons/actions, not direct textChanged etc.
if current_item: if current_item:
ft_data = current_item.data(Qt.UserRole) logger.info(f"Current item text: {current_item.text()}")
if not isinstance(ft_data, dict): logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}")
logger.error(f"Invalid data for file type item {current_item.text()}. Expected dict, got {type(ft_data)}")
# Use placeholder data to avoid crashing UI
ft_data = {
"description": "Error: Invalid data", "color": "#ff0000", "examples": [],
"standard_type": "error", "bit_depth_rule": "respect",
"is_grayscale": False, "keybind": "X"
}
self.ft_description_edit.setText(ft_data.get('description', ''))
self._update_color_swatch_generic(self.ft_color_swatch_label, ft_data.get('color', '#ffffff'))
self.ft_examples_list_widget.clear()
for example in ft_data.get('examples', []):
self.ft_examples_list_widget.addItem(example)
self.ft_standard_type_edit.setText(ft_data.get('standard_type', ''))
bdr_index = self.ft_bit_depth_combo.findText(ft_data.get('bit_depth_rule', 'respect'))
if bdr_index != -1:
self.ft_bit_depth_combo.setCurrentIndex(bdr_index)
else:
self.ft_bit_depth_combo.setCurrentIndex(0) # Default to 'respect'
self.ft_is_grayscale_check.setChecked(ft_data.get('is_grayscale', False))
self.ft_keybind_edit.setText(ft_data.get('keybind', ''))
else: else:
# Clear details if no item is selected logger.info("Current item is None for file_type_details.")
self.ft_description_edit.clear()
self._update_color_swatch_generic(self.ft_color_swatch_label, "#ffffff")
self.ft_examples_list_widget.clear()
self.ft_standard_type_edit.clear()
self.ft_bit_depth_combo.setCurrentIndex(0)
self.ft_is_grayscale_check.setChecked(False)
self.ft_keybind_edit.clear()
# Reconnect signals try:
self.ft_description_edit.textChanged.connect(self._on_file_type_detail_changed) # Disconnect signals temporarily
self.ft_standard_type_edit.textChanged.connect(self._on_file_type_detail_changed) logger.debug("Disconnecting file type detail signals...")
self.ft_bit_depth_combo.currentIndexChanged.connect(self._on_file_type_detail_changed) try: self.ft_description_edit.textChanged.disconnect(self._on_file_type_detail_changed)
self.ft_is_grayscale_check.stateChanged.connect(self._on_file_type_detail_changed) except TypeError: pass
self.ft_keybind_edit.textChanged.connect(self._on_file_type_detail_changed) try: self.ft_standard_type_edit.textChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
try: self.ft_bit_depth_combo.currentIndexChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
try: self.ft_is_grayscale_check.stateChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
try: self.ft_keybind_edit.textChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
logger.debug("Finished disconnecting file type detail signals.")
if current_item:
ft_data = current_item.data(Qt.UserRole)
if not isinstance(ft_data, dict):
logger.error(f"Invalid data for file type item {current_item.text()}. Expected dict, got {type(ft_data)}")
ft_data = {
"description": "Error: Invalid data", "color": "#ff0000", "examples": [],
"standard_type": "error", "bit_depth_rule": "respect",
"is_grayscale": False, "keybind": "X"
}
self.ft_description_edit.setText(ft_data.get('description', ''))
self._update_color_swatch_generic(self.ft_color_swatch_label, ft_data.get('color', '#ffffff'))
self.ft_examples_list_widget.clear()
for example in ft_data.get('examples', []):
self.ft_examples_list_widget.addItem(example)
self.ft_standard_type_edit.setText(ft_data.get('standard_type', ''))
bdr_index = self.ft_bit_depth_combo.findText(ft_data.get('bit_depth_rule', 'respect'))
if bdr_index != -1:
self.ft_bit_depth_combo.setCurrentIndex(bdr_index)
else:
self.ft_bit_depth_combo.setCurrentIndex(0) # Default to 'respect'
self.ft_is_grayscale_check.setChecked(ft_data.get('is_grayscale', False))
self.ft_keybind_edit.setText(ft_data.get('keybind', ''))
logger.debug(f"Populated details for file type {current_item.text()}")
else:
# Clear details if no item is selected
self.ft_description_edit.clear()
self._update_color_swatch_generic(self.ft_color_swatch_label, "#ffffff")
self.ft_examples_list_widget.clear()
self.ft_standard_type_edit.clear()
self.ft_bit_depth_combo.setCurrentIndex(0)
self.ft_is_grayscale_check.setChecked(False)
self.ft_keybind_edit.clear()
logger.debug("Cleared file type details as no item is selected.")
except Exception as e:
logger.error(f"Error in _display_file_type_details: {e}", exc_info=True)
finally:
# Reconnect signals
logger.debug("Reconnecting file type detail signals...")
try:
self.ft_description_edit.textChanged.connect(self._on_file_type_detail_changed)
self.ft_standard_type_edit.textChanged.connect(self._on_file_type_detail_changed)
self.ft_bit_depth_combo.currentIndexChanged.connect(self._on_file_type_detail_changed)
self.ft_is_grayscale_check.stateChanged.connect(self._on_file_type_detail_changed)
self.ft_keybind_edit.textChanged.connect(self._on_file_type_detail_changed)
logger.debug("Finished reconnecting file type detail signals.")
except Exception as e:
logger.error(f"Failed to reconnect file type detail signals: {e}", exc_info=True)
logger.info("_display_file_type_details finished.")
def _update_color_swatch_generic(self, swatch_label, color_hex): def _update_color_swatch_generic(self, swatch_label, color_hex):
"""Generic color swatch update for any QLabel.""" """Generic color swatch update for any QLabel."""
if swatch_label: # Check if the swatch label exists and is passed correctly if swatch_label: # Check if the swatch label exists and is passed correctly
palette = swatch_label.palette() palette = swatch_label.palette()
palette.setColor(QPalette.Background, QColor(color_hex)) palette.setColor(QPalette.Window, QColor(color_hex))
swatch_label.setPalette(palette) swatch_label.setPalette(palette)
swatch_label.update() # Ensure the label repaints swatch_label.update() # Ensure the label repaints
@ -814,41 +951,59 @@ class DefinitionsEditorDialog(QDialog):
self.supplier_list_widget.addItem(item) self.supplier_list_widget.addItem(item)
def _display_supplier_details(self, current_item, previous_item=None): def _display_supplier_details(self, current_item, previous_item=None):
# Disconnect signals temporarily logger.info(f"_display_supplier_details called. Current: {current_item.text() if current_item else 'None'}, Previous: {previous_item.text() if previous_item else 'None'}")
if hasattr(self, 'supplier_normal_map_type_combo'):
try: self.supplier_normal_map_type_combo.currentIndexChanged.disconnect(self._on_supplier_detail_changed)
except TypeError: pass
if current_item: if current_item:
supplier_name = current_item.text() logger.info(f"Current item text: {current_item.text()}")
# Prefer getting data directly from self.supplier_data to ensure it's the master copy logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}")
# The UserRole data on the item should be a reflection or copy.
supplier_data = self.supplier_data.get(supplier_name)
if not isinstance(supplier_data, dict):
logger.error(f"Invalid data for supplier item {supplier_name}. Expected dict, got {type(supplier_data)}")
# Fallback if data is somehow corrupted or missing from self.supplier_data
# This might happen if an item is in the list but not in self.supplier_data
item_data_role = current_item.data(Qt.UserRole)
if isinstance(item_data_role, dict):
supplier_data = item_data_role
else:
supplier_data = {"normal_map_type": "OpenGL"} # Absolute fallback
normal_map_type = supplier_data.get('normal_map_type', 'OpenGL')
nmt_index = self.supplier_normal_map_type_combo.findText(normal_map_type)
if nmt_index != -1:
self.supplier_normal_map_type_combo.setCurrentIndex(nmt_index)
else:
self.supplier_normal_map_type_combo.setCurrentIndex(0) # Default to OpenGL
else: else:
# Clear details if no item is selected logger.info("Current item is None for supplier_details.")
if hasattr(self, 'supplier_normal_map_type_combo'):
self.supplier_normal_map_type_combo.setCurrentIndex(0) # Default to OpenGL
# Reconnect signals try:
if hasattr(self, 'supplier_normal_map_type_combo'): # Disconnect signals temporarily
self.supplier_normal_map_type_combo.currentIndexChanged.connect(self._on_supplier_detail_changed) if hasattr(self, 'supplier_normal_map_type_combo'):
try:
self.supplier_normal_map_type_combo.currentIndexChanged.disconnect(self._on_supplier_detail_changed)
logger.debug("Disconnected supplier_normal_map_type_combo.currentIndexChanged")
except TypeError:
logger.debug("supplier_normal_map_type_combo.currentIndexChanged was not connected or already disconnected.")
pass
if current_item:
supplier_name = current_item.text()
supplier_data = self.supplier_data.get(supplier_name)
if not isinstance(supplier_data, dict):
logger.error(f"Invalid data for supplier item {supplier_name}. Expected dict, got {type(supplier_data)}")
item_data_role = current_item.data(Qt.UserRole)
if isinstance(item_data_role, dict):
supplier_data = item_data_role
else:
supplier_data = {"normal_map_type": "OpenGL"}
normal_map_type = supplier_data.get('normal_map_type', 'OpenGL')
nmt_index = self.supplier_normal_map_type_combo.findText(normal_map_type)
if nmt_index != -1:
self.supplier_normal_map_type_combo.setCurrentIndex(nmt_index)
else:
self.supplier_normal_map_type_combo.setCurrentIndex(0)
logger.debug(f"Populated details for supplier {current_item.text()}")
else:
# Clear details if no item is selected
if hasattr(self, 'supplier_normal_map_type_combo'):
self.supplier_normal_map_type_combo.setCurrentIndex(0)
logger.debug("Cleared supplier details as no item is selected.")
except Exception as e:
logger.error(f"Error in _display_supplier_details: {e}", exc_info=True)
finally:
# Reconnect signals
if hasattr(self, 'supplier_normal_map_type_combo'):
try:
self.supplier_normal_map_type_combo.currentIndexChanged.connect(self._on_supplier_detail_changed)
logger.debug("Reconnected supplier_normal_map_type_combo.currentIndexChanged")
except Exception as e:
logger.error(f"Failed to reconnect supplier_normal_map_type_combo.currentIndexChanged: {e}", exc_info=True)
logger.info("_display_supplier_details finished.")
def _on_supplier_detail_changed(self): def _on_supplier_detail_changed(self):
current_item = self.supplier_list_widget.currentItem() current_item = self.supplier_list_widget.currentItem()
@ -1036,6 +1191,71 @@ class DefinitionsEditorDialog(QDialog):
else: else:
event.accept() event.accept()
def eventFilter(self, watched, event: QEvent): # Renamed from mouse_event_filter
event_type = event.type()
if watched == self.tab_widget:
# Construct a more identifiable name for the tab widget in logs
tab_widget_name_for_log = self.tab_widget.objectName() if self.tab_widget.objectName() else watched.__class__.__name__
prefix = f"EventFilter (QTabWidget '{tab_widget_name_for_log}'):"
if event_type == QEvent.MouseButtonPress or event_type == QEvent.MouseButtonRelease:
event_name = "Press" if event_type == QEvent.MouseButtonPress else "Release"
# Ensure event has position method (it's a QMouseEvent)
if hasattr(event, 'position') and hasattr(event, 'globalPosition') and hasattr(event, 'button'):
log_line = (f"{prefix} MouseButton{event_name} "
f"global_pos={event.globalPosition().toPoint()}, "
f"widget_pos={event.position().toPoint()}, "
f"button={event.button()}, accepted={event.isAccepted()}")
logger.info(log_line)
current_page = self.tab_widget.currentWidget()
if current_page:
# event.position() is relative to self.tab_widget (the watched object)
tab_widget_event_pos_float = event.position() # QPointF
tab_widget_event_pos = tab_widget_event_pos_float.toPoint() # QPoint
# Map event position from tab_widget coordinates to global, then to page coordinates
global_pos = self.tab_widget.mapToGlobal(tab_widget_event_pos)
page_event_pos = current_page.mapFromGlobal(global_pos)
is_over_page = current_page.rect().contains(page_event_pos)
page_name_for_log = current_page.objectName() if current_page.objectName() else current_page.__class__.__name__
logger.info(f"{prefix} Event mapped to page '{page_name_for_log}' coords: {page_event_pos}. "
f"Page rect: {current_page.rect()}. Is over page: {is_over_page}")
if is_over_page:
logger.info(f"{prefix} Event IS OVER CURRENT PAGE. "
f"Current event.isAccepted(): {event.isAccepted()}. "
f"Returning False from filter to allow propagation to QTabWidget's default handling.")
# Returning False means this filter does not stop the event.
# The event will be sent to self.tab_widget.event() for its default handling,
# which should then propagate to children if appropriate.
return False
else:
logger.info(f"{prefix} Event is NOT over current page (likely on tab bar). Allowing default QTabWidget handling.")
else:
logger.info(f"{prefix} No current page for tab_widget during mouse event.")
else:
logger.warning(f"{prefix} MouseButton{event_name} received, but event object lacks expected QMouseEvent attributes.")
# Example: Log other event types if needed for debugging, but keep it concise
# elif event_type == QEvent.Enter:
# logger.debug(f"{prefix} Enter event")
# elif event_type == QEvent.Leave:
# logger.debug(f"{prefix} Leave event")
# elif event_type == QEvent.FocusIn:
# logger.debug(f"{prefix} FocusIn event")
# elif event_type == QEvent.FocusOut:
# logger.debug(f"{prefix} FocusOut event")
# For other watched objects (if any were installed on), or for events on self.tab_widget
# that were not explicitly handled (e.g., not mouse press/release over page),
# call the base class implementation.
return super().eventFilter(watched, event)
if __name__ == '__main__': if __name__ == '__main__':
# This is for testing the dialog independently # This is for testing the dialog independently
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication