"""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