From bcc246cf92a136e7b38e4db9d310691bde5faadf Mon Sep 17 00:00:00 2001 From: David Perl Date: Thu, 17 Apr 2025 13:36:23 +0200 Subject: [PATCH] WIP drag and drop form widget --- bec_widgets/cli/server.py | 1 + .../widgets/dap/dap_task_manager/__init__.py | 0 .../dap/dap_task_manager/drag_and_drop.py | 193 ++++++++++++++++++ .../dap/dap_task_manager/task_editor.py | 6 + .../scan_metadata/_metadata_widgets.py | 5 +- 5 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 bec_widgets/widgets/dap/dap_task_manager/__init__.py create mode 100644 bec_widgets/widgets/dap/dap_task_manager/drag_and_drop.py create mode 100644 bec_widgets/widgets/dap/dap_task_manager/task_editor.py diff --git a/bec_widgets/cli/server.py b/bec_widgets/cli/server.py index d3510f31..1409a479 100644 --- a/bec_widgets/cli/server.py +++ b/bec_widgets/cli/server.py @@ -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) diff --git a/bec_widgets/widgets/dap/dap_task_manager/__init__.py b/bec_widgets/widgets/dap/dap_task_manager/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bec_widgets/widgets/dap/dap_task_manager/drag_and_drop.py b/bec_widgets/widgets/dap/dap_task_manager/drag_and_drop.py new file mode 100644 index 00000000..b367a7ae --- /dev/null +++ b/bec_widgets/widgets/dap/dap_task_manager/drag_and_drop.py @@ -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() diff --git a/bec_widgets/widgets/dap/dap_task_manager/task_editor.py b/bec_widgets/widgets/dap/dap_task_manager/task_editor.py new file mode 100644 index 00000000..e6085b94 --- /dev/null +++ b/bec_widgets/widgets/dap/dap_task_manager/task_editor.py @@ -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 +""" diff --git a/bec_widgets/widgets/editors/scan_metadata/_metadata_widgets.py b/bec_widgets/widgets/editors/scan_metadata/_metadata_widgets.py index d0297105..c6fd87c2 100644 --- a/bec_widgets/widgets/editors/scan_metadata/_metadata_widgets.py +++ b/bec_widgets/widgets/editors/scan_metadata/_metadata_widgets.py @@ -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