1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-12-29 18:31:17 +01:00

fix: macro support

This commit is contained in:
2025-10-14 09:07:44 +02:00
parent 68334d6cf5
commit 2c2f108fff
5 changed files with 192 additions and 32 deletions

View File

@@ -158,6 +158,7 @@ class DeveloperWidget(BECWidget, QWidget):
# Connect editor signals
self.explorer.file_open_requested.connect(self._open_new_file)
self.monaco.macro_file_updated.connect(self.explorer.refresh_macro_file)
self.toolbar.show_bundles(["save", "execution", "settings"])
@@ -225,7 +226,7 @@ class DeveloperWidget(BECWidget, QWidget):
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)
self.monaco.open_file(file_name, scope)
# Set read-only mode for shared files
if "shared" in scope:

View File

@@ -266,6 +266,45 @@ class MacroTreeWidget(QWidget):
self._scan_macro_functions()
def _create_file_item(self, py_file: Path) -> QStandardItem | None:
"""Create a file item with its functions
Args:
py_file: Path to the Python file
Returns:
QStandardItem representing the file, or None if no functions found
"""
# Skip files starting with underscore
if py_file.name.startswith("_"):
return None
try:
functions = self._extract_functions_from_file(py_file)
if not functions:
return None
# Create a file node
file_item = QStandardItem(py_file.stem)
file_item.setData({"file_path": str(py_file), "type": "file"}, Qt.ItemDataRole.UserRole)
# Add function nodes
for func_name, func_info in functions.items():
func_item = QStandardItem(func_name)
func_data = {
"function_name": func_name,
"file_path": str(py_file),
"line_number": func_info.get("line_number", 1),
"type": "function",
}
func_item.setData(func_data, Qt.ItemDataRole.UserRole)
file_item.appendRow(func_item)
return file_item
except Exception as e:
logger.warning(f"Failed to parse {py_file}: {e}")
return None
def _scan_macro_functions(self):
"""Scan the directory for Python files and extract macro functions"""
self.model.clear()
@@ -278,34 +317,9 @@ class MacroTreeWidget(QWidget):
python_files = list(Path(self.directory).glob("*.py"))
for py_file in python_files:
# Skip files starting with underscore
if py_file.name.startswith("_"):
continue
try:
functions = self._extract_functions_from_file(py_file)
if functions:
# Create a file node
file_item = QStandardItem(py_file.stem)
file_item.setData(
{"file_path": str(py_file), "type": "file"}, Qt.ItemDataRole.UserRole
)
# Add function nodes
for func_name, func_info in functions.items():
func_item = QStandardItem(func_name)
func_data = {
"function_name": func_name,
"file_path": str(py_file),
"line_number": func_info.get("line_number", 1),
"type": "function",
}
func_item.setData(func_data, Qt.ItemDataRole.UserRole)
file_item.appendRow(func_item)
self.model.appendRow(file_item)
except Exception as e:
logger.warning(f"Failed to parse {py_file}: {e}")
file_item = self._create_file_item(py_file)
if file_item:
self.model.appendRow(file_item)
self.tree.expandAll()
@@ -399,6 +413,51 @@ class MacroTreeWidget(QWidget):
return
self._scan_macro_functions()
def refresh_file_item(self, file_path: str):
"""Refresh a single file item by re-scanning its functions
Args:
file_path: Path to the Python file to refresh
"""
if not file_path or not os.path.exists(file_path):
logger.warning(f"Cannot refresh file item: {file_path} does not exist")
return
py_file = Path(file_path)
# Find existing file item in the model
existing_item = None
existing_row = -1
for row in range(self.model.rowCount()):
item = self.model.item(row)
if not item or not item.data(Qt.ItemDataRole.UserRole):
continue
item_data = item.data(Qt.ItemDataRole.UserRole)
if item_data.get("type") == "file" and item_data.get("file_path") == str(py_file):
existing_item = item
existing_row = row
break
# Store expansion state if item exists
was_expanded = existing_item and self.tree.isExpanded(existing_item.index())
# Remove existing item if found
if existing_item and existing_row >= 0:
self.model.removeRow(existing_row)
# Create new item using the helper method
new_item = self._create_file_item(py_file)
if new_item:
# Insert at the same position or append if it was a new file
insert_row = existing_row if existing_row >= 0 else self.model.rowCount()
self.model.insertRow(insert_row, new_item)
# Restore expansion state
if was_expanded:
self.tree.expand(new_item.index())
else:
self.tree.expand(new_item.index())
def expand_all(self):
"""Expand all items in the tree"""
self.tree.expandAll()

View File

@@ -2,10 +2,11 @@ from __future__ import annotations
import os
import pathlib
from typing import Any, cast
from typing import Any, Literal, cast
import PySide6QtAds as QtAds
from bec_lib.logger import bec_logger
from bec_lib.macro_update_handler import has_executable_code
from PySide6QtAds import CDockWidget
from qtpy.QtCore import QEvent, QTimer, Signal
from qtpy.QtWidgets import QFileDialog, QMessageBox, QToolButton, QVBoxLayout, QWidget
@@ -25,6 +26,7 @@ class MonacoDock(BECWidget, QWidget):
focused_editor = Signal(object) # Emitted when the focused editor changes
save_enabled = Signal(bool) # Emitted when the save action is enabled/disabled
signature_help = Signal(str) # Emitted when signature help is requested
macro_file_updated = Signal(str) # Emitted when a macro file is saved
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
@@ -234,7 +236,7 @@ class MonacoDock(BECWidget, QWidget):
QTimer.singleShot(0, self._scan_and_fix_areas)
return new_dock
def open_file(self, file_name: str):
def open_file(self, file_name: str, scope: str | None = None) -> None:
"""
Open a file in the specified area. If the file is already open, activate it.
"""
@@ -260,12 +262,14 @@ class MonacoDock(BECWidget, QWidget):
editor_dock.setWindowTitle(file)
editor_dock.setTabToolTip(file_name)
editor_widget.open_file(file_name)
editor_widget.metadata["scope"] = scope
return
# File is not open, create a new editor
editor_dock = self.add_editor(title=file, tooltip=file_name)
widget = cast(MonacoWidget, editor_dock.widget())
widget.open_file(file_name)
widget.metadata["scope"] = scope
def save_file(
self, widget: MonacoWidget | None = None, force_save_as: bool = False, format_on_save=True
@@ -281,11 +285,22 @@ class MonacoDock(BECWidget, QWidget):
widget = self.last_focused_editor.widget() if self.last_focused_editor else None
if not widget:
return
if "macros" in widget.metadata.get("scope", ""):
if not self._validate_macros(widget.get_text()):
return
if widget.current_file and not force_save_as:
if format_on_save and pathlib.Path(widget.current_file).suffix == ".py":
widget.format()
with open(widget.current_file, "w", encoding="utf-8") as f:
f.write(widget.get_text())
if "macros" in widget.metadata.get("scope", ""):
self._update_macros(widget)
# Emit signal to refresh macro tree widget
self.macro_file_updated.emit(widget.current_file)
# pylint: disable=protected-access
widget._original_content = widget.get_text()
widget.save_enabled.emit(False)
@@ -317,9 +332,53 @@ class MonacoDock(BECWidget, QWidget):
dock.setWindowTitle(file.name)
dock.setTabToolTip(str(file))
break
if "macros" in widget.metadata.get("scope", ""):
self._update_macros(widget)
# Emit signal to refresh macro tree widget
self.macro_file_updated.emit(str(file))
print(f"Save file called, last focused editor: {self.last_focused_editor}")
def _validate_macros(self, source: str) -> bool:
# pylint: disable=protected-access
# Ensure the macro does not contain executable code before saving
exec_code, line_number = has_executable_code(source)
if exec_code:
if line_number is None:
msg = "The macro contains executable code. Please remove it before saving."
else:
msg = f"The macro contains executable code on line {line_number}. Please remove it before saving."
QMessageBox.warning(self, "Save Error", msg)
return False
return True
def _update_macros(self, widget: MonacoWidget):
# pylint: disable=protected-access
if not widget.current_file:
return
# Check which macros have changed and broadcast the change
macros = self.client.macros._update_handler.get_macros_from_file(widget.current_file)
existing_macros = self.client.macros._update_handler.get_existing_macros(
widget.current_file
)
removed_macros = set(existing_macros.keys()) - set(macros.keys())
added_macros = set(macros.keys()) - set(existing_macros.keys())
for name, info in macros.items():
if name in added_macros:
self.client.macros._update_handler.broadcast(
action="add", name=name, file_path=widget.current_file
)
if (
name in existing_macros
and info.get("source", "") != existing_macros[name]["source"]
):
self.client.macros._update_handler.broadcast(
action="reload", name=name, file_path=widget.current_file
)
for name in removed_macros:
self.client.macros._update_handler.broadcast(action="remove", name=name)
def set_vim_mode(self, enabled: bool):
"""
Set Vim mode for all editor widgets.

View File

@@ -63,6 +63,7 @@ class MonacoWidget(BECWidget, QWidget):
self.editor.context_menu_action_triggered.connect(self._handle_context_menu_action)
self._current_file = None
self._original_content = ""
self.metadata = {}
@property
def current_file(self):

View File

@@ -108,7 +108,11 @@ class IDEExplorer(BECWidget, QWidget):
def add_macro_section(self):
section = CollapsibleSection(
parent=self, title="MACROS", indentation=0, show_add_button=True
parent=self,
title="MACROS",
indentation=0,
show_add_button=True,
tooltip="Macros are reusable functions that can be called from scripts or the console.",
)
section.header_add_button.setIcon(material_icon("refresh", size=(20, 20)))
section.header_add_button.setToolTip("Reload all macros")
@@ -315,6 +319,42 @@ def {function_name}():
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to reload macros: {str(e)}")
def refresh_macro_file(self, file_path: str):
"""Refresh a single macro file in the tree widget.
Args:
file_path: Path to the macro file that was updated
"""
target_section = self.main_explorer.get_section("MACROS")
if not target_section or not hasattr(target_section, "content_widget"):
return
# Determine if this is a local or shared macro based on the file path
local_section = target_section.content_widget.get_section("Local")
shared_section = target_section.content_widget.get_section("Shared")
# Check if file belongs to local macros directory
if (
local_section
and hasattr(local_section, "content_widget")
and hasattr(local_section.content_widget, "directory")
):
local_macro_dir = local_section.content_widget.directory
if local_macro_dir and file_path.startswith(local_macro_dir):
local_section.content_widget.refresh_file_item(file_path)
return
# Check if file belongs to shared macros directory
if (
shared_section
and hasattr(shared_section, "content_widget")
and hasattr(shared_section.content_widget, "directory")
):
shared_macro_dir = shared_section.content_widget.directory
if shared_macro_dir and file_path.startswith(shared_macro_dir):
shared_section.content_widget.refresh_file_item(file_path)
return
if __name__ == "__main__":
from qtpy.QtWidgets import QApplication