Asset-Frameworker/gui/definitions_editor_dialog.py

1267 lines
65 KiB
Python

import logging
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QTabWidget, QWidget, QListWidget, QListWidgetItem, QPushButton,
QHBoxLayout, QLabel, QGroupBox, QDialogButtonBox, QFormLayout,
QTextEdit, QColorDialog, QInputDialog, QMessageBox, QFrame, QComboBox,
QLineEdit, QCheckBox, QAbstractItemView
)
from PySide6.QtGui import QColor, QPalette, QMouseEvent # Added QMouseEvent
from PySide6.QtCore import Qt, QEvent
from PySide6.QtGui import QColor, QPalette, QMouseEvent
from PySide6.QtCore import Qt, QEvent
# Import the Configuration class
from configuration import Configuration, ConfigurationError
logger = logging.getLogger(__name__)
class DebugListWidget(QListWidget):
def mousePressEvent(self, event: QMouseEvent):
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, config: Configuration, parent=None):
super().__init__(parent)
self.config = config # Store the Configuration object
self.setWindowTitle("Definitions Editor")
self.setGeometry(200, 200, 800, 600) # x, y, width, height
self.asset_type_data = {}
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()
main_layout = QVBoxLayout(self)
self.tab_widget = QTabWidget()
main_layout.addWidget(self.tab_widget)
self._create_ui() # Creates and adds tabs to self.tab_widget
self.button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
self.button_box.accepted.connect(self.save_definitions)
self.button_box.rejected.connect(self.reject)
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...")
try:
self.asset_type_data = load_asset_definitions()
logger.info(f"Loaded {len(self.asset_type_data)} asset type definitions.")
except Exception as e:
logger.error(f"Failed to load asset type definitions: {e}")
self.asset_type_data = {} # Ensure it's an empty dict on failure
try:
self.file_type_data = load_file_type_definitions()
logger.info(f"Loaded {len(self.file_type_data)} file type definitions.")
except Exception as e:
logger.error(f"Failed to load file type definitions: {e}")
self.file_type_data = {}
try:
self.supplier_data = load_supplier_settings()
logger.info(f"Loaded {len(self.supplier_data)} supplier settings.")
except Exception as e:
logger.error(f"Failed to load supplier settings: {e}")
self.supplier_data = {}
logger.info("Finished loading definitions.")
def _create_ui(self):
self.tab_widget.addTab(self._create_asset_types_tab(), "Asset Type Definitions")
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
left_pane_layout = QVBoxLayout()
lbl_list_title = QLabel(f"{title_singular}s:")
left_pane_layout.addWidget(lbl_list_title)
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.")
left_pane_layout.addWidget(list_widget)
buttons_layout = QHBoxLayout()
btn_add = QPushButton(f"Add {title_singular}")
btn_remove = QPushButton(f"Remove Selected {title_singular}")
# Connections for these buttons will be specific to each tab type
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)
left_pane_layout.addLayout(buttons_layout)
tab_layout.addLayout(left_pane_layout, 1) # 1 part for left pane
# Right Pane - This will be customized by specific tab creation methods
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)
# Description
self.asset_description_edit = QTextEdit()
details_layout.addRow("Description:", self.asset_description_edit)
# Color
color_layout = QHBoxLayout()
self.asset_color_swatch_label = QLabel()
self.asset_color_swatch_label.setFixedSize(20, 20)
self.asset_color_swatch_label.setAutoFillBackground(True)
self._update_color_swatch("#ffffff") # Default color
btn_choose_color = QPushButton("Choose Color...")
btn_choose_color.clicked.connect(self._choose_asset_color)
color_layout.addWidget(self.asset_color_swatch_label)
color_layout.addWidget(btn_choose_color)
color_layout.addStretch()
details_layout.addRow("Color:", color_layout)
# Examples
examples_group = QGroupBox("Examples")
examples_layout = QVBoxLayout(examples_group)
self.asset_examples_list_widget = QListWidget()
examples_layout.addWidget(self.asset_examples_list_widget)
example_buttons_layout = QHBoxLayout()
btn_add_example = QPushButton("Add Example")
btn_remove_example = QPushButton("Remove Selected Example")
btn_add_example.clicked.connect(self._add_asset_example)
btn_remove_example.clicked.connect(self._remove_asset_example)
example_buttons_layout.addWidget(btn_add_example)
example_buttons_layout.addWidget(btn_remove_example)
examples_layout.addLayout(example_buttons_layout)
details_layout.addRow(examples_group)
# Replace the generic right_pane_widget with our specific groupbox
# To do this, we need to find the layout of right_pane_container's parent (which is tab_layout)
# and replace the widget.
parent_layout = right_pane_container.parentWidget().layout()
if parent_layout:
parent_layout.replaceWidget(right_pane_container, right_pane_groupbox)
right_pane_container.deleteLater() # Remove the placeholder
# Connect signals for editing
self.asset_description_edit.textChanged.connect(self._on_asset_detail_changed)
# Initial population of list widget (if not already done by _create_tab_pane)
# and display details for the first item if any.
self._populate_asset_type_list() # Ensure data is loaded with UserRole
if self.asset_type_list_widget.count() > 0:
self.asset_type_list_widget.setCurrentRow(0)
# self._display_asset_type_details(self.asset_type_list_widget.currentItem()) # Already connected
return tab_page
def _populate_asset_type_list(self):
self.asset_type_list_widget.clear()
for key, asset_data_item in self.asset_type_data.items():
item = QListWidgetItem(key)
# Ensure asset_data_item is a dictionary, if not, create a default one
if not isinstance(asset_data_item, dict):
logger.warning(f"Asset data for '{key}' is not a dict: {asset_data_item}. Using default.")
asset_data_item = {"description": str(asset_data_item), "color": "#ffffff", "examples": []}
# Ensure essential keys exist
asset_data_item.setdefault('description', '')
asset_data_item.setdefault('color', '#ffffff')
asset_data_item.setdefault('examples', [])
item.setData(Qt.UserRole, asset_data_item)
self.asset_type_list_widget.addItem(item)
def _display_asset_type_details(self, current_item, previous_item=None):
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:
logger.info(f"Current item text: {current_item.text()}")
logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}")
else:
logger.info("Current item is None for asset_type_details.")
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.Window, QColor(color_hex))
self.asset_color_swatch_label.setPalette(palette)
def _choose_asset_color(self):
current_item = self.asset_type_list_widget.currentItem()
if not current_item:
return
asset_data = current_item.data(Qt.UserRole)
initial_color = QColor(asset_data.get('color', '#ffffff'))
color = QColorDialog.getColor(initial_color, self, "Choose Asset Type Color")
if color.isValid():
color_hex = color.name()
self._update_color_swatch(color_hex)
asset_data['color'] = color_hex
current_item.setData(Qt.UserRole, asset_data) # Update data in item
self.unsaved_changes = True
# No need to call _on_asset_detail_changed explicitly for color, direct update is fine
def _on_asset_detail_changed(self):
current_item = self.asset_type_list_widget.currentItem()
if not current_item:
return
asset_data = current_item.data(Qt.UserRole)
if not isinstance(asset_data, dict): return # Should not happen
# Update description
asset_data['description'] = self.asset_description_edit.toPlainText()
# Examples are handled by their own add/remove buttons
# Color is handled by _choose_asset_color
current_item.setData(Qt.UserRole, asset_data) # Save changes back to the item's data
self.unsaved_changes = True
def _add_asset_type(self):
new_name, ok = QInputDialog.getText(self, "Add Asset Type", "Enter name for the new asset type:")
if ok and new_name:
if new_name in self.asset_type_data:
QMessageBox.warning(self, "Name Exists", f"An asset type named '{new_name}' already exists.")
return
default_asset_type = {
"description": "",
"color": "#ffffff",
"examples": []
}
self.asset_type_data[new_name] = default_asset_type
item = QListWidgetItem(new_name)
item.setData(Qt.UserRole, default_asset_type) # Store a copy
self.asset_type_list_widget.addItem(item)
self.asset_type_list_widget.setCurrentItem(item) # Triggers _display_asset_type_details
logger.info(f"Added new asset type: {new_name}")
self.unsaved_changes = True
elif ok and not new_name:
QMessageBox.warning(self, "Invalid Name", "Asset type name cannot be empty.")
def _remove_asset_type(self):
current_item = self.asset_type_list_widget.currentItem()
if not current_item:
QMessageBox.information(self, "No Selection", "Please select an asset type to remove.")
return
asset_name = current_item.text()
reply = QMessageBox.question(self, "Confirm Removal",
f"Are you sure you want to remove the asset type '{asset_name}'?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
if asset_name in self.asset_type_data:
del self.asset_type_data[asset_name]
row = self.asset_type_list_widget.row(current_item)
self.asset_type_list_widget.takeItem(row)
logger.info(f"Removed asset type: {asset_name}")
self.unsaved_changes = True
if self.asset_type_list_widget.count() > 0:
new_row_to_select = max(0, row - 1) if row > 0 else 0
if self.asset_type_list_widget.count() > new_row_to_select: # Ensure new_row_to_select is valid
self.asset_type_list_widget.setCurrentRow(new_row_to_select)
else: # if list becomes empty or selection is out of bounds
self._display_asset_type_details(None, None)
else:
self._display_asset_type_details(None, None) # Clear details if list is empty
def _add_asset_example(self):
current_asset_item = self.asset_type_list_widget.currentItem()
if not current_asset_item:
QMessageBox.information(self, "No Asset Type Selected", "Please select an asset type first.")
return
new_example, ok = QInputDialog.getText(self, "Add Example", "Enter new example string:")
if ok and new_example:
asset_data = current_asset_item.data(Qt.UserRole)
if not isinstance(asset_data, dict) or 'examples' not in asset_data:
logger.error("Asset data is not a dict or 'examples' key is missing.")
QMessageBox.critical(self, "Error", "Internal data error for selected asset type.")
return
if not isinstance(asset_data['examples'], list): # Ensure 'examples' is a list
asset_data['examples'] = []
asset_data['examples'].append(new_example)
current_asset_item.setData(Qt.UserRole, asset_data) # Update data in item
self.asset_examples_list_widget.addItem(new_example)
logger.info(f"Added example '{new_example}' to asset type '{current_asset_item.text()}'")
self.unsaved_changes = True
elif ok and not new_example:
QMessageBox.warning(self, "Invalid Example", "Example string cannot be empty.")
def _remove_asset_example(self):
current_asset_item = self.asset_type_list_widget.currentItem()
if not current_asset_item:
QMessageBox.information(self, "No Asset Type Selected", "Please select an asset type first.")
return
current_example_item = self.asset_examples_list_widget.currentItem()
if not current_example_item:
QMessageBox.information(self, "No Example Selected", "Please select an example to remove.")
return
example_text = current_example_item.text()
# No confirmation needed as per typical list item removal, but can be added if desired.
# reply = QMessageBox.question(self, "Confirm Removal",
# f"Are you sure you want to remove the example '{example_text}'?",
# QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
# if reply == QMessageBox.No:
# return
asset_data = current_asset_item.data(Qt.UserRole)
if not isinstance(asset_data, dict) or 'examples' not in asset_data or not isinstance(asset_data['examples'], list):
logger.error("Asset data issue during example removal.")
QMessageBox.critical(self, "Error", "Internal data error for selected asset type.")
return
try:
asset_data['examples'].remove(example_text)
current_asset_item.setData(Qt.UserRole, asset_data) # Update data in item
row = self.asset_examples_list_widget.row(current_example_item)
self.asset_examples_list_widget.takeItem(row)
logger.info(f"Removed example '{example_text}' from asset type '{current_asset_item.text()}'")
self.unsaved_changes = True
except ValueError:
logger.warning(f"Example '{example_text}' not found in internal list for asset '{current_asset_item.text()}'. UI might be out of sync.")
# Still remove from UI if it was there
row = self.asset_examples_list_widget.row(current_example_item)
if row >=0: self.asset_examples_list_widget.takeItem(row)
def _update_file_type_color_swatch(self, color_hex, swatch_label):
if hasattr(self, swatch_label): # Check if the specific swatch label exists
palette = swatch_label.palette()
palette.setColor(QPalette.Window, QColor(color_hex))
swatch_label.setPalette(palette)
def _create_file_types_tab(self):
tab_page, right_pane_container = self._create_tab_pane("File Type", self.file_type_data, "file_type_list_widget")
right_pane_groupbox = QGroupBox("Details for Selected File Type")
details_layout = QFormLayout(right_pane_groupbox)
# Description
self.ft_description_edit = QTextEdit()
details_layout.addRow("Description:", self.ft_description_edit)
# Color
ft_color_layout = QHBoxLayout()
self.ft_color_swatch_label = QLabel()
self.ft_color_swatch_label.setFixedSize(20, 20)
self.ft_color_swatch_label.setAutoFillBackground(True)
self._update_color_swatch_generic(self.ft_color_swatch_label, "#ffffff") # Default
btn_ft_choose_color = QPushButton("Choose Color...")
btn_ft_choose_color.clicked.connect(self._choose_file_type_color)
ft_color_layout.addWidget(self.ft_color_swatch_label)
ft_color_layout.addWidget(btn_ft_choose_color)
ft_color_layout.addStretch()
details_layout.addRow("Color:", ft_color_layout)
# Examples
ft_examples_group = QGroupBox("Examples")
ft_examples_layout = QVBoxLayout(ft_examples_group)
self.ft_examples_list_widget = QListWidget()
ft_examples_layout.addWidget(self.ft_examples_list_widget)
ft_example_buttons_layout = QHBoxLayout()
btn_ft_add_example = QPushButton("Add Example")
btn_ft_remove_example = QPushButton("Remove Selected Example")
btn_ft_add_example.clicked.connect(self._add_file_type_example)
btn_ft_remove_example.clicked.connect(self._remove_file_type_example)
ft_example_buttons_layout.addWidget(btn_ft_add_example)
ft_example_buttons_layout.addWidget(btn_ft_remove_example)
ft_examples_layout.addLayout(ft_example_buttons_layout)
details_layout.addRow(ft_examples_group)
# Standard Type
self.ft_standard_type_edit = QLineEdit()
details_layout.addRow("Standard Type:", self.ft_standard_type_edit)
# Bit Depth Rule
self.ft_bit_depth_combo = QComboBox()
self.ft_bit_depth_combo.addItems(["preserve", "force_8bit", "force_16bit"])
details_layout.addRow("Bit Depth Policy:", self.ft_bit_depth_combo)
# Is Grayscale
self.ft_is_grayscale_check = QCheckBox("Is Grayscale")
details_layout.addRow(self.ft_is_grayscale_check) # No label for checkbox itself
# Keybind
self.ft_keybind_edit = QLineEdit()
self.ft_keybind_edit.setMaxLength(1) # Basic validation
details_layout.addRow("Keybind:", self.ft_keybind_edit)
parent_layout = right_pane_container.parentWidget().layout()
if parent_layout:
parent_layout.replaceWidget(right_pane_container, right_pane_groupbox)
right_pane_container.deleteLater()
# Connect signals for editing
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)
self._populate_file_type_list()
if self.file_type_list_widget.count() > 0:
self.file_type_list_widget.setCurrentRow(0)
# _display_file_type_details is connected to currentItemChanged
return tab_page
def _populate_file_type_list(self):
self.file_type_list_widget.clear()
for key, ft_data_item in self.file_type_data.items():
item = QListWidgetItem(key)
if not isinstance(ft_data_item, dict):
logger.warning(f"File type data for '{key}' is not a dict: {ft_data_item}. Using default.")
ft_data_item = {
"description": str(ft_data_item), "color": "#ffffff", "examples": [],
"standard_type": "", "bit_depth_policy": "preserve",
"is_grayscale": False, "keybind": ""
}
# Ensure all essential keys exist with defaults
ft_data_item.setdefault('description', '')
ft_data_item.setdefault('color', '#ffffff')
ft_data_item.setdefault('examples', [])
ft_data_item.setdefault('standard_type', '')
ft_data_item.setdefault('bit_depth_policy', 'preserve')
ft_data_item.setdefault('is_grayscale', False)
ft_data_item.setdefault('keybind', '')
item.setData(Qt.UserRole, ft_data_item)
self.file_type_list_widget.addItem(item)
def _display_file_type_details(self, current_item, previous_item=None):
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:
logger.info(f"Current item text: {current_item.text()}")
logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}")
else:
logger.info("Current item is None for file_type_details.")
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_policy": "preserve",
"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_policy', 'preserve'))
if bdr_index != -1:
self.ft_bit_depth_combo.setCurrentIndex(bdr_index)
else:
self.ft_bit_depth_combo.setCurrentIndex(0) # Default to 'preserve'
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.Window, QColor(color_hex))
swatch_label.setPalette(palette)
swatch_label.update() # Ensure the label repaints
# --- File Type action methods ---
def _add_file_type(self):
new_id, ok = QInputDialog.getText(self, "Add File Type", "Enter ID for the new file type (e.g., MAP_ALB):")
if ok and new_id:
new_id = new_id.strip() # Remove leading/trailing whitespace
if not new_id: # Check if empty after strip
QMessageBox.warning(self, "Invalid ID", "File type ID cannot be empty.")
return
if new_id in self.file_type_data:
QMessageBox.warning(self, "ID Exists", f"A file type with ID '{new_id}' already exists.")
return
default_file_type = {
"description": "",
"color": "#ffffff",
"examples": [],
"standard_type": "",
"bit_depth_policy": "preserve",
"is_grayscale": False,
"keybind": ""
}
self.file_type_data[new_id] = default_file_type
item = QListWidgetItem(new_id)
item.setData(Qt.UserRole, default_file_type.copy()) # Store a copy for the item
self.file_type_list_widget.addItem(item)
self.file_type_list_widget.setCurrentItem(item) # Triggers _display_file_type_details
logger.info(f"Added new file type: {new_id}")
self.unsaved_changes = True
elif ok and not new_id.strip(): # Also catch if user entered only spaces and pressed OK
QMessageBox.warning(self, "Invalid ID", "File type ID cannot be empty.")
def _remove_file_type(self):
current_item = self.file_type_list_widget.currentItem()
if not current_item:
QMessageBox.information(self, "No Selection", "Please select a file type to remove.")
return
file_type_id = current_item.text()
reply = QMessageBox.question(self, "Confirm Removal",
f"Are you sure you want to remove the file type '{file_type_id}'?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
if file_type_id in self.file_type_data:
del self.file_type_data[file_type_id]
row = self.file_type_list_widget.row(current_item)
self.file_type_list_widget.takeItem(row)
logger.info(f"Removed file type: {file_type_id}")
self.unsaved_changes = True
if self.file_type_list_widget.count() > 0:
new_row_to_select = max(0, row - 1) if row > 0 else 0
if self.file_type_list_widget.count() > new_row_to_select:
self.file_type_list_widget.setCurrentRow(new_row_to_select)
else: # if list becomes empty or selection is out of bounds
self._display_file_type_details(None, None) # Clear details
else:
self._display_file_type_details(None, None) # Clear details if list is empty
def _choose_file_type_color(self):
current_item = self.file_type_list_widget.currentItem()
if not current_item:
return
ft_data = current_item.data(Qt.UserRole)
if not isinstance(ft_data, dict): # Should not happen
logger.error("File type item data is not a dict in _choose_file_type_color.")
return
initial_color = QColor(ft_data.get('color', '#ffffff'))
color = QColorDialog.getColor(initial_color, self, "Choose File Type Color")
if color.isValid():
color_hex = color.name()
self._update_color_swatch_generic(self.ft_color_swatch_label, color_hex)
ft_data['color'] = color_hex
current_item.setData(Qt.UserRole, ft_data) # Update data in item
self.unsaved_changes = True
def _add_file_type_example(self):
current_ft_item = self.file_type_list_widget.currentItem()
if not current_ft_item:
QMessageBox.information(self, "No File Type Selected", "Please select a file type first.")
return
new_example, ok = QInputDialog.getText(self, "Add File Type Example", "Enter new example string (e.g., _alb.png, .exr):")
if ok and new_example:
new_example = new_example.strip()
if not new_example:
QMessageBox.warning(self, "Invalid Example", "Example string cannot be empty.")
return
ft_data = current_ft_item.data(Qt.UserRole)
if not isinstance(ft_data, dict) or 'examples' not in ft_data:
logger.error("File type data is not a dict or 'examples' key is missing.")
QMessageBox.critical(self, "Error", "Internal data error for selected file type.")
return
if not isinstance(ft_data['examples'], list): # Ensure 'examples' is a list
ft_data['examples'] = []
if new_example in ft_data['examples']:
QMessageBox.information(self, "Example Exists", f"The example '{new_example}' already exists for this file type.")
return
ft_data['examples'].append(new_example)
current_ft_item.setData(Qt.UserRole, ft_data) # Update data in item
self.ft_examples_list_widget.addItem(new_example)
logger.info(f"Added example '{new_example}' to file type '{current_ft_item.text()}'")
self.unsaved_changes = True
elif ok and not new_example.strip():
QMessageBox.warning(self, "Invalid Example", "Example string cannot be empty.")
def _remove_file_type_example(self):
current_ft_item = self.file_type_list_widget.currentItem()
if not current_ft_item:
QMessageBox.information(self, "No File Type Selected", "Please select a file type first.")
return
current_example_item = self.ft_examples_list_widget.currentItem()
if not current_example_item:
QMessageBox.information(self, "No Example Selected", "Please select an example to remove.")
return
example_text = current_example_item.text()
ft_data = current_ft_item.data(Qt.UserRole)
if not isinstance(ft_data, dict) or 'examples' not in ft_data or not isinstance(ft_data['examples'], list):
logger.error("File type data issue during example removal.")
QMessageBox.critical(self, "Error", "Internal data error for selected file type.")
return
try:
ft_data['examples'].remove(example_text)
current_ft_item.setData(Qt.UserRole, ft_data) # Update data in item
row = self.ft_examples_list_widget.row(current_example_item)
self.ft_examples_list_widget.takeItem(row)
logger.info(f"Removed example '{example_text}' from file type '{current_ft_item.text()}'")
self.unsaved_changes = True
except ValueError:
logger.warning(f"Example '{example_text}' not found in internal list for file type '{current_ft_item.text()}'. UI might be out of sync.")
row = self.ft_examples_list_widget.row(current_example_item)
if row >=0: self.ft_examples_list_widget.takeItem(row)
def _on_file_type_detail_changed(self):
current_item = self.file_type_list_widget.currentItem()
if not current_item:
return
ft_data = current_item.data(Qt.UserRole)
if not isinstance(ft_data, dict):
logger.error("File type item data is not a dict in _on_file_type_detail_changed.")
return
# Update based on which widget triggered (or update all)
ft_data['description'] = self.ft_description_edit.toPlainText()
ft_data['standard_type'] = self.ft_standard_type_edit.text()
ft_data['bit_depth_policy'] = self.ft_bit_depth_combo.currentText()
ft_data['is_grayscale'] = self.ft_is_grayscale_check.isChecked()
# Keybind validation (force uppercase)
keybind_text = self.ft_keybind_edit.text()
if keybind_text: # MaxLength(1) is already set
# Disconnect to prevent recursive call during setText
try: self.ft_keybind_edit.textChanged.disconnect(self._on_file_type_detail_changed)
except TypeError: pass
self.ft_keybind_edit.setText(keybind_text.upper())
# Reconnect
self.ft_keybind_edit.textChanged.connect(self._on_file_type_detail_changed)
ft_data['keybind'] = keybind_text.upper()
else:
ft_data['keybind'] = ''
current_item.setData(Qt.UserRole, ft_data)
logger.debug(f"File type '{current_item.text()}' data updated: {ft_data}")
self.unsaved_changes = True
# --- End Placeholder methods ---
def _create_suppliers_tab(self):
tab_page, right_pane_container = self._create_tab_pane("Supplier", self.supplier_data, "supplier_list_widget")
right_pane_groupbox = QGroupBox("Details for Selected Supplier")
details_layout = QFormLayout(right_pane_groupbox)
# Normal Map Type
self.supplier_normal_map_type_combo = QComboBox()
self.supplier_normal_map_type_combo.addItems(["OpenGL", "DirectX"])
details_layout.addRow("Normal Map Type:", self.supplier_normal_map_type_combo)
# Replace the generic right_pane_widget
parent_layout = right_pane_container.parentWidget().layout()
if parent_layout:
parent_layout.replaceWidget(right_pane_container, right_pane_groupbox)
right_pane_container.deleteLater()
# Connect signals for editing
self.supplier_normal_map_type_combo.currentIndexChanged.connect(self._on_supplier_detail_changed)
# Initial population and display
self._populate_supplier_list()
if self.supplier_list_widget.count() > 0:
self.supplier_list_widget.setCurrentRow(0)
# _display_supplier_details is connected to currentItemChanged
return tab_page
def _populate_supplier_list(self):
self.supplier_list_widget.clear()
for key, sup_data_item in self.supplier_data.items():
item = QListWidgetItem(key)
if not isinstance(sup_data_item, dict):
logger.warning(f"Supplier data for '{key}' is not a dict: {sup_data_item}. Using default.")
sup_data_item = {"normal_map_type": "OpenGL"}
sup_data_item.setdefault('normal_map_type', 'OpenGL') # Ensure key exists
item.setData(Qt.UserRole, sup_data_item)
self.supplier_list_widget.addItem(item)
def _display_supplier_details(self, current_item, previous_item=None):
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:
logger.info(f"Current item text: {current_item.text()}")
logger.info(f"Current item data (UserRole): {current_item.data(Qt.UserRole)}")
else:
logger.info("Current item is None for supplier_details.")
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()
if not current_item:
return
supplier_name = current_item.text()
if supplier_name not in self.supplier_data:
logger.error(f"Supplier '{supplier_name}' not found in self.supplier_data during detail change.")
return # Or create it, but that might be unexpected here
# Ensure the entry in self.supplier_data is a dictionary
if not isinstance(self.supplier_data[supplier_name], dict):
self.supplier_data[supplier_name] = {} # Initialize if it's not a dict
new_normal_map_type = self.supplier_normal_map_type_combo.currentText()
self.supplier_data[supplier_name]['normal_map_type'] = new_normal_map_type
# Update the item's UserRole data as well to keep it in sync
current_item.setData(Qt.UserRole, self.supplier_data[supplier_name].copy())
logger.debug(f"Supplier '{supplier_name}' normal_map_type updated to: {new_normal_map_type}")
self.unsaved_changes = True
def _add_supplier(self):
new_name, ok = QInputDialog.getText(self, "Add Supplier", "Enter name for the new supplier:")
if ok and new_name:
new_name = new_name.strip()
if not new_name:
QMessageBox.warning(self, "Invalid Name", "Supplier name cannot be empty.")
return
if new_name in self.supplier_data:
QMessageBox.warning(self, "Name Exists", f"A supplier named '{new_name}' already exists.")
return
default_supplier_settings = {"normal_map_type": "OpenGL"}
self.supplier_data[new_name] = default_supplier_settings
item = QListWidgetItem(new_name)
item.setData(Qt.UserRole, default_supplier_settings.copy()) # Store a copy
self.supplier_list_widget.addItem(item)
self.supplier_list_widget.setCurrentItem(item) # Triggers display
logger.info(f"Added new supplier: {new_name}")
self.unsaved_changes = True
elif ok and not new_name.strip():
QMessageBox.warning(self, "Invalid Name", "Supplier name cannot be empty.")
def _remove_supplier(self):
current_item = self.supplier_list_widget.currentItem()
if not current_item:
QMessageBox.information(self, "No Selection", "Please select a supplier to remove.")
return
supplier_name = current_item.text()
reply = QMessageBox.question(self, "Confirm Removal",
f"Are you sure you want to remove the supplier '{supplier_name}'?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
if supplier_name in self.supplier_data:
del self.supplier_data[supplier_name]
row = self.supplier_list_widget.row(current_item)
self.supplier_list_widget.takeItem(row)
logger.info(f"Removed supplier: {supplier_name}")
self.unsaved_changes = True
# Select another item or clear details
if self.supplier_list_widget.count() > 0:
new_row_to_select = max(0, row - 1) if row > 0 else 0
if self.supplier_list_widget.count() > new_row_to_select:
self.supplier_list_widget.setCurrentRow(new_row_to_select)
else:
self._display_supplier_details(None, None)
else:
self._display_supplier_details(None, None) # Clear details if list is empty
def save_definitions(self):
logger.info("Attempting to save definitions...")
try:
# --- Asset Type Definitions ---
# Ensure self.asset_type_data is consistent with the QListWidget items.
# All edits should have updated the item's UserRole data.
# Add/Remove operations update self.asset_type_data directly.
# This loop ensures any in-place modifications to item data (like description, color)
# are reflected in the self.asset_type_data before saving.
current_keys_in_list = set()
if hasattr(self, 'asset_type_list_widget'): # Check if the widget exists
for i in range(self.asset_type_list_widget.count()):
item = self.asset_type_list_widget.item(i)
key = item.text()
current_keys_in_list.add(key)
# Update self.asset_type_data with the (potentially modified) UserRole data
item_data = item.data(Qt.UserRole)
if isinstance(item_data, dict):
self.asset_type_data[key] = item_data
else:
logger.warning(f"Item '{key}' in asset_type_list_widget has non-dict UserRole data: {type(item_data)}. Skipping update for this item in self.asset_type_data.")
# Remove any keys from self.asset_type_data that are no longer in the list
# (should be handled by _remove_asset_type, but this is a safeguard)
keys_to_remove_from_dict = set(self.asset_type_data.keys()) - current_keys_in_list
for key in keys_to_remove_from_dict:
logger.info(f"Removing orphaned key '{key}' from self.asset_type_data before saving.")
del self.asset_type_data[key]
save_asset_definitions(self.asset_type_data)
logger.info("Asset Type definitions saved successfully.")
# --- File Type Definitions ---
if hasattr(self, 'file_type_data') and hasattr(self, 'file_type_list_widget'):
current_ft_keys_in_list = set()
for i in range(self.file_type_list_widget.count()):
item = self.file_type_list_widget.item(i)
key = item.text()
current_ft_keys_in_list.add(key)
item_data = item.data(Qt.UserRole)
if isinstance(item_data, dict):
self.file_type_data[key] = item_data
else:
logger.warning(f"Item '{key}' in file_type_list_widget has non-dict UserRole data: {type(item_data)}. Skipping.")
keys_to_remove_ft = set(self.file_type_data.keys()) - current_ft_keys_in_list
for key in keys_to_remove_ft:
logger.info(f"Removing orphaned key '{key}' from self.file_type_data before saving.")
del self.file_type_data[key]
save_file_type_definitions(self.file_type_data)
logger.info("File Type definitions saved successfully.")
else:
logger.info("File type data or list widget not found, skipping save for file types.")
# --- Supplier Settings ---
if hasattr(self, 'supplier_data') and hasattr(self, 'supplier_list_widget'):
current_s_keys_in_list = set()
for i in range(self.supplier_list_widget.count()):
item = self.supplier_list_widget.item(i)
key = item.text()
current_s_keys_in_list.add(key)
item_data = item.data(Qt.UserRole)
if isinstance(item_data, dict):
self.supplier_data[key] = item_data # Ensure self.supplier_data is up-to-date
else:
logger.warning(f"Item '{key}' in supplier_list_widget has non-dict UserRole data: {type(item_data)}. Skipping update for this item in self.supplier_data.")
keys_to_remove_s = set(self.supplier_data.keys()) - current_s_keys_in_list
for key in keys_to_remove_s:
logger.info(f"Removing orphaned key '{key}' from self.supplier_data before saving.")
del self.supplier_data[key]
save_supplier_settings(self.supplier_data)
logger.info("Supplier settings saved successfully.")
else:
logger.info("Supplier data or list widget not found, skipping save for suppliers.")
QMessageBox.information(self, "Save Successful", "Definitions saved successfully.")
self.unsaved_changes = False # Reset flag
self.accept() # Close dialog on successful save
except Exception as e:
logger.error(f"Failed to save definitions: {e}", exc_info=True)
QMessageBox.critical(self, "Save Error", f"Could not save definitions: {e}")
# Optionally, do not close the dialog on error by removing self.accept() or calling self.reject()
def reject(self):
if self.unsaved_changes:
reply = QMessageBox.question(self, "Unsaved Changes",
"You have unsaved changes. Are you sure you want to cancel?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.No:
return # Do not close
super().reject() # Proceed with closing
def closeEvent(self, event):
if self.unsaved_changes:
reply = QMessageBox.question(self, "Unsaved Changes",
"You have unsaved changes. Are you sure you want to close?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
event.accept()
else:
event.ignore()
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
import sys
# Setup basic logging for testing
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
# Create dummy config files if they don't exist for testing
config_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'config'))
os.makedirs(config_dir, exist_ok=True)
asset_types_path = os.path.join(config_dir, 'asset_type_definitions.json')
file_types_path = os.path.join(config_dir, 'file_type_definitions.json')
suppliers_path = os.path.join(config_dir, 'suppliers.json')
if not os.path.exists(asset_types_path):
with open(asset_types_path, 'w') as f:
f.write('{"GenericModel": {"description": "A generic 3D model"}, "TextureSet": {"description": "A set of PBR textures"}}')
if not os.path.exists(file_types_path):
with open(file_types_path, 'w') as f:
f.write('{".fbx": {"description": "Filmbox format"}, ".png": {"description": "Portable Network Graphics"}}')
if not os.path.exists(suppliers_path):
with open(suppliers_path, 'w') as f:
f.write('{"Poliigon": {"api_key": "dummy_key"}, "Local": {}}')
app = QApplication(sys.argv)
dialog = DefinitionsEditorDialog()
dialog.show()
sys.exit(app.exec_())