diff --git a/bec_widgets/utils/forms_from_types/forms.py b/bec_widgets/utils/forms_from_types/forms.py index 9797af2e0..545e3d92a 100644 --- a/bec_widgets/utils/forms_from_types/forms.py +++ b/bec_widgets/utils/forms_from_types/forms.py @@ -106,7 +106,9 @@ def populate(self): def _add_griditem(self, item: FormItemSpec, row: int): grid = self._form_grid.layout() - label = QLabel(parent=self._form_grid, text=item.name) + # Use title from FieldInfo if available, otherwise use the property name + label_text = item.info.title if item.info.title else item.name + label = QLabel(parent=self._form_grid, text=label_text) label.setProperty("_model_field_name", item.name) label.setToolTip(item.info.description or item.name) grid.addWidget(label, row, 0) diff --git a/bec_widgets/utils/forms_from_types/items.py b/bec_widgets/utils/forms_from_types/items.py index a0b8e1f7a..691bc1e0e 100644 --- a/bec_widgets/utils/forms_from_types/items.py +++ b/bec_widgets/utils/forms_from_types/items.py @@ -231,6 +231,8 @@ class StrFormItem(DynamicFormItem): def __init__(self, parent: QWidget | None = None, *, spec: FormItemSpec) -> None: super().__init__(parent=parent, spec=spec) self._main_widget.textChanged.connect(self._value_changed) + if spec.info.description: + self._main_widget.setPlaceholderText(spec.info.description) def _add_main_widget(self) -> None: self._main_widget = QLineEdit() diff --git a/bec_widgets/widgets/control/scan_control/scan_control.py b/bec_widgets/widgets/control/scan_control/scan_control.py index 1a633ec3a..c0bc1e24b 100644 --- a/bec_widgets/widgets/control/scan_control/scan_control.py +++ b/bec_widgets/widgets/control/scan_control/scan_control.py @@ -9,6 +9,7 @@ from qtpy.QtWidgets import ( QApplication, QComboBox, + QGroupBox, QHBoxLayout, QLabel, QPushButton, @@ -171,7 +172,13 @@ def _init_UI(self): self.layout.addStretch() def _add_metadata_form(self): - self.layout.addWidget(self._metadata_form) + # Wrap metadata form in a group box + self._metadata_group = QGroupBox("Scan Metadata", self) + self._metadata_group.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) + metadata_layout = QVBoxLayout(self._metadata_group) + metadata_layout.addWidget(self._metadata_form) + + self.layout.addWidget(self._metadata_group) self._metadata_form.update_with_new_scan(self.comboBox_scan_selection.currentText()) self.scan_selected.connect(self._metadata_form.update_with_new_scan) self._metadata_form.form_data_updated.connect(self.update_scan_metadata) diff --git a/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py b/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py index 5df774f02..6271c0fed 100644 --- a/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py +++ b/bec_widgets/widgets/editors/scan_metadata/scan_metadata.py @@ -53,6 +53,8 @@ def __init__( super().__init__(parent=parent, data_model=self._md_schema, client=client, **kwargs) + self._layout.setContentsMargins(0, 0, 0, 0) + self._form_grid_container.layout().setContentsMargins(0, 0, 0, 0) self._layout.addWidget(self._additional_md_box) self._additional_md_box_layout.addWidget(self._additional_metadata) @@ -78,12 +80,27 @@ def hide_optional_metadata(self, hide: bool): def get_form_data(self): """Get the entered metadata as a dict""" - return self._additional_metadata.dump_dict() | self._dict_from_grid() + form_data = self._additional_metadata.dump_dict() | self._dict_from_grid() + + # If scan_name is empty, set it to the current scan + if "scan_name" in form_data and not form_data["scan_name"]: + form_data["scan_name"] = self._scan_name + + return form_data def populate(self): self._additional_metadata.update_disallowed_keys(list(self._md_schema.model_fields.keys())) super().populate() + # Set scan_name field to current scan if it exists and is empty + if "scan_name" not in self.widget_dict: + return + scan_name_widget = self.widget_dict["scan_name"] + if not hasattr(scan_name_widget, "getValue") or scan_name_widget.getValue(): + return + if hasattr(scan_name_widget, "setValue"): + scan_name_widget.setValue(self._scan_name) + def set_schema_from_scan(self, scan_name: str | None): self._scan_name = scan_name or "" self.set_schema(get_metadata_schema_for_scan(self._scan_name)) diff --git a/bec_widgets/widgets/services/bec_queue/bec_queue.py b/bec_widgets/widgets/services/bec_queue/bec_queue.py index aa37cc70c..c0bba7105 100644 --- a/bec_widgets/widgets/services/bec_queue/bec_queue.py +++ b/bec_widgets/widgets/services/bec_queue/bec_queue.py @@ -1,5 +1,7 @@ from __future__ import annotations +import json + from bec_lib import messages from bec_lib.endpoints import MessageEndpoints from bec_qthemes import material_icon @@ -59,8 +61,10 @@ def __init__( # Set up the table self.table = QTableWidget(self) # self.layout.addWidget(self.table) - self.table.setColumnCount(4) - self.table.setHorizontalHeaderLabels(["Scan Number", "Type", "Status", "Cancel"]) + self.table.setColumnCount(5) + self.table.setHorizontalHeaderLabels( + ["Scan Number", "Scan Name", "Type", "Status", "Cancel"] + ) header = self.table.horizontalHeader() header.setSectionResizeMode(QHeaderView.Stretch) @@ -169,15 +173,23 @@ def update_queue(self, content, _metadata): blocks = item.request_blocks scan_types = [] scan_numbers = [] + scan_names = [] scan_ids = [] + user_metadatas = [] status = item.status for request_block in blocks: scan_type = request_block.msg.scan_type + user_metadata = request_block.msg.metadata.get("user_metadata", {}) + scan_name = user_metadata.get("scan_name", scan_type) if scan_type: scan_types.append(scan_type) scan_number = request_block.scan_number if scan_number: scan_numbers.append(str(scan_number)) + if scan_name: + scan_names.append(scan_name) + if user_metadata: + user_metadatas.append(user_metadata) scan_id = request_block.scan_id if scan_id: scan_ids.append(scan_id) @@ -185,9 +197,18 @@ def update_queue(self, content, _metadata): scan_types = ", ".join(scan_types) if scan_numbers: scan_numbers = ", ".join(scan_numbers) + if scan_names: + scan_names = ", ".join(scan_names) + # Pretty print user metadata as tooltip + tooltip = "" + if user_metadatas: + if len(user_metadatas) == 1: + tooltip = json.dumps(user_metadatas[0], indent=2) + else: + tooltip = json.dumps(user_metadatas, indent=2) if scan_ids: scan_ids = ", ".join(scan_ids) - self.set_row(index, scan_numbers, scan_types, status, scan_ids) + self.set_row(index, scan_numbers, scan_names, scan_types, status, scan_ids, tooltip) busy = ( False if all(item.status in ("STOPPED", "COMPLETED", "IDLE") for item in queue_info) @@ -196,13 +217,13 @@ def update_queue(self, content, _metadata): self.set_global_state("warning" if busy else "default") self.queue_busy.emit(busy) - def format_item(self, content: str, status=False) -> QTableWidgetItem: + def format_item(self, content: str, status=False, tooltip: str = "") -> QTableWidgetItem: """ Format the content of the table item. Args: content (str): The content to be formatted. - + tooltip (str): Optional tooltip to display. Returns: QTableWidgetItem: The formatted item. """ @@ -210,7 +231,8 @@ def format_item(self, content: str, status=False) -> QTableWidgetItem: content = "" item = QTableWidgetItem(content) item.setTextAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) - # item.setFlags(item.flags() & ~Qt.ItemFlag.ItemIsEditable) + if tooltip: + item.setToolTip(tooltip) if status: try: @@ -220,23 +242,36 @@ def format_item(self, content: str, status=False) -> QTableWidgetItem: return item return item - def set_row(self, index: int, scan_number: str, scan_type: str, status: str, scan_id: str): + def set_row( + self, + index: int, + scan_number: str, + scan_name: str, + scan_type: str, + status: str, + scan_id: str, + tooltip: str = "", + ): """ Set the row of the table. Args: index (int): The index of the row. scan_number (str): The scan number. + scan_name (str): The scan name. scan_type (str): The scan type. status (str): The status. + scan_id (str): The scan id. + tooltip (str): Optional tooltip to display (pretty-printed user metadata). """ abort_button = self._create_abort_button(scan_id) abort_button.button.clicked.connect(self.delete_selected_row) - self.table.setItem(index, 0, self.format_item(scan_number)) - self.table.setItem(index, 1, self.format_item(scan_type)) - self.table.setItem(index, 2, self.format_item(status, status=True)) - self.table.setCellWidget(index, 3, abort_button) + self.table.setItem(index, 0, self.format_item(scan_number, tooltip=tooltip)) + self.table.setItem(index, 1, self.format_item(scan_name, tooltip=tooltip)) + self.table.setItem(index, 2, self.format_item(scan_type, tooltip=tooltip)) + self.table.setItem(index, 3, self.format_item(status, status=True, tooltip=tooltip)) + self.table.setCellWidget(index, 4, abort_button) def _create_abort_button(self, scan_id: str) -> AbortButton: """ @@ -279,7 +314,7 @@ def reset_content(self): """ self.table.setRowCount(1) - self.set_row(0, "", "", "", "") + self.set_row(0, "", "", "", "", "") if __name__ == "__main__": # pragma: no cover diff --git a/tests/unit_tests/test_bec_queue.py b/tests/unit_tests/test_bec_queue.py index e3f0f7a11..25ab3079f 100644 --- a/tests/unit_tests/test_bec_queue.py +++ b/tests/unit_tests/test_bec_queue.py @@ -100,7 +100,8 @@ def test_bec_queue(bec_queue, bec_queue_msg_full): assert bec_queue.table.rowCount() == 1 assert bec_queue.table.item(0, 0).text() == "1289" assert bec_queue.table.item(0, 1).text() == "line_scan" - assert bec_queue.table.item(0, 2).text() == "COMPLETED" + assert bec_queue.table.item(0, 2).text() == "line_scan" + assert bec_queue.table.item(0, 3).text() == "COMPLETED" def test_bec_queue_empty(bec_queue): @@ -109,6 +110,7 @@ def test_bec_queue_empty(bec_queue): assert bec_queue.table.item(0, 0).text() == "" assert bec_queue.table.item(0, 1).text() == "" assert bec_queue.table.item(0, 2).text() == "" + assert bec_queue.table.item(0, 3).text() == "" def test_queue_abort(bec_queue, bec_queue_msg_full): @@ -118,9 +120,10 @@ def test_queue_abort(bec_queue, bec_queue_msg_full): assert bec_queue.table.rowCount() == 1 assert bec_queue.table.item(0, 0).text() == "1289" assert bec_queue.table.item(0, 1).text() == "line_scan" - assert bec_queue.table.item(0, 2).text() == "COMPLETED" + assert bec_queue.table.item(0, 2).text() == "line_scan" + assert bec_queue.table.item(0, 3).text() == "COMPLETED" - abort_button = bec_queue.table.cellWidget(0, 3) + abort_button = bec_queue.table.cellWidget(0, 4) abort_button.button.click() bec_queue.update_queue(bec_queue_msg_full.content, {}) @@ -128,9 +131,10 @@ def test_queue_abort(bec_queue, bec_queue_msg_full): assert bec_queue.table.rowCount() == 1 assert bec_queue.table.item(0, 0).text() == "1289" assert bec_queue.table.item(0, 1).text() == "line_scan" - assert bec_queue.table.item(0, 2).text() == "COMPLETED" + assert bec_queue.table.item(0, 2).text() == "line_scan" + assert bec_queue.table.item(0, 3).text() == "COMPLETED" - abort_button = bec_queue.table.cellWidget(0, 3) + abort_button = bec_queue.table.cellWidget(0, 4) abort_button.button.click() bec_queue.update_queue(bec_queue_msg_full.content, {}) @@ -138,7 +142,8 @@ def test_queue_abort(bec_queue, bec_queue_msg_full): assert bec_queue.table.rowCount() == 1 assert bec_queue.table.item(0, 0).text() == "1289" assert bec_queue.table.item(0, 1).text() == "line_scan" - assert bec_queue.table.item(0, 2).text() == "COMPLETED" + assert bec_queue.table.item(0, 2).text() == "line_scan" + assert bec_queue.table.item(0, 3).text() == "COMPLETED" - abort_button = bec_queue.table.cellWidget(0, 3) + abort_button = bec_queue.table.cellWidget(0, 4) abort_button.button.click() diff --git a/tests/unit_tests/test_scan_metadata.py b/tests/unit_tests/test_scan_metadata.py index 7f054d211..7943aeec4 100644 --- a/tests/unit_tests/test_scan_metadata.py +++ b/tests/unit_tests/test_scan_metadata.py @@ -41,6 +41,8 @@ class ExampleSchema(BasicScanMetadata): TEST_DICT = { + "scan_name": "", + "comment": "", "sample_name": "test name", "str_optional": None, "str_required": "something", @@ -75,22 +77,26 @@ def metadata_widget(empty_metadata_widget: ScanMetadata): widget._md_schema = ExampleSchema widget.populate() - sample_name = widget._form_grid.layout().itemAtPosition(0, 1).widget() - str_optional = widget._form_grid.layout().itemAtPosition(1, 1).widget() - str_required = widget._form_grid.layout().itemAtPosition(2, 1).widget() - bool_optional = widget._form_grid.layout().itemAtPosition(3, 1).widget() - bool_required_default = widget._form_grid.layout().itemAtPosition(4, 1).widget() - bool_required_nodefault = widget._form_grid.layout().itemAtPosition(5, 1).widget() - int_default = widget._form_grid.layout().itemAtPosition(6, 1).widget() - int_nodefault_optional = widget._form_grid.layout().itemAtPosition(7, 1).widget() - float_nodefault = widget._form_grid.layout().itemAtPosition(8, 1).widget() - decimal_dp_limits_nodefault = widget._form_grid.layout().itemAtPosition(9, 1).widget() - dict_default = widget._form_grid.layout().itemAtPosition(10, 1).widget() - unsupported_class = widget._form_grid.layout().itemAtPosition(11, 1).widget() + scan_name = widget._form_grid.layout().itemAtPosition(0, 1).widget() + comment = widget._form_grid.layout().itemAtPosition(1, 1).widget() + sample_name = widget._form_grid.layout().itemAtPosition(2, 1).widget() + str_optional = widget._form_grid.layout().itemAtPosition(3, 1).widget() + str_required = widget._form_grid.layout().itemAtPosition(4, 1).widget() + bool_optional = widget._form_grid.layout().itemAtPosition(5, 1).widget() + bool_required_default = widget._form_grid.layout().itemAtPosition(6, 1).widget() + bool_required_nodefault = widget._form_grid.layout().itemAtPosition(7, 1).widget() + int_default = widget._form_grid.layout().itemAtPosition(8, 1).widget() + int_nodefault_optional = widget._form_grid.layout().itemAtPosition(9, 1).widget() + float_nodefault = widget._form_grid.layout().itemAtPosition(10, 1).widget() + decimal_dp_limits_nodefault = widget._form_grid.layout().itemAtPosition(11, 1).widget() + dict_default = widget._form_grid.layout().itemAtPosition(12, 1).widget() + unsupported_class = widget._form_grid.layout().itemAtPosition(13, 1).widget() yield ( widget, { + "scan_name": scan_name, + "comment": comment, "sample_name": sample_name, "str_optional": str_optional, "str_required": str_required,