from bec_lib.logger import bec_logger from qtpy import PYQT6, PYSIDE6 from qtpy.QtCore import QFile, QIODevice from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets from bec_widgets.utils.generate_designer_plugin import DesignerPluginInfo from bec_widgets.utils.plugin_utils import get_custom_classes logger = bec_logger.logger if PYSIDE6: from qtpy.QtUiTools import QUiLoader class CustomUiLoader(QUiLoader): def __init__(self, baseinstance, custom_widgets: dict | None = None): super().__init__(baseinstance) self.custom_widgets = custom_widgets or {} self.baseinstance = baseinstance def createWidget(self, class_name, parent=None, name=""): if class_name in self.custom_widgets: widget = self.custom_widgets[class_name](self.baseinstance) return widget return super().createWidget(class_name, self.baseinstance, name) class UILoader: """Universal UI loader for PyQt6 and PySide6.""" def __init__(self, parent=None): self.parent = parent self.custom_widgets = ( get_custom_classes("bec_widgets") + get_all_plugin_widgets() ).as_dict() if PYSIDE6: self.loader = self.load_ui_pyside6 elif PYQT6: self.loader = self.load_ui_pyqt6 else: raise ImportError("No compatible Qt bindings found.") def load_ui_pyside6(self, ui_file, parent=None): """ Specific loader for PySide6 using QUiLoader. Args: ui_file(str): Path to the .ui file. parent(QWidget): Parent widget. Returns: QWidget: The loaded widget. """ parent = parent or self.parent loader = CustomUiLoader(parent, self.custom_widgets) file = QFile(ui_file) if not file.open(QIODevice.ReadOnly): raise IOError(f"Cannot open file: {ui_file}") widget = loader.load(file, parent) file.close() return widget def load_ui_pyqt6(self, ui_file, parent=None): """ Specific loader for PyQt6 using loadUi. Args: ui_file(str): Path to the .ui file. parent(QWidget): Parent widget. Returns: QWidget: The loaded widget. """ from PyQt6.uic.Loader.loader import DynamicUILoader class CustomDynamicUILoader(DynamicUILoader): def __init__(self, package, custom_widgets: dict = None): super().__init__(package) self.custom_widgets = custom_widgets or {} def _handle_custom_widgets(self, el): """Handle the element.""" def header2module(header): """header2module(header) -> string Convert paths to C++ header files to according Python modules >>> header2module("foo/bar/baz.h") 'foo.bar.baz' """ if header.endswith(".h"): header = header[:-2] mpath = [] for part in header.split("/"): # Ignore any empty parts or those that refer to the current # directory. if part not in ("", "."): if part == "..": # We should allow this for Python3. raise SyntaxError( "custom widget header file name may not contain '..'." ) mpath.append(part) return ".".join(mpath) for custom_widget in el: classname = custom_widget.findtext("class") header = custom_widget.findtext("header") if header: header = self._translate_bec_widgets_header(header) self.factory.addCustomWidget( classname, custom_widget.findtext("extends") or "QWidget", header2module(header), ) def _translate_bec_widgets_header(self, header): for name, value in self.custom_widgets.items(): if header == DesignerPluginInfo.pascal_to_snake(name): return value.__module__ return header return CustomDynamicUILoader("", self.custom_widgets).loadUi(ui_file, parent) def load_ui(self, ui_file, parent=None): """ Universal UI loader method. Args: ui_file(str): Path to the .ui file. parent(QWidget): Parent widget. Returns: QWidget: The loaded widget. """ if parent is None: parent = self.parent return self.loader(ui_file, parent)