0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat(launch_window): add custom UI file launching functionality and UI tile

This commit is contained in:
2025-04-14 21:42:22 +02:00
parent d60cf6c843
commit 3089ca15ec
2 changed files with 93 additions and 11 deletions

View File

@ -1,21 +1,36 @@
from __future__ import annotations from __future__ import annotations
import os import os
import xml.etree.ElementTree as ET
from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from qtpy.QtCore import Qt, Signal from qtpy.QtCore import Qt, Signal
from qtpy.QtGui import QPainter, QPainterPath, QPixmap from qtpy.QtGui import QPainter, QPainterPath, QPixmap
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QSizePolicy, QWidget from qtpy.QtWidgets import (
QApplication,
QFileDialog,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QWidget,
)
import bec_widgets import bec_widgets
from bec_widgets.cli.rpc.rpc_register import RPCRegister from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils.container_utils import WidgetContainerUtils from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.round_frame import RoundedFrame from bec_widgets.utils.round_frame import RoundedFrame
from bec_widgets.utils.toolbar import ModularToolBar from bec_widgets.utils.toolbar import ModularToolBar
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow, UILaunchWindow
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
if TYPE_CHECKING: # pragma: no cover
from qtpy.QtCore import QObject
logger = bec_logger.logger logger = bec_logger.logger
MODULE_PATH = os.path.dirname(bec_widgets.__file__) MODULE_PATH = os.path.dirname(bec_widgets.__file__)
@ -24,7 +39,12 @@ class LaunchTile(RoundedFrame):
open_signal = Signal() open_signal = Signal()
def __init__( def __init__(
self, parent=None, icon_path=None, top_label=None, main_label=None, description=None self,
parent: QObject | None = None,
icon_path: str | None = None,
top_label: str | None = None,
main_label: str | None = None,
description: str | None = None,
): ):
super().__init__(parent=parent, orientation="vertical") super().__init__(parent=parent, orientation="vertical")
@ -91,8 +111,6 @@ class LaunchTile(RoundedFrame):
) )
self.layout.addWidget(self.action_button, alignment=Qt.AlignCenter) self.layout.addWidget(self.action_button, alignment=Qt.AlignCenter)
# self.apply_theme("dark")
class LaunchWindow(BECMainWindow): class LaunchWindow(BECMainWindow):
RPC = True RPC = True
@ -134,17 +152,28 @@ class LaunchWindow(BECMainWindow):
) )
self.tile_auto_update.setFixedSize(250, 300) self.tile_auto_update.setFixedSize(250, 300)
self.tile_ui_file = LaunchTile(
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "auto_update.png"),
top_label="Get customized",
main_label="Launch Custom UI File",
description="GUI application with custom UI file.",
)
self.tile_ui_file.setFixedSize(250, 300)
# Add tiles to the main layout # Add tiles to the main layout
self.central_widget.layout.addWidget(self.tile_dock_area) self.central_widget.layout.addWidget(self.tile_dock_area)
self.central_widget.layout.addWidget(self.tile_auto_update) self.central_widget.layout.addWidget(self.tile_auto_update)
self.central_widget.layout.addWidget(self.tile_ui_file)
# hacky solution no time to waste # hacky solution no time to waste
self.tiles = [self.tile_dock_area, self.tile_auto_update] self.tiles = [self.tile_dock_area, self.tile_auto_update, self.tile_ui_file]
# Connect signals # Connect signals
self.tile_dock_area.action_button.clicked.connect(lambda: self.launch("dock_area")) self.tile_dock_area.action_button.clicked.connect(lambda: self.launch("dock_area"))
self.tile_auto_update.action_button.clicked.connect( self.tile_auto_update.action_button.clicked.connect(
lambda: self.launch("auto_update_dock_area", "auto_updates") lambda: self.launch("auto_update_dock_area", "auto_updates")
) )
self.tile_ui_file.action_button.clicked.connect(self._open_custom_ui_file)
self._update_theme() self._update_theme()
def launch( def launch(
@ -152,6 +181,7 @@ class LaunchWindow(BECMainWindow):
launch_script: str, launch_script: str,
name: str | None = None, name: str | None = None,
geometry: tuple[int, int, int, int] | None = None, geometry: tuple[int, int, int, int] | None = None,
**kwargs,
) -> QWidget: ) -> QWidget:
"""Launch the specified script. If the launch script creates a QWidget, it will be """Launch the specified script. If the launch script creates a QWidget, it will be
embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown embedded in a BECMainWindow. If the launch script creates a BECMainWindow, it will be shown
@ -181,6 +211,38 @@ class LaunchWindow(BECMainWindow):
launch_script = "dock_area" launch_script = "dock_area"
if not isinstance(launch_script, str): if not isinstance(launch_script, str):
raise ValueError(f"Launch script must be a string, but got {type(launch_script)}.") raise ValueError(f"Launch script must be a string, but got {type(launch_script)}.")
if launch_script == "custom_ui_file":
# Load the custom UI file
ui_file = kwargs.pop("ui_file", None)
if ui_file is None:
raise ValueError("UI file must be provided for custom UI file launch.")
filename = os.path.basename(ui_file).split(".")[0]
tree = ET.parse(ui_file)
root = tree.getroot()
# Check if the top-level widget is a QMainWindow
widget = root.find("widget")
if widget is None:
raise ValueError("No widget found in the UI file.")
if widget.attrib.get("class") == "QMainWindow":
raise ValueError(
"Loading a QMainWindow from a UI file is currently not supported."
"If you need this, please contact the BEC team or create a ticket on gitlab.psi.ch/bec/bec_widgets."
)
window = UILaunchWindow(object_name=filename)
QApplication.processEvents()
result_widget = UILoader(window).loader(ui_file)
window.setCentralWidget(result_widget)
window.setWindowTitle(f"BEC - {window.object_name}")
window.show()
logger.info(
f"Object name of new instance: {result_widget.objectName()}, {window.gui_id}"
)
return window
else:
launch = getattr(bw_launch, launch_script, None) launch = getattr(bw_launch, launch_script, None)
if launch is None: if launch is None:
raise ValueError(f"Launch script {launch_script} not found.") raise ValueError(f"Launch script {launch_script} not found.")
@ -190,7 +252,7 @@ class LaunchWindow(BECMainWindow):
# TODO Should we simply use the specified name as title here? # TODO Should we simply use the specified name as title here?
result_widget.window().setWindowTitle(f"BEC - {name}") result_widget.window().setWindowTitle(f"BEC - {name}")
logger.info(f"Created new dock area: {name}") logger.info(f"Created new dock area: {name}")
logger.info(f"Existing dock areas: {geometry}")
if geometry is not None: if geometry is not None:
result_widget.setGeometry(*geometry) result_widget.setGeometry(*geometry)
if isinstance(result_widget, BECMainWindow): if isinstance(result_widget, BECMainWindow):
@ -210,6 +272,16 @@ class LaunchWindow(BECMainWindow):
super().apply_theme(theme) super().apply_theme(theme)
@SafeSlot(popup_error=True)
def _open_custom_ui_file(self):
"""
Open a file dialog to select a custom UI file and launch it.
"""
ui_file, _ = QFileDialog.getOpenFileName(
self, "Select UI File", "", "UI Files (*.ui);;All Files (*)"
)
self.launch("custom_ui_file", ui_file=ui_file)
def show_launcher(self): def show_launcher(self):
self.show() self.show()

View File

@ -164,6 +164,16 @@ class BECMainWindow(BECWidget, QMainWindow):
central_widget = self.centralWidget() central_widget = self.centralWidget()
central_widget.close() central_widget.close()
central_widget.deleteLater() central_widget.deleteLater()
if not isinstance(central_widget, BECWidget):
# if the central widget is not a BECWidget, we need to call the cleanup method
# of all widgets whose parent is the current BECMainWindow
children = self.findChildren(BECWidget)
for child in children:
ancestor = WidgetHierarchy._get_becwidget_ancestor(child)
if ancestor is self:
child.cleanup()
child.close()
child.deleteLater()
super().cleanup() super().cleanup()