Compare commits

...

2 Commits

Author SHA1 Message Date
David Perl bcc246cf92 WIP drag and drop form widget 2025-04-17 16:33:40 +02:00
David Perl 550753078b WIP look at form widgets 2025-04-17 10:49:57 +02:00
7 changed files with 237 additions and 14 deletions
+1
View File
@@ -28,6 +28,7 @@ class SimpleFileLikeFromLogOutputFunc:
def __init__(self, log_func): def __init__(self, log_func):
self._log_func = log_func self._log_func = log_func
self._buffer = [] self._buffer = []
self.encoding = "utf8"
def write(self, buffer): def write(self, buffer):
self._buffer.append(buffer) self._buffer.append(buffer)
@@ -1,4 +1,4 @@
""" Module for DapComboBox widget class to select a DAP model from a combobox. """ """Module for DapComboBox widget class to select a DAP model from a combobox."""
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from qtpy.QtCore import Property, Signal, Slot from qtpy.QtCore import Property, Signal, Slot
@@ -16,7 +16,7 @@ class DapComboBox(BECWidget, QWidget):
Args: Args:
parent: Parent widget. parent: Parent widget.
client: BEC client object. client: BEC client object.
gui_id: GUI ID. gui_id: GUI ID.--
default: Default device name. default: Default device name.
""" """
@@ -154,7 +154,9 @@ class DapComboBox(BECWidget, QWidget):
def populate_fit_model_combobox(self): def populate_fit_model_combobox(self):
"""Populate the fit_model_combobox with the devices.""" """Populate the fit_model_combobox with the devices."""
# pylint: disable=protected-access # pylint: disable=protected-access
self.available_models = [model for model in self.client.dap._available_dap_plugins.keys()] self.available_models = [
model for model in self.client.dap._available_dap_plugins.keys()
]
self.fit_model_combobox.clear() self.fit_model_combobox.clear()
self.fit_model_combobox.addItems(self.available_models) self.fit_model_combobox.addItems(self.available_models)
@@ -0,0 +1,193 @@
import inspect
from bec_lib.signature_serializer import deserialize_dtype
from bec_server.data_processing.dap_framework_refactoring.dap_blocks import (
BlockWithLotsOfArgs,
DAPBlock,
GradientBlock,
SmoothBlock,
)
from pydantic.fields import FieldInfo
from PySide6.QtWidgets import (
QCheckBox,
QDoubleSpinBox,
QLabel,
QLayout,
QRadioButton,
QScrollArea,
)
from qtpy.QtCore import QMimeData, Qt, Signal
from qtpy.QtGui import QDrag, QPixmap
from qtpy.QtWidgets import (
QApplication,
QHBoxLayout,
QMainWindow,
QVBoxLayout,
QWidget,
)
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.expandable_frame import ExpandableGroupFrame
from bec_widgets.widgets.editors.scan_metadata._metadata_widgets import widget_from_type
from bec_widgets.widgets.editors.scan_metadata.scan_metadata import ScanMetadata
from tests.unit_tests.test_scan_metadata import metadata_widget
class DragItem(ExpandableGroupFrame):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setContentsMargins(25, 5, 25, 5)
self._layout = QVBoxLayout()
self.set_layout(self._layout)
def mouseMoveEvent(self, e):
if e.buttons() == Qt.MouseButton.LeftButton:
drag = QDrag(self)
mime = QMimeData()
drag.setMimeData(mime)
pixmap = QPixmap(self.size())
self.render(pixmap)
drag.setPixmap(pixmap)
drag.exec(Qt.DropAction.MoveAction)
class DragWidget(QWidget):
"""
Generic list sorting handler.
"""
orderChanged = Signal(list)
def __init__(self, *args, orientation=Qt.Orientation.Vertical, **kwargs):
super().__init__(*args, **kwargs)
self.setAcceptDrops(True)
# Store the orientation for drag checks later.
self.orientation = orientation
if self.orientation == Qt.Orientation.Vertical:
self.blayout = QVBoxLayout()
else:
self.blayout = QHBoxLayout()
self.setLayout(self.blayout)
def dragEnterEvent(self, e):
e.accept()
def dropEvent(self, e):
pos = e.position()
widget = e.source()
self.blayout.removeWidget(widget)
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w = self.blayout.itemAt(n).widget()
if self.orientation == Qt.Orientation.Vertical:
# Drag drop vertically.
drop_here = pos.y() < w.y() + w.size().height() // 2
else:
# Drag drop horizontally.
drop_here = pos.x() < w.x() + w.size().width() // 2
if drop_here:
break
else:
# We aren't on the left hand/upper side of any widget,
# so we're at the end. Increment 1 to insert after.
n += 1
self.blayout.insertWidget(n, widget)
self.orderChanged.emit(self.get_item_data())
e.accept()
def add_item(self, item):
self.blayout.addWidget(item)
def get_item_data(self):
data = []
for n in range(self.blayout.count()):
# Get the widget at each index in turn.
w: "DAPBlockWidget" = self.blayout.itemAt(n).widget()
data.append(w._title.text())
return data
class DAPBlockWidget(BECWidget, DragItem):
def __init__(
self,
parent=None,
content: type[DAPBlock] = None,
client=None,
gui_id: str | None = None,
**kwargs,
):
super().__init__(
parent=parent,
client=client,
gui_id=gui_id,
title=content.__name__,
**kwargs,
)
self._content = content
self.add_form(self._content)
def add_form(self, block_type: type[DAPBlock]):
run_signature = inspect.signature(block_type.run)
self._title.setText(block_type.__name__)
layout = self._contents.layout()
if layout is None:
return
self._add_widgets_for_signature(layout, run_signature)
def _add_widgets_for_signature(self, layout: QLayout, signature: inspect.Signature):
for arg_name, arg_spec in signature.parameters.items():
annotation: str | type = arg_spec.annotation
if isinstance(annotation, str):
annotation = deserialize_dtype(annotation) or annotation
w = QWidget()
w.setLayout(QHBoxLayout())
w.layout().addWidget(QLabel(arg_name))
w.layout().addWidget(
widget_from_type(annotation)(
FieldInfo(
annotation=annotation
) # FIXME this class should not be initialised directly...
)
)
w.layout().addWidget(QLabel(str(annotation)))
layout.addWidget(w)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.drag = DragWidget(orientation=Qt.Orientation.Vertical)
for block_type in [SmoothBlock, GradientBlock, BlockWithLotsOfArgs] * 2:
item = DAPBlockWidget(content=block_type)
self.drag.add_item(item)
# Print out the changed order.
self.drag.orderChanged.connect(print)
container = QWidget()
layout = QVBoxLayout()
layout.addStretch(1)
layout.addWidget(self.drag)
layout.addStretch(1)
container.setLayout(layout)
self.setCentralWidget(container)
if __name__ == "__main__":
app = QApplication([])
w = MainWindow()
w.show()
app.exec()
@@ -0,0 +1,6 @@
"""Panel to compose DAP task runs
- new ones are added as tabs
- can be enabled and disabled, continuous and oneoff
- fill in extra kwargs using thing from MD widget
- output to topic for the name, which looks like a data topic
"""
@@ -83,7 +83,6 @@ class ClearableBoolEntry(QWidget):
class MetadataWidget(QWidget): class MetadataWidget(QWidget):
valueChanged = Signal() valueChanged = Signal()
def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None: def __init__(self, info: FieldInfo, parent: QWidget | None = None) -> None:
@@ -250,7 +249,9 @@ def widget_from_type(annotation: type | None) -> Callable[[FieldInfo], MetadataW
if annotation in [bool, bool | None]: if annotation in [bool, bool | None]:
return BoolMetadataField return BoolMetadataField
else: else:
logger.warning(f"Type {annotation} is not (yet) supported in metadata form creation.") logger.warning(
f"Type {annotation} is not (yet) supported in metadata form creation."
)
return StrMetadataField return StrMetadataField
@@ -35,7 +35,7 @@ if TYPE_CHECKING:
logger = bec_logger.logger logger = bec_logger.logger
class ScanMetadata(BECWidget, QWidget): class PydanticModelForm(BECWidget, QWidget):
"""Dynamically generates a form for inclusion of metadata for a scan. Uses the """Dynamically generates a form for inclusion of metadata for a scan. Uses the
metadata schema registry supplied in the plugin repo to find pydantic models metadata schema registry supplied in the plugin repo to find pydantic models
associated with the scan type. Sets limits for numerical values if specified.""" associated with the scan type. Sets limits for numerical values if specified."""
@@ -75,7 +75,9 @@ class ScanMetadata(BECWidget, QWidget):
self._new_grid_layout() self._new_grid_layout()
self._grid_container.addLayout(self._md_grid_layout) self._grid_container.addLayout(self._md_grid_layout)
self._additional_md_box = ExpandableGroupFrame("Additional metadata", expanded=False) self._additional_md_box = ExpandableGroupFrame(
"Additional metadata", expanded=False
)
self._layout.addWidget(self._additional_md_box) self._layout.addWidget(self._additional_md_box)
self._additional_md_box_layout = QHBoxLayout() self._additional_md_box_layout = QHBoxLayout()
self._additional_md_box.set_layout(self._additional_md_box_layout) self._additional_md_box.set_layout(self._additional_md_box_layout)
@@ -129,7 +131,9 @@ class ScanMetadata(BECWidget, QWidget):
self._populate() self._populate()
def _populate(self): def _populate(self):
self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys())) self._additional_metadata.update_disallowed_keys(
list(self._md_schema.model_fields.keys())
)
for i, (field_name, info) in enumerate(self._md_schema.model_fields.items()): for i, (field_name, info) in enumerate(self._md_schema.model_fields.items()):
self._add_griditem(field_name, info, i) self._add_griditem(field_name, info, i)
@@ -146,7 +150,11 @@ class ScanMetadata(BECWidget, QWidget):
def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]: def _dict_from_grid(self) -> dict[str, str | int | float | Decimal | bool]:
grid = self._md_grid_layout grid = self._md_grid_layout
return { return {
grid.itemAtPosition(i, 0).widget().property("_model_field_name"): grid.itemAtPosition(i, 1).widget().getValue() # type: ignore # we only add 'MetadataWidget's here grid.itemAtPosition(i, 0)
.widget()
.property("_model_field_name"): grid.itemAtPosition(i, 1)
.widget()
.getValue() # type: ignore # we only add 'MetadataWidget's here
for i in range(grid.rowCount()) for i in range(grid.rowCount())
} }
@@ -182,6 +190,9 @@ class ScanMetadata(BECWidget, QWidget):
self._additional_md_box.setVisible(not hide) self._additional_md_box.setVisible(not hide)
class ScanMetadata(PydanticModelForm): ...
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
from unittest.mock import patch from unittest.mock import patch
@@ -190,15 +201,21 @@ if __name__ == "__main__": # pragma: no cover
from bec_widgets.utils.colors import set_theme from bec_widgets.utils.colors import set_theme
class ExampleSchema1(BasicScanMetadata): class ExampleSchema1(BasicScanMetadata):
abc: int = Field(gt=0, lt=2000, description="Heating temperature abc", title="A B C") abc: int = Field(
foo: str = Field(max_length=12, description="Sample database code", default="DEF123") gt=0, lt=2000, description="Heating temperature abc", title="A B C"
)
foo: str = Field(
max_length=12, description="Sample database code", default="DEF123"
)
xyz: Decimal = Field(decimal_places=4) xyz: Decimal = Field(decimal_places=4)
baz: bool baz: bool
class ExampleSchema2(BasicScanMetadata): class ExampleSchema2(BasicScanMetadata):
checkbox_up_top: bool checkbox_up_top: bool
checkbox_again: bool = Field( checkbox_again: bool = Field(
title="Checkbox Again", description="this one defaults to True", default=True title="Checkbox Again",
description="this one defaults to True",
default=True,
) )
different_items: int | None = Field( different_items: int | None = Field(
None, description="This is just one different item...", gt=-100, lt=0 None, description="This is just one different item...", gt=-100, lt=0
@@ -211,9 +228,12 @@ if __name__ == "__main__": # pragma: no cover
with patch( with patch(
"bec_lib.metadata_schema._get_metadata_schema_registry", "bec_lib.metadata_schema._get_metadata_schema_registry",
lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3}, lambda: {
"scan1": ExampleSchema1,
"scan2": ExampleSchema2,
"scan3": ExampleSchema3,
},
): ):
app = QApplication([]) app = QApplication([])
w = QWidget() w = QWidget()
selection = QComboBox() selection = QComboBox()