mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-08 17:57:54 +02:00
Compare commits
6 Commits
fix/ophyd_
...
feature/sc
| Author | SHA1 | Date | |
|---|---|---|---|
| 733bc04e39 | |||
| fa06da1ed6 | |||
| bbcefbb88f | |||
| a185f6f5fe | |||
| c63aa01757 | |||
| 7e469627a2 |
194
bec_widgets/examples/script_interface.py
Normal file
194
bec_widgets/examples/script_interface.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import pyqtgraph as pg
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Qt
|
||||
from qtpy.QtWidgets import QApplication, QFileDialog, QFrame, QSplitter, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
|
||||
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
from bec_widgets.widgets.editors.web_console.web_console import WebConsole
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class ScriptInterface(BECWidget, QWidget):
|
||||
"""
|
||||
A simple script interface widget that allows interaction with Monaco editor and Web Console.
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "terminal"
|
||||
|
||||
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
||||
super().__init__(
|
||||
parent=parent, client=client, gui_id=gui_id, config=config, theme_update=True, **kwargs
|
||||
)
|
||||
self.current_script_id = ""
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.toolbar = ModularToolBar(parent=self, orientation="horizontal")
|
||||
|
||||
self.splitter = QSplitter(self)
|
||||
self.splitter.setObjectName("splitter")
|
||||
self.splitter.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.splitter.setOrientation(Qt.Orientation.Vertical)
|
||||
self.splitter.setChildrenCollapsible(True)
|
||||
|
||||
self.monaco_editor = MonacoWidget(self)
|
||||
self.splitter.addWidget(self.monaco_editor)
|
||||
self.web_console = WebConsole(self)
|
||||
self.splitter.addWidget(self.web_console)
|
||||
layout.addWidget(self.toolbar)
|
||||
|
||||
layout.addWidget(self.splitter)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
self.toolbar.components.add_safe(
|
||||
"new_script", MaterialIconAction("add", "New Script", parent=self)
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"open", MaterialIconAction("folder_open", "Open Script", parent=self)
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"save", MaterialIconAction("save", "Save Script", parent=self)
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"run", MaterialIconAction("play_arrow", "Run Script", parent=self)
|
||||
)
|
||||
self.toolbar.components.add_safe(
|
||||
"stop", MaterialIconAction("stop", "Stop Script", parent=self)
|
||||
)
|
||||
|
||||
bundle = ToolbarBundle("file_io", self.toolbar.components)
|
||||
bundle.add_action("new_script")
|
||||
bundle.add_action("open")
|
||||
bundle.add_action("save")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
bundle = ToolbarBundle("script_execution", self.toolbar.components)
|
||||
bundle.add_action("run")
|
||||
bundle.add_action("stop")
|
||||
self.toolbar.add_bundle(bundle)
|
||||
|
||||
self.toolbar.components.get_action("open").action.triggered.connect(self.open_file_dialog)
|
||||
self.toolbar.components.get_action("run").action.triggered.connect(self.run_script)
|
||||
self.toolbar.components.get_action("stop").action.triggered.connect(
|
||||
self.web_console.send_ctrl_c
|
||||
)
|
||||
|
||||
self.set_save_button_enabled(False)
|
||||
|
||||
self.toolbar.show_bundles(["file_io", "script_execution"])
|
||||
self.web_console.set_readonly(True)
|
||||
self._init_file_content = ""
|
||||
|
||||
self._text_changed_proxy = pg.SignalProxy(
|
||||
self.monaco_editor.text_changed, rateLimit=1, slot=self._on_text_changed
|
||||
)
|
||||
|
||||
@SafeSlot(str)
|
||||
def _on_text_changed(self, text: str):
|
||||
"""
|
||||
Handle text changes in the Monaco editor.
|
||||
"""
|
||||
text = text[0]
|
||||
if text != self._init_file_content:
|
||||
self.set_save_button_enabled(True)
|
||||
else:
|
||||
self.set_save_button_enabled(False)
|
||||
|
||||
@property
|
||||
def current_script_id(self):
|
||||
return self._current_script_id
|
||||
|
||||
@current_script_id.setter
|
||||
def current_script_id(self, value):
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("Script ID must be a string.")
|
||||
self._current_script_id = value
|
||||
self._update_subscription()
|
||||
|
||||
def _update_subscription(self):
|
||||
if self.current_script_id:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_script_execution_info,
|
||||
MessageEndpoints.script_execution_info(self.current_script_id),
|
||||
)
|
||||
else:
|
||||
self.bec_dispatcher.disconnect_slot(
|
||||
self.on_script_execution_info,
|
||||
MessageEndpoints.script_execution_info(self.current_script_id),
|
||||
)
|
||||
|
||||
@SafeSlot(dict, dict)
|
||||
def on_script_execution_info(self, content: dict, metadata: dict):
|
||||
print(f"Script execution info: {content}")
|
||||
current_lines = content.get("current_lines")
|
||||
if not current_lines:
|
||||
self.monaco_editor.clear_highlighted_lines()
|
||||
return
|
||||
line_number = current_lines[0]
|
||||
self.monaco_editor.clear_highlighted_lines()
|
||||
self.monaco_editor.set_highlighted_lines(line_number, line_number)
|
||||
|
||||
def open_file_dialog(self):
|
||||
"""
|
||||
Open a file dialog to select a script file.
|
||||
"""
|
||||
start_dir = "./"
|
||||
dialog = QFileDialog(self)
|
||||
dialog.setDirectory(start_dir)
|
||||
dialog.setNameFilter("Python Files (*.py);;All Files (*)")
|
||||
dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
|
||||
|
||||
if dialog.exec():
|
||||
selected_files = dialog.selectedFiles()
|
||||
if not selected_files:
|
||||
return
|
||||
file_path = selected_files[0]
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
content = file.read()
|
||||
self.monaco_editor.set_text(content)
|
||||
self._init_file_content = content
|
||||
|
||||
logger.info(f"Selected files: {selected_files}")
|
||||
|
||||
def set_save_button_enabled(self, enabled: bool):
|
||||
"""
|
||||
Set the save button enabled state.
|
||||
"""
|
||||
action = self.toolbar.components.get_action("save")
|
||||
if action:
|
||||
action.action.setEnabled(enabled)
|
||||
|
||||
def run_script(self):
|
||||
print("Running script...")
|
||||
script_id = str(uuid.uuid4())
|
||||
self.current_script_id = script_id
|
||||
script_text = self.monaco_editor.get_text()
|
||||
|
||||
script_text = f'bec._run_script("{script_id}", """{script_text}""")'
|
||||
script_text = script_text.replace("\n", "\\n").replace("'", "\\'").strip()
|
||||
if not script_text.endswith("\n"):
|
||||
script_text += "\\n"
|
||||
self.web_console.write(script_text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
script_interface = ScriptInterface()
|
||||
script_interface.resize(800, 600)
|
||||
script_interface.show()
|
||||
sys.exit(app.exec_())
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import Literal
|
||||
|
||||
import qtmonaco
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
@@ -12,6 +13,7 @@ class MonacoWidget(BECWidget, QWidget):
|
||||
A simple Monaco editor widget
|
||||
"""
|
||||
|
||||
text_changed = Signal(str)
|
||||
PLUGIN = True
|
||||
ICON_NAME = "code"
|
||||
USER_ACCESS = [
|
||||
@@ -36,6 +38,7 @@ class MonacoWidget(BECWidget, QWidget):
|
||||
self.editor = qtmonaco.Monaco(self)
|
||||
layout.addWidget(self.editor)
|
||||
self.setLayout(layout)
|
||||
self.editor.text_changed.connect(self.text_changed.emit)
|
||||
self.editor.initialized.connect(self.apply_theme)
|
||||
|
||||
def apply_theme(self, theme: str | None = None) -> None:
|
||||
|
||||
@@ -6,11 +6,12 @@ import time
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from louie.saferef import safe_ref
|
||||
from qtpy.QtCore import QUrl, qInstallMessageHandler
|
||||
from qtpy.QtCore import QTimer, QUrl, Signal, qInstallMessageHandler
|
||||
from qtpy.QtWebEngineWidgets import QWebEnginePage, QWebEngineView
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -165,11 +166,16 @@ class WebConsole(BECWidget, QWidget):
|
||||
A simple widget to display a website
|
||||
"""
|
||||
|
||||
_js_callback = Signal(bool)
|
||||
initialized = Signal()
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "terminal"
|
||||
|
||||
def __init__(self, parent=None, config=None, client=None, gui_id=None, **kwargs):
|
||||
super().__init__(parent=parent, client=client, gui_id=gui_id, config=config, **kwargs)
|
||||
self._startup_cmd = "bec --nogui"
|
||||
self._is_initialized = False
|
||||
_web_console_registry.register(self)
|
||||
self._token = _web_console_registry._token
|
||||
layout = QVBoxLayout()
|
||||
@@ -181,6 +187,48 @@ class WebConsole(BECWidget, QWidget):
|
||||
layout.addWidget(self.browser)
|
||||
self.setLayout(layout)
|
||||
self.page.setUrl(QUrl(f"http://localhost:{_web_console_registry._server_port}"))
|
||||
self._startup_timer = QTimer()
|
||||
self._startup_timer.setInterval(1000)
|
||||
self._startup_timer.timeout.connect(self._check_page_ready)
|
||||
self._startup_timer.start()
|
||||
self._js_callback.connect(self._on_js_callback)
|
||||
|
||||
def _check_page_ready(self):
|
||||
"""
|
||||
Check if the page is ready and stop the timer if it is.
|
||||
"""
|
||||
if self.page.isLoading():
|
||||
return
|
||||
|
||||
self.page.runJavaScript("window.term !== undefined", self._js_callback.emit)
|
||||
|
||||
def _on_js_callback(self, ready: bool):
|
||||
"""
|
||||
Callback for when the JavaScript is ready.
|
||||
"""
|
||||
if not ready:
|
||||
return
|
||||
self._is_initialized = True
|
||||
self._startup_timer.stop()
|
||||
if self._startup_cmd:
|
||||
self.write(self._startup_cmd)
|
||||
self.initialized.emit()
|
||||
|
||||
@SafeProperty(str)
|
||||
def startup_cmd(self):
|
||||
"""
|
||||
Get the startup command for the web console.
|
||||
"""
|
||||
return self._startup_cmd
|
||||
|
||||
@startup_cmd.setter
|
||||
def startup_cmd(self, cmd: str):
|
||||
"""
|
||||
Set the startup command for the web console.
|
||||
"""
|
||||
if not isinstance(cmd, str):
|
||||
raise ValueError("Startup command must be a string.")
|
||||
self._startup_cmd = cmd
|
||||
|
||||
def write(self, data: str, send_return: bool = True):
|
||||
"""
|
||||
@@ -213,10 +261,19 @@ class WebConsole(BECWidget, QWidget):
|
||||
"document.querySelector('textarea.xterm-helper-textarea').dispatchEvent(new KeyboardEvent('keypress', {charCode: 3}))"
|
||||
)
|
||||
|
||||
def set_readonly(self, readonly: bool):
|
||||
"""
|
||||
Set the web console to read-only mode.
|
||||
"""
|
||||
if not isinstance(readonly, bool):
|
||||
raise ValueError("Readonly must be a boolean.")
|
||||
self.setEnabled(not readonly)
|
||||
|
||||
def cleanup(self):
|
||||
"""
|
||||
Clean up the registry by removing any instances that are no longer valid.
|
||||
"""
|
||||
self._startup_timer.stop()
|
||||
_web_console_registry.unregister(self)
|
||||
super().cleanup()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user