mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-04 16:02:51 +01:00
fix(screen_utils): screen utilities added and fixed sizing for widgets from launch window and main app
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
100
bec_widgets/utils/screen_utils.py
Normal file
100
bec_widgets/utils/screen_utils.py
Normal file
@@ -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))
|
||||
38
tests/unit_tests/test_screen_utils.py
Normal file
38
tests/unit_tests/test_screen_utils.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user