Source code for simvx.editor.panels.inspector_sections._base

"""Inspector section registry -- extensible node-type-specific inspector sections.

Each InspectorSection subclass handles a specific node type or pattern.
The inspector queries the registry via ``get_sections_for_node(node)`` and
calls ``build_rows()`` to create the section widgets.

Sections are registered with the ``@register_inspector_section`` decorator.
"""

import logging
from typing import TYPE_CHECKING, Any

from simvx.core import (
    CallableCommand,
    Control,
    Node,
    Signal,
)
from simvx.core.ui.theme import em, get_theme

if TYPE_CHECKING:
    pass


__all__ = [
    "InspectorContext", "InspectorSection", "register_inspector_section",
    "get_sections_for_node",
    "_font_size", "_row_h", "_label_w", "_indent", "_padding",
    "_make_property_row", "_make_vector_row", "_make_resource_picker",
]
log = logging.getLogger(__name__)

def _font_size() -> float:
    """Get the current font size in logical pixels."""
    t = get_theme()
    return t.font_size if t.font_size >= 8 else 11.0

def _row_h() -> float:
    return em(2.18)

def _label_w() -> float:
    return em(7.27)

def _indent() -> float:
    return em(1.09)

def _padding() -> float:
    return em(0.55)

# ============================================================================
# Shared widget helpers (imported from inspector to avoid duplication)
# ============================================================================

def _make_property_row(label_text: str, widget: Control) -> Control:
    """Create a PropertyRow from the shared section widgets module."""
    from ..section_widgets import PropertyRow
    return PropertyRow(label_text, widget)

def _make_vector_row(label_text: str, components: int, values: tuple, **kwargs) -> Control:
    from ..section_widgets import VectorRow
    return VectorRow(label_text, components, values, **kwargs)

def _make_resource_picker(current_path: str | None = None, file_filter: str = "*.*") -> Control:
    from ..section_widgets import ResourcePicker
    return ResourcePicker(current_path=current_path, file_filter=file_filter)

# ============================================================================
# InspectorContext -- stable API for sections to interact with the inspector
# ============================================================================

[docs] class InspectorContext: """Helper passed to InspectorSection.build_rows(). Provides undo-aware property editing without exposing PropertiesPanel internals. """ def __init__(self, inspector): self._inspector = inspector
[docs] def on_property_changed(self, node: Node, prop: str, old_val: Any, new_val: Any): """Push a PropertyCommand through the undo stack.""" self._inspector._on_property_changed(node, prop, old_val, new_val)
[docs] def on_callable_command(self, do_fn, undo_fn, description: str): """Push a CallableCommand through the undo stack.""" state = self._inspector.state if state is not None: cmd = CallableCommand(do_fn, undo_fn, description=description) state.undo_stack.push(cmd) state.modified = True else: do_fn()
[docs] def on_material_prop_changed(self, node, prop: str, value: Any): """Handle material sub-object property change with undo.""" self._inspector._on_material_prop_changed(node, prop, value)
[docs] def on_material_colour_changed(self, node, new_colour: tuple): """Handle material colour change with undo.""" self._inspector._on_material_colour_changed(node, new_colour)
[docs] def on_material_texture_changed(self, node, attr: str, path: str | None): """Handle material texture URI change with undo.""" self._inspector._on_material_texture_changed(node, attr, path)
[docs] def rebuild(self): """Request full inspector rebuild.""" self._inspector._rebuild()
[docs] @property def editor_state(self): return self._inspector.state
[docs] def register_widget(self, key: str, widget: Control): """Register a widget in the inspector's _property_widgets dict.""" self._inspector._property_widgets[key] = widget
[docs] @property def property_changed_signal(self) -> Signal: return self._inspector.property_changed
# ============================================================================ # InspectorSection base class # ============================================================================
[docs] class InspectorSection: """Base class for node-type-specific inspector sections. Subclasses override ``can_handle()``, ``build_rows()``, and optionally ``handled_properties()`` to claim properties from the generic section. """ section_title: str = "Section" priority: int = 0 # Higher = appears later
[docs] def can_handle(self, node: Node) -> bool: raise NotImplementedError
[docs] def build_rows(self, node: Node, ctx: InspectorContext) -> list[Control]: raise NotImplementedError
[docs] def handled_properties(self, node: Node) -> set[str]: """Property names this section manages (excluded from generic section).""" return set()
# ============================================================================ # Section registry # ============================================================================ _SECTION_REGISTRY: list[type[InspectorSection]] = []
[docs] def register_inspector_section(cls: type[InspectorSection]) -> type[InspectorSection]: """Decorator to register an InspectorSection subclass.""" _SECTION_REGISTRY.append(cls) return cls
[docs] def get_sections_for_node(node: Node) -> list[InspectorSection]: """Return instantiated sections applicable to the given node, sorted by priority.""" sections = [cls() for cls in _SECTION_REGISTRY if cls().can_handle(node)] sections.sort(key=lambda s: s.priority) return sections