diff --git a/bec_widgets/applications/main_app.py b/bec_widgets/applications/main_app.py
index da210c97..c1de6c4e 100644
--- a/bec_widgets/applications/main_app.py
+++ b/bec_widgets/applications/main_app.py
@@ -7,6 +7,7 @@ from bec_widgets.applications.views.device_manager_view.device_manager_widget im
DeviceManagerWidget,
)
from bec_widgets.applications.views.view import ViewBase, WaveformViewInline, WaveformViewPopup
+from bec_widgets.examples.developer_view.developer_view import DeveloperView
from bec_widgets.utils.colors import apply_theme
from bec_widgets.widgets.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
@@ -48,6 +49,7 @@ class BECMainApp(BECMainWindow):
self.add_section("BEC Applications", "bec_apps")
self.ads = AdvancedDockArea(self)
self.device_manager = DeviceManagerWidget(self)
+ self.developer_view = DeveloperView(self)
self.add_view(
icon="widgets", title="Dock Area", id="dock_area", widget=self.ads, mini_text="Docks"
@@ -59,6 +61,13 @@ class BECMainApp(BECMainWindow):
widget=self.device_manager,
mini_text="DM",
)
+ self.add_view(
+ icon="code_blocks",
+ title="IDE",
+ widget=self.developer_view,
+ id="developer_view",
+ exclusive=True,
+ )
if self._show_examples:
self.add_section("Examples", "examples")
@@ -195,7 +204,21 @@ if __name__ == "__main__": # pragma: no cover
app = QApplication([sys.argv[0], *qt_args])
apply_theme("dark")
w = BECMainApp(show_examples=args.examples)
- w.resize(1920, 1200)
+
+ screen = app.primaryScreen()
+ screen_geometry = screen.availableGeometry()
+ screen_width = screen_geometry.width()
+ screen_height = screen_geometry.height()
+ # 70% of screen height, keep 16:9 ratio
+ height = int(screen_height * 0.9)
+ width = int(height * (16 / 9))
+
+ # If width exceeds screen width, scale down
+ if width > screen_width * 0.9:
+ width = int(screen_width * 0.9)
+ height = int(width / (16 / 9))
+
+ w.resize(width, height)
w.show()
sys.exit(app.exec())
diff --git a/bec_widgets/applications/views/view.py b/bec_widgets/applications/views/view.py
index 3b98f756..635f68b1 100644
--- a/bec_widgets/applications/views/view.py
+++ b/bec_widgets/applications/views/view.py
@@ -1,6 +1,8 @@
from __future__ import annotations
-from qtpy.QtCore import QEventLoop
+from typing import List
+
+from qtpy.QtCore import QEventLoop, Qt, QTimer
from qtpy.QtWidgets import (
QDialog,
QDialogButtonBox,
@@ -9,6 +11,7 @@ from qtpy.QtWidgets import (
QLabel,
QMessageBox,
QPushButton,
+ QSplitter,
QStackedLayout,
QVBoxLayout,
QWidget,
@@ -20,6 +23,42 @@ from bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox im
from bec_widgets.widgets.plots.waveform.waveform import Waveform
+def set_splitter_weights(splitter: QSplitter, weights: List[float]) -> None:
+ """
+ Apply initial sizes to a splitter using weight ratios, e.g. [1,3,2,1].
+ Works for horizontal or vertical splitters and sets matching stretch factors.
+ """
+
+ def apply():
+ n = splitter.count()
+ if n == 0:
+ return
+ w = list(weights[:n]) + [1] * max(0, n - len(weights))
+ w = [max(0.0, float(x)) for x in w]
+ tot_w = sum(w)
+ if tot_w <= 0:
+ w = [1.0] * n
+ tot_w = float(n)
+ total_px = (
+ splitter.width()
+ if splitter.orientation() == Qt.Orientation.Horizontal
+ else splitter.height()
+ )
+ if total_px < 2:
+ QTimer.singleShot(0, apply)
+ return
+ sizes = [max(1, int(total_px * (wi / tot_w))) for wi in w]
+ diff = total_px - sum(sizes)
+ if diff != 0:
+ idx = max(range(n), key=lambda i: w[i])
+ sizes[idx] = max(1, sizes[idx] + diff)
+ splitter.setSizes(sizes)
+ for i, wi in enumerate(w):
+ splitter.setStretchFactor(i, max(1, int(round(wi * 100))))
+
+ QTimer.singleShot(0, apply)
+
+
class ViewBase(QWidget):
"""Wrapper for a content widget used inside the main app's stacked view.
@@ -76,6 +115,68 @@ class ViewBase(QWidget):
"""
return True
+ ####### Default view has to be done with setting up splitters ########
+ def set_default_view(self, horizontal_weights: list, vertical_weights: list):
+ """Apply initial weights to every horizontal and vertical splitter.
+
+ Examples:
+ horizontal_weights = [1, 3, 2, 1]
+ vertical_weights = [3, 7] # top:bottom = 30:70
+ """
+ splitters_h = []
+ splitters_v = []
+ for splitter in self.findChildren(QSplitter):
+ if splitter.orientation() == Qt.Orientation.Horizontal:
+ splitters_h.append(splitter)
+ elif splitter.orientation() == Qt.Orientation.Vertical:
+ splitters_v.append(splitter)
+
+ def apply_all():
+ for s in splitters_h:
+ set_splitter_weights(s, horizontal_weights)
+ for s in splitters_v:
+ set_splitter_weights(s, vertical_weights)
+
+ QTimer.singleShot(0, apply_all)
+
+ def set_stretch(self, *, horizontal=None, vertical=None):
+ """Update splitter weights and re-apply to all splitters.
+
+ Accepts either a list/tuple of weights (e.g., [1,3,2,1]) or a role dict
+ for convenience: horizontal roles = {"left","center","right"},
+ vertical roles = {"top","bottom"}.
+ """
+
+ def _coerce_h(x):
+ if x is None:
+ return None
+ if isinstance(x, (list, tuple)):
+ return list(map(float, x))
+ if isinstance(x, dict):
+ return [
+ float(x.get("left", 1)),
+ float(x.get("center", x.get("middle", 1))),
+ float(x.get("right", 1)),
+ ]
+ return None
+
+ def _coerce_v(x):
+ if x is None:
+ return None
+ if isinstance(x, (list, tuple)):
+ return list(map(float, x))
+ if isinstance(x, dict):
+ return [float(x.get("top", 1)), float(x.get("bottom", 1))]
+ return None
+
+ h = _coerce_h(horizontal)
+ v = _coerce_v(vertical)
+ if h is None:
+ h = [1, 1, 1]
+ if v is None:
+ v = [1, 1]
+ self.set_default_view(h, v)
+
####################################################################################################
# Example views for demonstration/testing purposes
diff --git a/bec_widgets/examples/developer_view/developer_view.py b/bec_widgets/examples/developer_view/developer_view.py
index 01f9dfbc..a6040e3c 100644
--- a/bec_widgets/examples/developer_view/developer_view.py
+++ b/bec_widgets/examples/developer_view/developer_view.py
@@ -1,413 +1,29 @@
-import re
-from typing import List
+from qtpy.QtWidgets import QWidget
-import markdown
-import PySide6QtAds as QtAds
-from bec_lib.endpoints import MessageEndpoints
-from bec_lib.script_executor import upload_script
-from bec_qthemes import material_icon
-from PySide6QtAds import CDockManager, CDockWidget
-from qtpy.QtCore import Qt, QTimer
-from qtpy.QtGui import QKeySequence, QShortcut
-from qtpy.QtWidgets import QSplitter, QTextEdit, QVBoxLayout, QWidget
-
-from bec_widgets 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.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
-from bec_widgets.widgets.editors.monaco.monaco_tab import MonacoDock
-from bec_widgets.widgets.editors.web_console.web_console import WebConsole
-from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
+from bec_widgets.applications.views.view import ViewBase
+from bec_widgets.examples.developer_view.developer_widget import DeveloperWidget
-def set_splitter_weights(splitter: QSplitter, weights: List[float]) -> None:
+class DeveloperView(ViewBase):
"""
- Apply initial sizes to a splitter using weight ratios, e.g. [1,3,2,1].
- Works for horizontal or vertical splitters and sets matching stretch factors.
+ A view for users to write scripts and macros and execute them within the application.
"""
- def apply():
- n = splitter.count()
- if n == 0:
- return
- w = list(weights[:n]) + [1] * max(0, n - len(weights))
- w = [max(0.0, float(x)) for x in w]
- tot_w = sum(w)
- if tot_w <= 0:
- w = [1.0] * n
- tot_w = float(n)
- total_px = (
- splitter.width() if splitter.orientation() == Qt.Horizontal else splitter.height()
- )
- if total_px < 2:
- QTimer.singleShot(0, apply)
- return
- sizes = [max(1, int(total_px * (wi / tot_w))) for wi in w]
- diff = total_px - sum(sizes)
- if diff != 0:
- idx = max(range(n), key=lambda i: w[i])
- sizes[idx] = max(1, sizes[idx] + diff)
- splitter.setSizes(sizes)
- for i, wi in enumerate(w):
- splitter.setStretchFactor(i, max(1, int(round(wi * 100))))
-
- QTimer.singleShot(0, apply)
-
-
-def markdown_to_html(md_text: str) -> str:
- """Convert Markdown with syntax highlighting to HTML (Qt-compatible)."""
-
- # Preprocess: convert consecutive >>> lines to Python code blocks
- def replace_python_examples(match):
- indent = match.group(1)
- examples = match.group(2)
- # Remove >>> prefix and clean up the code
- lines = []
- for line in examples.strip().split("\n"):
- line = line.strip()
- if line.startswith(">>> "):
- lines.append(line[4:]) # Remove '>>> '
- elif line.startswith(">>>"):
- lines.append(line[3:]) # Remove '>>>'
- code = "\n".join(lines)
-
- return f"{indent}```python\n{indent}{code}\n{indent}```"
-
- # Match one or more consecutive >>> lines (with same indentation)
- pattern = r"^(\s*)((?:>>> .+(?:\n|$))+)"
- md_text = re.sub(pattern, replace_python_examples, md_text, flags=re.MULTILINE)
-
- extensions = ["fenced_code", "codehilite", "tables", "sane_lists"]
- html = markdown.markdown(
- md_text,
- extensions=extensions,
- extension_configs={
- "codehilite": {"linenums": False, "guess_lang": False, "noclasses": True}
- },
- output_format="html",
- )
-
- # Remove hardcoded background colors that conflict with themes
- html = re.sub(r'style="background: #[^"]*"', 'style="background: transparent"', html)
- html = re.sub(r"background: #[^;]*;", "", html)
-
- # Add CSS to force code blocks to wrap
- css = """
-
- """
-
- return css + html
-
-
-class DeveloperView(BECWidget, QWidget):
-
- def __init__(self, parent=None, **kwargs):
- super().__init__(parent=parent, **kwargs)
-
- # Top-level layout hosting a toolbar and the dock manager
- self._root_layout = QVBoxLayout(self)
- self._root_layout.setContentsMargins(0, 0, 0, 0)
- self._root_layout.setSpacing(0)
- self.toolbar = ModularToolBar(self)
- self.init_developer_toolbar()
- self._root_layout.addWidget(self.toolbar)
-
- self.dock_manager = CDockManager(self)
- self.dock_manager.setStyleSheet("")
- self._root_layout.addWidget(self.dock_manager)
-
- # Initialize the widgets
- self.explorer = IDEExplorer(self)
- self.console = WebConsole(self)
- self.terminal = WebConsole(self, startup_cmd="")
- self.monaco = MonacoDock(self)
- self.monaco.save_enabled.connect(self._on_save_enabled_update)
- self.plotting_ads = AdvancedDockArea(self, mode="plot", default_add_direction="bottom")
- self.signature_help = QTextEdit(self)
- self.signature_help.setAcceptRichText(True)
- self.signature_help.setReadOnly(True)
- self.signature_help.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
- opt = self.signature_help.document().defaultTextOption()
- opt.setWrapMode(opt.WrapMode.WrapAnywhere)
- self.signature_help.document().setDefaultTextOption(opt)
- self.monaco.signature_help.connect(
- lambda text: self.signature_help.setHtml(markdown_to_html(text))
- )
-
- # Create the dock widgets
- self.explorer_dock = QtAds.CDockWidget("Explorer", self)
- self.explorer_dock.setWidget(self.explorer)
-
- self.console_dock = QtAds.CDockWidget("Console", self)
- self.console_dock.setWidget(self.console)
-
- self.monaco_dock = QtAds.CDockWidget("Monaco Editor", self)
- self.monaco_dock.setWidget(self.monaco)
-
- self.terminal_dock = QtAds.CDockWidget("Terminal", self)
- self.terminal_dock.setWidget(self.terminal)
-
- # Monaco will be central widget
- self.dock_manager.setCentralWidget(self.monaco_dock)
-
- # Add the dock widgets to the dock manager
- area_bottom = self.dock_manager.addDockWidget(
- QtAds.DockWidgetArea.BottomDockWidgetArea, self.console_dock
- )
- self.dock_manager.addDockWidgetTabToArea(self.terminal_dock, area_bottom)
-
- area_left = self.dock_manager.addDockWidget(
- QtAds.DockWidgetArea.LeftDockWidgetArea, self.explorer_dock
- )
- area_left.titleBar().setVisible(False)
-
- for dock in self.dock_manager.dockWidgets():
- # dock.setFeature(CDockWidget.DockWidgetDeleteOnClose, True)#TODO implement according to MonacoDock or AdvancedDockArea
- # dock.setFeature(CDockWidget.CustomCloseHandling, True) #TODO same
- dock.setFeature(CDockWidget.DockWidgetClosable, False)
- dock.setFeature(CDockWidget.DockWidgetFloatable, False)
- dock.setFeature(CDockWidget.DockWidgetMovable, False)
-
- self.plotting_ads_dock = QtAds.CDockWidget("Plotting Area", self)
- self.plotting_ads_dock.setWidget(self.plotting_ads)
-
- self.signature_dock = QtAds.CDockWidget("Signature Help", self)
- self.signature_dock.setWidget(self.signature_help)
-
- area_right = self.dock_manager.addDockWidget(
- QtAds.DockWidgetArea.RightDockWidgetArea, self.plotting_ads_dock
- )
- self.dock_manager.addDockWidgetTabToArea(self.signature_dock, area_right)
+ def __init__(
+ self,
+ parent: QWidget | None = None,
+ content: QWidget | None = None,
+ *,
+ id: str | None = None,
+ title: str | None = None,
+ ):
+ super().__init__(parent=parent, content=content, id=id, title=title)
+ self.developer_widget = DeveloperWidget(parent=self)
+ self.set_content(self.developer_widget)
# Apply stretch after the layout is done
self.set_default_view([2, 5, 3], [7, 3])
- # Connect editor signals
- self.explorer.file_open_requested.connect(self._open_new_file)
-
- self.toolbar.show_bundles(["save", "execution", "settings"])
-
- def init_developer_toolbar(self):
- """Initialize the developer toolbar with necessary actions and widgets."""
- save_button = MaterialIconAction(
- icon_name="save", tooltip="Save", label_text="Save", filled=True, parent=self
- )
- save_button.action.triggered.connect(self.on_save)
- self.toolbar.components.add_safe("save", save_button)
-
- save_as_button = MaterialIconAction(
- icon_name="save_as", tooltip="Save As", label_text="Save As", parent=self
- )
- self.toolbar.components.add_safe("save_as", save_as_button)
-
- save_bundle = ToolbarBundle("save", self.toolbar.components)
- save_bundle.add_action("save")
- save_bundle.add_action("save_as")
- self.toolbar.add_bundle(save_bundle)
-
- run_action = MaterialIconAction(
- icon_name="play_arrow",
- tooltip="Run current file",
- label_text="Run",
- filled=True,
- parent=self,
- )
- run_action.action.triggered.connect(self.on_execute)
- self.toolbar.components.add_safe("run", run_action)
-
- stop_action = MaterialIconAction(
- icon_name="stop",
- tooltip="Stop current execution",
- label_text="Stop",
- filled=True,
- parent=self,
- )
- stop_action.action.triggered.connect(self.on_stop)
- self.toolbar.components.add_safe("stop", stop_action)
-
- execution_bundle = ToolbarBundle("execution", self.toolbar.components)
- execution_bundle.add_action("run")
- execution_bundle.add_action("stop")
- self.toolbar.add_bundle(execution_bundle)
-
- vim_action = MaterialIconAction(
- icon_name="vim",
- tooltip="Toggle Vim Mode",
- label_text="Vim",
- filled=True,
- parent=self,
- checkable=True,
- )
- self.toolbar.components.add_safe("vim", vim_action)
- vim_action.action.triggered.connect(self.on_vim_triggered)
-
- settings_bundle = ToolbarBundle("settings", self.toolbar.components)
- settings_bundle.add_action("vim")
- self.toolbar.add_bundle(settings_bundle)
-
- save_shortcut = QShortcut(QKeySequence("Ctrl+S"), self)
- save_shortcut.activated.connect(self.on_save)
- save_as_shortcut = QShortcut(QKeySequence("Ctrl+Shift+S"), self)
- save_as_shortcut.activated.connect(self.on_save_as)
-
- ####### Default view has to be done with setting up splitters ########
- def set_default_view(self, horizontal_weights: list, vertical_weights: list):
- """Apply initial weights to every horizontal and vertical splitter.
-
- Examples:
- horizontal_weights = [1, 3, 2, 1]
- vertical_weights = [3, 7] # top:bottom = 30:70
- """
- splitters_h = []
- splitters_v = []
- for splitter in self.findChildren(QSplitter):
- if splitter.orientation() == Qt.Horizontal:
- splitters_h.append(splitter)
- elif splitter.orientation() == Qt.Vertical:
- splitters_v.append(splitter)
-
- def apply_all():
- for s in splitters_h:
- set_splitter_weights(s, horizontal_weights)
- for s in splitters_v:
- set_splitter_weights(s, vertical_weights)
-
- QTimer.singleShot(0, apply_all)
-
- def set_stretch(self, *, horizontal=None, vertical=None):
- """Update splitter weights and re-apply to all splitters.
-
- Accepts either a list/tuple of weights (e.g., [1,3,2,1]) or a role dict
- for convenience: horizontal roles = {"left","center","right"},
- vertical roles = {"top","bottom"}.
- """
-
- def _coerce_h(x):
- if x is None:
- return None
- if isinstance(x, (list, tuple)):
- return list(map(float, x))
- if isinstance(x, dict):
- return [
- float(x.get("left", 1)),
- float(x.get("center", x.get("middle", 1))),
- float(x.get("right", 1)),
- ]
- return None
-
- def _coerce_v(x):
- if x is None:
- return None
- if isinstance(x, (list, tuple)):
- return list(map(float, x))
- if isinstance(x, dict):
- return [float(x.get("top", 1)), float(x.get("bottom", 1))]
- return None
-
- h = _coerce_h(horizontal)
- v = _coerce_v(vertical)
- if h is None:
- h = [1, 1, 1]
- if v is None:
- v = [1, 1]
- self.set_default_view(h, v)
-
- def _open_new_file(self, file_name: str, scope: str):
- self.monaco.open_file(file_name)
-
- # Set read-only mode for shared files
- if "shared" in scope:
- self.monaco.set_file_readonly(file_name, True)
-
- # Add appropriate icon based on file type
- if "script" in scope:
- # Use script icon for script files
- icon = material_icon("script", size=(24, 24))
- self.monaco.set_file_icon(file_name, icon)
- elif "macro" in scope:
- # Use function icon for macro files
- icon = material_icon("function", size=(24, 24))
- self.monaco.set_file_icon(file_name, icon)
-
- @SafeSlot()
- def on_save(self):
- self.monaco.save_file()
-
- @SafeSlot()
- def on_save_as(self):
- self.monaco.save_file(force_save_as=True)
-
- @SafeSlot()
- def on_vim_triggered(self):
- self.monaco.set_vim_mode(self.toolbar.components.get_action("vim").action.isChecked())
-
- @SafeSlot(bool)
- def _on_save_enabled_update(self, enabled: bool):
- self.toolbar.components.get_action("save").action.setEnabled(enabled)
- self.toolbar.components.get_action("save_as").action.setEnabled(enabled)
-
- @SafeSlot()
- def on_execute(self):
- self.script_editor_tab = self.monaco.last_focused_editor
- if not self.script_editor_tab:
- return
- self.current_script_id = upload_script(
- self.client.connector, self.script_editor_tab.widget().get_text()
- )
- self.console.write(f'bec._run_script("{self.current_script_id}")')
- print(f"Uploaded script with ID: {self.current_script_id}")
-
- @SafeSlot()
- def on_stop(self):
- print("Stopping execution...")
-
- @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.script_editor_tab.widget().clear_highlighted_lines()
- return
- line_number = current_lines[0]
- self.script_editor_tab.widget().clear_highlighted_lines()
- self.script_editor_tab.widget().set_highlighted_lines(line_number, line_number)
-
if __name__ == "__main__":
import sys
@@ -421,6 +37,20 @@ if __name__ == "__main__":
apply_theme("dark")
_app = BECMainApp()
+ screen = app.primaryScreen()
+ screen_geometry = screen.availableGeometry()
+ screen_width = screen_geometry.width()
+ screen_height = screen_geometry.height()
+ # 70% of screen height, keep 16:9 ratio
+ height = int(screen_height * 0.9)
+ width = int(height * (16 / 9))
+
+ # If width exceeds screen width, scale down
+ if width > screen_width * 0.9:
+ width = int(screen_width * 0.9)
+ height = int(width / (16 / 9))
+
+ _app.resize(width, height)
developer_view = DeveloperView()
_app.add_view(
icon="code_blocks", title="IDE", widget=developer_view, id="developer_view", exclusive=True
diff --git a/bec_widgets/examples/developer_view/developer_widget.py b/bec_widgets/examples/developer_view/developer_widget.py
new file mode 100644
index 00000000..d6009666
--- /dev/null
+++ b/bec_widgets/examples/developer_view/developer_widget.py
@@ -0,0 +1,346 @@
+import re
+
+import markdown
+import PySide6QtAds as QtAds
+from bec_lib.endpoints import MessageEndpoints
+from bec_lib.script_executor import upload_script
+from bec_qthemes import material_icon
+from PySide6QtAds import CDockManager, CDockWidget
+from qtpy.QtGui import QKeySequence, QShortcut
+from qtpy.QtWidgets import QTextEdit, QVBoxLayout, QWidget
+
+from bec_widgets 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.containers.advanced_dock_area.advanced_dock_area import AdvancedDockArea
+from bec_widgets.widgets.editors.monaco.monaco_tab import MonacoDock
+from bec_widgets.widgets.editors.web_console.web_console import WebConsole
+from bec_widgets.widgets.utility.ide_explorer.ide_explorer import IDEExplorer
+
+
+def markdown_to_html(md_text: str) -> str:
+ """Convert Markdown with syntax highlighting to HTML (Qt-compatible)."""
+
+ # Preprocess: convert consecutive >>> lines to Python code blocks
+ def replace_python_examples(match):
+ indent = match.group(1)
+ examples = match.group(2)
+ # Remove >>> prefix and clean up the code
+ lines = []
+ for line in examples.strip().split("\n"):
+ line = line.strip()
+ if line.startswith(">>> "):
+ lines.append(line[4:]) # Remove '>>> '
+ elif line.startswith(">>>"):
+ lines.append(line[3:]) # Remove '>>>'
+ code = "\n".join(lines)
+
+ return f"{indent}```python\n{indent}{code}\n{indent}```"
+
+ # Match one or more consecutive >>> lines (with same indentation)
+ pattern = r"^(\s*)((?:>>> .+(?:\n|$))+)"
+ md_text = re.sub(pattern, replace_python_examples, md_text, flags=re.MULTILINE)
+
+ extensions = ["fenced_code", "codehilite", "tables", "sane_lists"]
+ html = markdown.markdown(
+ md_text,
+ extensions=extensions,
+ extension_configs={
+ "codehilite": {"linenums": False, "guess_lang": False, "noclasses": True}
+ },
+ output_format="html",
+ )
+
+ # Remove hardcoded background colors that conflict with themes
+ html = re.sub(r'style="background: #[^"]*"', 'style="background: transparent"', html)
+ html = re.sub(r"background: #[^;]*;", "", html)
+
+ # Add CSS to force code blocks to wrap
+ css = """
+
+ """
+
+ return css + html
+
+
+class DeveloperWidget(BECWidget, QWidget):
+
+ def __init__(self, parent=None, **kwargs):
+ super().__init__(parent=parent, **kwargs)
+
+ # Top-level layout hosting a toolbar and the dock manager
+ self._root_layout = QVBoxLayout(self)
+ self._root_layout.setContentsMargins(0, 0, 0, 0)
+ self._root_layout.setSpacing(0)
+ self.toolbar = ModularToolBar(self)
+ self.init_developer_toolbar()
+ self._root_layout.addWidget(self.toolbar)
+
+ self.dock_manager = CDockManager(self)
+ self.dock_manager.setStyleSheet("")
+ self._root_layout.addWidget(self.dock_manager)
+
+ # Initialize the widgets
+ self.explorer = IDEExplorer(self)
+ self.console = WebConsole(self)
+ self.terminal = WebConsole(self, startup_cmd="")
+ self.monaco = MonacoDock(self)
+ self.monaco.save_enabled.connect(self._on_save_enabled_update)
+ self.plotting_ads = AdvancedDockArea(self, mode="plot", default_add_direction="bottom")
+ self.signature_help = QTextEdit(self)
+ self.signature_help.setAcceptRichText(True)
+ self.signature_help.setReadOnly(True)
+ self.signature_help.setLineWrapMode(QTextEdit.LineWrapMode.WidgetWidth)
+ opt = self.signature_help.document().defaultTextOption()
+ opt.setWrapMode(opt.WrapMode.WrapAnywhere)
+ self.signature_help.document().setDefaultTextOption(opt)
+ self.monaco.signature_help.connect(
+ lambda text: self.signature_help.setHtml(markdown_to_html(text))
+ )
+
+ # Create the dock widgets
+ self.explorer_dock = QtAds.CDockWidget("Explorer", self)
+ self.explorer_dock.setWidget(self.explorer)
+
+ self.console_dock = QtAds.CDockWidget("Console", self)
+ self.console_dock.setWidget(self.console)
+
+ self.monaco_dock = QtAds.CDockWidget("Monaco Editor", self)
+ self.monaco_dock.setWidget(self.monaco)
+
+ self.terminal_dock = QtAds.CDockWidget("Terminal", self)
+ self.terminal_dock.setWidget(self.terminal)
+
+ # Monaco will be central widget
+ self.dock_manager.setCentralWidget(self.monaco_dock)
+
+ # Add the dock widgets to the dock manager
+ area_bottom = self.dock_manager.addDockWidget(
+ QtAds.DockWidgetArea.BottomDockWidgetArea, self.console_dock
+ )
+ self.dock_manager.addDockWidgetTabToArea(self.terminal_dock, area_bottom)
+
+ area_left = self.dock_manager.addDockWidget(
+ QtAds.DockWidgetArea.LeftDockWidgetArea, self.explorer_dock
+ )
+ area_left.titleBar().setVisible(False)
+
+ for dock in self.dock_manager.dockWidgets():
+ # dock.setFeature(CDockWidget.DockWidgetDeleteOnClose, True)#TODO implement according to MonacoDock or AdvancedDockArea
+ # dock.setFeature(CDockWidget.CustomCloseHandling, True) #TODO same
+ dock.setFeature(CDockWidget.DockWidgetClosable, False)
+ dock.setFeature(CDockWidget.DockWidgetFloatable, False)
+ dock.setFeature(CDockWidget.DockWidgetMovable, False)
+
+ self.plotting_ads_dock = QtAds.CDockWidget("Plotting Area", self)
+ self.plotting_ads_dock.setWidget(self.plotting_ads)
+
+ self.signature_dock = QtAds.CDockWidget("Signature Help", self)
+ self.signature_dock.setWidget(self.signature_help)
+
+ area_right = self.dock_manager.addDockWidget(
+ QtAds.DockWidgetArea.RightDockWidgetArea, self.plotting_ads_dock
+ )
+ self.dock_manager.addDockWidgetTabToArea(self.signature_dock, area_right)
+
+ # Connect editor signals
+ self.explorer.file_open_requested.connect(self._open_new_file)
+
+ self.toolbar.show_bundles(["save", "execution", "settings"])
+
+ def init_developer_toolbar(self):
+ """Initialize the developer toolbar with necessary actions and widgets."""
+ save_button = MaterialIconAction(
+ icon_name="save", tooltip="Save", label_text="Save", filled=True, parent=self
+ )
+ save_button.action.triggered.connect(self.on_save)
+ self.toolbar.components.add_safe("save", save_button)
+
+ save_as_button = MaterialIconAction(
+ icon_name="save_as", tooltip="Save As", label_text="Save As", parent=self
+ )
+ self.toolbar.components.add_safe("save_as", save_as_button)
+
+ save_bundle = ToolbarBundle("save", self.toolbar.components)
+ save_bundle.add_action("save")
+ save_bundle.add_action("save_as")
+ self.toolbar.add_bundle(save_bundle)
+
+ run_action = MaterialIconAction(
+ icon_name="play_arrow",
+ tooltip="Run current file",
+ label_text="Run",
+ filled=True,
+ parent=self,
+ )
+ run_action.action.triggered.connect(self.on_execute)
+ self.toolbar.components.add_safe("run", run_action)
+
+ stop_action = MaterialIconAction(
+ icon_name="stop",
+ tooltip="Stop current execution",
+ label_text="Stop",
+ filled=True,
+ parent=self,
+ )
+ stop_action.action.triggered.connect(self.on_stop)
+ self.toolbar.components.add_safe("stop", stop_action)
+
+ execution_bundle = ToolbarBundle("execution", self.toolbar.components)
+ execution_bundle.add_action("run")
+ execution_bundle.add_action("stop")
+ self.toolbar.add_bundle(execution_bundle)
+
+ vim_action = MaterialIconAction(
+ icon_name="vim",
+ tooltip="Toggle Vim Mode",
+ label_text="Vim",
+ filled=True,
+ parent=self,
+ checkable=True,
+ )
+ self.toolbar.components.add_safe("vim", vim_action)
+ vim_action.action.triggered.connect(self.on_vim_triggered)
+
+ settings_bundle = ToolbarBundle("settings", self.toolbar.components)
+ settings_bundle.add_action("vim")
+ self.toolbar.add_bundle(settings_bundle)
+
+ save_shortcut = QShortcut(QKeySequence("Ctrl+S"), self)
+ save_shortcut.activated.connect(self.on_save)
+ save_as_shortcut = QShortcut(QKeySequence("Ctrl+Shift+S"), self)
+ save_as_shortcut.activated.connect(self.on_save_as)
+
+ def _open_new_file(self, file_name: str, scope: str):
+ self.monaco.open_file(file_name)
+
+ # Set read-only mode for shared files
+ if "shared" in scope:
+ self.monaco.set_file_readonly(file_name, True)
+
+ # Add appropriate icon based on file type
+ if "script" in scope:
+ # Use script icon for script files
+ icon = material_icon("script", size=(24, 24))
+ self.monaco.set_file_icon(file_name, icon)
+ elif "macro" in scope:
+ # Use function icon for macro files
+ icon = material_icon("function", size=(24, 24))
+ self.monaco.set_file_icon(file_name, icon)
+
+ @SafeSlot()
+ def on_save(self):
+ self.monaco.save_file()
+
+ @SafeSlot()
+ def on_save_as(self):
+ self.monaco.save_file(force_save_as=True)
+
+ @SafeSlot()
+ def on_vim_triggered(self):
+ self.monaco.set_vim_mode(self.toolbar.components.get_action("vim").action.isChecked())
+
+ @SafeSlot(bool)
+ def _on_save_enabled_update(self, enabled: bool):
+ self.toolbar.components.get_action("save").action.setEnabled(enabled)
+ self.toolbar.components.get_action("save_as").action.setEnabled(enabled)
+
+ @SafeSlot()
+ def on_execute(self):
+ self.script_editor_tab = self.monaco.last_focused_editor
+ if not self.script_editor_tab:
+ return
+ self.current_script_id = upload_script(
+ self.client.connector, self.script_editor_tab.widget().get_text()
+ )
+ self.console.write(f'bec._run_script("{self.current_script_id}")')
+ print(f"Uploaded script with ID: {self.current_script_id}")
+
+ @SafeSlot()
+ def on_stop(self):
+ print("Stopping execution...")
+
+ @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.script_editor_tab.widget().clear_highlighted_lines()
+ return
+ line_number = current_lines[0]
+ self.script_editor_tab.widget().clear_highlighted_lines()
+ self.script_editor_tab.widget().set_highlighted_lines(line_number, line_number)
+
+
+if __name__ == "__main__":
+ import sys
+
+ from bec_qthemes import apply_theme
+ from qtpy.QtWidgets import QApplication
+
+ from bec_widgets.applications.main_app import BECMainApp
+
+ app = QApplication(sys.argv)
+ apply_theme("dark")
+
+ _app = BECMainApp()
+ screen = app.primaryScreen()
+ screen_geometry = screen.availableGeometry()
+ screen_width = screen_geometry.width()
+ screen_height = screen_geometry.height()
+ # 70% of screen height, keep 16:9 ratio
+ height = int(screen_height * 0.9)
+ width = int(height * (16 / 9))
+
+ # If width exceeds screen width, scale down
+ if width > screen_width * 0.9:
+ width = int(screen_width * 0.9)
+ height = int(width / (16 / 9))
+
+ _app.resize(width, height)
+ developer_view = DeveloperView()
+ _app.add_view(
+ icon="code_blocks", title="IDE", widget=developer_view, id="developer_view", 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_())