mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-18 14:25:37 +02:00
Compare commits
1 Commits
v3.3.2
...
feature/me
| Author | SHA1 | Date | |
|---|---|---|---|
| de5a58a63b |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,25 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v3.3.2 (2026-03-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Typos
|
||||
([`3d29a67`](https://github.com/bec-project/bec_widgets/commit/3d29a67c0b2175f2f29b8e5a7befce55f3d28fd3))
|
||||
|
||||
|
||||
## v3.3.1 (2026-03-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **dap_combobox**: Added safeguard for no DAP models
|
||||
([`79af15a`](https://github.com/bec-project/bec_widgets/commit/79af15a88b993cd5b6bf730796f995f20cf6f188))
|
||||
|
||||
- **dap_combobox**: Rewritten as proper combobox
|
||||
([`90222f3`](https://github.com/bec-project/bec_widgets/commit/90222f30821f822eb24b0179401d4e43050e0156))
|
||||
|
||||
|
||||
## v3.3.0 (2026-03-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -263,7 +263,7 @@ class BECMainApp(BECMainWindow):
|
||||
developer_view_step = self.guided_tour.register_widget(
|
||||
widget=sidebar_developer_view,
|
||||
title="Developer View",
|
||||
text="Click here to access the Developer view to write scripts and macros.",
|
||||
text="Click here to access the Developer view to write scripts and makros.",
|
||||
)
|
||||
tour_steps.append(developer_view_step)
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ class DeviceManagerDisplayWidget(DockAreaWidget):
|
||||
self._upload_redis_dialog: UploadRedisDialog | None = None
|
||||
self._dialog_validation_connection: QMetaObject.Connection | None = None
|
||||
|
||||
# NOTE: We need here a separate config helper instance to avoid conflicts with
|
||||
# NOTE: We need here a seperate config helper instance to avoid conflicts with
|
||||
# other communications to REDIS as uploading a config through a CommunicationConfigAction
|
||||
# will block if we use the config_helper from self.client.config._config_helper
|
||||
self._config_helper = config_helper.ConfigHelper(self.client.connector)
|
||||
@@ -607,8 +607,8 @@ class DeviceManagerDisplayWidget(DockAreaWidget):
|
||||
self.device_table_view._is_config_in_sync_with_redis()
|
||||
)
|
||||
validation_results = self.device_table_view.get_validation_results()
|
||||
for config, config_status, connection_status in validation_results.values():
|
||||
if connection_status == ConnectionStatus.CONNECTED.value:
|
||||
for config, config_status, connnection_status in validation_results.values():
|
||||
if connnection_status == ConnectionStatus.CONNECTED.value:
|
||||
self.device_table_view.update_device_validation(
|
||||
config, config_status, ConnectionStatus.CAN_CONNECT, ""
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ logger = bec_logger.logger
|
||||
|
||||
|
||||
class _WidgetsEnumType(str, enum.Enum):
|
||||
"""Enum for the available widgets, to be generated programmatically"""
|
||||
"""Enum for the available widgets, to be generated programatically"""
|
||||
|
||||
...
|
||||
|
||||
@@ -985,7 +985,7 @@ class Curve(RPCBase):
|
||||
|
||||
|
||||
class DapComboBox(RPCBase):
|
||||
"""Editable combobox listing the available DAP models."""
|
||||
"""The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC."""
|
||||
|
||||
@rpc_call
|
||||
def select_y_axis(self, y_axis: str):
|
||||
@@ -1011,7 +1011,7 @@ class DapComboBox(RPCBase):
|
||||
Slot to update the fit model.
|
||||
|
||||
Args:
|
||||
fit_name(str): Fit model name.
|
||||
default_device(str): Default device name.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ logger = bec_logger.logger
|
||||
if self._base:
|
||||
self.content += """
|
||||
class _WidgetsEnumType(str, enum.Enum):
|
||||
\"\"\" Enum for the available widgets, to be generated programmatically \"\"\"
|
||||
\"\"\" Enum for the available widgets, to be generated programatically \"\"\"
|
||||
...
|
||||
"""
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ class BECConnector:
|
||||
)
|
||||
self.config = ConnectionConfig(widget_class=self.__class__.__name__)
|
||||
|
||||
# If the gui_id is passed, it should be respected. However, this should be revisited since
|
||||
# If the gui_id is passed, it should be respected. However, this should be revisted since
|
||||
# the gui_id has to be unique, and may no longer be.
|
||||
if gui_id:
|
||||
self.config.gui_id = gui_id
|
||||
@@ -399,7 +399,7 @@ class BECConnector:
|
||||
"""
|
||||
self.config = config
|
||||
|
||||
# FIXME some thoughts are required to decide how this should work with rpc registry
|
||||
# FIXME some thoughts are required to decide how thhis should work with rpc registry
|
||||
def apply_config(self, config: dict, generate_new_id: bool = True) -> None:
|
||||
"""
|
||||
Apply the configuration to the widget.
|
||||
@@ -417,7 +417,7 @@ class BECConnector:
|
||||
else:
|
||||
self.gui_id = self.config.gui_id
|
||||
|
||||
# FIXME some thoughts are required to decide how this should work with rpc registry
|
||||
# FIXME some thoughts are required to decide how thhis should work with rpc registry
|
||||
def load_config(self, path: str | None = None, gui: bool = False):
|
||||
"""
|
||||
Load the configuration of the widget from YAML.
|
||||
|
||||
@@ -43,7 +43,7 @@ class WidgetContainerUtils:
|
||||
if list_of_names is None:
|
||||
list_of_names = []
|
||||
ii = 0
|
||||
while ii < 1000: # 1000 is arbitrary!
|
||||
while ii < 1000: # 1000 is arbritrary!
|
||||
name_candidate = f"{name}_{ii}"
|
||||
if name_candidate not in list_of_names:
|
||||
return name_candidate
|
||||
|
||||
@@ -71,7 +71,7 @@ class FormItemSpec(BaseModel):
|
||||
"""
|
||||
The specification for an item in a dynamically generated form. Uses a pydantic FieldInfo
|
||||
to store most annotation info, since one of the main purposes is to store data for
|
||||
forms generated from pydantic models, but can also be composed from other sources or by hand.
|
||||
forms genrated from pydantic models, but can also be composed from other sources or by hand.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
@@ -192,7 +192,7 @@ class DynamicFormItem(QWidget):
|
||||
@abstractmethod
|
||||
def _add_main_widget(self) -> None:
|
||||
self._main_widget: QWidget
|
||||
"""Add the main data entry widget to self._main_widget and apply any
|
||||
"""Add the main data entry widget to self._main_widget and appply any
|
||||
constraints from the field info"""
|
||||
|
||||
@SafeSlot()
|
||||
|
||||
@@ -15,7 +15,7 @@ class Kind(IFBase):
|
||||
"""
|
||||
This is used in the .kind attribute of all OphydObj (Signals, Devices).
|
||||
|
||||
A Device examines its components' .kind attribute to decide whether to
|
||||
A Device examines its components' .kind atttribute to decide whether to
|
||||
traverse it in read(), read_configuration(), or neither. Additionally, if
|
||||
decides whether to include its name in `hints['fields']`.
|
||||
"""
|
||||
|
||||
@@ -156,7 +156,7 @@ class RPCServer:
|
||||
if method == "raise" and hasattr(
|
||||
obj, "setWindowState"
|
||||
): # special case for raising windows, should work even if minimized
|
||||
# this is a special case for raising windows for gnome on Red Hat (RHEL) 9 systems where changing focus is suppressed by default
|
||||
# this is a special case for raising windows for gnome on rethat 9 systems where changing focus is supressed by default
|
||||
# The procedure is as follows:
|
||||
# 1. Get the current window state to check if the window is minimized and remove minimized flag
|
||||
# 2. Then in order to force gnome to raise the window, we set the window to stay on top temporarily
|
||||
@@ -442,5 +442,5 @@ class RPCServer:
|
||||
self.status = messages.BECStatus.IDLE
|
||||
self._heartbeat_timer.stop()
|
||||
self.emit_heartbeat()
|
||||
logger.info("Succeeded in shutting down CLI server")
|
||||
logger.info("Succeded in shutting down CLI server")
|
||||
self.client.shutdown()
|
||||
|
||||
@@ -224,7 +224,7 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
|
||||
self.bec_dispatcher.connect_slot(slot, MessageEndpoints.device_readback(new_device))
|
||||
|
||||
def _toggle_enable_buttons(self, ui: DeviceUpdateUIComponents, enable: bool) -> None:
|
||||
"""Toggle enable/disable on available buttons
|
||||
"""Toogle enable/disable on available buttons
|
||||
|
||||
Args:
|
||||
enable (bool): Enable buttons
|
||||
|
||||
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
from .available_device_group import AvailableDeviceGroup
|
||||
|
||||
|
||||
class _DeviceListWidget(QListWidget):
|
||||
class _DeviceListWiget(QListWidget):
|
||||
|
||||
def _item_iter(self):
|
||||
return (self.item(i) for i in range(self.count()))
|
||||
@@ -44,7 +44,7 @@ class Ui_AvailableDeviceGroup(object):
|
||||
self.n_included.setObjectName("n_included")
|
||||
title_layout.addWidget(self.n_included)
|
||||
|
||||
self.device_list = _DeviceListWidget(AvailableDeviceGroup)
|
||||
self.device_list = _DeviceListWiget(AvailableDeviceGroup)
|
||||
self.device_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
|
||||
self.device_list.setObjectName("device_list")
|
||||
self.device_list.setFrameStyle(0)
|
||||
|
||||
@@ -34,13 +34,13 @@ class HashModel(str, Enum):
|
||||
class DeviceResourceBackend(Protocol):
|
||||
@property
|
||||
def tag_groups(self) -> dict[str, set[HashableDevice]]:
|
||||
"""A dictionary of all available devices separated by tag groups. The same device may
|
||||
"""A dictionary of all availble devices separated by tag groups. The same device may
|
||||
appear more than once (in different groups)."""
|
||||
...
|
||||
|
||||
@property
|
||||
def all_devices(self) -> set[HashableDevice]:
|
||||
"""A set of all available devices. The same device may not appear more than once."""
|
||||
"""A set of all availble devices. The same device may not appear more than once."""
|
||||
...
|
||||
|
||||
@property
|
||||
|
||||
@@ -347,14 +347,14 @@ class ScanGroupBox(QGroupBox):
|
||||
|
||||
def get_parameters(self, device_object: bool = True):
|
||||
"""
|
||||
Returns the parameters from the widgets in the scan control layout formatted to run scan from BEC.
|
||||
Returns the parameters from the widgets in the scan control layout formated to run scan from BEC.
|
||||
"""
|
||||
if self.box_type == "args":
|
||||
return self._get_arg_parameters(device_object=device_object)
|
||||
return self._get_arg_parameterts(device_object=device_object)
|
||||
elif self.box_type == "kwargs":
|
||||
return self._get_kwarg_parameters(device_object=device_object)
|
||||
|
||||
def _get_arg_parameters(self, device_object: bool = True):
|
||||
def _get_arg_parameterts(self, device_object: bool = True):
|
||||
args = []
|
||||
for i in range(1, self.layout.rowCount()):
|
||||
for j in range(self.layout.columnCount()):
|
||||
|
||||
@@ -2,19 +2,22 @@
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Property, Signal, Slot
|
||||
from qtpy.QtWidgets import QComboBox
|
||||
from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class DapComboBox(BECWidget, QComboBox):
|
||||
class DapComboBox(BECWidget, QWidget):
|
||||
"""
|
||||
Editable combobox listing the available DAP models.
|
||||
The DAPComboBox widget is an extension to the QComboBox with all avaialble DAP model from BEC.
|
||||
|
||||
The widget behaves as a plain QComboBox and keeps ``fit_model_combobox`` as an alias to itself
|
||||
for backwards compatibility with older call sites.
|
||||
Args:
|
||||
parent: Parent widget.
|
||||
client: BEC client object.
|
||||
gui_id: GUI ID.
|
||||
default: Default device name.
|
||||
"""
|
||||
|
||||
ICON_NAME = "data_exploration"
|
||||
@@ -42,20 +45,19 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, **kwargs)
|
||||
self.fit_model_combobox = self # Just for backwards compatibility with older call sites, the widget itself is the combobox
|
||||
self._available_models: list[str] = []
|
||||
self.layout = QVBoxLayout(self)
|
||||
self.fit_model_combobox = QComboBox(self)
|
||||
self.layout.addWidget(self.fit_model_combobox)
|
||||
self.layout.setContentsMargins(0, 0, 0, 0)
|
||||
self._available_models = None
|
||||
self._x_axis = None
|
||||
self._y_axis = None
|
||||
self._is_valid_input = False
|
||||
|
||||
self.setEditable(True)
|
||||
|
||||
self.populate_fit_model_combobox()
|
||||
self.currentTextChanged.connect(self._on_text_changed)
|
||||
self.fit_model_combobox.currentTextChanged.connect(self._update_current_fit)
|
||||
# Set default fit model
|
||||
self.select_default_fit(default_fit)
|
||||
self.check_validity(self.currentText())
|
||||
|
||||
def select_default_fit(self, default_fit: str | None = "GaussianModel"):
|
||||
def select_default_fit(self, default_fit: str | None):
|
||||
"""Set the default fit model.
|
||||
|
||||
Args:
|
||||
@@ -63,8 +65,8 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
"""
|
||||
if self._validate_dap_model(default_fit):
|
||||
self.select_fit_model(default_fit)
|
||||
elif self.available_models:
|
||||
self.select_fit_model(self.available_models[0])
|
||||
else:
|
||||
self.select_fit_model("GaussianModel")
|
||||
|
||||
@property
|
||||
def available_models(self):
|
||||
@@ -112,40 +114,12 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
self._y_axis = y_axis
|
||||
self.y_axis_updated.emit(y_axis)
|
||||
|
||||
@Slot(str)
|
||||
def _on_text_changed(self, fit_name: str):
|
||||
"""
|
||||
Validate and emit updates for the current text.
|
||||
|
||||
Args:
|
||||
fit_name(str): The current text in the combobox, representing the selected fit model.
|
||||
"""
|
||||
self.check_validity(fit_name)
|
||||
if not self._is_valid_input:
|
||||
return
|
||||
|
||||
def _update_current_fit(self, fit_name: str):
|
||||
"""Update the current fit."""
|
||||
self.fit_model_updated.emit(fit_name)
|
||||
if self.x_axis is not None and self.y_axis is not None:
|
||||
self.new_dap_config.emit(self._x_axis, self._y_axis, fit_name)
|
||||
|
||||
@Slot(str)
|
||||
def check_validity(self, fit_name: str):
|
||||
"""
|
||||
Highlight invalid manual entries similarly to DeviceComboBox.
|
||||
|
||||
Args:
|
||||
fit_name(str): The current text in the combobox, representing the selected fit model.
|
||||
"""
|
||||
if self._validate_dap_model(fit_name):
|
||||
self._is_valid_input = True
|
||||
self.setStyleSheet("border: 1px solid transparent;")
|
||||
else:
|
||||
self._is_valid_input = False
|
||||
if self.isEnabled():
|
||||
self.setStyleSheet("border: 1px solid red;")
|
||||
else:
|
||||
self.setStyleSheet("border: 1px solid transparent;")
|
||||
|
||||
@Slot(str)
|
||||
def select_x_axis(self, x_axis: str):
|
||||
"""Slot to update the x axis.
|
||||
@@ -154,7 +128,7 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
x_axis(str): X axis.
|
||||
"""
|
||||
self.x_axis = x_axis
|
||||
self._on_text_changed(self.currentText())
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_y_axis(self, y_axis: str):
|
||||
@@ -164,26 +138,25 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
y_axis(str): Y axis.
|
||||
"""
|
||||
self.y_axis = y_axis
|
||||
self._on_text_changed(self.currentText())
|
||||
self._update_current_fit(self.fit_model_combobox.currentText())
|
||||
|
||||
@Slot(str)
|
||||
def select_fit_model(self, fit_name: str | None):
|
||||
"""Slot to update the fit model.
|
||||
|
||||
Args:
|
||||
fit_name(str): Fit model name.
|
||||
default_device(str): Default device name.
|
||||
"""
|
||||
if not self._validate_dap_model(fit_name):
|
||||
raise ValueError(f"Fit {fit_name} is not valid.")
|
||||
self.setCurrentText(fit_name)
|
||||
self.fit_model_combobox.setCurrentText(fit_name)
|
||||
|
||||
def populate_fit_model_combobox(self):
|
||||
"""Populate the fit_model_combobox with the devices."""
|
||||
# pylint: disable=protected-access
|
||||
available_plugins = getattr(getattr(self.client, "dap", None), "_available_dap_plugins", {})
|
||||
self.available_models = [model for model in available_plugins.keys()]
|
||||
self.clear()
|
||||
self.addItems(self.available_models)
|
||||
self.available_models = [model for model in self.client.dap._available_dap_plugins.keys()]
|
||||
self.fit_model_combobox.clear()
|
||||
self.fit_model_combobox.addItems(self.available_models)
|
||||
|
||||
def _validate_dap_model(self, model: str | None) -> bool:
|
||||
"""Validate the DAP model.
|
||||
@@ -193,23 +166,23 @@ class DapComboBox(BECWidget, QComboBox):
|
||||
"""
|
||||
if model is None:
|
||||
return False
|
||||
return model in self.available_models
|
||||
|
||||
@property
|
||||
def is_valid_input(self) -> bool:
|
||||
"""Whether the current text matches an available DAP model."""
|
||||
return self._is_valid_input
|
||||
if model not in self.available_models:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
app = QApplication([])
|
||||
apply_theme("dark")
|
||||
dialog = DapComboBox()
|
||||
dialog.show()
|
||||
sys.exit(app.exec_())
|
||||
widget = QWidget()
|
||||
widget.setFixedSize(200, 200)
|
||||
layout = QVBoxLayout()
|
||||
widget.setLayout(layout)
|
||||
layout.addWidget(DapComboBox())
|
||||
widget.show()
|
||||
app.exec_()
|
||||
|
||||
@@ -1778,7 +1778,7 @@ class Waveform(PlotBase):
|
||||
if parent_curve is None:
|
||||
logger.warning(
|
||||
f"No device curve found for DAP curve '{dap_curve.name()}'!"
|
||||
) # TODO triggered when DAP curve is removed from the curve dialog, why?
|
||||
) # TODO triggerd when DAP curve is removed from the curve dialog, why?
|
||||
continue
|
||||
|
||||
x_data, y_data = parent_curve.get_data()
|
||||
|
||||
@@ -276,7 +276,7 @@ class Ring(BECConnector, QWidget):
|
||||
for obj in dev_obj._info["signals"].values()
|
||||
if obj["kind_str"] == "hinted"
|
||||
and obj["signal_class"]
|
||||
not in ["ProgressSignal", "AsyncSignal", "AsyncMultiSignal", "DynamicSignal"]
|
||||
not in ["ProgressSignal", "AyncSignal", "AsyncMultiSignal", "DynamicSignal"]
|
||||
]
|
||||
|
||||
normal_signals = [
|
||||
|
||||
@@ -42,6 +42,9 @@ from bec_widgets.widgets.services.bec_atlas_admin_view.experiment_selection.expe
|
||||
from bec_widgets.widgets.services.bec_atlas_admin_view.experiment_selection.experiment_selection import (
|
||||
ExperimentSelection,
|
||||
)
|
||||
from bec_widgets.widgets.services.bec_messaging_config.bec_messaging_config_widget import (
|
||||
BECMessagingConfigWidget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtWidgets import QToolBar
|
||||
@@ -104,7 +107,8 @@ class OverviewWidget(QGroupBox):
|
||||
content_layout = QVBoxLayout(content)
|
||||
content.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
content.setFrameShadow(QFrame.Shadow.Raised)
|
||||
content.setStyleSheet("""
|
||||
content.setStyleSheet(
|
||||
"""
|
||||
QFrame
|
||||
{
|
||||
border: 1px solid #cccccc;
|
||||
@@ -113,7 +117,8 @@ class OverviewWidget(QGroupBox):
|
||||
{
|
||||
border: none;
|
||||
}
|
||||
""")
|
||||
"""
|
||||
)
|
||||
content_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
content.setFixedSize(400, 280)
|
||||
|
||||
@@ -299,6 +304,11 @@ class BECAtlasAdminView(BECWidget, QWidget):
|
||||
self.experiment_selection.setVisible(False)
|
||||
self.stacked_layout.addWidget(self.experiment_selection)
|
||||
|
||||
# Messaging Services widget
|
||||
self.messaging_config_widget = BECMessagingConfigWidget(parent=self)
|
||||
self.messaging_config_widget.setVisible(False)
|
||||
self.stacked_layout.addWidget(self.messaging_config_widget)
|
||||
|
||||
# Connect signals
|
||||
self.overview_widget.login_requested.connect(self._on_login_requested)
|
||||
self.overview_widget.change_experiment_requested.connect(
|
||||
@@ -392,6 +402,7 @@ class BECAtlasAdminView(BECWidget, QWidget):
|
||||
"""Show the overview panel."""
|
||||
self.overview_widget.setVisible(True)
|
||||
self.experiment_selection.setVisible(False)
|
||||
self.messaging_config_widget.setVisible(False)
|
||||
self.stacked_layout.setCurrentWidget(self.overview_widget)
|
||||
|
||||
def _on_experiment_selection_selected(self):
|
||||
@@ -401,12 +412,20 @@ class BECAtlasAdminView(BECWidget, QWidget):
|
||||
return
|
||||
self.overview_widget.setVisible(False)
|
||||
self.experiment_selection.setVisible(True)
|
||||
self.messaging_config_widget.setVisible(False)
|
||||
self.stacked_layout.setCurrentWidget(self.experiment_selection)
|
||||
|
||||
def _on_messaging_services_selected(self):
|
||||
"""Show the messaging services panel."""
|
||||
logger.info("Messaging services panel is not implemented yet.")
|
||||
return
|
||||
if not self._authenticated:
|
||||
logger.warning("Attempted to access messaging services without authentication.")
|
||||
return
|
||||
self.overview_widget.setVisible(False)
|
||||
self.experiment_selection.setVisible(False)
|
||||
self.messaging_config_widget.setVisible(True)
|
||||
if self._current_deployment_info is not None:
|
||||
self.messaging_config_widget.populate_from_deployment(self._current_deployment_info)
|
||||
self.stacked_layout.setCurrentWidget(self.messaging_config_widget)
|
||||
|
||||
########################
|
||||
## Internal slots
|
||||
@@ -446,6 +465,7 @@ class BECAtlasAdminView(BECWidget, QWidget):
|
||||
atlas_url=self._atlas_url,
|
||||
)
|
||||
self.atlas_http_service._set_current_deployment_info(deployment)
|
||||
self.messaging_config_widget.populate_from_deployment(deployment)
|
||||
|
||||
def _fetch_available_experiments(self):
|
||||
"""Fetch the list of available experiments for the authenticated user."""
|
||||
@@ -501,9 +521,7 @@ class BECAtlasAdminView(BECWidget, QWidget):
|
||||
|
||||
if authenticated:
|
||||
self.toolbar.components.get_action("experiment_selection").action.setEnabled(True)
|
||||
self.toolbar.components.get_action("messaging_services").action.setEnabled(
|
||||
False
|
||||
) # TODO activate once messaging is added
|
||||
self.toolbar.components.get_action("messaging_services").action.setEnabled(True)
|
||||
self.toolbar.components.get_action("logout").action.setEnabled(True)
|
||||
self._fetch_available_experiments() # Fetch experiments upon successful authentication
|
||||
self._atlas_info_widget.set_logged_in(info.email)
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
"""Module for the BEC messaging configuration widget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from qtpy.QtCore import Qt, QTimer, Signal # type: ignore[attr-defined]
|
||||
from qtpy.QtWidgets import (
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QPushButton,
|
||||
QSizePolicy,
|
||||
QSplitter,
|
||||
QTabWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from bec_widgets.utils.colors import apply_theme
|
||||
from bec_widgets.widgets.services.bec_messaging_config.service_cards import (
|
||||
CardType,
|
||||
ScopeListWidget,
|
||||
card_from_service,
|
||||
make_card,
|
||||
)
|
||||
from bec_widgets.widgets.services.bec_messaging_config.service_scope_event_table import (
|
||||
ServiceScopeEventTableWidget,
|
||||
)
|
||||
|
||||
|
||||
class ServiceConfigPanel(QWidget):
|
||||
"""Panel that manages global and local service scopes for one service type.
|
||||
|
||||
Args:
|
||||
card_type (CardType): The service type used when adding new scope cards.
|
||||
parent (QWidget | None): The parent widget.
|
||||
"""
|
||||
|
||||
config_changed = Signal()
|
||||
|
||||
def __init__(self, card_type: CardType, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._card_type: CardType = card_type
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(12, 12, 12, 12)
|
||||
root.setSpacing(0)
|
||||
|
||||
splitter = QSplitter(Qt.Orientation.Vertical)
|
||||
|
||||
# ── Local settings box ────────────────────────────────────────────
|
||||
self._local_box = QGroupBox("Current Experiment")
|
||||
self._local_list = ScopeListWidget()
|
||||
self._local_list.cards_changed.connect(self.config_changed)
|
||||
self._local_add_btn = QPushButton("+ Add")
|
||||
self._local_add_btn.setFixedWidth(120)
|
||||
self._local_add_btn.clicked.connect(
|
||||
lambda: self._local_list.add_card(make_card(self._card_type))
|
||||
)
|
||||
local_layout = QVBoxLayout(self._local_box)
|
||||
local_layout.setContentsMargins(16, 16, 16, 16)
|
||||
local_layout.setSpacing(12)
|
||||
local_layout.addWidget(self._local_add_btn, 0, Qt.AlignmentFlag.AlignRight)
|
||||
local_layout.addWidget(self._local_list, 1)
|
||||
splitter.addWidget(self._local_box)
|
||||
|
||||
# ── Global settings box ───────────────────────────────────────────
|
||||
self._global_box = QGroupBox("All Experiments")
|
||||
self._global_list = ScopeListWidget()
|
||||
self._global_list.cards_changed.connect(self.config_changed)
|
||||
self._global_add_btn = QPushButton("+ Add")
|
||||
self._global_add_btn.setFixedWidth(120)
|
||||
self._global_add_btn.clicked.connect(
|
||||
lambda: self._global_list.add_card(make_card(self._card_type))
|
||||
)
|
||||
global_layout = QVBoxLayout(self._global_box)
|
||||
global_layout.setContentsMargins(16, 16, 16, 16)
|
||||
global_layout.setSpacing(12)
|
||||
global_layout.addWidget(self._global_add_btn, 0, Qt.AlignmentFlag.AlignRight)
|
||||
global_layout.addWidget(self._global_list, 1)
|
||||
splitter.addWidget(self._global_box)
|
||||
|
||||
splitter.setSizes([300, 300])
|
||||
root.addWidget(splitter, 1)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def load_services(self, deployment_services: list, session_services: list) -> None:
|
||||
"""Populate both lists with services matching the panel service type."""
|
||||
self._clear_list(self._global_list)
|
||||
self._clear_list(self._local_list)
|
||||
for info in deployment_services:
|
||||
if getattr(info, "service_type", None) == self._card_type:
|
||||
self._global_list.add_card(card_from_service(info))
|
||||
for info in session_services:
|
||||
if getattr(info, "service_type", None) == self._card_type:
|
||||
self._local_list.add_card(card_from_service(info))
|
||||
|
||||
@staticmethod
|
||||
def _clear_list(list_widget: ScopeListWidget) -> None:
|
||||
"""Remove all cards from *list_widget*."""
|
||||
list_widget.clear_cards()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
def get_data(self) -> dict:
|
||||
"""Collect all card data from both the deployment and session lists."""
|
||||
return {
|
||||
"deployment": self._collect(self._global_list),
|
||||
"session": self._collect(self._local_list),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _collect(list_widget: ScopeListWidget) -> list[dict]:
|
||||
return [card.get_data() for card in list_widget.cards()]
|
||||
|
||||
|
||||
class BECMessagingConfigWidget(QWidget):
|
||||
"""Widget to configure SciLog, Signal, and MS Teams messaging services."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("BEC Messaging Configuration")
|
||||
self.setMinimumSize(540, 500)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(16, 16, 16, 16)
|
||||
root.setSpacing(12)
|
||||
|
||||
content_splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
|
||||
# ── Tab widget ────────────────────────────────────────────────────
|
||||
self._tabs = QTabWidget()
|
||||
|
||||
self._scilog_panel = ServiceConfigPanel("scilog")
|
||||
self._signal_panel = ServiceConfigPanel("signal")
|
||||
self._teams_panel = ServiceConfigPanel("teams")
|
||||
|
||||
for panel in (self._scilog_panel, self._signal_panel, self._teams_panel):
|
||||
panel.config_changed.connect(self._refresh_scope_event_table)
|
||||
|
||||
self._tabs.addTab(self._scilog_panel, "SciLog")
|
||||
self._tabs.addTab(self._signal_panel, "Signal")
|
||||
self._tabs.addTab(self._teams_panel, "MS Teams")
|
||||
|
||||
content_splitter.addWidget(self._tabs)
|
||||
|
||||
self._scope_event_table = ServiceScopeEventTableWidget(self)
|
||||
content_splitter.addWidget(self._scope_event_table)
|
||||
content_splitter.setStretchFactor(0, 3)
|
||||
content_splitter.setStretchFactor(1, 2)
|
||||
|
||||
root.addWidget(content_splitter, 1)
|
||||
|
||||
# ── Bottom action bar ─────────────────────────────────────────────
|
||||
bottom_row = QHBoxLayout()
|
||||
bottom_row.setSpacing(12)
|
||||
|
||||
self._status_label = QLabel("")
|
||||
self._status_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
bottom_row.addWidget(self._status_label, 1)
|
||||
|
||||
save_btn = QPushButton("Save && Apply")
|
||||
save_btn.setDefault(True)
|
||||
save_btn.clicked.connect(self._mock_save_to_atlas_api)
|
||||
bottom_row.addWidget(save_btn)
|
||||
|
||||
root.addLayout(bottom_row)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Initialisation from backend message
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def populate_from_deployment(self, msg: DeploymentInfoMessage) -> None:
|
||||
"""Populate all panels from a deployment info message.
|
||||
|
||||
Args:
|
||||
msg (DeploymentInfoMessage): Deployment information containing deployment and session services.
|
||||
"""
|
||||
deployment_services = list(msg.messaging_services)
|
||||
session_services = (
|
||||
list(msg.active_session.messaging_services) if msg.active_session is not None else []
|
||||
)
|
||||
self._scilog_panel.load_services(deployment_services, session_services)
|
||||
self._signal_panel.load_services(deployment_services, session_services)
|
||||
self._teams_panel.load_services(deployment_services, session_services)
|
||||
self._refresh_scope_event_table()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dummy REST methods (replace with real requests calls later)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_payload(self) -> dict:
|
||||
"""Collect the current UI state as a serializable dictionary."""
|
||||
return {
|
||||
"scilog": self._scilog_panel.get_data(),
|
||||
"signal": self._signal_panel.get_data(),
|
||||
"teams": self._teams_panel.get_data(),
|
||||
"event_subscriptions": self._scope_event_table.get_data(),
|
||||
}
|
||||
|
||||
def _refresh_scope_event_table(self) -> None:
|
||||
"""Refresh the event subscription table from the current service cards."""
|
||||
self._scope_event_table.set_services(self._collect_services_for_event_table())
|
||||
|
||||
def _collect_services_for_event_table(self) -> list[dict]:
|
||||
"""Collect all configured services for the event subscription table."""
|
||||
service_rows: list[dict] = []
|
||||
for panel in (self._scilog_panel, self._signal_panel, self._teams_panel):
|
||||
panel_data = panel.get_data()
|
||||
for source_name in ("deployment", "session"):
|
||||
for service in panel_data[source_name]:
|
||||
service_rows.append({**service, "source": source_name})
|
||||
return service_rows
|
||||
|
||||
def _mock_save_to_atlas_api(self) -> None:
|
||||
"""Simulate saving the current configuration to Atlas."""
|
||||
payload = self._build_payload()
|
||||
print("─" * 60)
|
||||
print("[BECMessagingConfigWidget] _mock_save_to_atlas_api payload:")
|
||||
print(json.dumps(payload, indent=2))
|
||||
print("─" * 60)
|
||||
self._set_status("✅ Saved!", timeout_ms=4000)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Status bar helper
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _set_status(self, message: str, *, timeout_ms: int = 0) -> None:
|
||||
"""Show a status message and optionally clear it after a timeout.
|
||||
|
||||
Args:
|
||||
message (str): The message to display in the status label.
|
||||
timeout_ms (int): Time in milliseconds before clearing the message.
|
||||
"""
|
||||
self._status_label.setText(message)
|
||||
if timeout_ms > 0:
|
||||
QTimer.singleShot(timeout_ms, lambda: self._status_label.setText(""))
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
from bec_lib.messages import (
|
||||
DeploymentInfoMessage,
|
||||
MessagingConfig,
|
||||
MessagingServiceScopeConfig,
|
||||
SciLogServiceInfo,
|
||||
SessionInfoMessage,
|
||||
SignalServiceInfo,
|
||||
TeamsServiceInfo,
|
||||
)
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
apply_theme("dark")
|
||||
|
||||
# ── Build a realistic mock DeploymentInfoMessage ──────────────────
|
||||
mock_deployment = DeploymentInfoMessage(
|
||||
deployment_id="dep-0001",
|
||||
name="mockup-beamline",
|
||||
messaging_config=MessagingConfig(
|
||||
signal=MessagingServiceScopeConfig(enabled=True),
|
||||
teams=MessagingServiceScopeConfig(enabled=True),
|
||||
scilog=MessagingServiceScopeConfig(enabled=True),
|
||||
),
|
||||
messaging_services=[
|
||||
SciLogServiceInfo(
|
||||
id="sl-global-1",
|
||||
scope="beamline",
|
||||
enabled=True,
|
||||
name="Beamline Log",
|
||||
logbook_id="lb-99001",
|
||||
),
|
||||
TeamsServiceInfo(
|
||||
id="teams-global-1",
|
||||
scope="beamline",
|
||||
enabled=True,
|
||||
name="BEC Channel",
|
||||
workflow_webhook_url="https://outlook.office.com/webhook/…",
|
||||
),
|
||||
SignalServiceInfo(
|
||||
id="signal-global-1",
|
||||
scope="beamline",
|
||||
enabled=False,
|
||||
name=None,
|
||||
group_id=None,
|
||||
group_link=None,
|
||||
),
|
||||
],
|
||||
active_session=SessionInfoMessage(
|
||||
name="session-2026-03-07",
|
||||
messaging_services=[
|
||||
SciLogServiceInfo(
|
||||
id="sl-local-1",
|
||||
scope="experiment",
|
||||
enabled=True,
|
||||
name="My Notebook",
|
||||
logbook_id="lb-12345",
|
||||
),
|
||||
SignalServiceInfo(
|
||||
id="signal-local-1",
|
||||
scope="experiment",
|
||||
enabled=True,
|
||||
name="Lab Signal Group",
|
||||
group_id="grp-8a3f291c",
|
||||
group_link="https://signal.group/#grp-8a3f291c",
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
widget = BECMessagingConfigWidget()
|
||||
widget.populate_from_deployment(mock_deployment)
|
||||
widget.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -0,0 +1,429 @@
|
||||
"""Module for service scope cards used by the messaging configuration widget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from enum import IntEnum
|
||||
from typing import TYPE_CHECKING, Literal, Type
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import QRegularExpression, Qt, QTimer, Signal # type: ignore[attr-defined]
|
||||
from qtpy.QtGui import QRegularExpressionValidator
|
||||
from qtpy.QtWidgets import (
|
||||
QCheckBox,
|
||||
QFrame,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QPushButton,
|
||||
QScrollArea,
|
||||
QSizePolicy,
|
||||
QSpacerItem,
|
||||
QStackedLayout,
|
||||
QToolButton,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from bec_lib import messages
|
||||
|
||||
CardType = Literal["scilog", "signal", "teams"]
|
||||
|
||||
|
||||
class ScopeListWidget(QScrollArea):
|
||||
"""A scrollable list that stacks scope cards neatly at the top."""
|
||||
|
||||
cards_changed = Signal()
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWidgetResizable(True)
|
||||
self.setFrameShape(QFrame.Shape.NoFrame)
|
||||
|
||||
self._container = QWidget()
|
||||
self._layout = QVBoxLayout(self._container)
|
||||
self._layout.setContentsMargins(4, 8, 4, 8)
|
||||
self._layout.setSpacing(16)
|
||||
|
||||
self._spacer = QSpacerItem(0, 0, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
|
||||
self._layout.addSpacerItem(self._spacer)
|
||||
|
||||
self.setWidget(self._container)
|
||||
|
||||
def add_card(self, card: BaseScopeCard) -> None:
|
||||
"""Insert a card above the trailing spacer.
|
||||
|
||||
Args:
|
||||
card (BaseScopeCard): The card widget to add to the list.
|
||||
"""
|
||||
idx = self._layout.count() - 1
|
||||
self._layout.insertWidget(idx, card)
|
||||
card.delete_requested.connect(lambda: self._remove_card(card))
|
||||
card.delete_requested.connect(self.cards_changed)
|
||||
card.data_changed.connect(self.cards_changed)
|
||||
self.cards_changed.emit()
|
||||
|
||||
def clear_cards(self) -> None:
|
||||
"""Remove all cards without touching the trailing spacer."""
|
||||
for index in range(self._layout.count() - 2, -1, -1):
|
||||
item = self._layout.itemAt(index)
|
||||
if item is None:
|
||||
continue
|
||||
card = item.widget()
|
||||
if isinstance(card, BaseScopeCard):
|
||||
self._layout.removeWidget(card)
|
||||
card.deleteLater()
|
||||
self.cards_changed.emit()
|
||||
|
||||
def cards(self) -> list[BaseScopeCard]:
|
||||
"""Return the cards currently stored in the list."""
|
||||
results: list[BaseScopeCard] = []
|
||||
for index in range(self._layout.count()):
|
||||
item = self._layout.itemAt(index)
|
||||
if item is None:
|
||||
continue
|
||||
card = item.widget()
|
||||
if isinstance(card, BaseScopeCard):
|
||||
results.append(card)
|
||||
return results
|
||||
|
||||
def _remove_card(self, card: BaseScopeCard) -> None:
|
||||
self._layout.removeWidget(card)
|
||||
card.deleteLater()
|
||||
self.cards_changed.emit()
|
||||
|
||||
|
||||
class BaseScopeCard(QFrame):
|
||||
"""Base card with shared identity, scope, and enabled fields."""
|
||||
|
||||
delete_requested = Signal()
|
||||
data_changed = Signal()
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._id: str = str(uuid.uuid4())
|
||||
|
||||
self.setFrameShape(QFrame.Shape.StyledPanel)
|
||||
self.setFrameShadow(QFrame.Shadow.Raised)
|
||||
self.setStyleSheet(
|
||||
"BaseScopeCard {"
|
||||
" border: 1px solid palette(mid);"
|
||||
" border-radius: 6px;"
|
||||
" background: palette(base);"
|
||||
"}"
|
||||
)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(20, 16, 20, 20)
|
||||
root.setSpacing(14)
|
||||
|
||||
header_row = QHBoxLayout()
|
||||
header_row.setSpacing(10)
|
||||
|
||||
self.enabled_checkbox = QCheckBox("Enabled")
|
||||
self.enabled_checkbox.setChecked(True)
|
||||
self.enabled_checkbox.toggled.connect(self.data_changed)
|
||||
header_row.addWidget(self.enabled_checkbox)
|
||||
header_row.addStretch(1)
|
||||
|
||||
self._delete_btn = QToolButton()
|
||||
delete_icon = material_icon(
|
||||
"delete", size=(25, 25), convert_to_pixmap=False, filled=False, color="#CC181E"
|
||||
)
|
||||
self._delete_btn.setToolTip("Delete this scope configuration")
|
||||
self._delete_btn.setIcon(delete_icon)
|
||||
self._delete_btn.clicked.connect(self.delete_requested)
|
||||
header_row.addWidget(self._delete_btn)
|
||||
|
||||
root.addLayout(header_row)
|
||||
|
||||
identity_row = QHBoxLayout()
|
||||
identity_row.setSpacing(16)
|
||||
|
||||
scope_col = QVBoxLayout()
|
||||
scope_col.setSpacing(4)
|
||||
scope_col.addWidget(QLabel("Scope"))
|
||||
self.scope_edit = QLineEdit()
|
||||
self.scope_edit.setPlaceholderText("e.g. user, admin")
|
||||
self.scope_edit.textChanged.connect(self.data_changed)
|
||||
scope_col.addWidget(self.scope_edit)
|
||||
identity_row.addLayout(scope_col, 1)
|
||||
|
||||
name_col = QVBoxLayout()
|
||||
name_col.setSpacing(4)
|
||||
name_col.addWidget(QLabel("Name (optional)"))
|
||||
self.name_edit = QLineEdit()
|
||||
self.name_edit.setPlaceholderText("display name")
|
||||
self.name_edit.textChanged.connect(self.data_changed)
|
||||
name_col.addWidget(self.name_edit)
|
||||
identity_row.addLayout(name_col, 1)
|
||||
|
||||
root.addLayout(identity_row)
|
||||
|
||||
self.content_layout = QVBoxLayout()
|
||||
self.content_layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.content_layout.setSpacing(12)
|
||||
root.addLayout(self.content_layout)
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""Return the common payload for a messaging service card."""
|
||||
return {
|
||||
"id": self._id,
|
||||
"scope": self.scope_edit.text(),
|
||||
"enabled": self.enabled_checkbox.isChecked(),
|
||||
"name": self.name_edit.text() or None,
|
||||
}
|
||||
|
||||
def set_data(self, info: messages.MessagingService) -> None: # type: ignore[name-defined]
|
||||
"""Populate the shared card fields from a messaging service.
|
||||
|
||||
Args:
|
||||
info (messages.MessagingService): The service object used to populate the card.
|
||||
"""
|
||||
self._id = info.id
|
||||
self.scope_edit.setText(info.scope)
|
||||
self.enabled_checkbox.setChecked(info.enabled)
|
||||
self.name_edit.setText(info.name or "")
|
||||
|
||||
|
||||
class SciLogScopeCard(BaseScopeCard):
|
||||
"""Card used to configure SciLog service settings."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
col = QVBoxLayout()
|
||||
col.setSpacing(4)
|
||||
col.addWidget(QLabel("Logbook ID"))
|
||||
self.logbook_id_edit = QLineEdit()
|
||||
self.logbook_id_edit.setPlaceholderText("e.g. lb-12345")
|
||||
self.logbook_id_edit.textChanged.connect(self.data_changed)
|
||||
col.addWidget(self.logbook_id_edit)
|
||||
self.content_layout.addLayout(col)
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""Return the SciLog-specific payload for this card."""
|
||||
data = super().get_data()
|
||||
data["service_type"] = "scilog"
|
||||
data["logbook_id"] = self.logbook_id_edit.text()
|
||||
return data
|
||||
|
||||
def set_data(self, info: messages.SciLogServiceInfo) -> None: # type: ignore[override]
|
||||
"""Populate the card from SciLog service information.
|
||||
|
||||
Args:
|
||||
info (messages.SciLogServiceInfo): The SciLog service object used to populate the card.
|
||||
"""
|
||||
super().set_data(info)
|
||||
self.logbook_id_edit.setText(info.logbook_id)
|
||||
|
||||
|
||||
class TeamsScopeCard(BaseScopeCard):
|
||||
"""Card used to configure MS Teams service settings."""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
fields_row = QHBoxLayout()
|
||||
fields_row.setSpacing(16)
|
||||
|
||||
col = QVBoxLayout()
|
||||
col.setSpacing(4)
|
||||
col.addWidget(QLabel("Workflow Webhook URL"))
|
||||
self.workflow_webhook_url_edit = edit = QLineEdit(parent=self)
|
||||
edit.setPlaceholderText("e.g. https://outlook.office.com/webhook/…")
|
||||
edit.textChanged.connect(self.data_changed)
|
||||
col.addWidget(edit)
|
||||
fields_row.addLayout(col, 1)
|
||||
|
||||
self.content_layout.addLayout(fields_row)
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""Return the MS Teams-specific payload for this card."""
|
||||
data = super().get_data()
|
||||
data["service_type"] = "teams"
|
||||
data["workflow_webhook_url"] = self.workflow_webhook_url_edit.text()
|
||||
return data
|
||||
|
||||
def set_data(self, info: messages.TeamsServiceInfo) -> None: # type: ignore[override]
|
||||
"""Populate the card from MS Teams service information.
|
||||
|
||||
Args:
|
||||
info (messages.TeamsServiceInfo): The MS Teams service object used to populate the card.
|
||||
"""
|
||||
super().set_data(info)
|
||||
self.workflow_webhook_url_edit.setText(info.workflow_webhook_url)
|
||||
|
||||
|
||||
class _SignalState(IntEnum):
|
||||
UNCONFIGURED = 0
|
||||
PENDING = 1
|
||||
CONFIGURED = 2
|
||||
|
||||
|
||||
class SignalScopeCard(BaseScopeCard):
|
||||
"""Card used to configure Signal service settings and linking state."""
|
||||
|
||||
_MOCK_GROUP_ID = "grp-8a3f291c"
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._state = _SignalState.UNCONFIGURED
|
||||
self._mock_group_id: str = ""
|
||||
self._mock_group_link: str = ""
|
||||
|
||||
stacked_container = QWidget()
|
||||
self._stacked = QStackedLayout(stacked_container)
|
||||
self._stacked.setContentsMargins(0, 0, 0, 0)
|
||||
self.content_layout.addWidget(stacked_container)
|
||||
|
||||
self._build_unconfigured_page()
|
||||
self._build_pending_page()
|
||||
self._build_configured_page()
|
||||
|
||||
self._stacked.setCurrentIndex(_SignalState.UNCONFIGURED)
|
||||
|
||||
def _build_unconfigured_page(self) -> None:
|
||||
page = QWidget()
|
||||
row = QHBoxLayout(page)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(6)
|
||||
|
||||
phone_col = QVBoxLayout()
|
||||
phone_col.setSpacing(4)
|
||||
phone_col.addWidget(QLabel("Phone Number"))
|
||||
self._phone_edit = QLineEdit()
|
||||
self._phone_edit.setValidator(
|
||||
QRegularExpressionValidator(QRegularExpression(r"^\+\S*$"), self._phone_edit)
|
||||
)
|
||||
self._phone_edit.setPlaceholderText("+41791234567")
|
||||
self._phone_edit.textChanged.connect(self.data_changed)
|
||||
phone_col.addWidget(self._phone_edit)
|
||||
row.addLayout(phone_col, 1)
|
||||
|
||||
start_linking_btn = QPushButton("Start Linking")
|
||||
start_linking_btn.setFixedWidth(100)
|
||||
start_linking_btn.clicked.connect(self._on_ping_clicked)
|
||||
row.addWidget(start_linking_btn, 0, Qt.AlignmentFlag.AlignBottom)
|
||||
|
||||
self._stacked.addWidget(page)
|
||||
|
||||
def _build_pending_page(self) -> None:
|
||||
page = QWidget()
|
||||
row = QHBoxLayout(page)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(6)
|
||||
|
||||
waiting_lbl = QLabel("⏳ Waiting for you to reply on Signal…")
|
||||
waiting_lbl.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
row.addWidget(waiting_lbl)
|
||||
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(self._on_cancel_clicked)
|
||||
row.addWidget(cancel_btn)
|
||||
|
||||
self._stacked.addWidget(page)
|
||||
|
||||
def _build_configured_page(self) -> None:
|
||||
page = QWidget()
|
||||
row = QHBoxLayout(page)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
row.setSpacing(6)
|
||||
|
||||
self._linked_lbl = QLabel("🟢 Linked (Group ID: —)")
|
||||
self._linked_lbl.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
row.addWidget(self._linked_lbl)
|
||||
|
||||
unlink_btn = QPushButton("Unlink")
|
||||
unlink_btn.clicked.connect(self._on_unlink_clicked)
|
||||
row.addWidget(unlink_btn)
|
||||
|
||||
self._stacked.addWidget(page)
|
||||
|
||||
def _on_ping_clicked(self) -> None:
|
||||
self._state = _SignalState.PENDING
|
||||
self._stacked.setCurrentIndex(_SignalState.PENDING)
|
||||
QTimer.singleShot(3000, self._mock_backend_confirmation)
|
||||
|
||||
def _mock_backend_confirmation(self) -> None:
|
||||
if self._state != _SignalState.PENDING:
|
||||
return
|
||||
self._mock_group_id = self._MOCK_GROUP_ID
|
||||
self._mock_group_link = f"https://signal.group/#{self._mock_group_id}"
|
||||
self._linked_lbl.setText(f"🟢 Linked (Group ID: {self._mock_group_id})")
|
||||
self._state = _SignalState.CONFIGURED
|
||||
self._stacked.setCurrentIndex(_SignalState.CONFIGURED)
|
||||
self.data_changed.emit()
|
||||
|
||||
def _on_cancel_clicked(self) -> None:
|
||||
self._state = _SignalState.UNCONFIGURED
|
||||
self._stacked.setCurrentIndex(_SignalState.UNCONFIGURED)
|
||||
self.data_changed.emit()
|
||||
|
||||
def _on_unlink_clicked(self) -> None:
|
||||
self._mock_group_id = ""
|
||||
self._mock_group_link = ""
|
||||
self._state = _SignalState.UNCONFIGURED
|
||||
self._stacked.setCurrentIndex(_SignalState.UNCONFIGURED)
|
||||
self.data_changed.emit()
|
||||
|
||||
def get_data(self) -> dict:
|
||||
"""Return the Signal-specific payload for this card."""
|
||||
data = super().get_data()
|
||||
data["service_type"] = "signal"
|
||||
configured = self._state == _SignalState.CONFIGURED
|
||||
data["group_id"] = self._mock_group_id if configured else None
|
||||
data["group_link"] = self._mock_group_link if configured else None
|
||||
return data
|
||||
|
||||
def set_data(self, info: messages.SignalServiceInfo) -> None: # type: ignore[override]
|
||||
"""Populate the card from Signal service information.
|
||||
|
||||
Args:
|
||||
info (messages.SignalServiceInfo): The Signal service object used to populate the card.
|
||||
"""
|
||||
super().set_data(info)
|
||||
if info.group_id:
|
||||
self._mock_group_id = info.group_id
|
||||
self._mock_group_link = info.group_link or ""
|
||||
self._linked_lbl.setText(f"🟢 Linked (Group ID: {self._mock_group_id})")
|
||||
self._state = _SignalState.CONFIGURED
|
||||
self._stacked.setCurrentIndex(_SignalState.CONFIGURED)
|
||||
return
|
||||
self._mock_group_id = ""
|
||||
self._mock_group_link = ""
|
||||
self._state = _SignalState.UNCONFIGURED
|
||||
self._stacked.setCurrentIndex(_SignalState.UNCONFIGURED)
|
||||
|
||||
|
||||
_CARD_CLASSES: dict[CardType, Type[BaseScopeCard]] = {
|
||||
"scilog": SciLogScopeCard,
|
||||
"signal": SignalScopeCard,
|
||||
"teams": TeamsScopeCard,
|
||||
}
|
||||
|
||||
|
||||
def make_card(card_type: CardType) -> BaseScopeCard:
|
||||
"""Create a new service card for the requested card type.
|
||||
|
||||
Args:
|
||||
card_type (CardType): The service type for the card to create.
|
||||
"""
|
||||
return _CARD_CLASSES[card_type]()
|
||||
|
||||
|
||||
def card_from_service(info: object) -> BaseScopeCard:
|
||||
"""Create and populate a card from a messaging service object.
|
||||
|
||||
Args:
|
||||
info (object): A messaging service object with a ``service_type`` attribute.
|
||||
"""
|
||||
service_type: str = getattr(info, "service_type", "")
|
||||
card_class = _CARD_CLASSES.get(service_type) # type: ignore[arg-type]
|
||||
if card_class is None:
|
||||
raise ValueError(f"Unknown service_type: {service_type!r}")
|
||||
card = card_class()
|
||||
card.set_data(info) # type: ignore[arg-type]
|
||||
return card
|
||||
@@ -0,0 +1,125 @@
|
||||
"""Module for the service scope event subscription table widget."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QCheckBox,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QSizePolicy,
|
||||
QTableWidget,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
|
||||
class ServiceScopeEventTableWidget(QWidget):
|
||||
"""Widget that manages per-scope event subscriptions for messaging services."""
|
||||
|
||||
EVENT_NAMES = ("new_scan", "scan_finished", "alarm")
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._services: list[dict] = []
|
||||
self._subscriptions: dict[str, dict[str, bool]] = {}
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(0, 0, 0, 0)
|
||||
root.setSpacing(0)
|
||||
|
||||
self._table = QTableWidget(len(self.EVENT_NAMES), 0, self)
|
||||
self._table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
||||
self._table.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
||||
self._table.setAlternatingRowColors(True)
|
||||
self._table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
self._table.setVerticalHeaderLabels(list(self.EVENT_NAMES))
|
||||
self._table.horizontalHeader().setStretchLastSection(True)
|
||||
|
||||
header = self._table.horizontalHeader()
|
||||
header.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
self._table.verticalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
|
||||
|
||||
root.addWidget(self._table, 1)
|
||||
|
||||
def set_services(self, services: list[dict]) -> None:
|
||||
"""Update the table rows to match the current services.
|
||||
|
||||
Args:
|
||||
services (list[dict]): Service dictionaries collected from the service configuration panels.
|
||||
"""
|
||||
self._services = [dict(service) for service in services]
|
||||
known_ids = {str(service.get("id", "")) for service in self._services if service.get("id")}
|
||||
self._subscriptions = {
|
||||
service_id: subscriptions
|
||||
for service_id, subscriptions in self._subscriptions.items()
|
||||
if service_id in known_ids
|
||||
}
|
||||
|
||||
self._table.clearContents()
|
||||
self._table.setRowCount(len(self.EVENT_NAMES))
|
||||
self._table.setColumnCount(len(self._services))
|
||||
self._table.setHorizontalHeaderLabels(
|
||||
[self._format_service_label(service) for service in self._services]
|
||||
)
|
||||
|
||||
for column, service in enumerate(self._services):
|
||||
service_id = str(service.get("id", ""))
|
||||
|
||||
event_states = self._subscriptions.setdefault(
|
||||
service_id, {event_name: False for event_name in self.EVENT_NAMES}
|
||||
)
|
||||
for row, event_name in enumerate(self.EVENT_NAMES):
|
||||
self._table.setCellWidget(
|
||||
row,
|
||||
column,
|
||||
self._make_checkbox_cell(
|
||||
service_id, event_name, event_states.get(event_name, False)
|
||||
),
|
||||
)
|
||||
|
||||
def get_data(self) -> list[dict]:
|
||||
"""Return the event subscriptions for the current services."""
|
||||
results: list[dict] = []
|
||||
for service in self._services:
|
||||
service_id = str(service.get("id", ""))
|
||||
results.append(
|
||||
{
|
||||
"id": service_id,
|
||||
"source": service.get("source"),
|
||||
"service_type": service.get("service_type"),
|
||||
"scope": service.get("scope"),
|
||||
"events": dict(
|
||||
self._subscriptions.get(
|
||||
service_id, {event_name: False for event_name in self.EVENT_NAMES}
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
def _format_service_label(self, service: dict) -> str:
|
||||
service_name = str(service.get("service_type", ""))
|
||||
scope_name = str(service.get("scope", ""))
|
||||
source_name = str(service.get("source", ""))
|
||||
return f"{service_name}\n{scope_name}\n({source_name})"
|
||||
|
||||
def _make_checkbox_cell(self, service_id: str, event_name: str, checked: bool) -> QWidget:
|
||||
checkbox = QCheckBox()
|
||||
checkbox.setChecked(checked)
|
||||
checkbox.toggled.connect(
|
||||
lambda state, current_service_id=service_id, current_event_name=event_name: self._set_event_state(
|
||||
current_service_id, current_event_name, state
|
||||
)
|
||||
)
|
||||
|
||||
container = QWidget()
|
||||
layout = QHBoxLayout(container)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(checkbox)
|
||||
return container
|
||||
|
||||
def _set_event_state(self, service_id: str, event_name: str, checked: bool) -> None:
|
||||
self._subscriptions.setdefault(service_id, {})[event_name] = checked
|
||||
@@ -88,7 +88,7 @@ class DeviceBrowser(BECWidget, QWidget):
|
||||
self.setLayout(layout)
|
||||
|
||||
def init_warning_label(self):
|
||||
self.ui.scan_running_warning.setText("Warning: editing disabled while scan is running!")
|
||||
self.ui.scan_running_warning.setText("Warning: editing diabled while scan is running!")
|
||||
self.ui.scan_running_warning.setStyleSheet(
|
||||
"background-color: #fcba03; color: rgb(0, 0, 0);"
|
||||
)
|
||||
|
||||
@@ -160,8 +160,8 @@ class ScanHistoryMetadataViewer(BECWidget, QtWidgets.QGroupBox):
|
||||
Clear the view by resetting the labels and values.
|
||||
"""
|
||||
layout = self.layout()
|
||||
layout_counts = layout.count()
|
||||
for i in range(layout_counts):
|
||||
lauout_counts = layout.count()
|
||||
for i in range(lauout_counts):
|
||||
item = layout.itemAt(i)
|
||||
if item.widget():
|
||||
item.widget().close()
|
||||
|
||||
@@ -305,7 +305,7 @@ class ScanHistoryView(BECWidget, QtWidgets.QTreeWidget):
|
||||
def remove_scan(self, index: int):
|
||||
"""
|
||||
Remove a scan entry from the tree widget.
|
||||
We support negative indexing where -1, -2, etc.
|
||||
We supoprt negative indexing where -1, -2, etc.
|
||||
|
||||
Args:
|
||||
index (int): The index of the scan entry to remove.
|
||||
|
||||
@@ -31,7 +31,7 @@ api_reference/api_reference.md
|
||||
|
||||
## Introduction
|
||||
|
||||
An introduction into the single-responsibility principle and the modular design of BEC Widgets.
|
||||
An introduction into the single-resposibility principle and the modular design of BEC Widgets.
|
||||
```
|
||||
|
||||
```{grid-item-card}
|
||||
|
||||
@@ -19,7 +19,7 @@ cd bec_widgets
|
||||
```
|
||||
**Install in Editable Mode**:
|
||||
|
||||
Please install the package in editable mode into your BEC Python environment.
|
||||
Please install the package in editable mode into your BEC Python environemnt.
|
||||
```bash
|
||||
pip install -e '.[dev,pyside6]'
|
||||
```
|
||||
|
||||
@@ -16,7 +16,7 @@ that the widgets are discoverable.
|
||||
- make sure that the widget class inherits from both `BECWidget` as well as `QWidget` or a subclass
|
||||
of it, such as `QComboBox` or `QLineEdit`.
|
||||
- make sure it initialises each of these superclasses in its `__init__()` method, and passes the
|
||||
`parent` keyword argument on to `QWidget.__init__()`.
|
||||
`parent` keyword argumment on to `QWidget.__init__()`.
|
||||
- add `PLUGIN = True` as a class variable to the widget class
|
||||
- add `USER_ACCESS = [...]`, including any methods and properties which should be accessible in the
|
||||
client to the list, as strings.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
````
|
||||
|
||||
````{tab} Examples - CLI
|
||||
In the following examples, we will use `BECIPythonClient` as the main object to interact with the `BECDockArea`. These tutorials focus on how to work with the `BECDockArea` framework, such as adding and removing docks, saving and restoring layouts, and managing the docked widgets. By default the `BECDockArea` is referred to as `gui` in `BECIPythonClient`. For more detailed examples of each individual component, please refer to the example sections of each individual [`widget`](user.widgets).
|
||||
In the following examples, we will use `BECIPythonClient` as the main object to interact with the `BECDockArea`. These tutorials focus on how to work with the `BECDockArea` framework, such as adding and removing docks, saving and restoring layouts, and managing the docked widgets. By default the `BECDockArea` is refered as `gui` in `BECIPythonClient`. For more detailed examples of each individual component, please refer to the example sections of each individual [`widget`](user.widgets).
|
||||
|
||||
## Example 1 - Adding Docks to BECDockArea
|
||||
|
||||
@@ -62,7 +62,7 @@ dock_area.waveform_dock
|
||||
dock_area.motor_dock
|
||||
dock_area.image_dock
|
||||
|
||||
# If objects were closed, we will keep a reference that will indicate that the dock was deleted
|
||||
# If objects were closed, we will keep a refernce that will indicate that the dock was deleted
|
||||
# Try closing the window with the dock_area via mouse click on x
|
||||
|
||||
dock_area
|
||||
|
||||
@@ -79,7 +79,7 @@ if __name__ == "__main__":
|
||||
|
||||
````
|
||||
|
||||
````{tab} Examples - BEC designer
|
||||
````{tab} Examples - BEC desginer
|
||||
The various properties can also be set when the SignalLabel widget is added to a UI in BEC designer:
|
||||
|
||||
```{figure} ./designer_screenshot.png
|
||||
|
||||
@@ -215,7 +215,7 @@ Display custom text or HTML content.
|
||||
Display website content.
|
||||
```
|
||||
|
||||
```{grid-item-card} Toggle Widget
|
||||
```{grid-item-card} Toogle Widget
|
||||
:link: user.widgets.toggle
|
||||
:link-type: ref
|
||||
:img-top: /assets/widget_screenshots/toggle.png
|
||||
@@ -244,7 +244,7 @@ Modern progress bar for BEC.
|
||||
:link-type: ref
|
||||
:img-top: /assets/widget_screenshots/position_indicator.png
|
||||
|
||||
Display position of motor within its limits.
|
||||
Display position of motor withing its limits.
|
||||
```
|
||||
|
||||
```{grid-item-card} LMFit Dialog
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "3.3.2"
|
||||
version = "3.3.0"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.11"
|
||||
classifiers = [
|
||||
|
||||
@@ -32,8 +32,8 @@ def threads_check_fixture(threads_check):
|
||||
|
||||
@pytest.fixture
|
||||
def gui_id():
|
||||
"""New gui id each time, to ensure no 'gui is alive' zombie key can perturb"""
|
||||
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturb
|
||||
"""New gui id each time, to ensure no 'gui is alive' zombie key can perturbate"""
|
||||
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturbate
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
|
||||
@@ -22,7 +22,7 @@ from bec_widgets.cli.client_utils import BECGuiClient
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def gui_id():
|
||||
"""New gui id each time, to ensure no 'gui is alive' zombie key can perturb"""
|
||||
"""New gui id each time, to ensure no 'gui is alive' zombie key can perturbate"""
|
||||
return f"figure_{random.randint(0,100)}"
|
||||
|
||||
|
||||
|
||||
@@ -17,8 +17,6 @@ def dap_combobox(qtbot, mocked_client):
|
||||
|
||||
def test_dap_combobox_init(dap_combobox):
|
||||
"""Test DapComboBox init."""
|
||||
assert dap_combobox.fit_model_combobox is dap_combobox
|
||||
assert dap_combobox.isEditable() is True
|
||||
assert dap_combobox.fit_model_combobox.currentText() == "GaussianModel"
|
||||
assert dap_combobox.available_models == ["GaussianModel", "LorentzModel", "SineModel"]
|
||||
assert dap_combobox._validate_dap_model("GaussianModel") is True
|
||||
@@ -32,7 +30,7 @@ def test_dap_combobox_set_axis(dap_combobox):
|
||||
container = []
|
||||
|
||||
def my_callback(msg: str):
|
||||
"""Callback function to store the messages."""
|
||||
"""Calback function to store the messages."""
|
||||
container.append(msg)
|
||||
|
||||
dap_combobox.x_axis_updated.connect(my_callback)
|
||||
@@ -51,7 +49,7 @@ def test_dap_combobox_select_fit(dap_combobox):
|
||||
container = []
|
||||
|
||||
def my_callback(msg: str):
|
||||
"""Callback function to store the messages."""
|
||||
"""Calback function to store the messages."""
|
||||
container.append(msg)
|
||||
|
||||
dap_combobox.fit_model_updated.connect(my_callback)
|
||||
@@ -66,32 +64,10 @@ def test_dap_combobox_currentTextchanged(dap_combobox):
|
||||
container = []
|
||||
|
||||
def my_callback(msg: str):
|
||||
"""Callback function to store the messages."""
|
||||
"""Calback function to store the messages."""
|
||||
container.append(msg)
|
||||
|
||||
assert dap_combobox.fit_model_combobox.currentText() == "GaussianModel"
|
||||
dap_combobox.fit_model_updated.connect(my_callback)
|
||||
dap_combobox.fit_model_combobox.setCurrentText("SineModel")
|
||||
assert container[0] == "SineModel"
|
||||
|
||||
|
||||
def test_dap_combobox_init_without_available_models(qtbot, mocked_client):
|
||||
mocked_client.dap._available_dap_plugins = {}
|
||||
|
||||
widget = create_widget(qtbot, DapComboBox, client=mocked_client)
|
||||
|
||||
assert widget.available_models == []
|
||||
assert widget.fit_model_combobox.count() == 0
|
||||
assert widget.fit_model_combobox.currentText() == ""
|
||||
|
||||
|
||||
def test_dap_combobox_invalid_manual_entry_highlighted(dap_combobox):
|
||||
dap_combobox.setCurrentText("not-a-model")
|
||||
|
||||
assert dap_combobox.is_valid_input is False
|
||||
assert "red" in dap_combobox.styleSheet()
|
||||
|
||||
dap_combobox.setCurrentText("GaussianModel")
|
||||
|
||||
assert dap_combobox.is_valid_input is True
|
||||
assert "transparent" in dap_combobox.styleSheet()
|
||||
|
||||
@@ -1422,7 +1422,7 @@ class TestDeviceConfigTemplate:
|
||||
qtbot.waitExposed(template)
|
||||
yield template
|
||||
|
||||
def test_device_config_template_default_init(
|
||||
def test_device_config_teamplate_default_init(
|
||||
self, device_config_template: DeviceConfigTemplate, qtbot
|
||||
):
|
||||
"""Test DeviceConfigTemplate default initialization."""
|
||||
|
||||
@@ -113,7 +113,7 @@ def test_client_generator_with_black_formatting():
|
||||
|
||||
|
||||
class _WidgetsEnumType(str, enum.Enum):
|
||||
"""Enum for the available widgets, to be generated programmatically"""
|
||||
"""Enum for the available widgets, to be generated programatically"""
|
||||
|
||||
...
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ def metadata_widget(empty_metadata_widget: ScanMetadata):
|
||||
)
|
||||
|
||||
|
||||
def fill_components(components: dict[str, DynamicFormItem]):
|
||||
def fill_commponents(components: dict[str, DynamicFormItem]):
|
||||
components["sample_name"].setValue("test name")
|
||||
components["str_optional"].setValue(None)
|
||||
components["str_required"].setValue("something")
|
||||
@@ -147,7 +147,7 @@ def test_griditems_are_correct_class(
|
||||
|
||||
def test_grid_to_dict(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]):
|
||||
widget, components = metadata_widget = metadata_widget
|
||||
fill_components(components)
|
||||
fill_commponents(components)
|
||||
|
||||
assert widget._dict_from_grid() == TEST_DICT
|
||||
assert widget.get_form_data() == TEST_DICT | {"extra_field": "extra_data"}
|
||||
@@ -159,7 +159,7 @@ def test_validation(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormIt
|
||||
widget._validity.compact_status.default_led[:114]
|
||||
)
|
||||
|
||||
fill_components(components)
|
||||
fill_commponents(components)
|
||||
widget.validate_form()
|
||||
assert widget._validity_message.text() == "No errors!"
|
||||
|
||||
@@ -178,7 +178,7 @@ def test_numbers_clipped_to_limits(
|
||||
metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]],
|
||||
):
|
||||
widget, components = metadata_widget = metadata_widget
|
||||
fill_components(components)
|
||||
fill_commponents(components)
|
||||
|
||||
components["decimal_dp_limits_nodefault"].setValue(-56)
|
||||
assert components["decimal_dp_limits_nodefault"].getValue() == pytest.approx(1.01)
|
||||
|
||||
@@ -637,7 +637,7 @@ def test_fetch_scan_data_and_access(qtbot, mocked_client, monkeypatch):
|
||||
wf._fetch_scan_data_and_access()
|
||||
hist_mock.assert_called_once_with(-1)
|
||||
|
||||
# Check live mode
|
||||
# Ckeck live mode
|
||||
dummy_scan = create_dummy_scan_item()
|
||||
wf.scan_item = dummy_scan
|
||||
data_dict, access_key = wf._fetch_scan_data_and_access()
|
||||
|
||||
Reference in New Issue
Block a user