diff --git a/bec_widgets/applications/views/developer_view/developer_view.py b/bec_widgets/applications/views/developer_view/developer_view.py index 38b27d43..3dbda099 100644 --- a/bec_widgets/applications/views/developer_view/developer_view.py +++ b/bec_widgets/applications/views/developer_view/developer_view.py @@ -133,8 +133,4 @@ if __name__ == "__main__": exclusive=True, ) _app.show() - # developer_view.show() - # developer_view.setWindowTitle("Developer View") - # developer_view.resize(1920, 1080) - # developer_view.set_stretch(horizontal=[1, 3, 2], vertical=[5, 5]) #can be set during runtime sys.exit(app.exec_()) diff --git a/bec_widgets/applications/views/developer_view/developer_widget.py b/bec_widgets/applications/views/developer_view/developer_widget.py index a30ca295..e5f1f9a4 100644 --- a/bec_widgets/applications/views/developer_view/developer_widget.py +++ b/bec_widgets/applications/views/developer_view/developer_widget.py @@ -4,9 +4,10 @@ import re import markdown from bec_lib.endpoints import MessageEndpoints +from bec_lib.messages import ProcedureRequestMessage from bec_lib.script_executor import upload_script from bec_qthemes import material_icon -from qtpy.QtGui import QKeySequence, QShortcut +from qtpy.QtGui import QKeySequence, QShortcut # type: ignore from qtpy.QtWidgets import QTextEdit from bec_widgets.utils.error_popups import SafeSlot @@ -16,6 +17,7 @@ from bec_widgets.utils.toolbars.toolbar import ModularToolBar from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea from bec_widgets.widgets.containers.qt_ads import CDockWidget +from bec_widgets.widgets.control.procedure_control.procedure_panel import ProcedurePanel from bec_widgets.widgets.editors.monaco.monaco_dock import MonacoDock from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget from bec_widgets.widgets.editors.web_console.web_console import BECShell, WebConsole @@ -125,6 +127,9 @@ class DeveloperWidget(DockAreaWidget): self._current_script_id: str | None = None self.script_editor_tab = None + self.procedures = ProcedurePanel(self) + self.procedures.setObjectName("Procedure Control") + self._initialize_layout() # Connect editor signals @@ -183,24 +188,16 @@ class DeveloperWidget(DockAreaWidget): ) # Plotting area on the right with signature help tabbed alongside - self.plotting_ads_dock = self.new( - self.plotting_ads, - where="right", - closable=False, - floatable=False, - movable=False, - return_dock=True, - title_buttons={"float": True}, - ) - self.signature_dock = self.new( - self.signature_help, - closable=False, - floatable=False, - movable=False, - tab_with=self.plotting_ads_dock, - return_dock=True, - title_buttons={"float": False, "close": False}, - ) + _r_panel = { + "closable": False, + "floatable": False, + "movable": False, + "return_dock": True, + "title_buttons": {"float": True}, + } + self.plotting_dock = self.new(self.plotting_ads, where="right", **_r_panel) + self.signature_dock = self.new(self.signature_help, **_r_panel, tab_with=self.plotting_dock) + self.procedure_dock = self.new(self.procedures, **_r_panel, tab_with=self.plotting_dock) self.set_layout_ratios(horizontal=[2, 5, 3], vertical=[7, 3]) @@ -233,6 +230,16 @@ class DeveloperWidget(DockAreaWidget): run_action.action.triggered.connect(self.on_execute) self.toolbar.components.add_safe("run", run_action) + submit_action = MaterialIconAction( + icon_name="animated_images", + tooltip="Run current file as a BEC procedure", + label_text="Run on server", + filled=True, + parent=self, + ) + submit_action.action.triggered.connect(self.on_submit_procedure) + self.toolbar.components.add_safe("run_proc", submit_action) + stop_action = MaterialIconAction( icon_name="stop", tooltip="Stop current execution", @@ -246,6 +253,7 @@ class DeveloperWidget(DockAreaWidget): execution_bundle = ToolbarBundle("execution", self.toolbar.components) execution_bundle.add_action("run") execution_bundle.add_action("stop") + execution_bundle.add_action("run_proc") self.toolbar.add_bundle(execution_bundle) vim_action = MaterialIconAction( @@ -305,24 +313,41 @@ class DeveloperWidget(DockAreaWidget): self.toolbar.components.get_action("save").action.setEnabled(enabled) self.toolbar.components.get_action("save_as").action.setEnabled(enabled) - @SafeSlot() - def on_execute(self): - """Upload and run the currently focused script in the Monaco editor.""" + def _try_upload(self) -> str | None: self.script_editor_tab = self.monaco.last_focused_editor if not self.script_editor_tab: - return - widget = self.script_editor_tab.widget() - if not isinstance(widget, MonacoWidget): - return + return None + if not isinstance(widget := self.script_editor_tab.widget(), MonacoWidget): + return None if widget.modified: # Save the file before execution if there are unsaved changes self.monaco.save_file() if widget.modified: # If still modified, user likely cancelled save dialog - return - self.current_script_id = upload_script(self.client.connector, widget.get_text()) - self.console.write(f'bec._run_script("{self.current_script_id}")') + return None + return upload_script(self.client.connector, widget.get_text()) + + @SafeSlot() + def on_execute(self): + """Upload and run the currently focused script in the Monaco editor.""" print(f"Uploaded script with ID: {self.current_script_id}") + if (script_id := self._try_upload()) is not None: + self.current_script_id = script_id + self.console.write(f'bec._run_script("{self.current_script_id}")') + print(f"Uploaded script with ID: {self.current_script_id}") + + @SafeSlot() + def on_submit_procedure(self): + """Upload and run the currently focused script in the Monaco editor as a procedure.""" + if (script_id := self._try_upload()) is not None: + self.current_script_id = script_id + print(f"Uploaded script with ID: {self.current_script_id}") + self.client.connector.xadd( + MessageEndpoints.procedure_request(), + ProcedureRequestMessage( + identifier="run_script", args_kwargs=((self.current_script_id,), {}) + ).model_dump(), + ) @SafeSlot() def on_stop(self): diff --git a/bec_widgets/widgets/control/procedure_control/procedure_control.py b/bec_widgets/widgets/control/procedure_control/procedure_control.py index cb123c9b..217950be 100644 --- a/bec_widgets/widgets/control/procedure_control/procedure_control.py +++ b/bec_widgets/widgets/control/procedure_control/procedure_control.py @@ -12,8 +12,12 @@ from bec_lib.messages import ( from bec_qthemes._icon.material_icons import material_icon from bec_server.scan_server.procedures.helper import FrontendProcedureHelper from pydantic import BaseModel, ConfigDict +from qtpy.QtCore import QSize, Qt, Signal from qtpy.QtWidgets import ( + QDialog, + QDialogButtonBox, QHBoxLayout, + QPushButton, QToolButton, QTreeWidget, QTreeWidgetItem, @@ -107,6 +111,9 @@ class JobItem(_ActionItem): self._msg = config.msg self._init_params_display() + def queue(self): + return self._msg.queue + def _init_params_display(self): self.setText(self._config.params_column, self._short_params_text()) self.setToolTip(self._config.params_column, self._long_params_html()) @@ -176,6 +183,9 @@ class QueueItem(_ActionItem): _ItemConfig(base=self._config, msg=msg), ) + def queue(self): + return self._queue + @SafeSlot() def _abort_self(self): self._config.helper.request.abort_queue(self._queue) @@ -203,10 +213,13 @@ class CategoryItem(QTreeWidgetItem): self._queues[queue] = QueueItem( self, [queue], _QueueConfig(base=self._config, queue=queue, msgs=msgs) ) + self._queues[queue].setExpanded(True) class ProcedureControl(BECWidget, QWidget): + queue_selected = Signal(str) + def __init__(self, parent=None, client=None, config=None, gui_id: str | None = None, **kwargs): config = config or ConnectionConfig() super().__init__(parent=parent, client=client, config=config, gui_id=gui_id, **kwargs) @@ -215,6 +228,17 @@ class ProcedureControl(BECWidget, QWidget): self._setup_ui() self.bec_dispatcher.connect_slot(self._update, MessageEndpoints.procedure_queue_notif()) self._init_queues() + self._content.itemSelectionChanged.connect(self.on_selection_changed) + + def on_selection_changed(self): + selected_items = self._content.selectedItems() + if len(selected_items) != 1: + self.queue_selected.emit("") + return + if isinstance((item := selected_items[0]), (QueueItem, JobItem)): + self.queue_selected.emit(item.queue()) + return + self.queue_selected.emit("") @SafeSlot(ProcedureQNotifMessage, dict) def _update(self, msg: dict | ProcedureQNotifMessage, _): @@ -235,7 +259,6 @@ class ProcedureControl(BECWidget, QWidget): self._content.setAlternatingRowColors(True) self._content.setHeaderLabels(["name", "status", "params", "actions"]) self._layout.addWidget(self._content) - self._content.header().resizeSection(0, 250) config = partial(_BaseConfig, helper=self._helper, tree=self._content, actions_column=3) @@ -245,6 +268,7 @@ class ProcedureControl(BECWidget, QWidget): config(actions={"abort"}, child_actions={"abort"}, active_queue=True), ) self._content.addTopLevelItem(self._active_queues) + self._active_queues.setExpanded(True) self._unhandled_queues = CategoryItem( self._content, @@ -252,6 +276,7 @@ class ProcedureControl(BECWidget, QWidget): config(actions={"delete"}, child_actions={"delete", "resubmit"}), ) self._content.addTopLevelItem(self._unhandled_queues) + self._active_queues.setExpanded(True) def _init_queues(self): for queue in self._helper.get.active_and_pending_queue_names(): @@ -260,6 +285,47 @@ class ProcedureControl(BECWidget, QWidget): self._unhandled_queues.update(queue, self._helper.get.unhandled_queue(queue)) +class ProcedureSubmissionOptionsDialog(QDialog): + """ + Dialog to customize procedure options + """ + + def __init__(self, parent=None, client=None): + super().__init__(parent) + self.setWindowTitle("Procedure execution options") + + self._setup_ui() + + def sizeHint(self) -> QSize: + return QSize(600, 800) + + def _setup_ui(self): + """Setup the dialog UI with ScanControl widget and buttons.""" + layout = QVBoxLayout(self) + + # Create the scan control widget + + # Create dialog buttons + button_box = QDialogButtonBox(Qt.Orientation.Horizontal, self) + + # Create custom buttons with appropriate text + insert_button = QPushButton("Insert") + cancel_button = QPushButton("Cancel") + + button_box.addButton(insert_button, QDialogButtonBox.ButtonRole.AcceptRole) + button_box.addButton(cancel_button, QDialogButtonBox.ButtonRole.RejectRole) + + layout.addWidget(button_box) + + # Connect button signals + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + + def accept(self): + """Override accept to generate code before closing.""" + super().accept() + + if __name__ == "__main__": import sys diff --git a/bec_widgets/widgets/control/procedure_control/procedure_logs.py b/bec_widgets/widgets/control/procedure_control/procedure_logs.py index 2c3e48e1..b82be468 100644 --- a/bec_widgets/widgets/control/procedure_control/procedure_logs.py +++ b/bec_widgets/widgets/control/procedure_control/procedure_logs.py @@ -32,15 +32,15 @@ class ProcedureLogs(BECWidget, QWidget): self._layout.addWidget(self.widget) @SafeSlot(dict, dict) - def _trigger_update(self, msg, _): + def _update(self, msg, _): self.widget.append(msg.get("data").strip()) - def _update(self): + def _init_content(self): if self._queue is None: self.widget.setText("") return - if msgs := self._conn.xread(MessageEndpoints.procedure_logs(self._queue)): - self.widget.append("".join(msg.get("data").data.strip() for msg in msgs)) + if msgs := self._conn.xread(MessageEndpoints.procedure_logs(self._queue), from_start=True): + self.widget.append("\n".join(msg.get("data").data.strip() for msg in msgs)) @SafeSlot(None) @SafeSlot(str) @@ -53,16 +53,18 @@ class ProcedureLogs(BECWidget, QWidget): @queue.setter def queue(self, queue: str | None) -> None: + if self._queue == queue: + return if self._queue is not None: self.bec_dispatcher.disconnect_slot( - self._trigger_update, MessageEndpoints.procedure_logs(self._queue) + self._update, MessageEndpoints.procedure_logs(self._queue) ) - self._queue = queue + self._queue = queue or None if self._queue is not None: self.bec_dispatcher.connect_slot( - self._trigger_update, MessageEndpoints.procedure_logs(self._queue) + self._update, MessageEndpoints.procedure_logs(self._queue) ) - self._update() + self._init_content() if __name__ == "__main__": diff --git a/bec_widgets/widgets/control/procedure_control/procedure_panel.py b/bec_widgets/widgets/control/procedure_control/procedure_panel.py new file mode 100644 index 00000000..cb6f54bc --- /dev/null +++ b/bec_widgets/widgets/control/procedure_control/procedure_panel.py @@ -0,0 +1,30 @@ +from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget +from bec_widgets.widgets.control.procedure_control.procedure_control import ProcedureControl +from bec_widgets.widgets.control.procedure_control.procedure_logs import ProcedureLogs + + +class ProcedurePanel(DockAreaWidget): + + def __init__(self, parent=None, **kwargs): + super().__init__(parent=parent, **kwargs) + self.procedure_control = ProcedureControl(parent=self) + self.procedure_control.setObjectName("Procedure Queue Control") + self.procedure_logs = ProcedureLogs(parent=self) + self.procedure_logs.setObjectName("Procedure Logs") + + _dock_kwargs = {"closable": False, "movable": False, "floatable": False} + self.new(self.procedure_control, **_dock_kwargs) + self.new(self.procedure_logs, where="bottom", **_dock_kwargs) + + self.procedure_control.queue_selected.connect(self.procedure_logs.set_queue) + + +if __name__ == "__main__": + import sys + + from qtpy.QtWidgets import QApplication + + app = QApplication(sys.argv) + widget = ProcedurePanel() + widget.show() + sys.exit(app.exec())