1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-08 17:57:54 +02:00

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

View File

@@ -28,6 +28,7 @@ class SimpleFileLikeFromLogOutputFunc:
def __init__(self, log_func):
self._log_func = log_func
self._buffer = []
self.encoding = "utf8"
def write(self, buffer):
self._buffer.append(buffer)

View File

@@ -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 qtpy.QtCore import Property, Signal, Slot
@@ -16,7 +16,7 @@ class DapComboBox(BECWidget, QWidget):
Args:
parent: Parent widget.
client: BEC client object.
gui_id: GUI ID.
gui_id: GUI ID.--
default: Default device name.
"""
@@ -154,7 +154,9 @@ class DapComboBox(BECWidget, QWidget):
def populate_fit_model_combobox(self):
"""Populate the fit_model_combobox with the devices."""
# 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.addItems(self.available_models)

View File

@@ -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()

View File

@@ -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
"""

View File

@@ -83,7 +83,6 @@ class ClearableBoolEntry(QWidget):
class MetadataWidget(QWidget):
valueChanged = Signal()
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]:
return BoolMetadataField
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

View File

@@ -35,7 +35,7 @@ if TYPE_CHECKING:
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
metadata schema registry supplied in the plugin repo to find pydantic models
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._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._additional_md_box_layout = QHBoxLayout()
self._additional_md_box.set_layout(self._additional_md_box_layout)
@@ -129,7 +131,9 @@ class ScanMetadata(BECWidget, QWidget):
self._populate()
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()):
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]:
grid = self._md_grid_layout
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())
}
@@ -182,6 +190,9 @@ class ScanMetadata(BECWidget, QWidget):
self._additional_md_box.setVisible(not hide)
class ScanMetadata(PydanticModelForm): ...
if __name__ == "__main__": # pragma: no cover
from unittest.mock import patch
@@ -190,15 +201,21 @@ if __name__ == "__main__": # pragma: no cover
from bec_widgets.utils.colors import set_theme
class ExampleSchema1(BasicScanMetadata):
abc: int = Field(gt=0, lt=2000, description="Heating temperature abc", title="A B C")
foo: str = Field(max_length=12, description="Sample database code", default="DEF123")
abc: int = Field(
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)
baz: bool
class ExampleSchema2(BasicScanMetadata):
checkbox_up_top: bool
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(
None, description="This is just one different item...", gt=-100, lt=0
@@ -211,9 +228,12 @@ if __name__ == "__main__": # pragma: no cover
with patch(
"bec_lib.metadata_schema._get_metadata_schema_registry",
lambda: {"scan1": ExampleSchema1, "scan2": ExampleSchema2, "scan3": ExampleSchema3},
lambda: {
"scan1": ExampleSchema1,
"scan2": ExampleSchema2,
"scan3": ExampleSchema3,
},
):
app = QApplication([])
w = QWidget()
selection = QComboBox()