feat(elide_label): general elide label

This commit is contained in:
2026-06-15 16:14:11 +02:00
committed by Klaus Wakonig
parent 2a8a7261be
commit 1fac492b0b
4 changed files with 111 additions and 3 deletions
+46
View File
@@ -0,0 +1,46 @@
from __future__ import annotations
from qtpy.QtCore import QSize, Qt
from qtpy.QtWidgets import QLabel, QSizePolicy, QWidget
class ElidingLabel(QLabel):
"""A ``QLabel`` that elides its text with an ellipsis when too narrow to show it in full.
``QLabel`` itself has no elide support (only item views expose ``setTextElideMode``), so this
computes the elided string with ``QFontMetrics.elidedText`` and refreshes it on every resize.
``text()`` always returns the full, unelided text, so callers see the logical value while the
display shrinks. The label is allowed to shrink below its text width, so a long string never
forces its container wider or taller.
"""
def __init__(
self, parent: QWidget | None = None, mode: Qt.TextElideMode = Qt.TextElideMode.ElideRight
) -> None:
super().__init__(parent)
self._mode = mode
self._full_text = ""
self.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Preferred)
def setText(self, text: str) -> None: # noqa: N802
self._full_text = text or ""
self._elide()
def text(self) -> str:
return self._full_text
def set_elide_mode(self, mode: Qt.TextElideMode) -> None:
"""Set how the text is shortened (``ElideRight``/``ElideLeft``/``ElideMiddle``)."""
self._mode = mode
self._elide()
def minimumSizeHint(self) -> QSize: # noqa: N802
return QSize(0, super().minimumSizeHint().height())
def resizeEvent(self, event) -> None: # noqa: N802
super().resizeEvent(event)
self._elide()
def _elide(self) -> None:
super().setText(self.fontMetrics().elidedText(self._full_text, self._mode, self.width()))
@@ -24,6 +24,7 @@ from qtpy.QtWidgets import (
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import Colors, get_accent_colors, get_theme_name, rgba, theme_color
from bec_widgets.utils.eliding_label import ElidingLabel
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.forms_from_types.pydantic_widget_form import (
OptionalValueWidget,
@@ -88,6 +89,9 @@ class BeamlineStatePill(BECWidget, QWidget):
self.setObjectName("BeamlineStatePill")
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
# Floor below which the pill keeps its structure; the title/detail elide rather than
# pushing the pill wider or taller, so collapsed rows stay a consistent size.
self.setMinimumWidth(200)
self._state_name: str | None = None
self._state_config: messages.BeamlineStateConfig | None = None
self._status = "unknown"
@@ -129,7 +133,7 @@ class BeamlineStatePill(BECWidget, QWidget):
self._icon_label.setFixedSize(32, 32)
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._icon_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
self._name_label = QLabel(self)
self._name_label = ElidingLabel(self)
self._name_label.setObjectName("beamline_state_name")
self._name_label.setTextFormat(Qt.TextFormat.PlainText)
self._name_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
@@ -137,10 +141,9 @@ class BeamlineStatePill(BECWidget, QWidget):
self._status_label.setObjectName("beamline_state_status")
self._status_label.setTextFormat(Qt.TextFormat.PlainText)
self._status_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
self._detail_label = QLabel(self)
self._detail_label = ElidingLabel(self)
self._detail_label.setObjectName("beamline_state_detail")
self._detail_label.setTextFormat(Qt.TextFormat.PlainText)
self._detail_label.setWordWrap(True)
self._detail_label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
self._interlock_button = QToolButton(self)
self._interlock_button.setObjectName("beamline_state_interlock")
@@ -3,6 +3,7 @@ from bec_lib import bl_states, messages
from qtpy.QtCore import QCoreApplication, QEvent, Qt
from qtpy.QtWidgets import QMessageBox, QStyleOptionViewItem
from bec_widgets.utils.eliding_label import ElidingLabel
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.utils.widget_io import WidgetIO
from bec_widgets.widgets.services.beamline_states import beamline_state_manager as manager_module
@@ -227,6 +228,21 @@ def test_beamline_state_pill_does_not_override_themed_input_controls(qtbot, mock
assert "QCheckBox::indicator" not in stylesheet
def test_beamline_state_pill_title_and_detail_elide_without_wrapping(qtbot, mocked_client):
pill = create_widget(qtbot, BeamlineStatePill, state_name="x", client=mocked_client)
assert isinstance(pill._name_label, ElidingLabel)
assert isinstance(pill._detail_label, ElidingLabel)
# The detail no longer word-wraps, so a long message can't make a collapsed pill taller.
assert not pill._detail_label.wordWrap()
assert pill.minimumWidth() == 200
pill.update_state({"name": "x", "status": "valid", "label": "L" * 200}, {})
# A long title/detail keeps its full logical text instead of forcing the pill to grow.
assert pill._detail_label.text() == "L" * 200
def test_beamline_state_manager_adds_and_removes_pills(qtbot, mocked_client):
beamline_state_manager = create_widget(qtbot, BeamlineStateManager, client=mocked_client)
beamline_state_manager.update_available_states(
+43
View File
@@ -0,0 +1,43 @@
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QLabel
from bec_widgets.utils.eliding_label import ElidingLabel
def test_eliding_label_keeps_full_text_but_elides_display(qtbot):
label = ElidingLabel()
qtbot.addWidget(label)
full = "a very long label text that will not fit in a narrow widget"
label.setText(full)
label.resize(50, 20)
label._elide()
# The logical value is preserved, while the rendered text is shortened with an ellipsis.
assert label.text() == full
assert QLabel.text(label) != full
assert QLabel.text(label).endswith("")
def test_eliding_label_shows_full_text_when_wide_enough(qtbot):
label = ElidingLabel()
qtbot.addWidget(label)
label.setText("short")
label.resize(400, 20)
label._elide()
assert label.text() == "short"
assert QLabel.text(label) == "short"
def test_eliding_label_respects_elide_mode(qtbot):
label = ElidingLabel()
qtbot.addWidget(label)
label.setText("a very long label text that will not fit in a narrow widget")
label.resize(50, 20)
label.set_elide_mode(Qt.TextElideMode.ElideMiddle)
assert "" in QLabel.text(label)
assert not QLabel.text(label).endswith("")