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

WIP launch window moved to BECMainWindow + regenerated client

This commit is contained in:
2025-04-08 18:01:27 +02:00
parent 53bdf143ae
commit fc44850c73
3 changed files with 86 additions and 469 deletions

View File

@ -13,116 +13,30 @@ from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
logger = bec_logger.logger
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class LaunchWindow(BECWidget, QMainWindow):
def __init__(self, parent=None, gui_id: str = None, *args, **kwargs):
super().__init__(parent=parent, gui_id=gui_id, **kwargs)
class LaunchWindow(BECMainWindow):
RPC = True
def __init__(
self, parent=None, gui_id: str = None, window_title="BEC Launcher", *args, **kwargs
):
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
self.app = QApplication.instance()
self.resize(500, 300)
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
self._init_ui()
def _init_ui(self):
# Set the window title
self.setWindowTitle("BEC Launcher")
# Load ui file
ui_file_path = os.path.join(MODULE_PATH, "applications/launch_dialog.ui")
self.load_ui(ui_file_path)
# Set Menu and Status bar
self._setup_menu_bar()
# BEC Specific UI
self._init_bec_specific_ui()
# TODO can be implemented for toolbar
def load_ui(self, ui_file):
loader = UILoader(self)
self.ui = loader.loader(ui_file)
self.setCentralWidget(self.ui)
self.ui.open_dock_area.setText("Open Dock Area")
self.ui.open_dock_area.clicked.connect(lambda: self.launch("dock_area"))
def _init_bec_specific_ui(self):
if getattr(self.app, "gui_id", None):
self.statusBar().showMessage(f"App ID: {self.app.gui_id}")
else:
logger.warning(
"Application is not a BECApplication instance. Status bar will not show App ID. Please initialize the application with BECApplication."
)
def list_app_hierarchy(self):
"""
List the hierarchy of the application.
"""
self.app.list_hierarchy()
def _setup_menu_bar(self):
"""
Setup the menu bar for the main window.
"""
menu_bar = self.menuBar()
########################################
# Theme menu
theme_menu = menu_bar.addMenu("Theme")
theme_group = QActionGroup(self)
light_theme_action = QAction("Light Theme", self, checkable=True)
dark_theme_action = QAction("Dark Theme", self, checkable=True)
theme_group.addAction(light_theme_action)
theme_group.addAction(dark_theme_action)
theme_group.setExclusive(True)
theme_menu.addAction(light_theme_action)
theme_menu.addAction(dark_theme_action)
# Connect theme actions
light_theme_action.triggered.connect(lambda: self.change_theme("light"))
dark_theme_action.triggered.connect(lambda: self.change_theme("dark"))
# Set the default theme
# TODO can be fetched from app
dark_theme_action.setChecked(True)
########################################
# Help menu
help_menu = menu_bar.addMenu("Help")
help_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxQuestion)
bug_icon = QApplication.style().standardIcon(QStyle.SP_MessageBoxInformation)
bec_docs = QAction("BEC Docs", self)
bec_docs.setIcon(help_icon)
widgets_docs = QAction("BEC Widgets Docs", self)
widgets_docs.setIcon(help_icon)
bug_report = QAction("Bug Report", self)
bug_report.setIcon(bug_icon)
bec_docs.triggered.connect(BECWebLinksMixin.open_bec_docs)
widgets_docs.triggered.connect(BECWebLinksMixin.open_bec_widgets_docs)
bug_report.triggered.connect(BECWebLinksMixin.open_bec_bug_report)
help_menu.addAction(bec_docs)
help_menu.addAction(widgets_docs)
help_menu.addAction(bug_report)
debug_bar = menu_bar.addMenu(f"DEBUG {self.__class__.__name__}")
list_hierarchy = QAction("List App Hierarchy", self)
list_hierarchy.triggered.connect(self.list_app_hierarchy)
debug_bar.addAction(list_hierarchy)
def change_theme(self, theme):
apply_theme(theme)
def launch(
self,
launch_script: str,
@ -137,7 +51,7 @@ class LaunchWindow(BECWidget, QMainWindow):
Returns:
BECDockArea: The newly created dock area.
"""
from bec_widgets.applications.bw_launch import dock_area
from bec_widgets.applications import bw_launch
with RPCRegister.delayed_broadcast() as rpc_register:
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea)
@ -149,16 +63,30 @@ class LaunchWindow(BECWidget, QMainWindow):
else:
name = "dock_area"
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
dock_area = dock_area(name) # BECDockArea(name=name)
dock_area.resize(dock_area.minimumSizeHint())
if launch_script is None:
launch_script = "dock_area"
if not isinstance(launch_script, str):
raise ValueError(f"Launch script must be a string, but got {type(launch_script)}.")
launch = getattr(bw_launch, launch_script, None)
if launch is None:
raise ValueError(f"Launch script {launch_script} not found.")
result_widget = launch(name)
result_widget.resize(result_widget.minimumSizeHint())
# TODO Should we simply use the specified name as title here?
dock_area.window().setWindowTitle(f"BEC - {name}")
result_widget.window().setWindowTitle(f"BEC - {name}")
logger.info(f"Created new dock area: {name}")
logger.info(f"Existing dock areas: {geometry}")
if geometry is not None:
dock_area.setGeometry(*geometry)
dock_area.show()
return dock_area
result_widget.setGeometry(*geometry)
if isinstance(result_widget, BECMainWindow):
result_widget.show()
else:
window = BECMainWindow()
window.setCentralWidget(result_widget)
window.show()
return result_widget
def show_launcher(self):
self.show()

View File

@ -15,8 +15,6 @@ class Widgets(str, enum.Enum):
Enum for the available widgets.
"""
AbortButton = "AbortButton"
BECColorMapWidget = "BECColorMapWidget"
BECDockArea = "BECDockArea"
BECProgressBar = "BECProgressBar"
BECQueue = "BECQueue"
@ -26,48 +24,19 @@ class Widgets(str, enum.Enum):
DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit"
Image = "Image"
LMFitDialog = "LMFitDialog"
LogPanel = "LogPanel"
Minesweeper = "Minesweeper"
MotorMap = "MotorMap"
MultiWaveform = "MultiWaveform"
PositionIndicator = "PositionIndicator"
PositionerBox = "PositionerBox"
PositionerBox2D = "PositionerBox2D"
PositionerControlLine = "PositionerControlLine"
ResetButton = "ResetButton"
ResumeButton = "ResumeButton"
RingProgressBar = "RingProgressBar"
ScanControl = "ScanControl"
ScatterWaveform = "ScatterWaveform"
SignalComboBox = "SignalComboBox"
SignalLineEdit = "SignalLineEdit"
StopButton = "StopButton"
TextBox = "TextBox"
VSCodeEditor = "VSCodeEditor"
Waveform = "Waveform"
WebsiteWidget = "WebsiteWidget"
class AbortButton(RPCBase):
"""A button that abort the scan."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class BECColorMapWidget(RPCBase):
@property
@rpc_call
def colormap(self):
"""
Get the current colormap name.
"""
class BECDock(RPCBase):
@property
@rpc_call
@ -357,14 +326,6 @@ class BECDockArea(RPCBase):
"""
class BECMainWindow(RPCBase):
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class BECProgressBar(RPCBase):
"""A custom progress bar with smooth transitions. The displayed text can be customized using a template."""
@ -624,14 +585,6 @@ class DapComboBox(RPCBase):
"""
class DemoApp(RPCBase):
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class DeviceBrowser(RPCBase):
@rpc_call
def remove(self):
@ -670,16 +623,6 @@ class DeviceLineEdit(RPCBase):
"""
class DeviceSignalInputBase(RPCBase):
"""Mixin base class for device signal input widgets."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class Image(RPCBase):
@property
@rpc_call
@ -1339,16 +1282,6 @@ class ImageItem(RPCBase):
"""
class LMFitDialog(RPCBase):
"""Dialog for displaying the fit summary and params for LMFit DAP processes"""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class LogPanel(RPCBase):
"""Displays a log panel"""
@ -1371,9 +1304,6 @@ class LogPanel(RPCBase):
"""
class Minesweeper(RPCBase): ...
class MotorMap(RPCBase):
@property
@rpc_call
@ -2211,64 +2141,6 @@ class PositionIndicator(RPCBase):
"""
class PositionerBox(RPCBase):
"""Simple Widget to control a positioner in box form"""
@rpc_call
def set_positioner(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class PositionerBox2D(RPCBase):
"""Simple Widget to control two positioners in box form"""
@rpc_call
def set_positioner_hor(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
@rpc_call
def set_positioner_ver(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class PositionerBoxBase(RPCBase):
"""Contains some core logic for positioner box widgets"""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class PositionerControlLine(RPCBase):
"""A widget that controls a single device."""
@rpc_call
def set_positioner(self, positioner: "str | Positioner"):
"""
Set the device
Args:
positioner (Positioner | str) : Positioner to set, accepts str or the device
"""
class PositionerGroup(RPCBase):
"""Simple Widget to control a positioner in box form"""
@ -2281,26 +2153,6 @@ class PositionerGroup(RPCBase):
"""
class ResetButton(RPCBase):
"""A button that resets the scan queue."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class ResumeButton(RPCBase):
"""A button that continue scan queue."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class Ring(RPCBase):
@rpc_call
def _get_all_rpc(self) -> "dict":
@ -2588,16 +2440,6 @@ class ScanControl(RPCBase):
"""
class ScanMetadata(RPCBase):
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the"""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class ScatterCurve(RPCBase):
"""Scatter curve item for the scatter waveform widget."""
@ -2951,36 +2793,6 @@ class ScatterWaveform(RPCBase):
"""
class SignalComboBox(RPCBase):
"""Line edit widget for device input with autocomplete for device names."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class SignalLineEdit(RPCBase):
"""Line edit widget for device input with autocomplete for device names."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class StopButton(RPCBase):
"""A button that stops the current scan."""
@rpc_call
def remove(self):
"""
Cleanup the BECConnector
"""
class TextBox(RPCBase):
"""A widget that displays text in plain and HTML format"""
@ -3511,60 +3323,3 @@ class WebsiteWidget(RPCBase):
"""
Go forward in the history
"""
class WindowWithUi(RPCBase):
"""This is just testing app wiht UI file which could be connected to RPC."""
@rpc_call
def new_dock_area(
self, name: str | None = None, geometry: tuple[int, int, int, int] | None = None
) -> "BECDockArea":
"""
Create a new dock area.
Args:
name(str): The name of the dock area.
geometry(tuple): The geometry parameters to be passed to the dock area.
Returns:
BECDockArea: The newly created dock area.
"""
@property
@rpc_call
def all_connections(self) -> list:
"""
None
"""
@rpc_call
def change_theme(self, theme):
"""
None
"""
@property
@rpc_call
def dock_area(self):
"""
None
"""
@rpc_call
def register_all_rpc(self):
"""
None
"""
@property
@rpc_call
def widget_list(self) -> list:
"""
Return a list of all widgets in the application.
"""
@rpc_call
def list_app_hierarchy(self):
"""
List the hierarchy of the application.
"""

View File

@ -1,65 +1,77 @@
import os
from typing import TYPE_CHECKING
from bec_lib.logger import bec_logger
from qtpy.QtGui import QAction, QActionGroup
from qtpy.QtCore import QSize
from qtpy.QtGui import QAction, QActionGroup, QIcon
from qtpy.QtWidgets import QApplication, QMainWindow, QStyle
from bec_widgets.cli.rpc.rpc_register import RPCRegister
import bec_widgets
from bec_lib.logger import bec_logger
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_qapp import BECApplication
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.widget_io import WidgetHierarchy
from bec_widgets.widgets.containers.main_window.addons.web_links import BECWebLinksMixin
if TYPE_CHECKING:
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
logger = bec_logger.logger
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class BECMainWindow(BECWidget, QMainWindow):
def __init__(self, parent=None, gui_id: str = None, *args, **kwargs):
RPC = False
def __init__(
self,
parent=None,
gui_id: str = None,
client=None,
window_title: str = "BEC",
*args,
**kwargs,
):
super().__init__(parent=parent, gui_id=gui_id, **kwargs)
self.app = QApplication.instance()
# self._upgrade_qapp() #TODO consider to make upgrade function to any QApplication to BECQApplication
self.setWindowTitle(window_title)
self._init_ui()
def _init_ui(self):
# Set the window title
self.setWindowTitle("BEC")
# Set the icon
self._init_bec_icon()
# Set Menu and Status bar
self._setup_menu_bar()
# BEC Specific UI
self._init_bec_specific_ui()
# self.ui = UILoader
# ui_file_path = os.path.join(os.path.dirname(__file__), "general_app.ui")
# self.load_ui(ui_file_path)
self.display_app_id()
def _init_bec_icon(self):
icon = self.app.windowIcon()
if icon.isNull():
print("No icon is set, setting default icon")
icon = QIcon()
icon.addFile(
os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
size=QSize(48, 48),
)
self.app.setWindowIcon(icon)
else:
print("An icon is set")
# TODO can be implemented for toolbar
def load_ui(self, ui_file):
loader = UILoader(self)
self.ui = loader.loader(ui_file)
self.setCentralWidget(self.ui)
def _init_bec_specific_ui(self):
if getattr(self.app, "is_bec_app", False):
self.statusBar().showMessage(f"App ID: {self.app.gui_id}")
else:
logger.warning(
"Application is not a BECApplication instance. Status bar will not show App ID. Please initialize the application with BECApplication."
)
def display_app_id(self):
server_id = self.bec_dispatcher.cli_server.gui_id
self.statusBar().showMessage(f"App ID: {server_id}")
def list_app_hierarchy(self):
"""
List the hierarchy of the application.
"""
self.app.list_hierarchy()
def _fetch_theme(self) -> str:
return self.app.theme.theme
def _setup_menu_bar(self):
"""
@ -86,8 +98,11 @@ class BECMainWindow(BECWidget, QMainWindow):
dark_theme_action.triggered.connect(lambda: self.change_theme("dark"))
# Set the default theme
# TODO can be fetched from app
dark_theme_action.setChecked(True)
theme = self.app.theme.theme
if theme == "light":
light_theme_action.setChecked(True)
elif theme == "dark":
dark_theme_action.setChecked(True)
########################################
# Help menu
@ -111,74 +126,15 @@ class BECMainWindow(BECWidget, QMainWindow):
help_menu.addAction(widgets_docs)
help_menu.addAction(bug_report)
debug_bar = menu_bar.addMenu(f"DEBUG {self.__class__.__name__}")
list_hierarchy = QAction("List App Hierarchy", self)
list_hierarchy.triggered.connect(self.list_app_hierarchy)
debug_bar.addAction(list_hierarchy)
def change_theme(self, theme):
@SafeSlot(str)
def change_theme(self, theme: str):
apply_theme(theme)
def _dump(self):
"""Return a dictionary with informations about the application state, for use in tests"""
# TODO: ModularToolBar and something else leak top-level widgets (3 or 4 QMenu + 2 QWidget);
# so, a filtering based on title is applied here, but the solution is to not have those widgets
# as top-level (so for now, a window with no title does not appear in _dump() result)
# NOTE: the main window itself is excluded, since we want to dump dock areas
info = {
tlw.gui_id: {
"title": tlw.windowTitle(),
"visible": tlw.isVisible(),
"class": str(type(tlw)),
}
for tlw in QApplication.instance().topLevelWidgets()
if tlw is not self and tlw.windowTitle()
}
# Add the main window dock area
info[self.centralWidget().gui_id] = {
"title": self.windowTitle(),
"visible": self.isVisible(),
"class": str(type(self.centralWidget())),
}
return info
def new_dock_area(
self, name: str | None = None, geometry: tuple[int, int, int, int] | None = None
) -> "BECDockArea":
"""Create a new dock area.
Args:
name(str): The name of the dock area.
geometry(tuple): The geometry parameters to be passed to the dock area.
Returns:
BECDockArea: The newly created dock area.
"""
from bec_widgets.widgets.containers.dock.dock_area import BECDockArea
with RPCRegister.delayed_broadcast() as rpc_register:
existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea)
if name is not None:
if name in existing_dock_areas:
raise ValueError(
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
)
else:
name = "dock_area"
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
dock_area = WindowWithUi() # BECDockArea(name=name)
dock_area.resize(dock_area.minimumSizeHint())
# TODO Should we simply use the specified name as title here?
dock_area.window().setWindowTitle(f"BEC - {name}")
logger.info(f"Created new dock area: {name}")
logger.info(f"Existing dock areas: {geometry}")
if geometry is not None:
dock_area.setGeometry(*geometry)
dock_area.show()
return dock_area
def cleanup(self):
super().close()
central_widget = self.centralWidget()
central_widget.close()
central_widget.deleteLater()
super().cleanup()
class WindowWithUi(BECMainWindow):
@ -187,18 +143,10 @@ class WindowWithUi(BECMainWindow):
"""
USER_ACCESS = [
"new_dock_area",
"all_connections",
"change_theme",
"dock_area",
"register_all_rpc",
"widget_list",
"list_app_hierarchy",
]
USER_ACCESS = ["new_dock_area", "all_connections", "change_theme", "hierarchy"]
def __init__(self, *args, name: str = None, **kwargs):
super().__init__(*args, **kwargs)
super().__init__(gui_id="test", *args, **kwargs)
if name is None:
name = self.__class__.__name__
else:
@ -213,28 +161,14 @@ class WindowWithUi(BECMainWindow):
self.ui = loader.loader(ui_file)
self.setCentralWidget(self.ui)
# TODO actually these propertiers are not much exposed now in the real CLI
@property
def dock_area(self):
dock_area = self.ui.dock_area
return dock_area
@property
def all_connections(self) -> list:
all_connections = self.rpc_register.list_all_connections()
all_connections_keys = list(all_connections.keys())
return all_connections_keys
def register_all_rpc(self):
app = QApplication.instance()
app.register_all()
@property
def widget_list(self) -> list:
"""Return a list of all widgets in the application."""
app = QApplication.instance()
all_widgets = app.list_all_bec_widgets()
return all_widgets
def hierarchy(self):
WidgetHierarchy.print_widget_hierarchy(self, only_bec_widgets=True)
if __name__ == "__main__":