From 572797626cffd3274de3d47861437cdfddd78346 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Fri, 30 Jan 2026 15:33:46 +0100 Subject: [PATCH] fix(screen_utils): screen utilities added and fixed sizing for widgets from launch window and main app --- bec_widgets/applications/launch_window.py | 86 +++++++++---------- bec_widgets/applications/main_app.py | 30 +++---- bec_widgets/utils/screen_utils.py | 100 ++++++++++++++++++++++ tests/unit_tests/test_screen_utils.py | 38 ++++++++ 4 files changed, 189 insertions(+), 65 deletions(-) create mode 100644 bec_widgets/utils/screen_utils.py create mode 100644 tests/unit_tests/test_screen_utils.py diff --git a/bec_widgets/applications/launch_window.py b/bec_widgets/applications/launch_window.py index 0386690e..03a7bea2 100644 --- a/bec_widgets/applications/launch_window.py +++ b/bec_widgets/applications/launch_window.py @@ -27,6 +27,7 @@ from bec_widgets.utils.error_popups import SafeSlot from bec_widgets.utils.name_utils import pascal_to_snake from bec_widgets.utils.plugin_utils import get_plugin_auto_updates from bec_widgets.utils.round_frame import RoundedFrame +from bec_widgets.utils.screen_utils import apply_window_geometry, centered_geometry_for_app from bec_widgets.utils.toolbars.toolbar import ModularToolBar from bec_widgets.utils.ui_loader import UILoader from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates @@ -75,23 +76,28 @@ class LaunchTile(RoundedFrame): circular_pixmap.fill(Qt.transparent) painter = QPainter(circular_pixmap) - painter.setRenderHints(QPainter.Antialiasing, True) + painter.setRenderHints(QPainter.RenderHint.Antialiasing, True) path = QPainterPath() path.addEllipse(0, 0, size, size) painter.setClipPath(path) - pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation) + pixmap = pixmap.scaled( + size, + size, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) painter.drawPixmap(0, 0, pixmap) painter.end() self.icon_label.setPixmap(circular_pixmap) - self.layout.addWidget(self.icon_label, alignment=Qt.AlignCenter) + self.layout.addWidget(self.icon_label, alignment=Qt.AlignmentFlag.AlignCenter) # Top label self.top_label = QLabel(top_label.upper()) font_top = self.top_label.font() font_top.setPointSize(10) self.top_label.setFont(font_top) - self.layout.addWidget(self.top_label, alignment=Qt.AlignCenter) + self.layout.addWidget(self.top_label, alignment=Qt.AlignmentFlag.AlignCenter) # Main label self.main_label = QLabel(main_label) @@ -101,7 +107,7 @@ class LaunchTile(RoundedFrame): font_main.setPointSize(14) font_main.setBold(True) self.main_label.setFont(font_main) - self.main_label.setAlignment(Qt.AlignCenter) + self.main_label.setAlignment(Qt.AlignmentFlag.AlignCenter) # Shrink font if the default would wrap on this platform / DPI content_width = ( @@ -117,13 +123,13 @@ class LaunchTile(RoundedFrame): self.layout.addWidget(self.main_label) - self.spacer_top = QSpacerItem(0, 10, QSizePolicy.Fixed, QSizePolicy.Fixed) + self.spacer_top = QSpacerItem(0, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) self.layout.addItem(self.spacer_top) # Description self.description_label = QLabel(description) self.description_label.setWordWrap(True) - self.description_label.setAlignment(Qt.AlignCenter) + self.description_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self.layout.addWidget(self.description_label) # Selector @@ -133,7 +139,9 @@ class LaunchTile(RoundedFrame): else: self.selector = None - self.spacer_bottom = QSpacerItem(0, 0, QSizePolicy.Fixed, QSizePolicy.Expanding) + self.spacer_bottom = QSpacerItem( + 0, 0, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Expanding + ) self.layout.addItem(self.spacer_bottom) # Action button @@ -153,7 +161,7 @@ class LaunchTile(RoundedFrame): } """ ) - self.layout.addWidget(self.action_button, alignment=Qt.AlignCenter) + self.layout.addWidget(self.action_button, alignment=Qt.AlignmentFlag.AlignCenter) def _fit_label_to_width(self, label: QLabel, max_width: int, min_pt: int = 10): """ @@ -176,12 +184,13 @@ class LaunchTile(RoundedFrame): metrics = QFontMetrics(font) label.setFont(font) label.setWordWrap(False) - label.setText(metrics.elidedText(label.text(), Qt.ElideRight, max_width)) + label.setText(metrics.elidedText(label.text(), Qt.TextElideMode.ElideRight, max_width)) class LaunchWindow(BECMainWindow): RPC = True TILE_SIZE = (250, 300) + DEFAULT_LAUNCH_SIZE = (800, 600) USER_ACCESS = ["show_launcher", "hide_launcher"] def __init__( @@ -206,7 +215,7 @@ class LaunchWindow(BECMainWindow): self.toolbar = ModularToolBar(parent=self) self.addToolBar(Qt.TopToolBarArea, self.toolbar) self.spacer = QWidget(self) - self.spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.spacer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) self.toolbar.addWidget(self.spacer) self.toolbar.addWidget(self.dark_mode_button) @@ -315,7 +324,7 @@ class LaunchWindow(BECMainWindow): ) tile.setFixedWidth(self.TILE_SIZE[0]) tile.setMinimumHeight(self.TILE_SIZE[1]) - tile.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) + tile.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.MinimumExpanding) if action_button: tile.action_button.clicked.connect(action_button) if show_selector and selector_items: @@ -425,6 +434,8 @@ class LaunchWindow(BECMainWindow): from bec_widgets.applications import bw_launch with RPCRegister.delayed_broadcast() as rpc_register: + if geometry is None and launch_script != "custom_ui_file": + geometry = self._default_launch_geometry() existing_dock_areas = rpc_register.get_names_of_rpc_by_class_type(BECDockArea) if name is not None: WidgetContainerUtils.raise_for_invalid_name(name) @@ -448,13 +459,13 @@ class LaunchWindow(BECMainWindow): if launch_script == "auto_update": auto_update = kwargs.pop("auto_update", None) - return self._launch_auto_update(auto_update) + return self._launch_auto_update(auto_update, geometry=geometry) if launch_script == "widget": widget = kwargs.pop("widget", None) if widget is None: raise ValueError("Widget name must be provided.") - return self._launch_widget(widget) + return self._launch_widget(widget, geometry=geometry) launch = getattr(bw_launch, launch_script, None) if launch is None: @@ -466,13 +477,13 @@ class LaunchWindow(BECMainWindow): logger.info(f"Created new dock area: {name}") if isinstance(result_widget, BECMainWindow): - self._apply_window_geometry(result_widget, geometry) + apply_window_geometry(result_widget, geometry) result_widget.show() else: window = BECMainWindowNoRPC() window.setCentralWidget(result_widget) window.setWindowTitle(f"BEC - {result_widget.objectName()}") - self._apply_window_geometry(window, geometry) + apply_window_geometry(window, geometry) window.show() return result_widget @@ -508,12 +519,14 @@ class LaunchWindow(BECMainWindow): window.setCentralWidget(loaded) window.setWindowTitle(f"BEC - {filename}") - self._apply_window_geometry(window, None) + apply_window_geometry(window, None) window.show() logger.info(f"Launched custom UI: {filename}, type: {type(window).__name__}") return window - def _launch_auto_update(self, auto_update: str) -> AutoUpdates: + def _launch_auto_update( + self, auto_update: str, geometry: tuple[int, int, int, int] | None = None + ) -> AutoUpdates: if auto_update in self.available_auto_updates: auto_update_cls = self.available_auto_updates[auto_update] window = auto_update_cls() @@ -524,11 +537,13 @@ class LaunchWindow(BECMainWindow): window.resize(window.minimumSizeHint()) window.setWindowTitle(f"BEC - {window.objectName()}") - self._apply_window_geometry(window, None) + apply_window_geometry(window, geometry) window.show() return window - def _launch_widget(self, widget: type[BECWidget]) -> QWidget: + def _launch_widget( + self, widget: type[BECWidget], geometry: tuple[int, int, int, int] | None = None + ) -> QWidget: name = pascal_to_snake(widget.__name__) WidgetContainerUtils.raise_for_invalid_name(name) @@ -541,7 +556,7 @@ class LaunchWindow(BECMainWindow): window.setCentralWidget(widget_instance) window.resize(window.minimumSizeHint()) window.setWindowTitle(f"BEC - {widget_instance.objectName()}") - self._apply_window_geometry(window, None) + apply_window_geometry(window, geometry) window.show() return window @@ -589,30 +604,9 @@ class LaunchWindow(BECMainWindow): raise ValueError(f"Widget {widget} not found in available widgets.") return self.launch("widget", widget=self.available_widgets[widget]) - def _apply_window_geometry( - self, window: QWidget, geometry: tuple[int, int, int, int] | None - ) -> None: - """Apply a provided geometry or center the window with an 80% layout.""" - if geometry is not None: - window.setGeometry(*geometry) - return - default_geometry = self._default_window_geometry(window) - if default_geometry is not None: - window.setGeometry(*default_geometry) - else: - window.resize(window.minimumSizeHint()) - - @staticmethod - def _default_window_geometry(window: QWidget) -> tuple[int, int, int, int] | None: - screen = window.screen() or QApplication.primaryScreen() - if screen is None: - return None - available = screen.availableGeometry() - width = int(available.width() * 0.8) - height = int(available.height() * 0.8) - x = available.x() + (available.width() - width) // 2 - y = available.y() + (available.height() - height) // 2 - return x, y, width, height + def _default_launch_geometry(self) -> tuple[int, int, int, int] | None: + width, height = self.DEFAULT_LAUNCH_SIZE + return centered_geometry_for_app(width=width, height=height) @SafeSlot(popup_error=True) def _open_custom_ui_file(self): @@ -703,7 +697,7 @@ class LaunchWindow(BECMainWindow): self.hide() -if __name__ == "__main__": +if __name__ == "__main__": # pragma: no cover import sys from bec_widgets.utils.colors import apply_theme diff --git a/bec_widgets/applications/main_app.py b/bec_widgets/applications/main_app.py index aef39ff8..163132dd 100644 --- a/bec_widgets/applications/main_app.py +++ b/bec_widgets/applications/main_app.py @@ -7,6 +7,11 @@ from bec_widgets.applications.views.developer_view.developer_view import Develop from bec_widgets.applications.views.device_manager_view.device_manager_view import DeviceManagerView from bec_widgets.applications.views.view import ViewBase, WaveformViewInline, WaveformViewPopup from bec_widgets.utils.colors import apply_theme +from bec_widgets.utils.screen_utils import ( + apply_centered_size, + available_screen_geometry, + main_app_size_for_screen, +) from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow @@ -211,25 +216,12 @@ def main(): # pragma: no cover apply_theme("dark") w = BECMainApp(show_examples=args.examples) - screen = app.primaryScreen() - screen_geometry = screen.availableGeometry() - screen_width = screen_geometry.width() - screen_height = screen_geometry.height() - # 70% of screen height, keep 16:9 ratio - height = int(screen_height * 0.9) - width = int(height * (16 / 9)) - - # If width exceeds screen width, scale down - if width > screen_width * 0.9: - width = int(screen_width * 0.9) - height = int(width / (16 / 9)) - - w.resize(width, height) - - # Center the window on the screen - x = screen_geometry.x() + (screen_geometry.width() - width) // 2 - y = screen_geometry.y() + (screen_geometry.height() - height) // 2 - w.move(x, y) + screen_geometry = available_screen_geometry() + if screen_geometry is not None: + width, height = main_app_size_for_screen(screen_geometry) + apply_centered_size(w, width, height, available=screen_geometry) + else: + w.resize(w.minimumSizeHint()) w.show() diff --git a/bec_widgets/utils/screen_utils.py b/bec_widgets/utils/screen_utils.py new file mode 100644 index 00000000..122086c6 --- /dev/null +++ b/bec_widgets/utils/screen_utils.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from qtpy.QtWidgets import QApplication, QWidget + +if TYPE_CHECKING: # pragma: no cover + from qtpy.QtCore import QRect + + +def available_screen_geometry(*, widget: QWidget | None = None) -> QRect | None: + """ + Get the available geometry of the screen associated with the given widget or application. + + Args: + widget(QWidget | None): The widget to get the screen from. + Returns: + QRect | None: The available geometry of the screen, or None if no screen is found. + """ + screen = widget.screen() if widget is not None else None + if screen is None: + app = QApplication.instance() + screen = app.primaryScreen() if app is not None else None + if screen is None: + return None + return screen.availableGeometry() + + +def centered_geometry(available: "QRect", width: int, height: int) -> tuple[int, int, int, int]: + """ + Calculate centered geometry within the available rectangle. + + Args: + available(QRect): The available rectangle to center within. + width(int): The desired width. + height(int): The desired height. + + Returns: + tuple[int, int, int, int]: The (x, y, width, height) of the centered geometry. + """ + x = available.x() + (available.width() - width) // 2 + y = available.y() + (available.height() - height) // 2 + return x, y, width, height + + +def centered_geometry_for_app(width: int, height: int) -> tuple[int, int, int, int] | None: + available = available_screen_geometry() + if available is None: + return None + return centered_geometry(available, width, height) + + +def scaled_centered_geometry_for_window( + window: QWidget, *, width_ratio: float = 0.8, height_ratio: float = 0.8 +) -> tuple[int, int, int, int] | None: + available = available_screen_geometry(widget=window) + if available is None: + return None + width = int(available.width() * width_ratio) + height = int(available.height() * height_ratio) + return centered_geometry(available, width, height) + + +def apply_window_geometry( + window: QWidget, + geometry: tuple[int, int, int, int] | None, + *, + width_ratio: float = 0.8, + height_ratio: float = 0.8, +) -> None: + if geometry is not None: + window.setGeometry(*geometry) + return + default_geometry = scaled_centered_geometry_for_window( + window, width_ratio=width_ratio, height_ratio=height_ratio + ) + if default_geometry is not None: + window.setGeometry(*default_geometry) + else: + window.resize(window.minimumSizeHint()) + + +def main_app_size_for_screen(available: "QRect") -> tuple[int, int]: + height = int(available.height() * 0.9) + width = int(height * (16 / 9)) + if width > available.width() * 0.9: + width = int(available.width() * 0.9) + height = int(width / (16 / 9)) + return width, height + + +def apply_centered_size( + window: QWidget, width: int, height: int, *, available: "QRect" | None = None +) -> None: + if available is None: + available = available_screen_geometry(widget=window) + if available is None: + window.resize(width, height) + return + window.setGeometry(*centered_geometry(available, width, height)) diff --git a/tests/unit_tests/test_screen_utils.py b/tests/unit_tests/test_screen_utils.py new file mode 100644 index 00000000..32d469b3 --- /dev/null +++ b/tests/unit_tests/test_screen_utils.py @@ -0,0 +1,38 @@ +from qtpy.QtCore import QRect +from qtpy.QtWidgets import QWidget + +from bec_widgets.utils.screen_utils import ( + apply_centered_size, + centered_geometry, + main_app_size_for_screen, +) + + +def test_centered_geometry_returns_expected_tuple(): + available = QRect(100, 50, 800, 600) + result = centered_geometry(available, 400, 300) + assert result == (300, 200, 400, 300) + + +def test_main_app_size_for_screen_respects_16_9_and_screen_caps(): + available = QRect(0, 0, 1920, 1080) + width, height = main_app_size_for_screen(available) + assert (width, height) == (1728, 972) + + narrow = QRect(0, 0, 1000, 800) + width, height = main_app_size_for_screen(narrow) + assert (width, height) == (900, 506) + + +def test_apply_centered_size_uses_provided_geometry(qtbot): + widget = QWidget() + qtbot.addWidget(widget) + + available = QRect(10, 20, 600, 400) + apply_centered_size(widget, 200, 100, available=available) + + geometry = widget.geometry() + assert geometry.x() == 210 + assert geometry.y() == 170 + assert geometry.width() == 200 + assert geometry.height() == 100