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:
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user