diff --git a/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py b/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py index 001f541..7aaf895 100644 --- a/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py +++ b/debye_bec/bec_widgets/widgets/scheduler/item_dialog.py @@ -26,6 +26,11 @@ can reopen the dialog pre-filled instead of asking the user to start over. from __future__ import annotations +from bec_lib.logger import bec_logger +from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import ( + BECDeviceFilter, + DeviceComboBox, +) from bec_widgets.widgets.control.scan_control.scan_control import ScanControl, ScanParameterConfig from qtpy.QtWidgets import ( QCheckBox, @@ -42,7 +47,9 @@ from qtpy.QtWidgets import ( QWidget, ) -from .scan_form import list_movable_device_names +from ..scan_control_xas.scan_control_xas import ScanControlXAS + +logger = bec_logger.logger _DSPIN_RANGE = (-1e12, 1e12) @@ -68,6 +75,8 @@ class ScheduleItemDialog(QDialog): self._build_custom_tab() buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.button(QDialogButtonBox.Ok).setText("Add") + buttons.setStyleSheet("QPushButton {qproperty-icon: none;}") buttons.accepted.connect(self._on_accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) @@ -84,17 +93,17 @@ class ScheduleItemDialog(QDialog): # client=None resolves to the same process-wide BEC client # (bec_dispatcher.client) our own widget uses - no second Redis # connection is opened. - self.scan_control = ScanControl(parent=tab, client=self._client) + self.scan_control = ScanControlXAS(parent=tab, client=self._client) self.scan_control.button_run_scan.hide() layout.addWidget(self.scan_control) - hint = QLabel( - "Configure the scan above, then confirm with OK below - it will be " - "added to the schedule, not started immediately." - ) - hint.setWordWrap(True) - hint.setStyleSheet("color: gray;") - layout.addWidget(hint) + # hint = QLabel( + # "Configure the scan above, then confirm with OK below - it will be " + # "added to the schedule, not started immediately." + # ) + # hint.setWordWrap(True) + # hint.setStyleSheet("color: gray;") + # layout.addWidget(hint) self.tabs.addTab(tab, "Scan") @@ -127,6 +136,7 @@ class ScheduleItemDialog(QDialog): name=scan_name, args=args, kwargs=kwargs ) self.scan_control.current_scan = scan_name + self.scan_control.restore_scan_parameters(scan_name) # ------------------------------------------------------------------ # # Move tab @@ -134,18 +144,23 @@ class ScheduleItemDialog(QDialog): def _build_move_tab(self): tab = QWidget() form = QFormLayout(tab) + form.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.FieldsStayAtSizeHint) + + # self.move_device_combo = QComboBox() + # self.move_device_combo.addItems(list_movable_device_names(self._dev)) + # self.move_device_combo.setMaxVisibleItems(12) + + self.move_device_combo = DeviceComboBox(self, device_filter=[BECDeviceFilter.POSITIONER]) - self.move_device_combo = QComboBox() - self.move_device_combo.addItems(list_movable_device_names(self._dev)) form.addRow("Device", self.move_device_combo) self.move_value_spin = QDoubleSpinBox() self.move_value_spin.setDecimals(6) self.move_value_spin.setRange(*_DSPIN_RANGE) - form.addRow("Target value", self.move_value_spin) + form.addRow("Value", self.move_value_spin) - self.move_relative_check = QCheckBox("Relative move") - form.addRow("", self.move_relative_check) + self.move_relative_check = QCheckBox("") + form.addRow("Relative move", self.move_relative_check) self.tabs.addTab(tab, "Move") @@ -209,6 +224,8 @@ class ScheduleItemDialog(QDialog): # showing the raw command text as-is. self.custom_edit.setText(state.get("text", initial["command"])) self.tabs.setCurrentIndex(2) + else: + logger.warning(f"Unknown kind: {kind}") def _on_accept(self): try: diff --git a/debye_bec/bec_widgets/widgets/scheduler/scan_form.py b/debye_bec/bec_widgets/widgets/scheduler/scan_form.py deleted file mode 100644 index 3c77272..0000000 --- a/debye_bec/bec_widgets/widgets/scheduler/scan_form.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Small device-listing helper for the "Move" tab of `item_dialog.py`. - -Scan parameter introspection/forms used to live in this module too, but -that's now handled by embedding BEC's own `bec_widgets` `ScanControl` -widget directly (see `item_dialog.py`) instead of a plugin-owned -reimplementation - one less UI to keep in sync with BEC's scan -capabilities. -""" - -from __future__ import annotations - - -def list_movable_device_names(dev) -> list[str]: - """Device names that expose `.move(...)` (i.e. positioner-like devices).""" - return sorted(name for name, device in dev.items() if hasattr(device, "move")) diff --git a/debye_bec/bec_widgets/widgets/scheduler/scheduler.py b/debye_bec/bec_widgets/widgets/scheduler/scheduler.py index efb2e1d..d7fdbe2 100644 --- a/debye_bec/bec_widgets/widgets/scheduler/scheduler.py +++ b/debye_bec/bec_widgets/widgets/scheduler/scheduler.py @@ -18,6 +18,7 @@ from typing import Literal, Optional from bec_lib.endpoints import MessageEndpoints from bec_lib.logger import bec_logger from bec_lib.messages import VariableMessage +from bec_qthemes import material_icon from bec_qthemes._theme import AccentColors from bec_widgets.utils.bec_connector import ConnectionConfig from bec_widgets.utils.bec_widget import BECWidget @@ -71,6 +72,15 @@ _STATUS_ICON = { ScheduleItemStatus.ABORTED: "\u23f9", # stop } +ICON_SIZE = 20 +_ICON_MAP = { + ScheduleItemStatus.PENDING: ("hourglass", "#919090"), + ScheduleItemStatus.RUNNING: ("cycle", "#2980b9"), + ScheduleItemStatus.COMPLETED: ("check", "#27ae60"), + ScheduleItemStatus.FAILED: ("warning", "#e74c3c"), + ScheduleItemStatus.ABORTED: ("cancel", "#e74c3c"), +} + class ScheduleWidgetConfig(ConnectionConfig): """ @@ -239,16 +249,15 @@ class Scheduler(BECWidget, QWidget): edit_row.addWidget(button) edit_row.addStretch() schedule_layout.addLayout(edit_row) - # layout.addLayout(edit_row) - # layout.addWidget(QLabel(f"Schedule: {self.schedule_name}")) schedule_layout.addWidget(QLabel(f"Schedule: {self.schedule_name}")) self.list_widget = MyListWidget() self.list_widget.currentRowChanged.connect(self._on_selection_changed) self.list_widget.deletePressed.connect(self._on_delete_clicked) self.list_widget.emptySpaceClicked.connect(self._on_empty_space_clicked) - # layout.ada(scheduldWidget(self.list_widget) + self.list_widget.itemDoubleClicked.connect(self._on_edit_clicked) + schedule_layout.addWidget(self.list_widget) self.add_btn.clicked.connect(self._on_add_clicked) @@ -270,7 +279,6 @@ class Scheduler(BECWidget, QWidget): run_row.addWidget(button) run_row.addStretch() control_layout.addLayout(run_row) - # layout.addLayout(run_row) self.run_btn.clicked.connect(self.run_schedule) self.abort_btn.clicked.connect(self.abort_schedule) @@ -288,7 +296,6 @@ class Scheduler(BECWidget, QWidget): settings_row.addWidget(self.guard_btn) settings_row.addStretch() settings_layout.addLayout(settings_row) - # layout.addLayout(settings_row) self.notifications_btn.clicked.connect(self._on_notifications_clicked) self.guard_btn.clicked.connect(self._on_guard_clicked) @@ -437,11 +444,26 @@ class Scheduler(BECWidget, QWidget): self.list_widget.clear() selected_row = None for row, item in enumerate(items): - text = f"{_STATUS_ICON.get(item.status, '?')} {item.command}" + + # text = f"{_STATUS_ICON.get(item.status, '?')} {item.command}" + + text = f"{item.command}" + + icon_name, color = _ICON_MAP[item.status] + icon = material_icon( + icon_name, size=(ICON_SIZE, ICON_SIZE), color=color, convert_to_pixmap=True + ) + # if item.status == ScheduleItemStatus.RUNNING: + # self._spin_anim.start() + # else: + # self._spin_anim.stop() + # self._label.setPixmap(icon) + if item.error: text += f" ({item.error.strip().splitlines()[-1]})" list_item = QListWidgetItem(text, self.list_widget) list_item.setData(_ITEM_ID_ROLE, item.item_id) + list_item.setIcon(icon) if item.item_id == self._selected_item_id: selected_row = row if selected_row is not None: @@ -450,32 +472,32 @@ class Scheduler(BECWidget, QWidget): self._selected_item_id = None self.list_widget.blockSignals(False) - idx = None - selected_item = None - if self._selected_item_id is not None: - for i, item in enumerate(items): - if item.item_id == self._selected_item_id: - idx, selected_item = i, item - break + # idx = None + # selected_item = None + # if self._selected_item_id is not None: + # for i, item in enumerate(items): + # if item.item_id == self._selected_item_id: + # idx, selected_item = i, item + # break - # Editable = the selected item hasn't started yet. Thanks to the - # protected-prefix invariant, everything after the first PENDING - # item is guaranteed PENDING too, so a plain index/status check is - # enough here - see module docstring. - can_edit_selected = ( - selected_item is not None and selected_item.status == ScheduleItemStatus.PENDING - ) + # # Editable = the selected item hasn't started yet. Thanks to the + # # protected-prefix invariant, everything after the first PENDING + # # item is guaranteed PENDING too, so a plain index/status check is + # # enough here - see module docstring. + # can_edit_selected = ( + # selected_item is not None and selected_item.status == ScheduleItemStatus.PENDING + # ) - self.run_btn.setEnabled(not is_running) - # Adding is always safe: new items are clamped into the PENDING - # suffix regardless of what's selected (see add_item()). - self.add_btn.setEnabled(True) - self.edit_btn.setEnabled(can_edit_selected) - self.delete_btn.setEnabled(can_edit_selected) - self.move_up_btn.setEnabled(can_edit_selected and idx not in (None, 0)) - self.move_down_btn.setEnabled( - can_edit_selected and idx is not None and idx < len(items) - 1 - ) + # self.run_btn.setEnabled(not is_running) + # # Adding is always safe: new items are clamped into the PENDING + # # suffix regardless of what's selected (see add_item()). + # self.add_btn.setEnabled(True) + # self.edit_btn.setEnabled(can_edit_selected) + # self.delete_btn.setEnabled(can_edit_selected) + # self.move_up_btn.setEnabled(can_edit_selected and idx not in (None, 0)) + # self.move_down_btn.setEnabled( + # can_edit_selected and idx is not None and idx < len(items) - 1 + # ) if self._guard.enabled: state = "OK" if self._guard.is_clear() else "PAUSED - waiting to resume" @@ -489,11 +511,39 @@ class Scheduler(BECWidget, QWidget): else: self.guard_status_label.hide() + def _update_buttons(self): + with self._lock: + items = list(self.schedule.items) + is_running = self.schedule.is_running + + idx = None + selected_item = None + + if self._selected_item_id is not None: + for i, item in enumerate(items): + if item.item_id == self._selected_item_id: + idx, selected_item = i, item + break + + can_edit_selected = ( + selected_item is not None and selected_item.status == ScheduleItemStatus.PENDING + ) + + self.run_btn.setEnabled(not is_running) + self.add_btn.setEnabled(True) + self.edit_btn.setEnabled(can_edit_selected) + self.delete_btn.setEnabled(can_edit_selected) + self.move_up_btn.setEnabled(can_edit_selected and idx not in (None, 0)) + self.move_down_btn.setEnabled( + can_edit_selected and idx is not None and idx < len(items) - 1 + ) + @SafeSlot() def _on_selection_changed(self, row: int): + logger.info("On selection changed") item = self.list_widget.item(row) if row >= 0 else None self._selected_item_id = item.data(_ITEM_ID_ROLE) if item is not None else None - self._refresh_ui() + self._update_buttons() @SafeSlot() def _on_empty_space_clicked(self): @@ -567,9 +617,10 @@ class Scheduler(BECWidget, QWidget): ) self._selected_item_id = new_id self._refresh_ui() + self._update_buttons() @SafeSlot() - def _on_edit_clicked(self): + def _on_edit_clicked(self, row=None): if self._selected_item_id is None: return with self._lock: @@ -583,6 +634,8 @@ class Scheduler(BECWidget, QWidget): if initial is None: return + logger.info(f"Initial: {initial}") + dialog = ScheduleItemDialog( self.scans, self.dev, parent=self, initial=initial, client=self.client )