Source code for simvx.editor.testing.demo_steps.handlers.inspector

"""Inspector widget interaction handlers (rename, edit, set_colour, set_anchor_preset)."""

import logging

from simvx.core.scripted_demo import DemoRunner

from .._helpers import (
    _colour_to_hex,
    _drive_backspace_clear,
    _drive_click,
    _drive_type_chars,
    _find_prop_widget,
    _get_editor,
    _init_click,
    _send_key,
)
from ..steps import (
    InspectorEdit,
    InspectorRename,
    InspectorSetAnchorPreset,
    InspectorSetColour,
)

log = logging.getLogger(__name__)


def _handle_inspector_rename(runner: DemoRunner, step: InspectorRename, dt: float):
    runner._action_desc = f"Rename: {step.new_name}"
    editor = _get_editor(runner)
    if step._phase == 0:
        if not editor or not editor.inspector_panel or not editor.inspector_panel._name_edit:
            runner._advance()
            return
        edit = editor.inspector_panel._name_edit
        gx, gy, gw, gh = edit.get_global_rect()
        _init_click(step, runner, (gx + gw / 2, gy + gh / 2))
        step._edit_ref = edit
        step._phase = 1
    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            _send_key(runner, "end")
            step._clear_count = len(step._edit_ref.text)
            step._cleared = 0
            step._mc_t = 0.0
            step._phase = 2
    elif step._phase == 2:
        if _drive_backspace_clear(runner, step, dt):
            step._char_idx = 0
            step._mc_t = 0.0
            step._phase = 3
    elif step._phase == 3:
        if _drive_type_chars(runner, step, dt, step.new_name):
            step._phase = 4
    elif step._phase == 4:
        _send_key(runner, "enter")
        if editor and editor.scene_tree_panel:
            editor.scene_tree_panel._rebuild_tree()
        runner._advance()

def _handle_inspector_edit(runner: DemoRunner, step: InspectorEdit, dt: float):
    runner._action_desc = f"Set: {step.prop} = {step.value}"
    editor = _get_editor(runner)
    if step._phase == 0:
        if not editor or not editor.inspector_panel:
            runner._advance()
            return
        widget = _find_prop_widget(editor.inspector_panel, step.prop, step.component)
        if not widget:
            # In visual mode the inspector rebuild can lag several frames
            retries = getattr(step, "_retry_count", 0)
            if retries < 30:
                step._retry_count = retries + 1
                return
            runner._advance()
            return
        step._widget_ref = widget
        gx, gy, gw, gh = widget.get_global_rect()

        # Click centre of the widget (SpinBox value area, or TextEdit body)
        target = (gx + gw / 2, gy + gh / 2)

        _init_click(step, runner, target)
        step._phase = 1

    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            from simvx.core.ui.advanced import SpinBox as _SpinBox
            from simvx.core.ui.widgets import TextEdit as _TextEdit

            widget = step._widget_ref
            if isinstance(widget, _SpinBox):
                # Ensure SpinBox entered editing mode (click routing can
                # miss due to UI clipping or overlapping widgets)
                if not widget._editing:
                    widget._enter_edit()
                step._char_idx = 0
                step._mc_t = 0.0
                step._phase = 3
            elif isinstance(widget, _TextEdit):
                _send_key(runner, "end")
                step._clear_count = len(widget.text)
                step._cleared = 0
                step._mc_t = 0.0
                step._phase = 2
            else:
                runner._advance()

    elif step._phase == 2:  # clear TextEdit via backspace
        if _drive_backspace_clear(runner, step, dt):
            val_str = str(step.value)
            if not val_str:
                step._phase = 4
            else:
                step._char_idx = 0
                step._mc_t = 0.0
                step._phase = 3

    elif step._phase == 3:  # type value
        if _drive_type_chars(runner, step, dt, str(step.value)):
            from simvx.core.ui.advanced import SpinBox as _SpinBox

            widget = step._widget_ref
            if isinstance(widget, _SpinBox):
                # Ensure the input text matches the intended value,
                # character-by-character typing can lose characters if
                # the widget momentarily loses focus between frames.
                widget._input_text = str(step.value)
            step._phase = 4
            step._mc_t = 0.0

    elif step._phase == 4:  # enter to submit
        _send_key(runner, "enter")
        runner._advance()

def _handle_inspector_set_colour(runner: DemoRunner, step: InspectorSetColour, dt: float):
    runner._action_desc = f"Colour: {step.prop}"
    editor = _get_editor(runner)
    if step._phase == 0:
        # Find the ColourPicker widget in inspector. In visual mode the
        # inspector rebuild may lag a frame behind the step, retry once
        # on the next frame before failing.
        if not editor or not editor.inspector_panel:
            raise ValueError(f"InspectorSetColour: no inspector panel for {step.prop}")
        widget = editor.inspector_panel._property_widgets.get(step.prop)
        if widget is None:
            widget = editor.inspector_panel._property_widgets.get(f"mat_{step.prop}")
        if not widget:
            # In visual mode the inspector rebuild can lag several frames
            retries = getattr(step, "_retry_count", 0)
            if retries < 30:
                step._retry_count = retries + 1
                return
            raise ValueError(f"InspectorSetColour: property widget not found: {step.prop}")
        from simvx.core import ColourPicker

        if not isinstance(widget, ColourPicker):
            raise ValueError(f"InspectorSetColour: widget for {step.prop} is not a ColourPicker")
        step._widget_ref = widget
        # Click the hex input area
        hx, hy, hw, hh = widget._hex_rect()
        _init_click(step, runner, (hx + hw / 2, hy + hh / 2))
        step._phase = 1

    elif step._phase == 1:
        # Drive click on hex rect → activates hex editing mode
        if _drive_click(runner, step, dt):
            step._clear_count = len(step._widget_ref._hex_text)
            step._cleared = 0
            step._mc_t = 0.0
            step._phase = 2

    elif step._phase == 2:
        # Clear existing hex text via backspace
        if _drive_backspace_clear(runner, step, dt):
            step._char_idx = 0
            step._mc_t = 0.0
            step._phase = 3

    elif step._phase == 3:
        # Type hex colour value char by char
        if _drive_type_chars(runner, step, dt, _colour_to_hex(step.colour)):
            step._phase = 4

    elif step._phase == 4:
        # Press enter to commit the hex value
        _send_key(runner, "enter")
        runner._advance()

def _handle_inspector_set_anchor_preset(
    runner: DemoRunner, step: InspectorSetAnchorPreset, dt: float,
):
    """Click the named preset button in the inspector's AnchorPresetButton."""
    runner._action_desc = f"Anchor preset: {step.preset}"
    editor = _get_editor(runner)
    from simvx.core.ui.enums import AnchorPreset

    from ....panels.anchor_preset_widget import AnchorPresetButton

    if step._phase == 0:
        if not editor or not editor.inspector_panel:
            raise ValueError("InspectorSetAnchorPreset: no inspector panel")
        widget = editor.inspector_panel._property_widgets.get("__anchor_preset")
        if widget is None:
            if step._retry_count < 30:
                step._retry_count += 1
                return
            raise ValueError("InspectorSetAnchorPreset: no AnchorPresetButton in inspector "
                             "(selected node is not a Control with Layout properties)")
        if not isinstance(widget, AnchorPresetButton):
            raise ValueError("InspectorSetAnchorPreset: widget is not an AnchorPresetButton")
        try:
            preset = AnchorPreset[step.preset]
        except KeyError as exc:
            raise ValueError(f"InspectorSetAnchorPreset: unknown preset '{step.preset}'") from exc
        btn = widget._preset_buttons.get(preset)
        if btn is None:
            raise ValueError(f"InspectorSetAnchorPreset: preset button missing for {step.preset}")
        bx, by, bw, bh = btn.get_global_rect()
        if bw <= 0 or bh <= 0:
            if step._retry_count < 30:
                step._retry_count += 1
                return
            raise ValueError("InspectorSetAnchorPreset: preset button not laid out")
        step._widget_ref = widget
        _init_click(step, runner, (bx + bw / 2, by + bh / 2))
        step._phase = 1

    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            runner._advance()