mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-03-05 00:12:49 +01:00
f - feat: add macros
This commit is contained in:
@@ -3,6 +3,7 @@ from typing import List
|
||||
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
|
||||
@@ -251,6 +252,20 @@ class DeveloperView(BECWidget, QWidget):
|
||||
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()
|
||||
|
||||
408
bec_widgets/widgets/containers/explorer/macro_tree_widget.py
Normal file
408
bec_widgets/widgets/containers/explorer/macro_tree_widget.py
Normal file
@@ -0,0 +1,408 @@
|
||||
import ast
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QModelIndex, QRect, Qt, Signal
|
||||
from qtpy.QtGui import QPainter, QStandardItem, QStandardItemModel
|
||||
from qtpy.QtWidgets import QStyledItemDelegate, QTreeView, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import get_theme_palette
|
||||
from bec_widgets.utils.toolbars.actions import MaterialIconAction
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MacroItemDelegate(QStyledItemDelegate):
|
||||
"""Custom delegate to show action buttons on hover for macro functions"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.hovered_index = QModelIndex()
|
||||
self.macro_actions: list[Any] = []
|
||||
self.button_rects: list[QRect] = []
|
||||
self.current_macro_info = {}
|
||||
|
||||
def add_macro_action(self, action: Any) -> None:
|
||||
"""Add an action for macro functions"""
|
||||
self.macro_actions.append(action)
|
||||
|
||||
def clear_actions(self) -> None:
|
||||
"""Remove all actions"""
|
||||
self.macro_actions.clear()
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
"""Paint the item with action buttons on hover"""
|
||||
# Paint the default item
|
||||
super().paint(painter, option, index)
|
||||
|
||||
# Early return if not hovering over this item
|
||||
if index != self.hovered_index:
|
||||
return
|
||||
|
||||
# Only show actions for macro functions (not directories)
|
||||
item = index.model().itemFromIndex(index)
|
||||
if not item or not item.data(Qt.ItemDataRole.UserRole):
|
||||
return
|
||||
|
||||
macro_info = item.data(Qt.ItemDataRole.UserRole)
|
||||
if not isinstance(macro_info, dict) or "function_name" not in macro_info:
|
||||
return
|
||||
|
||||
self.current_macro_info = macro_info
|
||||
|
||||
if self.macro_actions:
|
||||
self._draw_action_buttons(painter, option, self.macro_actions)
|
||||
|
||||
def _draw_action_buttons(self, painter, option, actions: list[Any]):
|
||||
"""Draw action buttons on the right side"""
|
||||
button_size = 18
|
||||
margin = 4
|
||||
spacing = 2
|
||||
|
||||
# Calculate total width needed for all buttons
|
||||
total_width = len(actions) * button_size + (len(actions) - 1) * spacing
|
||||
|
||||
# Clear previous button rects and create new ones
|
||||
self.button_rects.clear()
|
||||
|
||||
# Calculate starting position (right side of the item)
|
||||
start_x = option.rect.right() - total_width - margin
|
||||
current_x = start_x
|
||||
|
||||
painter.save()
|
||||
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
|
||||
# Get theme colors for better integration
|
||||
palette = get_theme_palette()
|
||||
button_bg = palette.button().color()
|
||||
button_bg.setAlpha(150) # Semi-transparent
|
||||
|
||||
for action in actions:
|
||||
if not action.isVisible():
|
||||
continue
|
||||
|
||||
# Calculate button position
|
||||
button_rect = QRect(
|
||||
current_x,
|
||||
option.rect.top() + (option.rect.height() - button_size) // 2,
|
||||
button_size,
|
||||
button_size,
|
||||
)
|
||||
self.button_rects.append(button_rect)
|
||||
|
||||
# Draw button background
|
||||
painter.setBrush(button_bg)
|
||||
painter.setPen(palette.mid().color())
|
||||
painter.drawRoundedRect(button_rect, 3, 3)
|
||||
|
||||
# Draw action icon
|
||||
icon = action.icon()
|
||||
if not icon.isNull():
|
||||
icon_rect = button_rect.adjusted(2, 2, -2, -2)
|
||||
icon.paint(painter, icon_rect)
|
||||
|
||||
# Move to next button position
|
||||
current_x += button_size + spacing
|
||||
|
||||
painter.restore()
|
||||
|
||||
def editorEvent(self, event, model, option, index):
|
||||
"""Handle mouse events for action buttons"""
|
||||
# Early return if not a left click
|
||||
if not (
|
||||
event.type() == event.Type.MouseButtonPress
|
||||
and event.button() == Qt.MouseButton.LeftButton
|
||||
):
|
||||
return super().editorEvent(event, model, option, index)
|
||||
|
||||
# Check which button was clicked
|
||||
visible_actions = [action for action in self.macro_actions if action.isVisible()]
|
||||
for i, button_rect in enumerate(self.button_rects):
|
||||
if button_rect.contains(event.pos()) and i < len(visible_actions):
|
||||
# Trigger the action
|
||||
visible_actions[i].trigger()
|
||||
return True
|
||||
|
||||
return super().editorEvent(event, model, option, index)
|
||||
|
||||
def set_hovered_index(self, index):
|
||||
"""Set the currently hovered index"""
|
||||
self.hovered_index = index
|
||||
|
||||
|
||||
class MacroTreeWidget(QWidget):
|
||||
"""A tree widget that displays macro functions from Python files"""
|
||||
|
||||
macro_selected = Signal(str, str) # Function name, file path
|
||||
macro_open_requested = Signal(str, str) # Function name, file path
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Create layout
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
|
||||
# Create tree view
|
||||
self.tree = QTreeView()
|
||||
self.tree.setHeaderHidden(True)
|
||||
self.tree.setRootIsDecorated(True)
|
||||
|
||||
# Disable editing to prevent renaming on double-click
|
||||
self.tree.setEditTriggers(QTreeView.EditTrigger.NoEditTriggers)
|
||||
|
||||
# Enable mouse tracking for hover effects
|
||||
self.tree.setMouseTracking(True)
|
||||
|
||||
# Create model for macro functions
|
||||
self.model = QStandardItemModel()
|
||||
self.tree.setModel(self.model)
|
||||
|
||||
# Create and set custom delegate
|
||||
self.delegate = MacroItemDelegate(self.tree)
|
||||
self.tree.setItemDelegate(self.delegate)
|
||||
|
||||
# Add default open button for macros
|
||||
action = MaterialIconAction(icon_name="file_open", tooltip="Open macro file", parent=self)
|
||||
action.action.triggered.connect(self._on_macro_open_requested)
|
||||
self.delegate.add_macro_action(action.action)
|
||||
|
||||
# Apply BEC styling
|
||||
self._apply_styling()
|
||||
|
||||
# Macro specific properties
|
||||
self.directory = None
|
||||
|
||||
# Connect signals
|
||||
self.tree.clicked.connect(self._on_item_clicked)
|
||||
self.tree.doubleClicked.connect(self._on_item_double_clicked)
|
||||
|
||||
# Install event filter for hover tracking
|
||||
self.tree.viewport().installEventFilter(self)
|
||||
|
||||
# Add to layout
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
def _apply_styling(self):
|
||||
"""Apply styling to the tree widget"""
|
||||
# Get theme colors for subtle tree lines
|
||||
palette = get_theme_palette()
|
||||
subtle_line_color = palette.mid().color()
|
||||
subtle_line_color.setAlpha(80)
|
||||
|
||||
# Standard editable styling
|
||||
opacity_modifier = ""
|
||||
cursor_style = ""
|
||||
|
||||
# pylint: disable=f-string-without-interpolation
|
||||
tree_style = f"""
|
||||
QTreeView {{
|
||||
border: none;
|
||||
outline: 0;
|
||||
show-decoration-selected: 0;
|
||||
{opacity_modifier}
|
||||
{cursor_style}
|
||||
}}
|
||||
QTreeView::branch {{
|
||||
border-image: none;
|
||||
background: transparent;
|
||||
}}
|
||||
|
||||
QTreeView::item {{
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}}
|
||||
QTreeView::item:hover {{
|
||||
background: palette(midlight);
|
||||
border: none;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
text-decoration: none;
|
||||
}}
|
||||
QTreeView::item:selected {{
|
||||
background: palette(highlight);
|
||||
color: palette(highlighted-text);
|
||||
}}
|
||||
QTreeView::item:selected:hover {{
|
||||
background: palette(highlight);
|
||||
}}
|
||||
"""
|
||||
|
||||
self.tree.setStyleSheet(tree_style)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
"""Handle mouse move events for hover tracking"""
|
||||
# Early return if not the tree viewport
|
||||
if obj != self.tree.viewport():
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
if event.type() == event.Type.MouseMove:
|
||||
index = self.tree.indexAt(event.pos())
|
||||
if index.isValid():
|
||||
self.delegate.set_hovered_index(index)
|
||||
else:
|
||||
self.delegate.set_hovered_index(QModelIndex())
|
||||
self.tree.viewport().update()
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
if event.type() == event.Type.Leave:
|
||||
self.delegate.set_hovered_index(QModelIndex())
|
||||
self.tree.viewport().update()
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
return super().eventFilter(obj, event)
|
||||
|
||||
def set_directory(self, directory):
|
||||
"""Set the macros directory and scan for macro functions"""
|
||||
self.directory = directory
|
||||
|
||||
# Early return if directory doesn't exist
|
||||
if not directory or not os.path.exists(directory):
|
||||
return
|
||||
|
||||
self._scan_macro_functions()
|
||||
|
||||
def _scan_macro_functions(self):
|
||||
"""Scan the directory for Python files and extract macro functions"""
|
||||
self.model.clear()
|
||||
self.model.setHorizontalHeaderLabels(["Macros"])
|
||||
|
||||
if not self.directory or not os.path.exists(self.directory):
|
||||
return
|
||||
|
||||
# Get all Python files in the directory
|
||||
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}")
|
||||
|
||||
self.tree.expandAll()
|
||||
|
||||
def _extract_functions_from_file(self, file_path: Path) -> dict:
|
||||
"""Extract function definitions from a Python file"""
|
||||
functions = {}
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse the AST
|
||||
tree = ast.parse(content)
|
||||
|
||||
# Only get top-level function definitions
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
functions[node.name] = {
|
||||
"line_number": node.lineno,
|
||||
"docstring": ast.get_docstring(node) or "",
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to parse {file_path}: {e}")
|
||||
|
||||
return functions
|
||||
|
||||
def _on_item_clicked(self, index: QModelIndex):
|
||||
"""Handle item clicks"""
|
||||
item = self.model.itemFromIndex(index)
|
||||
if not item:
|
||||
return
|
||||
|
||||
data = item.data(Qt.ItemDataRole.UserRole)
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data.get("type") == "function":
|
||||
function_name = data.get("function_name")
|
||||
file_path = data.get("file_path")
|
||||
if function_name and file_path:
|
||||
logger.info(f"Macro function selected: {function_name} in {file_path}")
|
||||
self.macro_selected.emit(function_name, file_path)
|
||||
|
||||
def _on_item_double_clicked(self, index: QModelIndex):
|
||||
"""Handle item double-clicks"""
|
||||
item = self.model.itemFromIndex(index)
|
||||
if not item:
|
||||
return
|
||||
|
||||
data = item.data(Qt.ItemDataRole.UserRole)
|
||||
if not data:
|
||||
return
|
||||
|
||||
if data.get("type") == "function":
|
||||
function_name = data.get("function_name")
|
||||
file_path = data.get("file_path")
|
||||
if function_name and file_path:
|
||||
logger.info(
|
||||
f"Macro open requested via double-click: {function_name} in {file_path}"
|
||||
)
|
||||
self.macro_open_requested.emit(function_name, file_path)
|
||||
|
||||
def _on_macro_open_requested(self):
|
||||
"""Handle macro open action triggered"""
|
||||
logger.info("Macro open requested")
|
||||
# Early return if no hovered item
|
||||
if not self.delegate.hovered_index.isValid():
|
||||
return
|
||||
|
||||
macro_info = self.delegate.current_macro_info
|
||||
if not macro_info or macro_info.get("type") != "function":
|
||||
return
|
||||
|
||||
function_name = macro_info.get("function_name")
|
||||
file_path = macro_info.get("file_path")
|
||||
if function_name and file_path:
|
||||
self.macro_open_requested.emit(function_name, file_path)
|
||||
|
||||
def add_macro_action(self, action: Any) -> None:
|
||||
"""Add an action for macro items"""
|
||||
self.delegate.add_macro_action(action)
|
||||
|
||||
def clear_actions(self) -> None:
|
||||
"""Remove all actions from items"""
|
||||
self.delegate.clear_actions()
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the tree view"""
|
||||
if self.directory is None:
|
||||
return
|
||||
self._scan_macro_functions()
|
||||
|
||||
def expand_all(self):
|
||||
"""Expand all items in the tree"""
|
||||
self.tree.expandAll()
|
||||
|
||||
def collapse_all(self):
|
||||
"""Collapse all items in the tree"""
|
||||
self.tree.collapseAll()
|
||||
@@ -3,7 +3,7 @@ from pathlib import Path
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import QModelIndex, QRect, QRegularExpression, QSortFilterProxyModel, Qt, Signal
|
||||
from qtpy.QtGui import QAction, QPainter
|
||||
from qtpy.QtGui import QPainter
|
||||
from qtpy.QtWidgets import QFileSystemModel, QStyledItemDelegate, QTreeView, QVBoxLayout, QWidget
|
||||
|
||||
from bec_widgets.utils.colors import get_theme_palette
|
||||
@@ -15,19 +15,20 @@ logger = bec_logger.logger
|
||||
class FileItemDelegate(QStyledItemDelegate):
|
||||
"""Custom delegate to show action buttons on hover"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, tree_widget):
|
||||
super().__init__(tree_widget)
|
||||
self.setObjectName("file_item_delegate")
|
||||
self.hovered_index = QModelIndex()
|
||||
self.file_actions: list[QAction] = []
|
||||
self.dir_actions: list[QAction] = []
|
||||
self.button_rects: list[QRect] = []
|
||||
self.file_actions = []
|
||||
self.dir_actions = []
|
||||
self.button_rects = []
|
||||
self.current_file_path = ""
|
||||
|
||||
def add_file_action(self, action: QAction) -> None:
|
||||
def add_file_action(self, action) -> None:
|
||||
"""Add an action for files"""
|
||||
self.file_actions.append(action)
|
||||
|
||||
def add_dir_action(self, action: QAction) -> None:
|
||||
def add_dir_action(self, action) -> None:
|
||||
"""Add an action for directories"""
|
||||
self.dir_actions.append(action)
|
||||
|
||||
@@ -67,7 +68,7 @@ class FileItemDelegate(QStyledItemDelegate):
|
||||
if actions:
|
||||
self._draw_action_buttons(painter, option, actions)
|
||||
|
||||
def _draw_action_buttons(self, painter, option, actions: list[QAction]):
|
||||
def _draw_action_buttons(self, painter, option, actions):
|
||||
"""Draw action buttons on the right side"""
|
||||
button_size = 18
|
||||
margin = 4
|
||||
@@ -229,12 +230,18 @@ class ScriptTreeWidget(QWidget):
|
||||
subtle_line_color = palette.mid().color()
|
||||
subtle_line_color.setAlpha(80)
|
||||
|
||||
# Standard editable styling
|
||||
opacity_modifier = ""
|
||||
cursor_style = ""
|
||||
|
||||
# pylint: disable=f-string-without-interpolation
|
||||
tree_style = f"""
|
||||
QTreeView {{
|
||||
border: none;
|
||||
outline: 0;
|
||||
show-decoration-selected: 0;
|
||||
{opacity_modifier}
|
||||
{cursor_style}
|
||||
}}
|
||||
QTreeView::branch {{
|
||||
border-image: none;
|
||||
@@ -357,11 +364,11 @@ class ScriptTreeWidget(QWidget):
|
||||
|
||||
self.file_open_requested.emit(file_path)
|
||||
|
||||
def add_file_action(self, action: QAction) -> None:
|
||||
def add_file_action(self, action) -> None:
|
||||
"""Add an action for file items"""
|
||||
self.delegate.add_file_action(action)
|
||||
|
||||
def add_dir_action(self, action: QAction) -> None:
|
||||
def add_dir_action(self, action) -> None:
|
||||
"""Add an action for directory items"""
|
||||
self.delegate.add_dir_action(action)
|
||||
|
||||
|
||||
@@ -51,6 +51,11 @@ class MonacoDock(BECWidget, QWidget):
|
||||
dock = CDockWidget(f"Untitled_{count + 1}")
|
||||
dock.setWidget(widget)
|
||||
|
||||
# Connect to modification status changes to update tab titles
|
||||
widget.save_enabled.connect(
|
||||
lambda modified: self._update_tab_title_for_modification(dock, modified)
|
||||
)
|
||||
|
||||
dock.setFeature(CDockWidget.DockWidgetDeleteOnClose, True)
|
||||
dock.setFeature(CDockWidget.CustomCloseHandling, True)
|
||||
dock.setFeature(CDockWidget.DockWidgetClosable, True)
|
||||
@@ -83,6 +88,24 @@ class MonacoDock(BECWidget, QWidget):
|
||||
logger.info(f"Editor '{widget.current_file}' has unsaved changes: {widget.get_text()}")
|
||||
self.save_enabled.emit(widget.modified)
|
||||
|
||||
def _update_tab_title_for_modification(self, dock: CDockWidget, modified: bool):
|
||||
"""Update the tab title to show modification status with a dot indicator."""
|
||||
current_title = dock.windowTitle()
|
||||
|
||||
# Remove existing modification indicator (dot and space)
|
||||
if current_title.startswith("• "):
|
||||
base_title = current_title[2:] # Remove "• "
|
||||
else:
|
||||
base_title = current_title
|
||||
|
||||
# Add or remove the modification indicator
|
||||
if modified:
|
||||
new_title = f"• {base_title}"
|
||||
else:
|
||||
new_title = base_title
|
||||
|
||||
dock.setWindowTitle(new_title)
|
||||
|
||||
def _on_signature_change(self, signature: dict):
|
||||
signatures = signature.get("signatures", [])
|
||||
if not signatures:
|
||||
@@ -108,8 +131,11 @@ class MonacoDock(BECWidget, QWidget):
|
||||
self.last_focused_editor = new_widget
|
||||
|
||||
def _on_editor_close_requested(self, dock: CDockWidget, widget: QWidget):
|
||||
# Cast widget to MonacoWidget since we know that's what it is
|
||||
monaco_widget = cast(MonacoWidget, widget)
|
||||
|
||||
# Check if we have unsaved changes
|
||||
if widget.modified:
|
||||
if monaco_widget.modified:
|
||||
# Prompt the user to save changes
|
||||
response = QMessageBox.question(
|
||||
self,
|
||||
@@ -120,7 +146,7 @@ class MonacoDock(BECWidget, QWidget):
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if response == QMessageBox.StandardButton.Yes:
|
||||
self.save_file(widget)
|
||||
self.save_file(monaco_widget)
|
||||
elif response == QMessageBox.StandardButton.Cancel:
|
||||
return
|
||||
|
||||
@@ -128,14 +154,16 @@ class MonacoDock(BECWidget, QWidget):
|
||||
total = len(self.dock_manager.dockWidgets())
|
||||
if total <= 1:
|
||||
# Do not remove the last dock; just wipe its editor content
|
||||
if hasattr(widget, "set_text"):
|
||||
widget.set_text("")
|
||||
# Temporarily disable read-only mode if the editor is read-only
|
||||
# so we can clear the content for reuse
|
||||
monaco_widget.set_readonly(False)
|
||||
monaco_widget.set_text("")
|
||||
dock.setWindowTitle("Untitled")
|
||||
dock.setTabToolTip("Untitled")
|
||||
return
|
||||
|
||||
# Otherwise, proceed to close and delete the dock
|
||||
widget.close()
|
||||
monaco_widget.close()
|
||||
dock.closeDockWidget()
|
||||
dock.deleteDockWidget()
|
||||
if self.last_focused_editor is dock:
|
||||
@@ -278,8 +306,18 @@ class MonacoDock(BECWidget, QWidget):
|
||||
with open(file, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
widget._original_content = text
|
||||
|
||||
# Update the current_file before emitting save_enabled to ensure proper tracking
|
||||
widget._current_file = str(file)
|
||||
widget.save_enabled.emit(False)
|
||||
|
||||
# Find the dock widget containing this monaco widget and update title
|
||||
for dock in self.dock_manager.dockWidgets():
|
||||
if dock.widget() == widget:
|
||||
dock.setWindowTitle(file.name)
|
||||
dock.setTabToolTip(str(file))
|
||||
break
|
||||
|
||||
print(f"Save file called, last focused editor: {self.last_focused_editor}")
|
||||
|
||||
def set_vim_mode(self, enabled: bool):
|
||||
@@ -308,6 +346,41 @@ class MonacoDock(BECWidget, QWidget):
|
||||
return widget
|
||||
return None
|
||||
|
||||
def set_file_readonly(self, file_name: str, read_only: bool = True) -> bool:
|
||||
"""
|
||||
Set a specific file's editor to read-only mode.
|
||||
|
||||
Args:
|
||||
file_name (str): The file path to set read-only
|
||||
read_only (bool): Whether to set read-only mode (default: True)
|
||||
|
||||
Returns:
|
||||
bool: True if the file was found and read-only was set, False otherwise
|
||||
"""
|
||||
editor_dock = self._get_editor_dock(file_name)
|
||||
if editor_dock:
|
||||
editor_widget = cast(MonacoWidget, editor_dock.widget())
|
||||
editor_widget.set_readonly(read_only)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_file_icon(self, file_name: str, icon) -> bool:
|
||||
"""
|
||||
Set an icon for a specific file's tab.
|
||||
|
||||
Args:
|
||||
file_name (str): The file path to set icon for
|
||||
icon: The QIcon to set on the tab
|
||||
|
||||
Returns:
|
||||
bool: True if the file was found and icon was set, False otherwise
|
||||
"""
|
||||
editor_dock = self._get_editor_dock(file_name)
|
||||
if editor_dock:
|
||||
editor_dock.setIcon(icon)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import datetime
|
||||
import importlib
|
||||
import importlib.metadata
|
||||
import os
|
||||
import re
|
||||
|
||||
from bec_qthemes import material_icon
|
||||
from qtpy.QtCore import Signal
|
||||
from qtpy.QtWidgets import QInputDialog, QMessageBox, QVBoxLayout, QWidget
|
||||
|
||||
@@ -9,6 +12,7 @@ from bec_widgets.utils.bec_widget import BECWidget
|
||||
from bec_widgets.utils.error_popups import SafeProperty
|
||||
from bec_widgets.widgets.containers.explorer.collapsible_tree_section import CollapsibleSection
|
||||
from bec_widgets.widgets.containers.explorer.explorer import Explorer
|
||||
from bec_widgets.widgets.containers.explorer.macro_tree_widget import MacroTreeWidget
|
||||
from bec_widgets.widgets.containers.explorer.script_tree_widget import ScriptTreeWidget
|
||||
|
||||
|
||||
@@ -23,14 +27,14 @@ class IDEExplorer(BECWidget, QWidget):
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
super().__init__(parent=parent, **kwargs)
|
||||
self._sections = set()
|
||||
self._sections = [] # Use list to maintain order instead of set
|
||||
self.main_explorer = Explorer(parent=self)
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.addWidget(self.main_explorer)
|
||||
self.setLayout(layout)
|
||||
self.sections = ["scripts"]
|
||||
self.sections = ["scripts", "macros"]
|
||||
|
||||
@SafeProperty(list)
|
||||
def sections(self):
|
||||
@@ -39,10 +43,16 @@ class IDEExplorer(BECWidget, QWidget):
|
||||
@sections.setter
|
||||
def sections(self, value):
|
||||
existing_sections = set(self._sections)
|
||||
self._sections = set(value)
|
||||
self._update_section_visibility(self._sections - existing_sections)
|
||||
new_sections = set(value)
|
||||
# Find sections to add, maintaining the order from the input value list
|
||||
sections_to_add = [
|
||||
section for section in value if section in (new_sections - existing_sections)
|
||||
]
|
||||
self._sections = list(value) # Store as ordered list
|
||||
self._update_section_visibility(sections_to_add)
|
||||
|
||||
def _update_section_visibility(self, sections):
|
||||
# sections is now an ordered list, not a set
|
||||
for section in sections:
|
||||
self._add_section(section)
|
||||
|
||||
@@ -50,6 +60,8 @@ class IDEExplorer(BECWidget, QWidget):
|
||||
match section_name.lower():
|
||||
case "scripts":
|
||||
self.add_script_section()
|
||||
case "macros":
|
||||
self.add_macro_section()
|
||||
case _:
|
||||
pass
|
||||
|
||||
@@ -82,7 +94,8 @@ class IDEExplorer(BECWidget, QWidget):
|
||||
|
||||
if not plugin_scripts_dir or not os.path.exists(plugin_scripts_dir):
|
||||
return
|
||||
shared_script_section = CollapsibleSection(title="Shared", parent=self)
|
||||
shared_script_section = CollapsibleSection(title="Shared (Read-only)", parent=self)
|
||||
shared_script_section.setToolTip("Shared scripts (read-only)")
|
||||
shared_script_widget = ScriptTreeWidget(parent=self)
|
||||
shared_script_section.set_widget(shared_script_widget)
|
||||
shared_script_widget.set_directory(plugin_scripts_dir)
|
||||
@@ -93,6 +106,49 @@ class IDEExplorer(BECWidget, QWidget):
|
||||
# macros_section.set_widget(QLabel("Macros will be implemented later"))
|
||||
# self.main_explorer.add_section(macros_section)
|
||||
|
||||
def add_macro_section(self):
|
||||
section = CollapsibleSection(
|
||||
parent=self, title="MACROS", indentation=0, show_add_button=True
|
||||
)
|
||||
section.header_add_button.setIcon(material_icon("refresh", size=(20, 20)))
|
||||
section.header_add_button.setToolTip("Reload all macros")
|
||||
section.header_add_button.clicked.connect(self._reload_macros)
|
||||
|
||||
macro_explorer = Explorer(parent=self)
|
||||
macro_widget = MacroTreeWidget(parent=self)
|
||||
macro_widget.macro_open_requested.connect(self._emit_file_open_macros_local)
|
||||
macro_widget.macro_selected.connect(self._emit_file_preview_macros_local)
|
||||
local_macros_section = CollapsibleSection(title="Local", show_add_button=True, parent=self)
|
||||
local_macros_section.header_add_button.clicked.connect(self._add_local_macro)
|
||||
local_macros_section.set_widget(macro_widget)
|
||||
local_macro_dir = self.client._service_config.model.user_macros.base_path
|
||||
if not os.path.exists(local_macro_dir):
|
||||
os.makedirs(local_macro_dir)
|
||||
macro_widget.set_directory(local_macro_dir)
|
||||
macro_explorer.add_section(local_macros_section)
|
||||
|
||||
section.set_widget(macro_explorer)
|
||||
self.main_explorer.add_section(section)
|
||||
|
||||
plugin_macros_dir = None
|
||||
plugins = importlib.metadata.entry_points(group="bec")
|
||||
for plugin in plugins:
|
||||
if plugin.name == "plugin_bec":
|
||||
plugin = plugin.load()
|
||||
plugin_macros_dir = os.path.join(plugin.__path__[0], "macros")
|
||||
break
|
||||
|
||||
if not plugin_macros_dir or not os.path.exists(plugin_macros_dir):
|
||||
return
|
||||
shared_macro_section = CollapsibleSection(title="Shared (Read-only)", parent=self)
|
||||
shared_macro_section.setToolTip("Shared macros (read-only)")
|
||||
shared_macro_widget = MacroTreeWidget(parent=self)
|
||||
shared_macro_section.set_widget(shared_macro_widget)
|
||||
shared_macro_widget.set_directory(plugin_macros_dir)
|
||||
macro_explorer.add_section(shared_macro_section)
|
||||
shared_macro_widget.macro_open_requested.connect(self._emit_file_open_macros_shared)
|
||||
shared_macro_widget.macro_selected.connect(self._emit_file_preview_macros_shared)
|
||||
|
||||
def _emit_file_open_scripts_local(self, file_name: str):
|
||||
self.file_open_requested.emit(file_name, "scripts/local")
|
||||
|
||||
@@ -105,6 +161,18 @@ class IDEExplorer(BECWidget, QWidget):
|
||||
def _emit_file_preview_scripts_shared(self, file_name: str):
|
||||
self.file_preview_requested.emit(file_name, "scripts/shared")
|
||||
|
||||
def _emit_file_open_macros_local(self, function_name: str, file_path: str):
|
||||
self.file_open_requested.emit(file_path, "macros/local")
|
||||
|
||||
def _emit_file_preview_macros_local(self, function_name: str, file_path: str):
|
||||
self.file_preview_requested.emit(file_path, "macros/local")
|
||||
|
||||
def _emit_file_open_macros_shared(self, function_name: str, file_path: str):
|
||||
self.file_open_requested.emit(file_path, "macros/shared")
|
||||
|
||||
def _emit_file_preview_macros_shared(self, function_name: str, file_path: str):
|
||||
self.file_preview_requested.emit(file_path, "macros/shared")
|
||||
|
||||
def _add_local_script(self):
|
||||
"""Show a dialog to enter the name of a new script and create it."""
|
||||
|
||||
@@ -155,6 +223,98 @@ class IDEExplorer(BECWidget, QWidget):
|
||||
# Show error if file creation failed
|
||||
QMessageBox.critical(self, "Error", f"Failed to create script: {str(e)}")
|
||||
|
||||
def _add_local_macro(self):
|
||||
"""Show a dialog to enter the name of a new macro function and create it."""
|
||||
|
||||
target_section = self.main_explorer.get_section("MACROS")
|
||||
macro_dir_section = target_section.content_widget.get_section("Local")
|
||||
|
||||
local_macro_dir = macro_dir_section.content_widget.directory
|
||||
|
||||
# Prompt user for function name
|
||||
function_name, ok = QInputDialog.getText(self, "New Macro", f"Enter macro function name:")
|
||||
|
||||
if not ok or not function_name:
|
||||
return # User cancelled or didn't enter a name
|
||||
|
||||
# Sanitize function name
|
||||
function_name = re.sub(r"[^a-zA-Z0-9_]", "_", function_name)
|
||||
if not function_name or function_name[0].isdigit():
|
||||
QMessageBox.warning(
|
||||
self, "Invalid Name", "Function name must be a valid Python identifier."
|
||||
)
|
||||
return
|
||||
|
||||
# Create filename based on function name
|
||||
filename = f"{function_name}.py"
|
||||
file_path = os.path.join(local_macro_dir, filename)
|
||||
|
||||
# Check if file already exists
|
||||
if os.path.exists(file_path):
|
||||
response = QMessageBox.question(
|
||||
self,
|
||||
"File exists",
|
||||
f"The file '{filename}' already exists. Do you want to overwrite it?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
QMessageBox.StandardButton.No,
|
||||
)
|
||||
|
||||
if response != QMessageBox.StandardButton.Yes:
|
||||
return # User chose not to overwrite
|
||||
|
||||
try:
|
||||
# Create the file with a macro function template
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(
|
||||
f'''"""
|
||||
{function_name} macro - Created at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
||||
"""
|
||||
|
||||
|
||||
def {function_name}():
|
||||
"""
|
||||
Description of what this macro does.
|
||||
|
||||
Add your macro implementation here.
|
||||
"""
|
||||
print(f"Executing macro: {function_name}")
|
||||
# TODO: Add your macro code here
|
||||
pass
|
||||
'''
|
||||
)
|
||||
|
||||
# Refresh the macro tree to show the new function
|
||||
macro_dir_section.content_widget.refresh()
|
||||
|
||||
except Exception as e:
|
||||
# Show error if file creation failed
|
||||
QMessageBox.critical(self, "Error", f"Failed to create macro: {str(e)}")
|
||||
|
||||
def _reload_macros(self):
|
||||
"""Reload all macros using the BEC client."""
|
||||
try:
|
||||
if hasattr(self.client, "macros"):
|
||||
self.client.macros.load_all_user_macros()
|
||||
|
||||
# Refresh the macro tree widgets to show updated functions
|
||||
target_section = self.main_explorer.get_section("MACROS")
|
||||
if target_section and hasattr(target_section, "content_widget"):
|
||||
local_section = target_section.content_widget.get_section("Local")
|
||||
if local_section and hasattr(local_section, "content_widget"):
|
||||
local_section.content_widget.refresh()
|
||||
|
||||
shared_section = target_section.content_widget.get_section("Shared")
|
||||
if shared_section and hasattr(shared_section, "content_widget"):
|
||||
shared_section.content_widget.refresh()
|
||||
|
||||
QMessageBox.information(
|
||||
self, "Reload Macros", "Macros have been reloaded successfully."
|
||||
)
|
||||
else:
|
||||
QMessageBox.warning(self, "Reload Macros", "Macros functionality is not available.")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"Failed to reload macros: {str(e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from qtpy.QtWidgets import QApplication
|
||||
|
||||
Reference in New Issue
Block a user