diff --git a/ProjectNotes/issue_definitions_editor_list_selection.md b/ProjectNotes/issue_definitions_editor_list_selection.md new file mode 100644 index 0000000..73082d4 --- /dev/null +++ b/ProjectNotes/issue_definitions_editor_list_selection.md @@ -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. \ No newline at end of file diff --git a/config/asset_type_definitions.json b/config/asset_type_definitions.json index 59fbdce..55691ad 100644 --- a/config/asset_type_definitions.json +++ b/config/asset_type_definitions.json @@ -1,39 +1,39 @@ { "ASSET_TYPE_DEFINITIONS": { "Surface": { - "description": "A single Standard PBR material set for a surface.", "color": "#1f3e5d", + "description": "A single Standard PBR material set for a surface.", "examples": [ "Set: Wood01_COL + Wood01_NRM + WOOD01_ROUGH", "Set: Dif_Concrete + Normal_Concrete + Refl_Concrete" ] }, "Model": { - "description": "A set that contains models, can include PBR textureset", "color": "#b67300", + "description": "A set that contains models, can include PBR textureset", "examples": [ "Single = Chair.fbx", "Set = Plant02.fbx + Plant02_col + Plant02_SSS" ] }, "Decal": { - "description": "A alphamasked textureset", "color": "#68ac68", + "description": "A alphamasked textureset", "examples": [ "Set = DecalGraffiti01_Col + DecalGraffiti01_Alpha", "Single = DecalLeakStain03" ] }, "Atlas": { - "description": "A texture, name usually hints that it's an atlas", "color": "#955b8b", + "description": "A texture, name usually hints that it's an atlas", "examples": [ "Set = FoliageAtlas01_col + FoliageAtlas01_nrm" ] }, "UtilityMap": { - "description": "A useful image-asset consisting of only a single texture. Therefor each Utilitymap can only contain a single item.", "color": "#706b87", + "description": "A useful image-asset consisting of only a single texture. Therefor each Utilitymap can only contain a single item.", "examples": [ "Single = imperfection.png", "Single = smudges.png", diff --git a/config/file_type_definitions.json b/config/file_type_definitions.json index 161ee86..018a0d0 100644 --- a/config/file_type_definitions.json +++ b/config/file_type_definitions.json @@ -1,149 +1,155 @@ { "FILE_TYPE_DEFINITIONS": { "MAP_COL": { - "description": "Color/Albedo Map", + "bit_depth_rule": "force_8bit", "color": "#ffaa00", + "description": "Color/Albedo Map", "examples": [ "_col.", "_basecolor.", "albedo", "diffuse" ], - "standard_type": "COL", - "bit_depth_rule": "force_8bit", "is_grayscale": false, - "keybind": "C" + "keybind": "C", + "standard_type": "COL" }, "MAP_NRM": { - "description": "Normal Map", + "bit_depth_rule": "respect", "color": "#cca2f1", + "description": "Normal Map", "examples": [ "_nrm.", "_normal." ], - "standard_type": "NRM", - "bit_depth_rule": "respect", "is_grayscale": false, - "keybind": "N" + "keybind": "N", + "standard_type": "NRM" }, "MAP_METAL": { - "description": "Metalness Map", + "bit_depth_rule": "force_8bit", "color": "#dcf4f2", + "description": "Metalness Map", "examples": [ "_metal.", "_met." ], - "standard_type": "METAL", - "bit_depth_rule": "force_8bit", "is_grayscale": true, - "keybind": "M" + "keybind": "M", + "standard_type": "METAL" }, "MAP_ROUGH": { - "description": "Roughness Map", + "bit_depth_rule": "force_8bit", "color": "#bfd6bf", + "description": "Roughness Map", "examples": [ "_rough.", "_rgh.", "_gloss" ], - "standard_type": "ROUGH", - "bit_depth_rule": "force_8bit", "is_grayscale": true, - "keybind": "R" + "keybind": "R", + "standard_type": "ROUGH" }, "MAP_GLOSS": { - "description": "Glossiness Map", + "bit_depth_rule": "force_8bit", "color": "#d6bfd6", + "description": "Glossiness Map", "examples": [ "_gloss.", "_gls." ], - "standard_type": "GLOSS", - "bit_depth_rule": "force_8bit", "is_grayscale": true, - "keybind": "R" + "keybind": "R", + "standard_type": "GLOSS" }, "MAP_AO": { - "description": "Ambient Occlusion Map", + "bit_depth_rule": "force_8bit", "color": "#e3c7c7", + "description": "Ambient Occlusion Map", "examples": [ "_ao.", "_ambientocclusion." ], - "standard_type": "AO", - "bit_depth_rule": "force_8bit", - "is_grayscale": true + "is_grayscale": true, + "keybind": "", + "standard_type": "AO" }, "MAP_DISP": { - "description": "Displacement/Height Map", + "bit_depth_rule": "respect", "color": "#c6ddd5", + "description": "Displacement/Height Map", "examples": [ "_disp.", "_height." ], - "standard_type": "DISP", - "bit_depth_rule": "respect", "is_grayscale": true, - "keybind": "D" + "keybind": "D", + "standard_type": "DISP" }, "MAP_REFL": { - "description": "Reflection/Specular Map", + "bit_depth_rule": "force_8bit", "color": "#c2c2b9", + "description": "Reflection/Specular Map", "examples": [ "_refl.", "_specular." ], - "standard_type": "REFL", - "bit_depth_rule": "force_8bit", "is_grayscale": true, - "keybind": "M" + "keybind": "M", + "standard_type": "REFL" }, "MAP_SSS": { - "description": "Subsurface Scattering Map", + "bit_depth_rule": "respect", "color": "#a0d394", + "description": "Subsurface Scattering Map", "examples": [ "_sss.", "_subsurface." ], - "standard_type": "SSS", - "bit_depth_rule": "respect", - "is_grayscale": true + "is_grayscale": true, + "keybind": "", + "standard_type": "SSS" }, "MAP_FUZZ": { - "description": "Fuzz/Sheen Map", + "bit_depth_rule": "force_8bit", "color": "#a2d1da", + "description": "Fuzz/Sheen Map", "examples": [ "_fuzz.", "_sheen." ], - "standard_type": "FUZZ", - "bit_depth_rule": "force_8bit", - "is_grayscale": true + "is_grayscale": true, + "keybind": "", + "standard_type": "FUZZ" }, "MAP_IDMAP": { - "description": "ID Map (for masking)", + "bit_depth_rule": "force_8bit", "color": "#ca8fb4", + "description": "ID Map (for masking)", "examples": [ "_id.", "_matid." ], - "standard_type": "IDMAP", - "bit_depth_rule": "force_8bit", - "is_grayscale": false + "is_grayscale": false, + "keybind": "", + "standard_type": "IDMAP" }, "MAP_MASK": { - "description": "Generic Mask Map", + "bit_depth_rule": "force_8bit", "color": "#c6e2bf", + "description": "Generic Mask Map", "examples": [ "_mask." ], - "standard_type": "MASK", - "bit_depth_rule": "force_8bit", - "is_grayscale": true + "is_grayscale": true, + "keybind": "", + "standard_type": "MASK" }, "MAP_IMPERFECTION": { - "description": "Imperfection Map (scratches, dust)", + "bit_depth_rule": "force_8bit", "color": "#e6d1a6", + "description": "Imperfection Map (scratches, dust)", "examples": [ "_imp.", "_imperfection.", @@ -153,24 +159,26 @@ "hairs", "fingerprints" ], - "standard_type": "IMPERFECTION", - "bit_depth_rule": "force_8bit", - "is_grayscale": true + "is_grayscale": true, + "keybind": "", + "standard_type": "IMPERFECTION" }, "MODEL": { - "description": "3D Model File", + "bit_depth_rule": "", "color": "#3db2bd", + "description": "3D Model File", "examples": [ ".fbx", ".obj" ], - "standard_type": "", - "bit_depth_rule": "", - "is_grayscale": false + "is_grayscale": false, + "keybind": "", + "standard_type": "" }, "EXTRA": { - "description": "asset previews or metadata", + "bit_depth_rule": "", "color": "#8c8c8c", + "description": "asset previews or metadata", "examples": [ ".txt", ".zip", @@ -180,22 +188,21 @@ "_Cube.", "thumb" ], - "standard_type": "", - "bit_depth_rule": "", "is_grayscale": false, - "keybind": "E" + "keybind": "E", + "standard_type": "" }, "FILE_IGNORE": { - "description": "File to be ignored", + "bit_depth_rule": "", "color": "#673d35", + "description": "File to be ignored", "examples": [ "Thumbs.db", ".DS_Store" ], - "standard_type": "", - "bit_depth_rule": "", "is_grayscale": false, - "keybind": "X" + "keybind": "X", + "standard_type": "" } } } \ No newline at end of file diff --git a/gui/definitions_editor_dialog.py b/gui/definitions_editor_dialog.py index 56c146d..26a1403 100644 --- a/gui/definitions_editor_dialog.py +++ b/gui/definitions_editor_dialog.py @@ -1,12 +1,12 @@ import logging from PySide6.QtWidgets import ( - QDialog, QVBoxLayout, QTabWidget, QWidget, QListWidget, QPushButton, + QDialog, QVBoxLayout, QTabWidget, QWidget, QListWidget, QListWidgetItem, QPushButton, QHBoxLayout, QLabel, QGroupBox, QDialogButtonBox, QFormLayout, QTextEdit, QColorDialog, QInputDialog, QMessageBox, QFrame, QComboBox, - QLineEdit, QCheckBox + QLineEdit, QCheckBox, QAbstractItemView ) -from PySide6.QtGui import QColor, QPalette -from PySide6.QtCore import Qt +from PySide6.QtGui import QColor, QPalette, QMouseEvent # Added QMouseEvent +from PySide6.QtCore import Qt, QEvent # Assuming load_asset_definitions, load_file_type_definitions, load_supplier_settings # are in configuration.py at the root level. @@ -38,6 +38,17 @@ except ImportError as e: 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): def __init__(self, parent=None): super().__init__(parent) @@ -48,6 +59,7 @@ class DefinitionsEditorDialog(QDialog): self.file_type_data = {} self.supplier_data = {} self.unsaved_changes = False # For unsaved changes tracking + self.asset_types_tab_page_for_filtering = None # For event filtering self._load_all_definitions() @@ -64,6 +76,8 @@ class DefinitionsEditorDialog(QDialog): main_layout.addWidget(self.button_box) 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): 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_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): tab_page = QWidget() + tab_page.setFocusPolicy(Qt.FocusPolicy.ClickFocus) tab_layout = QHBoxLayout(tab_page) # Left Pane @@ -105,12 +156,32 @@ class DefinitionsEditorDialog(QDialog): lbl_list_title = QLabel(f"{title_singular}s:") 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 + 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): for key, value_dict in data_dict.items(): # Iterate over items for UserRole data item = QListWidgetItem(key) 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) else: 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": btn_add.clicked.connect(self._add_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) + 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": + # 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_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) + 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 btn_add.clicked.connect(self._add_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) + 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_remove) @@ -145,12 +238,17 @@ class DefinitionsEditorDialog(QDialog): right_pane_widget = QWidget() # Create a generic widget to be returned 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) return tab_page, right_pane_widget # Return the pane for customization 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") - + 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 right_pane_groupbox = QGroupBox("Details for Selected Asset Type") details_layout = QFormLayout(right_pane_groupbox) @@ -229,41 +327,61 @@ class DefinitionsEditorDialog(QDialog): self.asset_type_list_widget.addItem(item) def _display_asset_type_details(self, current_item, previous_item=None): - # Disconnect signals temporarily to prevent feedback loops during population - if hasattr(self, 'asset_description_edit'): - try: - self.asset_description_edit.textChanged.disconnect(self._on_asset_detail_changed) - except TypeError: # Signal not connected - pass - + 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 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.info(f"Current item text: {current_item.text()}") + logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}") 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.info("Current item is None for asset_type_details.") - # Reconnect signals - if hasattr(self, 'asset_description_edit'): - self.asset_description_edit.textChanged.connect(self._on_asset_detail_changed) + try: + # Disconnect signals temporarily to prevent feedback loops during population + 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): if hasattr(self, 'asset_color_swatch_label'): 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) def _choose_asset_color(self): @@ -420,9 +538,9 @@ class DefinitionsEditorDialog(QDialog): 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.setColor(QPalette.Background, QColor(color_hex)) + palette.setColor(QPalette.Window, QColor(color_hex)) swatch_label.setPalette(palette) def _create_file_types_tab(self): @@ -526,69 +644,88 @@ class DefinitionsEditorDialog(QDialog): self.file_type_list_widget.addItem(item) def _display_file_type_details(self, current_item, previous_item=None): - # Disconnect signals temporarily - 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. - + 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'}") 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)}") - # 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', '')) + logger.info(f"Current item text: {current_item.text()}") + logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}") 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.info("Current item is None for file_type_details.") - # Reconnect signals - 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) + try: + # Disconnect signals temporarily + logger.debug("Disconnecting file type detail signals...") + 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 + 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): """Generic color swatch update for any QLabel.""" if swatch_label: # Check if the swatch label exists and is passed correctly palette = swatch_label.palette() - palette.setColor(QPalette.Background, QColor(color_hex)) + palette.setColor(QPalette.Window, QColor(color_hex)) swatch_label.setPalette(palette) swatch_label.update() # Ensure the label repaints @@ -814,41 +951,59 @@ class DefinitionsEditorDialog(QDialog): self.supplier_list_widget.addItem(item) def _display_supplier_details(self, current_item, previous_item=None): - # Disconnect signals temporarily - 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 - + 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 current_item: - supplier_name = current_item.text() - # Prefer getting data directly from self.supplier_data to ensure it's the master copy - # 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 + logger.info(f"Current item text: {current_item.text()}") + logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}") else: - # Clear details if no item is selected - if hasattr(self, 'supplier_normal_map_type_combo'): - self.supplier_normal_map_type_combo.setCurrentIndex(0) # Default to OpenGL + logger.info("Current item is None for supplier_details.") - # Reconnect signals - if hasattr(self, 'supplier_normal_map_type_combo'): - self.supplier_normal_map_type_combo.currentIndexChanged.connect(self._on_supplier_detail_changed) + try: + # Disconnect signals temporarily + 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): current_item = self.supplier_list_widget.currentItem() @@ -1036,6 +1191,71 @@ class DefinitionsEditorDialog(QDialog): else: 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__': # This is for testing the dialog independently from PyQt5.QtWidgets import QApplication