diff --git a/bec_widgets/utils/compact_popup.py b/bec_widgets/utils/compact_popup.py deleted file mode 100644 index af8b48a2..00000000 --- a/bec_widgets/utils/compact_popup.py +++ /dev/null @@ -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 diff --git a/bec_widgets/utils/forms_from_types/forms.py b/bec_widgets/utils/forms_from_types/forms.py index f8a37df2..ea0385ce 100644 --- a/bec_widgets/utils/forms_from_types/forms.py +++ b/bec_widgets/utils/forms_from_types/forms.py @@ -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 diff --git a/bec_widgets/widgets/services/bec_queue/bec_queue.py b/bec_widgets/widgets/services/bec_queue/bec_queue.py index 8936d23a..8fb314a7 100644 --- a/bec_widgets/widgets/services/bec_queue/bec_queue.py +++ b/bec_widgets/widgets/services/bec_queue/bec_queue.py @@ -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: diff --git a/bec_widgets/widgets/services/bec_status_box/bec_status_box.py b/bec_widgets/widgets/services/bec_status_box/bec_status_box.py index dd284e64..da28048f 100644 --- a/bec_widgets/widgets/services/bec_status_box/bec_status_box.py +++ b/bec_widgets/widgets/services/bec_status_box/bec_status_box.py @@ -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( diff --git a/tests/unit_tests/test_compact_popup_widget.py b/tests/unit_tests/test_compact_popup_widget.py deleted file mode 100644 index 4e75c76d..00000000 --- a/tests/unit_tests/test_compact_popup_widget.py +++ /dev/null @@ -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) diff --git a/tests/unit_tests/test_scan_metadata.py b/tests/unit_tests/test_scan_metadata.py index 8880c670..83951e78 100644 --- a/tests/unit_tests/test_scan_metadata.py +++ b/tests/unit_tests/test_scan_metadata.py @@ -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)