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:
@ -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 per‑instance 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 y‑offset.
|
||||||
|
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 main‑label 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(
|
||||||
|
@ -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 tile’s 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}"
|
||||||
|
Reference in New Issue
Block a user