mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-05 04:48:40 +02:00
wip pill design with background
This commit is contained in:
@@ -9,7 +9,7 @@ from bec_lib import bl_states
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import QByteArray, QEvent, QMimeData, QPoint, Qt, QTimer, Signal, Slot
|
||||
from qtpy.QtGui import QColor, QDrag, QPalette
|
||||
from qtpy.QtGui import QColor, QCursor, QDrag, QPalette
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
@@ -17,6 +17,7 @@ from qtpy.QtWidgets import (
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFormLayout,
|
||||
QGraphicsDropShadowEffect,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
@@ -89,6 +90,8 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
super().__init__(
|
||||
parent=parent, client=client, config=config, gui_id=gui_id, theme_update=True, **kwargs
|
||||
)
|
||||
self.setObjectName("BeamlineStatePill")
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
||||
self._state_name: str | None = None
|
||||
self._title: str | None = None
|
||||
self._state_config: dict[str, Any] = {}
|
||||
@@ -96,14 +99,23 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._label = "No state information available."
|
||||
self._flash_active = False
|
||||
self._expanded = False
|
||||
self._hovered = False
|
||||
self._drag_payload_mode = "config"
|
||||
self._drag_start_position: QPoint | None = None
|
||||
self._drag_started = False
|
||||
self.setMouseTracking(True)
|
||||
|
||||
self._flash_timer = QTimer(self)
|
||||
self._flash_timer.setSingleShot(True)
|
||||
self._flash_timer.timeout.connect(self._clear_state_flash)
|
||||
|
||||
self._shadow = QGraphicsDropShadowEffect(self)
|
||||
self._shadow.setBlurRadius(18)
|
||||
self._shadow.setOffset(0, 2)
|
||||
self._shadow.setColor(QColor(0, 0, 0, 120))
|
||||
self._shadow.setEnabled(False)
|
||||
self.setGraphicsEffect(self._shadow)
|
||||
|
||||
self._header = QWidget(self)
|
||||
self._header.setObjectName("beamline_state_header")
|
||||
self._header.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
@@ -221,20 +233,14 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
layout.addWidget(self._settings)
|
||||
self.setLayout(layout)
|
||||
|
||||
for widget in (
|
||||
self._header,
|
||||
self._stripe,
|
||||
self._icon_label,
|
||||
self._name_label,
|
||||
self._status_label,
|
||||
self._detail_label,
|
||||
):
|
||||
for widget in self._hover_widgets():
|
||||
widget.installEventFilter(self)
|
||||
|
||||
self.setMinimumHeight(58)
|
||||
self.set_state_name(state_name, title=title)
|
||||
|
||||
def eventFilter(self, watched: object, event: QEvent) -> bool: # noqa: N802
|
||||
hover_widgets = set(self._hover_widgets())
|
||||
draggable_widgets = {
|
||||
self._header,
|
||||
self._stripe,
|
||||
@@ -243,6 +249,14 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._status_label,
|
||||
self._detail_label,
|
||||
}
|
||||
if watched in hover_widgets:
|
||||
if event.type() == QEvent.Type.Enter:
|
||||
self._set_hovered(True)
|
||||
return False
|
||||
if event.type() == QEvent.Type.Leave:
|
||||
QTimer.singleShot(0, self._sync_hover_state)
|
||||
return False
|
||||
|
||||
if watched not in draggable_widgets:
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
@@ -277,6 +291,14 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
return True
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def enterEvent(self, event: QEvent) -> None: # noqa: N802
|
||||
self._set_hovered(True)
|
||||
super().enterEvent(event)
|
||||
|
||||
def leaveEvent(self, event: QEvent) -> None: # noqa: N802
|
||||
QTimer.singleShot(0, self._sync_hover_state)
|
||||
super().leaveEvent(event)
|
||||
|
||||
@property
|
||||
def state_name(self) -> str | None:
|
||||
"""Name of the BEC beamline state displayed by this pill."""
|
||||
@@ -388,8 +410,20 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
colors = self._state_colors(self._status)
|
||||
accent = colors["accent"]
|
||||
on_accent = colors["on_accent"]
|
||||
active_card = self._hovered or self._expanded
|
||||
border = colors["flash_border"] if self._flash_active else colors["border"]
|
||||
background = colors["flash_background"] if self._flash_active else colors["background"]
|
||||
if active_card:
|
||||
background = (
|
||||
"qlineargradient("
|
||||
"x1:0, y1:0, x2:1, y2:0, "
|
||||
f"stop:0 {colors['gradient_accent']}, "
|
||||
f"stop:0.62 {colors['card_background']}, "
|
||||
f"stop:1 {colors['card_background']}"
|
||||
")"
|
||||
)
|
||||
border = colors["card_border"]
|
||||
self._shadow.setEnabled(active_card)
|
||||
|
||||
icon_name = self._STATUS_ICONS[self._status]
|
||||
self._icon_label.setPixmap(
|
||||
@@ -401,10 +435,13 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._detail_label.setText(self._label)
|
||||
self.setToolTip(self._label)
|
||||
self.setStyleSheet(
|
||||
"BeamlineStatePill {"
|
||||
f"background-color: {background};"
|
||||
"#BeamlineStatePill {"
|
||||
f"background: {background};"
|
||||
f"border: 1px solid {border};"
|
||||
"border-radius: 8px;"
|
||||
f"border-radius: {'12px' if active_card else '8px'};"
|
||||
"}"
|
||||
"QWidget#beamline_state_header {"
|
||||
"background: transparent;"
|
||||
"}"
|
||||
"QWidget#beamline_state_stripe {"
|
||||
f"background-color: {accent};"
|
||||
@@ -428,6 +465,7 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
"font-size: 11px;"
|
||||
"}"
|
||||
"QWidget#beamline_state_settings {"
|
||||
"background: transparent;"
|
||||
f"border-top: 1px solid {colors['border']};"
|
||||
"}"
|
||||
"QPushButton#beamline_state_remove_button {"
|
||||
@@ -448,6 +486,16 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
self._settings.setVisible(self._expanded)
|
||||
self._apply_visual_state()
|
||||
|
||||
def _set_hovered(self, hovered: bool) -> None:
|
||||
if hovered == self._hovered:
|
||||
return
|
||||
self._hovered = hovered
|
||||
self._apply_visual_state()
|
||||
|
||||
def _sync_hover_state(self) -> None:
|
||||
inside = self.rect().contains(self.mapFromGlobal(QCursor.pos()))
|
||||
self._set_hovered(inside)
|
||||
|
||||
def _populate_settings(self) -> None:
|
||||
state_type = self._state_type()
|
||||
self._state_type_value.setText(state_type or "-")
|
||||
@@ -634,6 +682,27 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
if enabled:
|
||||
spin_box.setValue(float(value))
|
||||
|
||||
def _hover_widgets(self) -> tuple[QWidget, ...]:
|
||||
return (
|
||||
self,
|
||||
self._header,
|
||||
self._stripe,
|
||||
self._icon_label,
|
||||
self._name_label,
|
||||
self._status_label,
|
||||
self._detail_label,
|
||||
self._expand_button,
|
||||
self._settings,
|
||||
self._title_edit,
|
||||
self._device_edit,
|
||||
self._signal_edit,
|
||||
self._low_limit.parentWidget(),
|
||||
self._high_limit.parentWidget(),
|
||||
self._tolerance,
|
||||
self._update_button,
|
||||
self._remove_button,
|
||||
)
|
||||
|
||||
def _settings_input_fields(self) -> tuple[QWidget, ...]:
|
||||
return (
|
||||
self._state_type_value,
|
||||
@@ -678,6 +747,11 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
return {
|
||||
"accent": accent.name(),
|
||||
"on_accent": on_primary.name(),
|
||||
"card_background": card_bg.name(),
|
||||
"card_border": cls._blend(border, accent, 0.45).name(),
|
||||
"gradient_accent": cls._rgba(
|
||||
accent, 110 if cls._is_light_theme(theme, palette) else 62
|
||||
),
|
||||
"background": cls._blend(card_bg, accent, 0.10).name(),
|
||||
"flash_background": cls._blend(card_bg, accent, 0.22).name(),
|
||||
"border": cls._blend(border, accent, 0.35).name(),
|
||||
@@ -686,6 +760,17 @@ class BeamlineStatePill(BECWidget, QWidget):
|
||||
"muted": cls._blend(card_bg, foreground, 0.66).name(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _is_light_theme(theme: Any, palette: QPalette) -> bool:
|
||||
theme_name = str(getattr(theme, "theme", "")).lower()
|
||||
if theme_name in {"light", "dark"}:
|
||||
return theme_name == "light"
|
||||
return palette.window().color().lightness() > 128
|
||||
|
||||
@staticmethod
|
||||
def _rgba(color: QColor, alpha: int) -> str:
|
||||
return f"rgba({color.red()}, {color.green()}, {color.blue()}, {max(0, min(255, alpha))})"
|
||||
|
||||
@staticmethod
|
||||
def _theme_color(theme: Any, key: str, fallback: QColor) -> QColor:
|
||||
if theme is None:
|
||||
|
||||
@@ -92,6 +92,30 @@ def test_beamline_state_pill_expands_and_emits_updated_limits(qtbot, mocked_clie
|
||||
assert signal.args[1]["tolerance"] == 0.1
|
||||
|
||||
|
||||
def test_beamline_state_pill_uses_card_style_when_expanded(qtbot, mocked_client):
|
||||
widget = BeamlineStatePill(state_name="limits", title="Limits", client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
assert "qlineargradient" not in widget.styleSheet()
|
||||
|
||||
widget._toggle_expanded()
|
||||
|
||||
assert "qlineargradient" in widget.styleSheet()
|
||||
assert widget._shadow.isEnabled()
|
||||
|
||||
|
||||
def test_beamline_state_pill_uses_card_style_when_hovered(qtbot, mocked_client):
|
||||
widget = BeamlineStatePill(state_name="limits", title="Limits", client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
assert "qlineargradient" not in widget.styleSheet()
|
||||
|
||||
widget.eventFilter(widget._header, QEvent(QEvent.Type.Enter))
|
||||
|
||||
assert "qlineargradient" in widget.styleSheet()
|
||||
assert widget._shadow.isEnabled()
|
||||
|
||||
|
||||
def test_beamline_state_pill_drag_payload_modes(qtbot, mocked_client):
|
||||
widget = BeamlineStatePill(state_name="limits", title="Limits", client=mocked_client)
|
||||
qtbot.addWidget(widget)
|
||||
|
||||
Reference in New Issue
Block a user