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