fix(widgets): remove CompactPopupWidget and all its usage

This commit is contained in:
wyzula_j authored and Jan Wyzula committed 2026-10-15 11:45:08 +02:00
1 parent 703513b175
commit c9a836f850
6 files changed
+43 -361

No files matched your search

-263
View File
@@ -1,263 +0,0 @@
import time
from types import SimpleNamespace
from bec_qthemes import material_icon
from qtpy.QtCore import Property, Qt, Signal
from qtpy.QtGui import QColor
from qtpy.QtWidgets import (
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QSizePolicy,
QSpacerItem,
QToolButton,
QVBoxLayout,
QWidget,
)
from bec_widgets.utils.colors import get_accent_colors
class LedLabel(QLabel):
success_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.145, y1:0.16, x2:1, y2:1, stop:0 %s, stop:1 %s);"
emergency_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.145, y1:0.16, x2:0.92, y2:0.988636, stop:0 %s, stop:1 %s);"
warning_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.232, y1:0.272, x2:0.98, y2:0.959773, stop:0 %s, stop:1 %s);"
default_led = "color: white;border-radius: 10;background-color: qlineargradient(spread:pad, x1:0.04, y1:0.0565909, x2:0.799, y2:0.795, stop:0 %s, stop:1 %s);"
def __init__(self, parent=None):
super().__init__(parent)
self.palette = get_accent_colors()
if self.palette is None:
# no theme!
self.palette = SimpleNamespace(
default=QColor("blue"),
success=QColor("green"),
warning=QColor("orange"),
emergency=QColor("red"),
)
self.setState("default")
self.setFixedSize(20, 20)
def setState(self, state: str):
match state:
case "success":
r, g, b, a = self.palette.success.getRgb()
self.setStyleSheet(
LedLabel.success_led
% (
f"rgba({r},{g},{b},{a})",
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
)
)
case "default":
r, g, b, a = self.palette.default.getRgb()
self.setStyleSheet(
LedLabel.default_led
% (
f"rgba({r},{g},{b},{a})",
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
)
)
case "warning":
r, g, b, a = self.palette.warning.getRgb()
self.setStyleSheet(
LedLabel.warning_led
% (
f"rgba({r},{g},{b},{a})",
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
)
)
case "emergency":
r, g, b, a = self.palette.emergency.getRgb()
self.setStyleSheet(
LedLabel.emergency_led
% (
f"rgba({r},{g},{b},{a})",
f"rgba({int(r*0.8)},{int(g*0.8)},{int(b*0.8)},{a})",
)
)
case unknown_state:
raise ValueError(
f"Unknown state {repr(unknown_state)}, must be one of default, success, warning or emergency"
)
class PopupDialog(QDialog):
def __init__(self, content_widget):
self.parent = content_widget.parent()
self.content_widget = content_widget
super().__init__(self.parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.content_widget.setParent(self)
QVBoxLayout(self)
self.layout().addWidget(self.content_widget)
self.content_widget.setVisible(True)
def closeEvent(self, event):
self.content_widget.setVisible(False)
self.content_widget.setParent(self.parent)
self.done(True)
class CompactPopupWidget(QWidget):
"""Container widget, that can display its content or have a compact form,
in this case clicking on a small button pops the contained widget up.
In the compact form, a LED-like indicator shows a status indicator.
"""
expand = Signal(bool)
def __init__(self, parent=None, layout=QVBoxLayout):
super().__init__(parent)
self._popup_window = None
self._expand_popup = True
QVBoxLayout(self)
self.compact_view_widget = QWidget(self)
self.compact_view_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
QHBoxLayout(self.compact_view_widget)
self.compact_view_widget.layout().setSpacing(5)
self.compact_view_widget.layout().setContentsMargins(0, 0, 0, 0)
self.compact_view_widget.layout().addSpacerItem(
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Fixed)
)
self.compact_label = QLabel(self.compact_view_widget)
self.compact_status = LedLabel(self.compact_view_widget)
self.compact_show_popup = QToolButton(self.compact_view_widget)
self.compact_show_popup.setIcon(
material_icon(icon_name="expand_content", size=(10, 10), convert_to_pixmap=False)
)
self.compact_view_widget.layout().addWidget(self.compact_label)
self.compact_view_widget.layout().addWidget(self.compact_status)
self.compact_view_widget.layout().addWidget(self.compact_show_popup)
self.compact_view_widget.setVisible(False)
self.layout().addWidget(self.compact_view_widget)
self.container = QWidget(self)
self.layout().addWidget(self.container)
self.container.setVisible(True)
layout(self.container)
self.layout = self.container.layout()
self._compact_view = False
self.compact_show_popup.clicked.connect(self.show_popup)
def set_global_state(self, state: str):
"""Set the LED-indicator state
The LED indicator represents the 'global' state. State can be one of the
following: "default", "success", "warning", "emergency"
"""
self.compact_status.setState(state)
def show_popup(self):
"""Display the contained widgets in a popup dialog"""
if self._expand_popup:
# show popup
self._popup_window = PopupDialog(self.container)
self._popup_window.show()
self._popup_window.finished.connect(lambda: self.expand.emit(False))
self.expand.emit(True)
else:
if self.compact_view:
# expand in place
self.compact_view = False
self.compact_view_widget.setVisible(True)
self.compact_label.setVisible(False)
self.compact_status.setVisible(False)
self.compact_show_popup.setIcon(
material_icon(
icon_name="collapse_content", size=(10, 10), convert_to_pixmap=False
)
)
self.expand.emit(True)
else:
# back to compact form
self.compact_label.setVisible(True)
self.compact_status.setVisible(True)
self.compact_show_popup.setIcon(
material_icon(
icon_name="expand_content", size=(10, 10), convert_to_pixmap=False
)
)
self.compact_view = True
self.expand.emit(False)
def setSizePolicy(self, size_policy1, size_policy2=None):
# setting size policy on the compact popup widget will set
# the policy for the container, and for itself
if size_policy2 is None:
# assuming first form: setSizePolicy(QSizePolicy)
self.container.setSizePolicy(size_policy1)
QWidget.setSizePolicy(self, size_policy1)
else:
self.container.setSizePolicy(size_policy1, size_policy2)
QWidget.setSizePolicy(self, size_policy1, size_policy2)
def addWidget(self, widget):
"""Add a widget to the popup container
The popup container corresponds to the "full view" (not compact)
The widget is reparented to the container, and added to the container layout
"""
widget.setParent(self.container)
self.container.layout().addWidget(widget)
@Property(bool)
def compact_view(self):
return self._compact_view
@compact_view.setter
def compact_view(self, set_compact: bool):
"""Sets the compact form
If set_compact is True, the compact view is displayed ; otherwise,
the full view is displayed. This is handled by toggling visibility of
the container widget or the compact view widget.
"""
self._compact_view = set_compact
if set_compact:
self.compact_view_widget.setVisible(True)
self.container.setVisible(False)
QWidget.setSizePolicy(self, QSizePolicy.Fixed, QSizePolicy.Fixed)
else:
self.compact_view_widget.setVisible(False)
self.container.setVisible(True)
QWidget.setSizePolicy(self, self.container.sizePolicy())
if self.parentWidget():
self.parentWidget().adjustSize()
else:
self.adjustSize()
@Property(str)
def label(self):
return self.compact_label.text()
@label.setter
def label(self, compact_label_text: str):
"""Set the label text associated to the compact view"""
self.compact_label.setText(compact_label_text)
@Property(str)
def tooltip(self):
return self.compact_label.toolTip()
@tooltip.setter
def tooltip(self, tooltip: str):
"""Set the tooltip text associated to the compact view"""
self.compact_label.setToolTip(tooltip)
self.compact_status.setToolTip(tooltip)
@Property(bool)
def expand_popup(self):
return self._expand_popup
@expand_popup.setter
def expand_popup(self, popup: bool):
self._expand_popup = popup
+27 -15
View File
@@ -4,13 +4,20 @@ from types import GenericAlias, NoneType, UnionType
from typing import NamedTuple
from bec_lib.logger import bec_logger
from bec_qthemes import material_icon
from pydantic import BaseModel, ValidationError
from qtpy.QtCore import Signal # type: ignore
from qtpy.QtWidgets import QApplication, QGridLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget
from qtpy.QtWidgets import (
QApplication,
QGridLayout,
QHBoxLayout,
QLabel,
QSizePolicy,
QVBoxLayout,
QWidget,
)
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.compact_popup import CompactPopupWidget
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.forms_from_types import styles
from bec_widgets.utils.forms_from_types.items import (
@@ -204,14 +211,13 @@ class PydanticModelForm(TypedForm):
**kwargs,
)
self._validity = CompactPopupWidget()
self._validity.compact_view = True # type: ignore
self._validity.label = "Validity" # type: ignore
self._validity.compact_show_popup.setIcon(
material_icon(icon_name="info", size=(10, 10), convert_to_pixmap=False)
)
self._validity_message = QLabel("Not yet validated")
self._validity.addWidget(self._validity_message)
self._validity = QWidget(self)
validity_layout = QHBoxLayout(self._validity)
validity_layout.setContentsMargins(0, 0, 0, 0)
self._validity_message = QLabel("Not yet validated", self._validity)
self._validity_message.setWordWrap(True)
validity_layout.addWidget(self._validity_message)
self._validity_state: bool | None = None
self._layout.addWidget(self._validity)
self.value_changed.connect(self.validate_form)
@@ -266,6 +272,14 @@ class PydanticModelForm(TypedForm):
"""Get the entered metadata as a dict."""
return self._dict_from_grid()
def _set_validity(self, valid: bool, message: str):
"""Show the validation result inline, coloured by the theme's accent colours."""
self._validity_state = valid
accents = get_accent_colors()
color = accents.success if valid else accents.emergency
self._validity_message.setStyleSheet(f"color: {color.name()};")
self._validity_message.setText(message)
def validate_form(self, *_) -> bool:
"""validate the currently entered metadata against the pydantic schema.
If successful, returns on metadata_emitted and returns true.
@@ -273,14 +287,12 @@ class PydanticModelForm(TypedForm):
try:
metadata_dict = self.get_form_data()
self._md_schema.model_validate(metadata_dict)
self._validity.set_global_state("success")
self._validity_message.setText("No errors!")
self._set_validity(True, "No errors!")
self.form_data_updated.emit(metadata_dict)
self.validity_proc.emit(True)
return True
except ValidationError as e:
self._validity.set_global_state("emergency")
self._validity_message.setText(str(e))
self._set_validity(False, str(e))
self.form_data_cleared.emit(None)
self.validity_proc.emit(False)
return False
@@ -12,7 +12,6 @@ from qtpy.QtWidgets import QHeaderView, QLabel, QTableWidget, QTableWidgetItem,
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.compact_popup import CompactPopupWidget
from bec_widgets.utils.toolbars.actions import WidgetAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
@@ -24,7 +23,7 @@ from bec_widgets.widgets.control.buttons.stop_button.stop_button import StopButt
logger = bec_logger.logger
class BECQueue(BECWidget, CompactPopupWidget):
class BECQueue(BECWidget, QWidget):
"""
Widget to display the BEC queue.
"""
@@ -52,18 +51,16 @@ class BECQueue(BECWidget, CompactPopupWidget):
refresh_upon_start: bool = True,
**kwargs,
):
super().__init__(
parent=parent, layout=QVBoxLayout, client=client, gui_id=gui_id, config=config, **kwargs
)
self.layout.setSpacing(0)
self.layout.setContentsMargins(0, 0, 0, 0)
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
self.main_layout = QVBoxLayout(self)
self.main_layout.setSpacing(0)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self._toolbar_hidden = False
# Set up the toolbar
self.set_toolbar()
# Set up the table
self.table = QTableWidget(self)
# self.layout.addWidget(self.table)
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(
["Scan Number", "Scan Name", "Type", "Status", "Cancel"]
@@ -72,9 +69,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
header = self.table.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
self.addWidget(self.table)
self.label = "BEC Queue"
self.tooltip = "BEC Queue status"
self.main_layout.addWidget(self.table)
self.bec_dispatcher.connect_slot(self.update_queue, MessageEndpoints.scan_queue_status())
self.reset_content()
@@ -108,7 +103,7 @@ class BECQueue(BECWidget, CompactPopupWidget):
self.toolbar.add_bundle(control_bundle)
self.toolbar.show_bundles(["queue_label", "control"])
self.addWidget(self.toolbar)
self.main_layout.addWidget(self.toolbar)
@Property(bool)
def hide_toolbar(self):
@@ -222,7 +217,6 @@ class BECQueue(BECWidget, CompactPopupWidget):
if all(item.status in ("STOPPED", "COMPLETED", "IDLE") for item in queue_info)
else True
)
self.set_global_state("warning" if busy else "default")
self.queue_busy.emit(busy)
def format_item(self, content: str, status=False, tooltip: str = "") -> QTableWidgetItem:
@@ -11,10 +11,9 @@ from typing import TYPE_CHECKING
from bec_lib.utils.import_utils import lazy_import_from
from pydantic import BaseModel
from qtpy.QtCore import QObject, QTimer, Signal, Slot
from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem
from qtpy.QtWidgets import QHBoxLayout, QTreeWidget, QTreeWidgetItem, QWidget
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.compact_popup import CompactPopupWidget
from bec_widgets.widgets.services.bec_status_box.status_item import StatusItem
if TYPE_CHECKING: # pragma: no cover
@@ -65,7 +64,7 @@ class BECServiceStatusMixin(QObject):
self._service_update_timer.deleteLater()
class BECStatusBox(BECWidget, CompactPopupWidget):
class BECStatusBox(BECWidget, QWidget):
"""An autonomous widget to display the status of BEC services.
Args:
@@ -92,7 +91,8 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
gui_id: str = None,
**kwargs,
):
super().__init__(parent=parent, layout=QHBoxLayout, client=client, gui_id=gui_id, **kwargs)
super().__init__(parent=parent, client=client, gui_id=gui_id, **kwargs)
self.main_layout = QHBoxLayout(self)
self.box_name = box_name
self.status_container = defaultdict(lambda: {"info": None, "item": None, "widget": None})
@@ -101,13 +101,11 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
bec_service_status_mixin = BECServiceStatusMixin(self, client=self.client)
self.bec_service_status = bec_service_status_mixin
self.label = box_name
self.tooltip = "BEC servers health status"
self.init_ui()
self.bec_service_status.services_update.connect(self.update_service_status)
self.bec_core_state.connect(self.update_top_item_status)
self.tree.itemDoubleClicked.connect(self.on_tree_item_double_clicked)
self.addWidget(self.tree)
self.main_layout.addWidget(self.tree)
def init_ui(self) -> None:
"""Init the UI for the BECStatusBox widget, should only take place once."""
@@ -171,7 +169,6 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
status (BECStatus): The state of the core services.
"""
self.status_container[self.box_name]["info"].status = status
self.set_global_state("emergency" if status == "NOTCONNECTED" else "success")
self.service_update.emit(self.status_container[self.box_name]["info"])
def _update_status_container(
@@ -1,59 +0,0 @@
# pylint: skip-file
from unittest import mock
import pytest
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
from bec_widgets.utils.compact_popup import CompactPopupWidget
class ContainedWidget(QWidget):
def __init__(self, parent):
super().__init__(parent)
class TestCompactPopupWidget(CompactPopupWidget):
def __init__(self):
super().__init__(layout=QVBoxLayout)
self.contained = QWidget(self)
self.addWidget(self.contained)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
@pytest.fixture
def compact_popup(qtbot):
widget = TestCompactPopupWidget()
qtbot.addWidget(widget)
widget.show()
qtbot.wait_until(widget.isVisible)
yield widget
def test_size_policy(compact_popup):
csp = compact_popup.sizePolicy()
assert csp.horizontalPolicy() == QSizePolicy.Expanding
assert csp.verticalPolicy() == QSizePolicy.Minimum
compact_popup.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
csp = compact_popup.sizePolicy()
assert csp.horizontalPolicy() == QSizePolicy.Minimum
assert csp.verticalPolicy() == QSizePolicy.Expanding
compact_popup.compact_view = True
csp = compact_popup.sizePolicy()
assert csp.horizontalPolicy() == QSizePolicy.Fixed
assert csp.verticalPolicy() == QSizePolicy.Fixed
compact_popup.compact_view = False
csp = compact_popup.sizePolicy()
assert csp.horizontalPolicy() == QSizePolicy.Minimum
assert csp.verticalPolicy() == QSizePolicy.Expanding
def test_open_full_view(qtbot, compact_popup):
qtbot.waitUntil(compact_popup.container.isVisible, timeout=1000)
compact_popup.compact_view = True
qtbot.waitUntil(compact_popup.compact_view_widget.isVisible, timeout=1000)
qtbot.mouseClick(compact_popup.compact_show_popup, Qt.LeftButton)
qtbot.waitUntil(compact_popup.container.isVisible, timeout=1000)
compact_popup._popup_window.close()
qtbot.waitUntil(lambda: not compact_popup.container.isVisible(), timeout=1000)
+4 -3
View File
@@ -155,16 +155,17 @@ def test_grid_to_dict(metadata_widget: tuple[ScanMetadata, dict[str, DynamicForm
def test_validation(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]):
widget, components = metadata_widget = metadata_widget
assert widget._validity.compact_status.styleSheet().startswith(
widget._validity.compact_status.default_led[:114]
)
assert widget._validity_state is None
assert widget._validity_message.text() == "Not yet validated"
fill_components(components)
widget.validate_form()
assert widget._validity_state is True
assert widget._validity_message.text() == "No errors!"
components["bool_required_nodefault"]._main_widget.clear()
widget.validate_form()
assert widget._validity_state is False
assert "Input should be a valid boolean" in widget._validity_message.text()
components["bool_required_nodefault"].setValue(True)