mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-13 11:11:49 +02:00
feat: allow editing device config from browser
This commit is contained in:
@ -37,6 +37,16 @@ class ExpandableGroupFrame(QFrame):
|
|||||||
self._layout.setContentsMargins(0, 0, 0, 0)
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
|
|
||||||
|
self._create_title_layout(title, icon)
|
||||||
|
|
||||||
|
self._contents = QWidget(self)
|
||||||
|
self._layout.addWidget(self._contents)
|
||||||
|
|
||||||
|
self._expansion_button.clicked.connect(self.switch_expanded_state)
|
||||||
|
self.expanded = self._expanded # type: ignore
|
||||||
|
self.expansion_state_changed.emit()
|
||||||
|
|
||||||
|
def _create_title_layout(self, title: str, icon: str):
|
||||||
self._title_layout = QHBoxLayout()
|
self._title_layout = QHBoxLayout()
|
||||||
self._layout.addLayout(self._title_layout)
|
self._layout.addLayout(self._title_layout)
|
||||||
|
|
||||||
@ -54,13 +64,6 @@ class ExpandableGroupFrame(QFrame):
|
|||||||
self._update_expansion_icon()
|
self._update_expansion_icon()
|
||||||
self._title_layout.addWidget(self._expansion_button, stretch=1)
|
self._title_layout.addWidget(self._expansion_button, stretch=1)
|
||||||
|
|
||||||
self._contents = QWidget(self)
|
|
||||||
self._layout.addWidget(self._contents)
|
|
||||||
|
|
||||||
self._expansion_button.clicked.connect(self.switch_expanded_state)
|
|
||||||
self.expanded = self._expanded # type: ignore
|
|
||||||
self.expansion_state_changed.emit()
|
|
||||||
|
|
||||||
def set_layout(self, layout: QLayout) -> None:
|
def set_layout(self, layout: QLayout) -> None:
|
||||||
self._contents.setLayout(layout)
|
self._contents.setLayout(layout)
|
||||||
self._contents.layout().setContentsMargins(0, 0, 0, 0) # type: ignore
|
self._contents.layout().setContentsMargins(0, 0, 0, 0) # type: ignore
|
||||||
|
@ -7,7 +7,7 @@ from bec_lib.logger import bec_logger
|
|||||||
from bec_qthemes import material_icon
|
from bec_qthemes import material_icon
|
||||||
from pydantic import BaseModel, ValidationError
|
from pydantic import BaseModel, ValidationError
|
||||||
from qtpy.QtCore import Signal # type: ignore
|
from qtpy.QtCore import Signal # type: ignore
|
||||||
from qtpy.QtWidgets import QApplication, QGridLayout, QLabel, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QApplication, QGridLayout, QLabel, QSizePolicy, QVBoxLayout, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
from bec_widgets.utils.compact_popup import CompactPopupWidget
|
||||||
@ -88,10 +88,13 @@ class TypedForm(BECWidget, QWidget):
|
|||||||
self._layout.addWidget(self._form_grid_container)
|
self._layout.addWidget(self._form_grid_container)
|
||||||
self._form_grid_container.setLayout(QVBoxLayout())
|
self._form_grid_container.setLayout(QVBoxLayout())
|
||||||
self._form_grid.setLayout(self._new_grid_layout())
|
self._form_grid.setLayout(self._new_grid_layout())
|
||||||
|
|
||||||
|
self._widget_types: dict | None = None
|
||||||
self._widget_from_type = widget_from_type
|
self._widget_from_type = widget_from_type
|
||||||
self._post_init()
|
self._post_init()
|
||||||
|
|
||||||
def _post_init(self):
|
def _post_init(self):
|
||||||
|
"""Override this if a subclass should do things after super().__init__ and before populate()"""
|
||||||
self.populate()
|
self.populate()
|
||||||
self.enabled = self._enabled # type: ignore # QProperty
|
self.enabled = self._enabled # type: ignore # QProperty
|
||||||
|
|
||||||
@ -106,7 +109,7 @@ class TypedForm(BECWidget, QWidget):
|
|||||||
label.setProperty("_model_field_name", item.name)
|
label.setProperty("_model_field_name", item.name)
|
||||||
label.setToolTip(item.info.description or item.name)
|
label.setToolTip(item.info.description or item.name)
|
||||||
grid.addWidget(label, row, 0)
|
grid.addWidget(label, row, 0)
|
||||||
widget = self._widget_from_type(item.item_type)(parent=self, spec=item)
|
widget = self._widget_from_type(item.item_type, self._widget_types)(parent=self, spec=item)
|
||||||
widget.valueChanged.connect(self.value_changed)
|
widget.valueChanged.connect(self.value_changed)
|
||||||
widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
|
widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
|
||||||
grid.addWidget(widget, row, 1)
|
grid.addWidget(widget, row, 1)
|
||||||
@ -185,7 +188,7 @@ class PydanticModelForm(TypedForm):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
data_model (type[BaseModel]): the model class for which to generate a form.
|
data_model (type[BaseModel]): the model class for which to generate a form.
|
||||||
enabled (bool): whether fields are enabled for editing.
|
enabled (bool, optional): whether fields are enabled for editing.
|
||||||
pretty_display (bool, optional): Whether to use a pretty display for the widget. Defaults to False. If True, disables the widget, doesn't add a clear button, and adapts the stylesheet for non-editable display.
|
pretty_display (bool, optional): Whether to use a pretty display for the widget. Defaults to False. If True, disables the widget, doesn't add a clear button, and adapts the stylesheet for non-editable display.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -10,6 +10,7 @@ from bec_lib.logger import bec_logger
|
|||||||
from bec_qthemes import material_icon
|
from bec_qthemes import material_icon
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
from pydantic.fields import FieldInfo
|
from pydantic.fields import FieldInfo
|
||||||
|
from pydantic_core import PydanticUndefined
|
||||||
from qtpy.QtCore import Signal # type: ignore
|
from qtpy.QtCore import Signal # type: ignore
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
@ -43,6 +44,7 @@ from bec_widgets.widgets.editors.scan_metadata._util import (
|
|||||||
field_minlen,
|
field_minlen,
|
||||||
field_precision,
|
field_precision,
|
||||||
)
|
)
|
||||||
|
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
@ -217,7 +219,7 @@ class StrMetadataField(DynamicFormItem):
|
|||||||
|
|
||||||
def setValue(self, value: str):
|
def setValue(self, value: str):
|
||||||
if value is None:
|
if value is None:
|
||||||
self._main_widget.setText("")
|
return self._main_widget.setText("")
|
||||||
self._main_widget.setText(str(value))
|
self._main_widget.setText(str(value))
|
||||||
|
|
||||||
|
|
||||||
@ -305,10 +307,26 @@ class BoolMetadataField(DynamicFormItem):
|
|||||||
self._main_widget.setChecked(value)
|
self._main_widget.setChecked(value)
|
||||||
|
|
||||||
|
|
||||||
|
class BoolToggleMetadataField(BoolMetadataField):
|
||||||
|
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
|
||||||
|
if spec.info.default is PydanticUndefined:
|
||||||
|
spec.info.default = False
|
||||||
|
super().__init__(parent=parent, spec=spec)
|
||||||
|
|
||||||
|
def _add_main_widget(self) -> None:
|
||||||
|
self._main_widget = ToggleSwitch()
|
||||||
|
self._layout.addWidget(self._main_widget)
|
||||||
|
self._main_widget.setToolTip(self._describe(""))
|
||||||
|
if self._default is not None:
|
||||||
|
self._main_widget.setChecked(self._default)
|
||||||
|
|
||||||
|
|
||||||
class DictMetadataField(DynamicFormItem):
|
class DictMetadataField(DynamicFormItem):
|
||||||
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
|
def __init__(self, *, parent: QWidget | None = None, spec: FormItemSpec) -> None:
|
||||||
super().__init__(parent=parent, spec=spec)
|
super().__init__(parent=parent, spec=spec)
|
||||||
self._main_widget.data_changed.connect(self._value_changed)
|
self._main_widget.data_changed.connect(self._value_changed)
|
||||||
|
if spec.info.default is not PydanticUndefined:
|
||||||
|
self._main_widget.set_default(spec.info.default)
|
||||||
|
|
||||||
def _set_pretty_display(self):
|
def _set_pretty_display(self):
|
||||||
self._main_widget.set_button_visibility(False)
|
self._main_widget.set_button_visibility(False)
|
||||||
@ -326,13 +344,11 @@ class DictMetadataField(DynamicFormItem):
|
|||||||
self._main_widget.replace_data(value)
|
self._main_widget.replace_data(value)
|
||||||
|
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
class _ItemAndWidgetType(NamedTuple):
|
||||||
|
# TODO: this should be generic but not supported in 3.10
|
||||||
|
item: type[int | float | str]
|
||||||
class _ItemAndWidgetType(NamedTuple, Generic[_T]):
|
|
||||||
item: type[_T]
|
|
||||||
widget: type
|
widget: type
|
||||||
default: _T
|
default: int | float | str
|
||||||
|
|
||||||
|
|
||||||
class ListMetadataField(DynamicFormItem):
|
class ListMetadataField(DynamicFormItem):
|
||||||
|
@ -17,6 +17,8 @@ from qtpy.QtWidgets import (
|
|||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
|
||||||
|
|
||||||
|
_NOT_SET = object()
|
||||||
|
|
||||||
|
|
||||||
class DictBackedTableModel(QAbstractTableModel):
|
class DictBackedTableModel(QAbstractTableModel):
|
||||||
def __init__(self, data):
|
def __init__(self, data):
|
||||||
@ -27,6 +29,7 @@ class DictBackedTableModel(QAbstractTableModel):
|
|||||||
data (list[list[str]]): list of key-value pairs to initialise with"""
|
data (list[list[str]]): list of key-value pairs to initialise with"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._data: list[list[str]] = data
|
self._data: list[list[str]] = data
|
||||||
|
self._default = _NOT_SET
|
||||||
self._disallowed_keys: list[str] = []
|
self._disallowed_keys: list[str] = []
|
||||||
|
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
@ -113,8 +116,13 @@ class DictBackedTableModel(QAbstractTableModel):
|
|||||||
for row in sorted(rows, reverse=True):
|
for row in sorted(rows, reverse=True):
|
||||||
self.removeRows(row, 1, QModelIndex())
|
self.removeRows(row, 1, QModelIndex())
|
||||||
|
|
||||||
|
def set_default(self, value: dict | None):
|
||||||
|
self._default = value
|
||||||
|
|
||||||
def dump_dict(self):
|
def dump_dict(self):
|
||||||
if self._data == [[]]:
|
if self._data in [[], [[]], [["", ""]]]:
|
||||||
|
if self._default is not _NOT_SET:
|
||||||
|
return self._default
|
||||||
return {}
|
return {}
|
||||||
return dict(self._data)
|
return dict(self._data)
|
||||||
|
|
||||||
@ -175,6 +183,9 @@ class DictBackedTable(QWidget):
|
|||||||
|
|
||||||
self._table_model.dataChanged.connect(lambda *_: self.data_changed.emit(self.dump_dict()))
|
self._table_model.dataChanged.connect(lambda *_: self.data_changed.emit(self.dump_dict()))
|
||||||
|
|
||||||
|
def set_default(self, value: dict | None):
|
||||||
|
self._table_model.set_default(value)
|
||||||
|
|
||||||
def set_button_visibility(self, value: bool):
|
def set_button_visibility(self, value: bool):
|
||||||
self._button_holder.setVisible(value)
|
self._button_holder.setVisible(value)
|
||||||
|
|
||||||
|
@ -67,4 +67,6 @@ def field_default(info: FieldInfo):
|
|||||||
|
|
||||||
|
|
||||||
def clearable_required(info: FieldInfo):
|
def clearable_required(info: FieldInfo):
|
||||||
return type(None) in get_args(info.annotation) or info.is_required()
|
return type(None) in get_args(info.annotation) or (
|
||||||
|
info.is_required() and info.default is PydanticUndefined
|
||||||
|
)
|
||||||
|
@ -0,0 +1,194 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bec_lib.atlas_models import Device as DeviceConfigModel
|
||||||
|
from bec_lib.config_helper import CONF as DEVICE_CONF_KEYS
|
||||||
|
from bec_lib.config_helper import ConfigHelper
|
||||||
|
from bec_lib.logger import bec_logger
|
||||||
|
from qtpy.QtCore import QSize, Qt
|
||||||
|
from qtpy.QtWidgets import (
|
||||||
|
QApplication,
|
||||||
|
QDialog,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QStackedLayout,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
|
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
||||||
|
DeviceConfigForm,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
|
||||||
|
|
||||||
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceConfigDialog(BECWidget, QDialog):
|
||||||
|
RPC = False
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, parent=None, device: str | None = None, config_helper: ConfigHelper | None = None
|
||||||
|
):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self._config_helper = config_helper or ConfigHelper(
|
||||||
|
self.client.connector, self.client._service_name
|
||||||
|
)
|
||||||
|
|
||||||
|
self._device = device
|
||||||
|
self.setWindowTitle(f"Edit config for: {device}")
|
||||||
|
self._container = QStackedLayout()
|
||||||
|
self._container.setStackingMode(QStackedLayout.StackAll)
|
||||||
|
|
||||||
|
self._add_form()
|
||||||
|
self._add_overlay()
|
||||||
|
self._add_buttons()
|
||||||
|
|
||||||
|
self.setLayout(self._container)
|
||||||
|
self._overlay_widget.setVisible(False)
|
||||||
|
|
||||||
|
def _add_form(self):
|
||||||
|
self._form_widget = QWidget()
|
||||||
|
self._layout = QVBoxLayout()
|
||||||
|
self._form_widget.setLayout(self._layout)
|
||||||
|
self._form = DeviceConfigForm()
|
||||||
|
self._layout.addWidget(self._form)
|
||||||
|
|
||||||
|
self._fetch_config()
|
||||||
|
self._fill_form()
|
||||||
|
self._container.addWidget(self._form_widget)
|
||||||
|
|
||||||
|
def _add_overlay(self):
|
||||||
|
self._overlay_widget = QWidget()
|
||||||
|
self._overlay_widget.setStyleSheet("background-color:rgba(128,128,128,128);")
|
||||||
|
self._overlay_widget.setAutoFillBackground(True)
|
||||||
|
self._overlay_layout = QVBoxLayout()
|
||||||
|
self._overlay_layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
self._overlay_widget.setLayout(self._overlay_layout)
|
||||||
|
|
||||||
|
self._spinner = SpinnerWidget(parent=self)
|
||||||
|
self._spinner.setMinimumSize(QSize(100, 100))
|
||||||
|
self._overlay_layout.addWidget(self._spinner)
|
||||||
|
self._container.addWidget(self._overlay_widget)
|
||||||
|
|
||||||
|
def _add_buttons(self):
|
||||||
|
button_box = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.Apply | QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
||||||
|
)
|
||||||
|
button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply)
|
||||||
|
button_box.accepted.connect(self.accept)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
self._layout.addWidget(button_box)
|
||||||
|
|
||||||
|
def _fetch_config(self):
|
||||||
|
self._initial_config = {}
|
||||||
|
if (
|
||||||
|
self.client.device_manager is not None
|
||||||
|
and self._device in self.client.device_manager.devices
|
||||||
|
):
|
||||||
|
self._initial_config = self.client.device_manager.devices.get(self._device)._config
|
||||||
|
|
||||||
|
def _fill_form(self):
|
||||||
|
self._form.set_data(DeviceConfigModel.model_validate(self._initial_config))
|
||||||
|
|
||||||
|
def updated_config(self):
|
||||||
|
new_config = self._form.get_form_data()
|
||||||
|
return {
|
||||||
|
k: v for k, v in new_config.items() if self._initial_config.get(k) != new_config.get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def apply(self):
|
||||||
|
self._process_update_action()
|
||||||
|
|
||||||
|
@SafeSlot()
|
||||||
|
def accept(self):
|
||||||
|
self._process_update_action()
|
||||||
|
return super().accept()
|
||||||
|
|
||||||
|
def _process_update_action(self):
|
||||||
|
updated_config = self.updated_config()
|
||||||
|
if (device_name := updated_config.get("name")) == "":
|
||||||
|
logger.warning("Can't create a device with no name!")
|
||||||
|
elif set(updated_config.keys()) & set(DEVICE_CONF_KEYS.NON_UPDATABLE):
|
||||||
|
logger.info(
|
||||||
|
f"Removing old device {self._device} and adding new device {device_name or self._device} with modified config: {updated_config}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._update_device_config(updated_config)
|
||||||
|
|
||||||
|
def _update_device_config(self, config: dict):
|
||||||
|
if config == {}:
|
||||||
|
logger.info("No changes made to device config")
|
||||||
|
return
|
||||||
|
logger.info(f"Sending request to update device config: {config}")
|
||||||
|
try:
|
||||||
|
self._start_waiting_display()
|
||||||
|
RID = self._config_helper.send_config_request(
|
||||||
|
action="update", config={self._device: config}, wait_for_response=False
|
||||||
|
)
|
||||||
|
reply = self._config_helper.wait_for_config_reply(
|
||||||
|
RID, timeout=self._config_helper.suggested_timeout_s(config)
|
||||||
|
)
|
||||||
|
self._config_helper.handle_update_reply(reply, RID)
|
||||||
|
self._stop_waiting_display()
|
||||||
|
except Exception as e:
|
||||||
|
self._stop_waiting_display()
|
||||||
|
logger.error(f"Error updating config: \n {''.join(traceback.format_exception(e))}")
|
||||||
|
finally:
|
||||||
|
self._fetch_config()
|
||||||
|
self._fill_form()
|
||||||
|
|
||||||
|
def _start_waiting_display(self):
|
||||||
|
self._overlay_widget.setVisible(True)
|
||||||
|
self._spinner.start()
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
def _stop_waiting_display(self):
|
||||||
|
self._overlay_widget.setVisible(False)
|
||||||
|
self._spinner.stop()
|
||||||
|
QApplication.processEvents()
|
||||||
|
|
||||||
|
|
||||||
|
def main(): # pragma: no cover
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from qtpy.QtWidgets import QApplication, QLineEdit, QPushButton, QWidget
|
||||||
|
|
||||||
|
from bec_widgets.utils.colors import set_theme
|
||||||
|
|
||||||
|
dialog = None
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
set_theme("light")
|
||||||
|
widget = QWidget()
|
||||||
|
widget.setLayout(QVBoxLayout())
|
||||||
|
|
||||||
|
device = QLineEdit()
|
||||||
|
widget.layout().addWidget(device)
|
||||||
|
|
||||||
|
def _destroy_dialog(*_):
|
||||||
|
nonlocal dialog
|
||||||
|
dialog = None
|
||||||
|
|
||||||
|
def accept(*args):
|
||||||
|
logger.success(f"submitted device config form {dialog} {args}")
|
||||||
|
_destroy_dialog()
|
||||||
|
|
||||||
|
def _show_dialog(*_):
|
||||||
|
nonlocal dialog
|
||||||
|
if dialog is None:
|
||||||
|
dialog = DeviceConfigDialog(device=device.text())
|
||||||
|
dialog.accepted.connect(accept)
|
||||||
|
dialog.rejected.connect(_destroy_dialog)
|
||||||
|
dialog.open()
|
||||||
|
|
||||||
|
button = QPushButton("Show device dialog")
|
||||||
|
widget.layout().addWidget(button)
|
||||||
|
button.clicked.connect(_show_dialog)
|
||||||
|
widget.show()
|
||||||
|
sys.exit(app.exec_())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,12 +1,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from bec_lib.atlas_models import Device as DeviceConfigModel
|
from bec_lib.atlas_models import Device as DeviceConfigModel
|
||||||
|
from pydantic import BaseModel
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
from bec_widgets.utils.colors import get_theme_name
|
from bec_widgets.utils.colors import get_theme_name
|
||||||
from bec_widgets.utils.forms_from_types import styles
|
from bec_widgets.utils.forms_from_types import styles
|
||||||
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
|
from bec_widgets.utils.forms_from_types.forms import PydanticModelForm
|
||||||
from bec_widgets.utils.forms_from_types.items import DEFAULT_WIDGET_TYPES, BoolMetadataField
|
from bec_widgets.utils.forms_from_types.items import (
|
||||||
|
DEFAULT_WIDGET_TYPES,
|
||||||
|
BoolMetadataField,
|
||||||
|
BoolToggleMetadataField,
|
||||||
|
widget_from_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceConfigForm(PydanticModelForm):
|
class DeviceConfigForm(PydanticModelForm):
|
||||||
@ -22,9 +30,13 @@ class DeviceConfigForm(PydanticModelForm):
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
self._widget_types = DEFAULT_WIDGET_TYPES.copy()
|
self._widget_types = DEFAULT_WIDGET_TYPES.copy()
|
||||||
self._widget_types["optional_bool"] = (lambda anno: anno is bool | None, BoolMetadataField)
|
self._widget_types["bool"] = (lambda anno: anno is bool, BoolToggleMetadataField)
|
||||||
|
self._widget_types["optional_bool"] = (lambda anno: anno == bool | None, BoolMetadataField)
|
||||||
self._validity.setVisible(False)
|
self._validity.setVisible(False)
|
||||||
self._connect_to_theme_change()
|
self._connect_to_theme_change()
|
||||||
|
self.populate()
|
||||||
|
|
||||||
|
def _post_init(self): ...
|
||||||
|
|
||||||
def set_pretty_display_theme(self, theme: str | None = None):
|
def set_pretty_display_theme(self, theme: str | None = None):
|
||||||
if theme is None:
|
if theme is None:
|
||||||
@ -36,3 +48,9 @@ class DeviceConfigForm(PydanticModelForm):
|
|||||||
qapp = QApplication.instance()
|
qapp = QApplication.instance()
|
||||||
if hasattr(qapp, "theme_signal"):
|
if hasattr(qapp, "theme_signal"):
|
||||||
qapp.theme_signal.theme_updated.connect(self.set_pretty_display_theme) # type: ignore
|
qapp.theme_signal.theme_updated.connect(self.set_pretty_display_theme) # type: ignore
|
||||||
|
|
||||||
|
def set_schema(self, schema: type[BaseModel]):
|
||||||
|
raise TypeError("This class doesn't support changing the schema")
|
||||||
|
|
||||||
|
def set_data(self, data: DeviceConfigModel): # type: ignore # This class locks the type
|
||||||
|
super().set_data(data)
|
||||||
|
@ -4,20 +4,24 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from bec_lib.atlas_models import Device as DeviceConfigModel
|
from bec_lib.atlas_models import Device as DeviceConfigModel
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
|
from bec_qthemes import material_icon
|
||||||
from qtpy.QtCore import QMimeData, QSize, Qt, Signal
|
from qtpy.QtCore import QMimeData, QSize, Qt, Signal
|
||||||
from qtpy.QtGui import QDrag
|
from qtpy.QtGui import QDrag
|
||||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QWidget
|
from qtpy.QtWidgets import QApplication, QHBoxLayout, QToolButton, QWidget
|
||||||
|
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
from bec_widgets.utils.error_popups import SafeSlot
|
||||||
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
|
||||||
|
from bec_widgets.widgets.services.device_browser.device_item.device_config_dialog import (
|
||||||
|
DeviceConfigDialog,
|
||||||
|
)
|
||||||
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
||||||
DeviceConfigForm,
|
DeviceConfigForm,
|
||||||
)
|
)
|
||||||
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from qtpy.QtGui import QMouseEvent
|
from qtpy.QtGui import QMouseEvent
|
||||||
|
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +43,19 @@ class DeviceItem(ExpandableGroupFrame):
|
|||||||
|
|
||||||
self.adjustSize()
|
self.adjustSize()
|
||||||
|
|
||||||
|
def _create_title_layout(self, title: str, icon: str):
|
||||||
|
super()._create_title_layout(title, icon)
|
||||||
|
self.edit_button = QToolButton()
|
||||||
|
self.edit_button.setIcon(
|
||||||
|
material_icon(icon_name="edit", size=(10, 10), convert_to_pixmap=False)
|
||||||
|
)
|
||||||
|
self._title_layout.insertWidget(self._title_layout.count() - 1, self.edit_button)
|
||||||
|
self.edit_button.clicked.connect(self._create_edit_dialog)
|
||||||
|
|
||||||
|
def _create_edit_dialog(self):
|
||||||
|
dialog = DeviceConfigDialog(parent=self, device=self.device)
|
||||||
|
dialog.open()
|
||||||
|
|
||||||
@SafeSlot()
|
@SafeSlot()
|
||||||
def switch_expanded_state(self):
|
def switch_expanded_state(self):
|
||||||
if not self.expanded and not self._expanded_first_time:
|
if not self.expanded and not self._expanded_first_time:
|
||||||
@ -92,6 +109,11 @@ if __name__ == "__main__": # pragma: no cover
|
|||||||
|
|
||||||
from qtpy.QtWidgets import QApplication
|
from qtpy.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
|
||||||
|
DeviceConfigForm,
|
||||||
|
)
|
||||||
|
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
|
@ -10,6 +10,7 @@ class ToggleSwitch(QWidget):
|
|||||||
A simple toggle.
|
A simple toggle.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
stateChanged = Signal(bool)
|
||||||
enabled = Signal(bool)
|
enabled = Signal(bool)
|
||||||
ICON_NAME = "toggle_on"
|
ICON_NAME = "toggle_on"
|
||||||
PLUGIN = True
|
PLUGIN = True
|
||||||
@ -42,11 +43,19 @@ class ToggleSwitch(QWidget):
|
|||||||
|
|
||||||
@checked.setter
|
@checked.setter
|
||||||
def checked(self, state):
|
def checked(self, state):
|
||||||
|
if self._checked != state:
|
||||||
|
self.stateChanged.emit(state)
|
||||||
self._checked = state
|
self._checked = state
|
||||||
self.update_colors()
|
self.update_colors()
|
||||||
self.set_thumb_pos_to_state()
|
self.set_thumb_pos_to_state()
|
||||||
self.enabled.emit(self._checked)
|
self.enabled.emit(self._checked)
|
||||||
|
|
||||||
|
def setChecked(self, state: bool):
|
||||||
|
self.checked = state
|
||||||
|
|
||||||
|
def isChecked(self):
|
||||||
|
return self.checked
|
||||||
|
|
||||||
@Property(QPointF)
|
@Property(QPointF)
|
||||||
def thumb_pos(self):
|
def thumb_pos(self):
|
||||||
return self._thumb_pos
|
return self._thumb_pos
|
||||||
|
@ -42,7 +42,7 @@ class ExampleSchema(BasicScanMetadata):
|
|||||||
|
|
||||||
TEST_DICT = {
|
TEST_DICT = {
|
||||||
"sample_name": "test name",
|
"sample_name": "test name",
|
||||||
"str_optional": "None",
|
"str_optional": None,
|
||||||
"str_required": "something",
|
"str_required": "something",
|
||||||
"bool_optional": None,
|
"bool_optional": None,
|
||||||
"bool_required_default": True,
|
"bool_required_default": True,
|
||||||
|
Reference in New Issue
Block a user