Source code for simvx.editor.panels.inspector_script
"""Inspector script section -- script attachment and embedding UI.
Provides methods for building the script section of the inspector:
attach, detach, embed, and open script for the selected node.
These are mixed into ``InspectorPanel`` via ``ScriptSectionMixin``.
"""
from __future__ import annotations
from simvx.core import (
Button,
HBoxContainer,
Label,
Node,
SizingMode,
)
from simvx.core.ui.theme import em, get_theme
def _row_h() -> float:
return em(2.18)
def _font_size() -> float:
return get_theme().font_size
[docs]
class ScriptSectionMixin:
"""Mixin that adds script section methods to InspectorPanel.
Expects the host class to provide:
- ``self.state``: EditorState or None
- ``self.add_child(child)``: add a child control
- ``self._rebuild()``: full inspector rebuild
"""
def _add_script_section(self, node: Node):
"""Add script attachment section below the header."""
if node.script:
# File-backed script -- show path + Open / Detach buttons
row = HBoxContainer()
row.size = self._script_row_size()
row.sizing = SizingMode.EXPAND
row.separation = 4.0
path_label = Label(node.script)
path_label.text_colour = (0.6, 0.8, 1.0, 1.0)
path_label.font_size = _font_size()
path_label.stretch_ratio = 4.0
row.add_child(path_label)
open_btn = Button("Open")
open_btn.font_size = _font_size()
open_btn.stretch_ratio = 1.0
open_btn.pressed.connect(lambda: self._on_open_script(node))
row.add_child(open_btn)
detach_btn = Button("Detach")
detach_btn.font_size = _font_size()
detach_btn.stretch_ratio = 1.0
detach_btn.pressed.connect(lambda: self._on_detach_script(node))
row.add_child(detach_btn)
self.add_child(row)
elif getattr(node, "_script_embedded", None):
# Embedded script -- show Open and Detach
row = HBoxContainer()
row.sizing = SizingMode.EXPAND
row.separation = 4
embed_label = Label(f"{node.name} (embedded)")
embed_label.text_colour = (0.6, 0.9, 0.6, 1.0)
embed_label.font_size = _font_size()
embed_label.stretch_ratio = 4.0
row.add_child(embed_label)
open_btn = Button("Open")
open_btn.font_size = _font_size()
open_btn.stretch_ratio = 1.0
open_btn.pressed.connect(lambda: self._on_open_script(node))
row.add_child(open_btn)
detach_btn = Button("Detach")
detach_btn.font_size = _font_size()
detach_btn.stretch_ratio = 1.0
detach_btn.pressed.connect(lambda: self._on_detach_embedded(node))
row.add_child(detach_btn)
self.add_child(row)
else:
# No script -- show Attach and Embed buttons
row = HBoxContainer()
row.sizing = SizingMode.FILL
row.separation = 4
attach_btn = Button("Attach Script")
attach_btn.font_size = _font_size()
attach_btn.pressed.connect(lambda: self._on_attach_script(node))
row.add_child(attach_btn)
self._attach_script_btn = attach_btn
embed_btn = Button("Embed Script")
embed_btn.font_size = _font_size()
embed_btn.pressed.connect(lambda: self._on_embed_script(node))
row.add_child(embed_btn)
self.add_child(row)
def _script_row_size(self):
from simvx.core import Vec2
return Vec2(self.size.x, _row_h())
def _on_open_script(self, node: Node):
"""Open the node's script in a workspace tab."""
if self.state:
self.state.selection.select(node)
self.state.workspace.open_script(
node, project_path_fn=lambda: self.state.project_path
)
def _on_detach_script(self, node: Node):
"""Detach the script from the node."""
if self.state:
self.state.detach_script(node)
self._rebuild()
def _on_attach_script(self, node: Node):
"""Create and attach a new file-backed script to the node."""
if not self.state:
return
template_name, class_name = self._resolve_template(node)
# snake_case for file
snake = ""
for i, ch in enumerate(node.name):
if ch.isupper() and i > 0 and node.name[i - 1].islower():
snake += "_"
snake += ch.lower()
snake = snake.replace(" ", "_")
rel_path = f"scripts/{snake}.py"
abs_path = self.state.create_script(node, template_name, class_name, rel_path)
if abs_path:
self._rebuild()
self._on_open_script(node)
def _on_embed_script(self, node: Node):
"""Create and embed a script directly in the node."""
if not self.state:
return
template_name, class_name = self._resolve_template(node)
from simvx.editor.templates import generate_script_text
source = generate_script_text(template_name, class_name)
node._script_embedded = source
self.state.modified = True
self.state.script_changed.emit()
self._rebuild()
self._on_open_script(node)
def _on_detach_embedded(self, node: Node):
"""Detach an embedded script from the node."""
node._script_embedded = None
if self.state:
self.state.modified = True
self.state.script_changed.emit()
self._rebuild()
def _resolve_template(self, node: Node) -> tuple[str, str]:
"""Determine the best template name and class name for a node."""
from simvx.editor.templates import TEMPLATES
template_name = "Node"
for name in (type(node).__name__, *[b.__name__ for b in type(node).__mro__[1:]]):
if name in TEMPLATES:
template_name = name
break
class_name = node.name.replace(" ", "").replace("_", "")
if not class_name[0:1].isupper():
class_name = class_name.capitalize()
return template_name, class_name