mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-12 18:51:50 +02:00
wip - feat: add monaco editor
This commit is contained in:
@ -40,6 +40,7 @@ _Widgets = {
|
||||
"Image": "Image",
|
||||
"LogPanel": "LogPanel",
|
||||
"Minesweeper": "Minesweeper",
|
||||
"MonacoWidget": "MonacoWidget",
|
||||
"MotorMap": "MotorMap",
|
||||
"MultiWaveform": "MultiWaveform",
|
||||
"PositionIndicator": "PositionIndicator",
|
||||
@ -1879,6 +1880,68 @@ class LogPanel(RPCBase):
|
||||
class Minesweeper(RPCBase): ...
|
||||
|
||||
|
||||
class MonacoWidget(RPCBase):
|
||||
"""A simple Monaco editor widget"""
|
||||
|
||||
@rpc_call
|
||||
def set_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the text in the Monaco editor.
|
||||
|
||||
Args:
|
||||
text (str): The text to set in the editor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def get_text(self) -> str:
|
||||
"""
|
||||
Get the current text from the Monaco editor.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_language(self, language: str) -> None:
|
||||
"""
|
||||
Set the programming language for syntax highlighting in the Monaco editor.
|
||||
|
||||
Args:
|
||||
language (str): The programming language to set (e.g., "python", "javascript").
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""
|
||||
Set the theme for the Monaco editor.
|
||||
|
||||
Args:
|
||||
theme (str): The theme to set (e.g., "vs-dark", "light").
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_read_only(self, read_only: bool) -> None:
|
||||
"""
|
||||
Set the Monaco editor to read-only mode.
|
||||
|
||||
Args:
|
||||
read_only (bool): If True, the editor will be read-only.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_cursor(
|
||||
self,
|
||||
line: int,
|
||||
column: int = 1,
|
||||
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set the cursor position in the Monaco editor.
|
||||
|
||||
Args:
|
||||
line (int): Line number (1-based).
|
||||
column (int): Column number (1-based), defaults to 1.
|
||||
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||
"""
|
||||
|
||||
|
||||
class MotorMap(RPCBase):
|
||||
"""Motor map widget for plotting motor positions in 2D including a trace of the last points."""
|
||||
|
||||
|
0
bec_widgets/widgets/editors/monaco/__init__.py
Normal file
0
bec_widgets/widgets/editors/monaco/__init__.py
Normal file
0
bec_widgets/widgets/editors/monaco/core/__init__.py
Normal file
0
bec_widgets/widgets/editors/monaco/core/__init__.py
Normal file
65
bec_widgets/widgets/editors/monaco/core/bridge_base.py
Normal file
65
bec_widgets/widgets/editors/monaco/core/bridge_base.py
Normal file
@ -0,0 +1,65 @@
|
||||
import json
|
||||
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
|
||||
class BaseBridge(QObject):
|
||||
initialized = Signal()
|
||||
sendDataChanged = Signal(str, str)
|
||||
completion = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._initialized = False
|
||||
self._buffer = []
|
||||
self.initialized.connect(self._process_startup_buffer)
|
||||
|
||||
def _process_startup_buffer(self):
|
||||
"""
|
||||
Process the buffer of data that was sent before the bridge was initialized.
|
||||
This is useful for sending initial data to the JavaScript side.
|
||||
"""
|
||||
for name, value in self._buffer:
|
||||
self._send_to_js(name, value)
|
||||
self._buffer.clear()
|
||||
|
||||
# Update the local buffer by reading the current state
|
||||
# This is mostly to ensure that we are in sync with the JS side
|
||||
self._send_to_js("read", "")
|
||||
|
||||
def _send_to_js(self, name, value):
|
||||
if not self._initialized:
|
||||
self._buffer.append((name, value))
|
||||
return
|
||||
data = json.dumps(value)
|
||||
self.sendDataChanged.emit(name, data)
|
||||
|
||||
@Slot(str, str)
|
||||
def receive_from_js(self, name, value):
|
||||
data = json.loads(value)
|
||||
|
||||
if name == "bridge_initialized":
|
||||
self._initialized = data
|
||||
self.initialized.emit()
|
||||
return
|
||||
if name == "setValue":
|
||||
self.on_value_changed(data)
|
||||
return
|
||||
print(f"Received from JS: {name} = {data}")
|
||||
self.setProperty(name, data)
|
||||
|
||||
@property
|
||||
def bridge_initialized(self):
|
||||
return self._initialized
|
||||
|
||||
@bridge_initialized.setter
|
||||
def bridge_initialized(self, value):
|
||||
if self._initialized != value:
|
||||
self._initialized = value
|
||||
self.initialized.emit()
|
||||
|
||||
def on_value_changed(self, value):
|
||||
"""
|
||||
Placeholder method to handle value changes.
|
||||
This can be overridden in subclasses to implement specific behavior.
|
||||
"""
|
101
bec_widgets/widgets/editors/monaco/core/editor_bridge.py
Normal file
101
bec_widgets/widgets/editors/monaco/core/editor_bridge.py
Normal file
@ -0,0 +1,101 @@
|
||||
from typing import Literal
|
||||
|
||||
from qtpy.QtCore import Signal
|
||||
|
||||
from bec_widgets.widgets.editors.monaco.core.bridge_base import BaseBridge
|
||||
|
||||
|
||||
class EditorBridge(BaseBridge):
|
||||
valueChanged = Signal()
|
||||
languageChanged = Signal()
|
||||
themeChanged = Signal()
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self._value = ""
|
||||
self._language = ""
|
||||
self._theme = ""
|
||||
self._readonly = False
|
||||
|
||||
def on_value_changed(self, value):
|
||||
"""Handle value changes from the JavaScript side."""
|
||||
self.setValue(value)
|
||||
|
||||
def getValue(self):
|
||||
return self._value
|
||||
|
||||
def setValue(self, value: str):
|
||||
"""
|
||||
Set the value in the editor.
|
||||
|
||||
Args:
|
||||
value (str): The new value to set in the editor.
|
||||
"""
|
||||
if self._value == value:
|
||||
return
|
||||
if self._readonly:
|
||||
raise ValueError("Editor is in read-only mode, cannot set value.")
|
||||
if not isinstance(value, str):
|
||||
raise TypeError("Value must be a string.")
|
||||
self._value = value
|
||||
self.valueChanged.emit()
|
||||
|
||||
def getLanguage(self):
|
||||
return self._language
|
||||
|
||||
def setLanguage(self, language):
|
||||
self._language = language
|
||||
self._send_to_js("language", language)
|
||||
self.languageChanged.emit()
|
||||
|
||||
def getTheme(self):
|
||||
return self._theme
|
||||
|
||||
def setTheme(self, theme):
|
||||
self._theme = theme
|
||||
self._send_to_js("theme", theme)
|
||||
self.themeChanged.emit()
|
||||
|
||||
def setReadOnly(self, read_only: bool):
|
||||
"""Set the editor to read-only mode."""
|
||||
self._send_to_js("readOnly", read_only)
|
||||
self._readonly = read_only
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@property
|
||||
def language(self):
|
||||
return self._language
|
||||
|
||||
@property
|
||||
def theme(self):
|
||||
return self._theme
|
||||
|
||||
def setHost(self, host: str):
|
||||
"""
|
||||
Set the host for the editor.
|
||||
|
||||
Args:
|
||||
host (str): The host URL for the editor.
|
||||
"""
|
||||
self._send_to_js("lsp_url", host)
|
||||
|
||||
def setCursor(
|
||||
self,
|
||||
line: int,
|
||||
column: int = 1,
|
||||
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||
):
|
||||
"""
|
||||
Set the cursor position in the editor.
|
||||
|
||||
Args:
|
||||
line (int): Line number (1-based).
|
||||
column (int): Column number (1-based), defaults to 1.
|
||||
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||
"""
|
||||
self._send_to_js(
|
||||
"setCursor", {"line": line, "column": column, "moveToPosition": move_to_position}
|
||||
)
|
BIN
bec_widgets/widgets/editors/monaco/core/monaco.rcc
Normal file
BIN
bec_widgets/widgets/editors/monaco/core/monaco.rcc
Normal file
Binary file not shown.
9
bec_widgets/widgets/editors/monaco/core/monaco_page.py
Normal file
9
bec_widgets/widgets/editors/monaco/core/monaco_page.py
Normal file
@ -0,0 +1,9 @@
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtWebEngineCore import QWebEnginePage
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MonacoPage(QWebEnginePage):
|
||||
def javaScriptConsoleMessage(self, level, message, line, source):
|
||||
logger.debug(f"[JS Console] {level.name} at line {line} in {source}: {message}")
|
122
bec_widgets/widgets/editors/monaco/core/monaco_web_view.py
Normal file
122
bec_widgets/widgets/editors/monaco/core/monaco_web_view.py
Normal file
@ -0,0 +1,122 @@
|
||||
from typing import Literal
|
||||
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWebChannel import QWebChannel
|
||||
from qtpy.QtWebEngineWidgets import QWebEngineView
|
||||
|
||||
from bec_widgets.widgets.editors.monaco.core.editor_bridge import EditorBridge
|
||||
from bec_widgets.widgets.editors.monaco.core.monaco_page import MonacoPage
|
||||
from bec_widgets.widgets.editors.monaco.core.resource_loader import (
|
||||
get_monaco_base_url,
|
||||
get_monaco_html,
|
||||
)
|
||||
|
||||
|
||||
def get_pylsp_host() -> str:
|
||||
"""
|
||||
Get the host address for the PyLSP server.
|
||||
This function initializes the PyLSP server if it is not already running
|
||||
and returns the host address in the format 'localhost:port'.
|
||||
Returns:
|
||||
str: The host address of the PyLSP server.
|
||||
"""
|
||||
# lazy import to only load when needed
|
||||
from bec_widgets.widgets.editors.monaco.core.pylsp_provider import pylsp_server
|
||||
|
||||
if not pylsp_server.is_running():
|
||||
pylsp_server.start()
|
||||
|
||||
return f"localhost:{pylsp_server.port}"
|
||||
|
||||
|
||||
class MonacoWebView(QWebEngineView):
|
||||
initialized = Signal()
|
||||
textChanged = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
|
||||
self.pylsp_host = get_pylsp_host()
|
||||
|
||||
self._setup_page()
|
||||
self._setup_bridge()
|
||||
self._load_editor()
|
||||
|
||||
def _setup_page(self):
|
||||
"""Initialize the web engine page."""
|
||||
page = MonacoPage(parent=self)
|
||||
self.setPage(page)
|
||||
|
||||
def _setup_bridge(self):
|
||||
"""Initialize the bridge for communication with JavaScript."""
|
||||
self._channel = QWebChannel(self)
|
||||
self._bridge = EditorBridge()
|
||||
|
||||
self.page().setWebChannel(self._channel)
|
||||
self._channel.registerObject("bridge", self._bridge)
|
||||
|
||||
self._bridge.initialized.connect(self._set_host)
|
||||
self._bridge.initialized.connect(self.initialized)
|
||||
self._bridge.valueChanged.connect(lambda: self.textChanged.emit(self._bridge.value))
|
||||
|
||||
def _load_editor(self):
|
||||
"""Load the Monaco Editor HTML content."""
|
||||
raw_html = get_monaco_html()
|
||||
base_url = get_monaco_base_url()
|
||||
self.setHtml(raw_html, base_url)
|
||||
|
||||
def _set_host(self):
|
||||
"""Set the LSP host once the bridge is initialized."""
|
||||
self._bridge.setHost(self.pylsp_host)
|
||||
|
||||
# Public API methods
|
||||
def text(self):
|
||||
return self._bridge.value
|
||||
|
||||
def set_text(self, text: str):
|
||||
self._bridge.setValue(text)
|
||||
|
||||
def set_cursor(
|
||||
self,
|
||||
line: int,
|
||||
column: int = 1,
|
||||
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||
):
|
||||
"""Set the cursor position in the editor.
|
||||
|
||||
Args:
|
||||
line (int): Line number (1-based)
|
||||
column (int): Column number (1-based), defaults to 1
|
||||
"""
|
||||
self._bridge.setCursor(line, column, move_to_position)
|
||||
|
||||
def get_language(self) -> str:
|
||||
"""Get the current programming language for syntax highlighting in the editor."""
|
||||
return self._bridge.getLanguage()
|
||||
|
||||
def set_language(self, language: str):
|
||||
"""Set the programming language for syntax highlighting in the editor.
|
||||
|
||||
Args:
|
||||
language (str): The programming language to set (e.g., "python", "javascript").
|
||||
"""
|
||||
self._bridge.setLanguage(language)
|
||||
|
||||
def get_theme(self):
|
||||
return self._bridge.getTheme()
|
||||
|
||||
def set_theme(self, theme: str):
|
||||
"""Set the theme for the Monaco editor.
|
||||
|
||||
Args:
|
||||
theme (str): The theme to apply (e.g., "vs", "vs-dark").
|
||||
"""
|
||||
self._bridge.setTheme(theme)
|
||||
|
||||
def set_read_only(self, read_only: bool):
|
||||
"""Set the editor to read-only mode."""
|
||||
self._bridge.setReadOnly(read_only)
|
||||
|
||||
def shutdown(self):
|
||||
if hasattr(self._bridge, "shutdown"):
|
||||
self._bridge.shutdown()
|
62
bec_widgets/widgets/editors/monaco/core/pylsp_provider.py
Normal file
62
bec_widgets/widgets/editors/monaco/core/pylsp_provider.py
Normal file
@ -0,0 +1,62 @@
|
||||
import atexit
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class PyLSPProvider:
|
||||
"""A provider for the PyLSP server."""
|
||||
|
||||
def __init__(self):
|
||||
self.port = None
|
||||
self.server_process = None
|
||||
atexit.register(self.stop)
|
||||
signal.signal(signal.SIGINT, self._handle_signal)
|
||||
signal.signal(signal.SIGTERM, self._handle_signal)
|
||||
|
||||
def _find_free_port(self):
|
||||
"""Find a free port for the PyLSP server."""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.bind(("localhost", 0))
|
||||
self.port = s.getsockname()[1]
|
||||
return self.port
|
||||
|
||||
def start(self):
|
||||
"""Start the PyLSP server."""
|
||||
if self.port is None:
|
||||
self._find_free_port()
|
||||
# Here you would start the PyLSP server using the found port
|
||||
logger.info(f"Starting PyLSP server on port {self.port}")
|
||||
self.server_process = subprocess.Popen(
|
||||
["pylsp", "--ws", "--port", str(self.port)],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the PyLSP server."""
|
||||
if not self.server_process:
|
||||
return
|
||||
|
||||
self.server_process.terminate()
|
||||
try:
|
||||
self.server_process.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.server_process.kill()
|
||||
self.server_process = None
|
||||
self.port = None
|
||||
|
||||
def _handle_signal(self, signum, frame):
|
||||
"""Handle termination signals."""
|
||||
self.stop()
|
||||
|
||||
def is_running(self):
|
||||
"""Check if the PyLSP server is running."""
|
||||
return self.server_process is not None and self.server_process.poll() is None
|
||||
|
||||
|
||||
pylsp_server = PyLSPProvider()
|
26
bec_widgets/widgets/editors/monaco/core/resource_loader.py
Normal file
26
bec_widgets/widgets/editors/monaco/core/resource_loader.py
Normal file
@ -0,0 +1,26 @@
|
||||
import os
|
||||
|
||||
from qtpy.QtCore import QFile, QIODevice, QResource, QUrl
|
||||
|
||||
QResource.registerResource(os.path.join(os.path.dirname(__file__), "monaco.rcc"))
|
||||
|
||||
|
||||
def load_resource_html(resource_path: str) -> str:
|
||||
"""Load HTML content from Qt resources."""
|
||||
file = QFile(resource_path)
|
||||
if file.open(QIODevice.OpenModeFlag.ReadOnly):
|
||||
content = file.readAll()
|
||||
file.close()
|
||||
return content.toStdString()
|
||||
else:
|
||||
raise FileNotFoundError(f"Resource not found: {resource_path}")
|
||||
|
||||
|
||||
def get_monaco_html():
|
||||
"""Get Monaco Editor HTML content from Qt resources."""
|
||||
return load_resource_html(":/monaco/dist/index.html")
|
||||
|
||||
|
||||
def get_monaco_base_url():
|
||||
"""Get the base URL for Monaco Editor resources."""
|
||||
return QUrl("qrc:/monaco/dist/")
|
144
bec_widgets/widgets/editors/monaco/monaco_widget.py
Normal file
144
bec_widgets/widgets/editors/monaco/monaco_widget.py
Normal file
@ -0,0 +1,144 @@
|
||||
from typing import Literal
|
||||
|
||||
from qtpy.QtWidgets import QApplication, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.colors import get_theme_name
|
||||
from bec_widgets.widgets.editors.monaco.core.monaco_web_view import MonacoWebView
|
||||
|
||||
|
||||
class MonacoWidget(BECWidget, QWidget):
|
||||
"""
|
||||
A simple Monaco editor widget
|
||||
"""
|
||||
|
||||
PLUGIN = True
|
||||
ICON_NAME = "code"
|
||||
USER_ACCESS = [
|
||||
"set_text",
|
||||
"get_text",
|
||||
"set_language",
|
||||
"set_theme",
|
||||
"set_read_only",
|
||||
"set_cursor",
|
||||
]
|
||||
|
||||
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
|
||||
)
|
||||
layout = QVBoxLayout()
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
self.editor = MonacoWebView(self)
|
||||
layout.addWidget(self.editor)
|
||||
self.setLayout(layout)
|
||||
self.editor.initialized.connect(self.apply_theme)
|
||||
|
||||
def apply_theme(self, theme: str | None = None) -> None:
|
||||
"""
|
||||
Apply the current theme to the Monaco editor.
|
||||
|
||||
Args:
|
||||
theme (str, optional): The theme to apply. If None, the current theme will be used.
|
||||
"""
|
||||
if theme is None:
|
||||
theme = get_theme_name()
|
||||
editor_theme = "vs" if theme == "light" else "vs-dark"
|
||||
self.set_theme(editor_theme)
|
||||
|
||||
def set_text(self, text: str) -> None:
|
||||
"""
|
||||
Set the text in the Monaco editor.
|
||||
|
||||
Args:
|
||||
text (str): The text to set in the editor.
|
||||
"""
|
||||
self.editor.set_text(text)
|
||||
|
||||
def get_text(self) -> str:
|
||||
"""
|
||||
Get the current text from the Monaco editor.
|
||||
"""
|
||||
return self.editor.text()
|
||||
|
||||
def set_cursor(
|
||||
self,
|
||||
line: int,
|
||||
column: int = 1,
|
||||
move_to_position: Literal[None, "center", "top", "position"] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set the cursor position in the Monaco editor.
|
||||
|
||||
Args:
|
||||
line (int): Line number (1-based).
|
||||
column (int): Column number (1-based), defaults to 1.
|
||||
move_to_position (Literal[None, "center", "top", "position"], optional): Position to move the cursor to.
|
||||
"""
|
||||
self.editor.set_cursor(line, column, move_to_position)
|
||||
|
||||
def set_language(self, language: str) -> None:
|
||||
"""
|
||||
Set the programming language for syntax highlighting in the Monaco editor.
|
||||
|
||||
Args:
|
||||
language (str): The programming language to set (e.g., "python", "javascript").
|
||||
"""
|
||||
self.editor.set_language(language)
|
||||
|
||||
def set_read_only(self, read_only: bool) -> None:
|
||||
"""
|
||||
Set the Monaco editor to read-only mode.
|
||||
|
||||
Args:
|
||||
read_only (bool): If True, the editor will be read-only.
|
||||
"""
|
||||
self.editor.set_read_only(read_only)
|
||||
|
||||
def set_theme(self, theme: str) -> None:
|
||||
"""
|
||||
Set the theme for the Monaco editor.
|
||||
|
||||
Args:
|
||||
theme (str): The theme to set (e.g., "vs-dark", "light").
|
||||
"""
|
||||
self.editor.set_theme(theme)
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up the widget before closing.
|
||||
"""
|
||||
self.editor.shutdown()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
qapp = QApplication([])
|
||||
widget = MonacoWidget()
|
||||
# set the default size
|
||||
widget.resize(800, 600)
|
||||
widget.set_language("python")
|
||||
widget.set_theme("github-dark")
|
||||
widget.set_text(
|
||||
"""
|
||||
import numpy as np
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_lib.devicemanager import DeviceContainer
|
||||
from bec_lib.scans import Scans
|
||||
dev: DeviceContainer
|
||||
scans: Scans
|
||||
|
||||
#######################################
|
||||
########## User Script #####################
|
||||
#######################################
|
||||
|
||||
# This is a comment
|
||||
def hello_world():
|
||||
print("Hello, world!")
|
||||
"""
|
||||
)
|
||||
|
||||
widget.show()
|
||||
qapp.exec_()
|
@ -0,0 +1 @@
|
||||
{'files': ['monaco_widget.py']}
|
54
bec_widgets/widgets/editors/monaco/monaco_widget_plugin.py
Normal file
54
bec_widgets/widgets/editors/monaco/monaco_widget_plugin.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
|
||||
|
||||
from bec_widgets.utils.bec_designer import designer_material_icon
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
|
||||
DOM_XML = """
|
||||
<ui language='c++'>
|
||||
<widget class='MonacoWidget' name='monaco_widget'>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
|
||||
class MonacoWidgetPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._form_editor = None
|
||||
|
||||
def createWidget(self, parent):
|
||||
t = MonacoWidget(parent)
|
||||
return t
|
||||
|
||||
def domXml(self):
|
||||
return DOM_XML
|
||||
|
||||
def group(self):
|
||||
return ""
|
||||
|
||||
def icon(self):
|
||||
return designer_material_icon(MonacoWidget.ICON_NAME)
|
||||
|
||||
def includeFile(self):
|
||||
return "monaco_widget"
|
||||
|
||||
def initialize(self, form_editor):
|
||||
self._form_editor = form_editor
|
||||
|
||||
def isContainer(self):
|
||||
return False
|
||||
|
||||
def isInitialized(self):
|
||||
return self._form_editor is not None
|
||||
|
||||
def name(self):
|
||||
return "MonacoWidget"
|
||||
|
||||
def toolTip(self):
|
||||
return ""
|
||||
|
||||
def whatsThis(self):
|
||||
return self.toolTip()
|
15
bec_widgets/widgets/editors/monaco/register_monaco_widget.py
Normal file
15
bec_widgets/widgets/editors/monaco/register_monaco_widget.py
Normal file
@ -0,0 +1,15 @@
|
||||
def main(): # pragma: no cover
|
||||
from qtpy import PYSIDE6
|
||||
|
||||
if not PYSIDE6:
|
||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||
return
|
||||
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
|
||||
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget_plugin import MonacoWidgetPlugin
|
||||
|
||||
QPyDesignerCustomWidgetCollection.addCustomWidget(MonacoWidgetPlugin())
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
main()
|
@ -13,16 +13,17 @@ classifiers = [
|
||||
"Topic :: Scientific/Engineering",
|
||||
]
|
||||
dependencies = [
|
||||
"bec_ipython_client>=3.42.4, <=4.0", # needed for jupyter console
|
||||
"bec_ipython_client>=3.42.4, <=4.0", # needed for jupyter console
|
||||
"bec_lib>=3.44, <=4.0",
|
||||
"bec_qthemes~=0.7, >=0.7",
|
||||
"black~=25.0", # needed for bw-generate-cli
|
||||
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
||||
"black~=25.0", # needed for bw-generate-cli
|
||||
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
||||
"pydantic~=2.0",
|
||||
"pyqtgraph~=0.13",
|
||||
"PySide6~=6.8.2",
|
||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||
"qtconsole~=5.5, >=5.5.1", # needed for jupyter console
|
||||
"qtpy~=2.4",
|
||||
"python-lsp-server[all,websockets] ~= 1.12",
|
||||
]
|
||||
|
||||
|
||||
|
39
tests/unit_tests/test_monaco_editor.py
Normal file
39
tests/unit_tests/test_monaco_editor.py
Normal file
@ -0,0 +1,39 @@
|
||||
import pytest
|
||||
|
||||
from bec_widgets.widgets.editors.monaco.monaco_widget import MonacoWidget
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def monaco_widget(qtbot):
|
||||
widget = MonacoWidget()
|
||||
qtbot.addWidget(widget)
|
||||
qtbot.waitExposed(widget)
|
||||
yield widget
|
||||
|
||||
|
||||
def test_monaco_widget_set_text(monaco_widget: MonacoWidget, qtbot):
|
||||
"""
|
||||
Test that the MonacoWidget can set text correctly.
|
||||
"""
|
||||
test_text = "Hello, Monaco!"
|
||||
monaco_widget.set_text(test_text)
|
||||
qtbot.waitUntil(lambda: monaco_widget.get_text() == test_text, timeout=1000)
|
||||
assert monaco_widget.get_text() == test_text
|
||||
|
||||
|
||||
def test_monaco_widget_readonly(monaco_widget: MonacoWidget, qtbot):
|
||||
"""
|
||||
Test that the MonacoWidget can be set to read-only mode.
|
||||
"""
|
||||
monaco_widget.set_text("Initial text")
|
||||
qtbot.waitUntil(lambda: monaco_widget.get_text() == "Initial text", timeout=1000)
|
||||
monaco_widget.set_read_only(True)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
monaco_widget.set_text("This should not change")
|
||||
|
||||
monaco_widget.set_read_only(False) # Set back to editable
|
||||
qtbot.wait(100)
|
||||
monaco_widget.set_text("Attempting to change text")
|
||||
qtbot.waitUntil(lambda: monaco_widget.get_text() == "Attempting to change text", timeout=1000)
|
||||
assert monaco_widget.get_text() == "Attempting to change text"
|
Reference in New Issue
Block a user