mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-30 08:39:50 +02:00
feat(elide_label): general elide label
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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("…")
|
||||
Reference in New Issue
Block a user