0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-13 19:21:50 +02:00

fix(launch_window): font and tile size fixed across OSs, closes #607

This commit is contained in:
2025-05-26 16:03:57 +02:00
committed by Jan Wyzula
parent 1ea467c5fc
commit ada0977a1b
2 changed files with 99 additions and 3 deletions

View File

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Callable
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from qtpy.QtCore import Qt, Signal # type: ignore from qtpy.QtCore import Qt, Signal # type: ignore
from qtpy.QtGui import QPainter, QPainterPath, QPixmap from qtpy.QtGui import QFontMetrics, QPainter, QPainterPath, QPixmap
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
QApplication, QApplication,
QComboBox, QComboBox,
@ -44,6 +44,7 @@ MODULE_PATH = os.path.dirname(bec_widgets.__file__)
class LaunchTile(RoundedFrame): class LaunchTile(RoundedFrame):
DEFAULT_SIZE = (250, 300)
open_signal = Signal() open_signal = Signal()
def __init__( def __init__(
@ -54,9 +55,15 @@ class LaunchTile(RoundedFrame):
main_label: str | None = None, main_label: str | None = None,
description: str | None = None, description: str | None = None,
show_selector: bool = False, show_selector: bool = False,
tile_size: tuple[int, int] | None = None,
): ):
super().__init__(parent=parent, orientation="vertical") super().__init__(parent=parent, orientation="vertical")
# Provide a perinstance TILE_SIZE so the class can compute layout
if tile_size is None:
tile_size = self.DEFAULT_SIZE
self.tile_size = tile_size
self.icon_label = QLabel(parent=self) self.icon_label = QLabel(parent=self)
self.icon_label.setFixedSize(100, 100) self.icon_label.setFixedSize(100, 100)
self.icon_label.setScaledContents(True) self.icon_label.setScaledContents(True)
@ -87,12 +94,26 @@ class LaunchTile(RoundedFrame):
# Main label # Main label
self.main_label = QLabel(main_label) self.main_label = QLabel(main_label)
# Desired default appearance
font_main = self.main_label.font() font_main = self.main_label.font()
font_main.setPointSize(14) font_main.setPointSize(14)
font_main.setBold(True) font_main.setBold(True)
self.main_label.setFont(font_main) self.main_label.setFont(font_main)
self.main_label.setWordWrap(True)
self.main_label.setAlignment(Qt.AlignCenter) self.main_label.setAlignment(Qt.AlignCenter)
# Shrink font if the default would wrap on this platform / DPI
content_width = (
self.tile_size[0]
- self.layout.contentsMargins().left()
- self.layout.contentsMargins().right()
)
self._fit_label_to_width(self.main_label, content_width)
# Give every tile the same reserved height for the title so the
# description labels start at an identical yoffset.
self.main_label.setFixedHeight(QFontMetrics(self.main_label.font()).height() + 2)
self.layout.addWidget(self.main_label) self.layout.addWidget(self.main_label)
self.spacer_top = QSpacerItem(0, 10, QSizePolicy.Fixed, QSizePolicy.Fixed) self.spacer_top = QSpacerItem(0, 10, QSizePolicy.Fixed, QSizePolicy.Fixed)
@ -133,6 +154,29 @@ class LaunchTile(RoundedFrame):
) )
self.layout.addWidget(self.action_button, alignment=Qt.AlignCenter) self.layout.addWidget(self.action_button, alignment=Qt.AlignCenter)
def _fit_label_to_width(self, label: QLabel, max_width: int, min_pt: int = 10):
"""
Fit the label text to the specified maximum width by adjusting the font size.
Args:
label(QLabel): The label to adjust.
max_width(int): The maximum width the label can occupy.
min_pt(int): The minimum font point size to use.
"""
font = label.font()
for pt in range(font.pointSize(), min_pt - 1, -1):
font.setPointSize(pt)
metrics = QFontMetrics(font)
if metrics.horizontalAdvance(label.text()) <= max_width:
label.setFont(font)
label.setWordWrap(False)
return
# If nothing fits, fall back to eliding
metrics = QFontMetrics(font)
label.setFont(font)
label.setWordWrap(False)
label.setText(metrics.elidedText(label.text(), Qt.ElideRight, max_width))
class LaunchWindow(BECMainWindow): class LaunchWindow(BECMainWindow):
RPC = True RPC = True
@ -146,6 +190,8 @@ class LaunchWindow(BECMainWindow):
self.app = QApplication.instance() self.app = QApplication.instance()
self.tiles: dict[str, LaunchTile] = {} self.tiles: dict[str, LaunchTile] = {}
# Track the smallest mainlabel font size chosen so far
self._min_main_label_pt: int | None = None
# Toolbar # Toolbar
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True) self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
@ -250,14 +296,34 @@ class LaunchWindow(BECMainWindow):
main_label=main_label, main_label=main_label,
description=description, description=description,
show_selector=show_selector, show_selector=show_selector,
tile_size=self.TILE_SIZE,
) )
tile.setFixedSize(*self.TILE_SIZE) tile.setFixedWidth(self.TILE_SIZE[0])
tile.setMinimumHeight(self.TILE_SIZE[1])
tile.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding)
if action_button: if action_button:
tile.action_button.clicked.connect(action_button) tile.action_button.clicked.connect(action_button)
if show_selector and selector_items: if show_selector and selector_items:
tile.selector.addItems(selector_items) tile.selector.addItems(selector_items)
self.central_widget.layout.addWidget(tile) self.central_widget.layout.addWidget(tile)
# keep all tiles' main labels at a unified point size
current_pt = tile.main_label.font().pointSize()
if self._min_main_label_pt is None or current_pt < self._min_main_label_pt:
# New global minimum shrink every existing tile to this size
self._min_main_label_pt = current_pt
for t in self.tiles.values():
f = t.main_label.font()
f.setPointSize(self._min_main_label_pt)
t.main_label.setFont(f)
t.main_label.setFixedHeight(QFontMetrics(f).height() + 2)
elif current_pt > self._min_main_label_pt:
# Tile is larger than global minimum shrink it to match
f = tile.main_label.font()
f.setPointSize(self._min_main_label_pt)
tile.main_label.setFont(f)
tile.main_label.setFixedHeight(QFontMetrics(f).height() + 2)
self.tiles[name] = tile self.tiles[name] = tile
def launch( def launch(

View File

@ -5,6 +5,7 @@ from unittest import mock
import pytest import pytest
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from qtpy.QtGui import QFontMetrics
import bec_widgets import bec_widgets
from bec_widgets.applications.launch_window import LaunchWindow from bec_widgets.applications.launch_window import LaunchWindow
@ -153,3 +154,32 @@ def test_launch_window_closes(bec_launch_window, connections, close_called):
mock_hide.assert_called_once() mock_hide.assert_called_once()
close_event.accept.assert_not_called() close_event.accept.assert_not_called()
close_event.ignore.assert_called_once() close_event.ignore.assert_called_once()
def test_main_label_fits_tile_width(bec_launch_window, qtbot):
"""
Every tiles main label must render in a single line and its text
width must not exceed the usable width of the tile.
"""
for name, tile in bec_launch_window.tiles.items():
label = tile.main_label
qtbot.waitUntil(lambda: label.isVisible())
metrics = QFontMetrics(label.font())
text_width = metrics.horizontalAdvance(label.text())
content_width = (
tile.tile_size[0]
- tile.layout.contentsMargins().left()
- tile.layout.contentsMargins().right()
)
assert text_width <= content_width, f"{name} main label exceeds tile width"
# _fit_label_to_width disables wrapping, so confirm that:
assert not label.wordWrap(), f"{name} main label is wrapped"
def test_main_label_point_size_uniform(bec_launch_window):
"""
The launcher should unify all main-label font sizes to the smallest
needed size, so every tile shares the same point size.
"""
point_sizes = {tile.main_label.font().pointSize() for tile in bec_launch_window.tiles.values()}
assert len(point_sizes) == 1, f"Non-uniform main-label point sizes: {point_sizes}"