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

"""Toolbar, dialog, and menu interaction handlers."""

import logging

from simvx.core import Node
from simvx.core.scripted_demo import DemoRunner

from .._helpers import (
    _drive_click,
    _drive_type_chars,
    _get_editor,
    _init_click,
    _resolve_root_type,
    _walk_children,
)
from ..steps import (
    ClickAddButton,
    ClickBottomTab,
    ClickInspectorButton,
    ClickMenu,
    ClickToolbar,
    DialogSelect,
    NewSceneSelect,
)

log = logging.getLogger(__name__)


def _handle_click_add_button(runner: DemoRunner, step: ClickAddButton, dt: float):
    runner._action_desc = "Click: + (Add Node)"
    editor = _get_editor(runner)
    if step._phase == 0:
        if not editor or not editor.scene_tree_panel:
            runner._advance()
            return
        btn = editor.scene_tree_panel._add_btn
        gx, gy, gw, gh = btn.get_global_rect()
        _init_click(step, runner, (gx + gw / 2, gy + gh / 2))
        step._phase = 1
    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            runner._advance()

def _handle_dialog_select(runner: DemoRunner, step: DialogSelect, dt: float):
    runner._action_desc = f"Select: {step.type_name}"
    editor = _get_editor(runner)
    if step._phase == 0:
        # Type filter chars into the dialog's focused filter field
        step._mc_t = getattr(step, "_mc_t", 0.0)
        if _drive_type_chars(runner, step, dt, step.type_name):
            step._phase = 1
            step._mc_t = 0.0
    elif step._phase == 1:
        # Wait for filter to apply
        step._mc_t += dt
        if step._mc_t >= 0.05:
            if not editor or not editor.scene_tree_panel:
                runner._advance()
                return
            dialog = editor.scene_tree_panel._add_dialog
            if not dialog.visible:
                runner._advance()
                return
            gx, gy, _, _ = dialog.get_global_rect()
            row_y = dialog.type_row_y(step.type_name)
            if row_y is not None:
                _init_click(step, runner, (gx + dialog.DIALOG_WIDTH / 2, row_y + dialog.ROW_HEIGHT / 2))
                step._phase = 2
                return
            runner._advance()  # type not found in filtered list
    elif step._phase == 2:
        if _drive_click(runner, step, dt):
            runner._advance()

def _handle_click_bottom_tab(runner: DemoRunner, step: ClickBottomTab, dt: float):
    """Click a tab in the editor's bottom TabContainer by name."""
    runner._action_desc = f"Click bottom tab: {step.name}"
    editor = _get_editor(runner)
    if step._phase == 0:
        tabs = getattr(editor, "_bottom_tabs", None) if editor else None
        if not tabs:
            raise ValueError("ClickBottomTab: editor has no _bottom_tabs container")
        from simvx.core import Control

        tab_controls = [c for c in tabs.children if isinstance(c, Control)]
        idx = next((i for i, c in enumerate(tab_controls) if c.name == step.name), -1)
        if idx < 0:
            available = ", ".join(c.name for c in tab_controls) or "<empty>"
            raise ValueError(
                f"ClickBottomTab: no tab named {step.name!r}. Available: {available}"
            )
        gx, gy, gw, _gh = tabs.get_global_rect()
        plus_w = 28.0 if tabs.show_new_tab_button else 0.0
        usable_w = gw - plus_w
        tab_width = usable_w / max(len(tab_controls), 1)
        cx = gx + idx * tab_width + tab_width / 2
        cy = gy + tabs.tab_height / 2
        _init_click(step, runner, (cx, cy))
        step._phase = 1
    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            runner._advance()

def _handle_click_toolbar(runner: DemoRunner, step: ClickToolbar, dt: float):
    runner._action_desc = f"Click: {step.name}"
    editor = _get_editor(runner)
    if step._phase == 0:
        if not editor:
            runner._advance()
            return
        play_controls = getattr(editor, "_play_controls", None)
        btn_map = {
            "Play": getattr(play_controls, "play_btn", None),
            "Stop": getattr(play_controls, "stop_btn", None),
            "Pause": getattr(play_controls, "pause_btn", None),
            "3D": getattr(editor, "_btn_3d", None),
            "2D": getattr(editor, "_btn_2d", None),
            "Code": getattr(editor, "_btn_code", None),
        }
        btn = btn_map.get(step.name)
        if not btn:
            runner._advance()
            return
        gx, gy, gw, gh = btn.get_global_rect()
        _init_click(step, runner, (gx + gw / 2, gy + gh / 2))
        step._phase = 1
    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            runner._advance()

def _handle_click_inspector_button(runner: DemoRunner, step: ClickInspectorButton, dt: float):
    runner._action_desc = f"Click: {step.label}"
    editor = _get_editor(runner)
    if step._phase == 0:
        if not editor or not editor.inspector_panel:
            raise ValueError("ClickInspectorButton: no inspector panel")
        from simvx.core import Button as _Button

        btn = None
        for child in _walk_children(editor.inspector_panel):
            if isinstance(child, _Button) and child.text == step.label:
                btn = child
                break
        if not btn:
            raise ValueError(f"ClickInspectorButton: button '{step.label}' not found in inspector")
        gx, gy, gw, gh = btn.get_global_rect()
        _init_click(step, runner, (gx + gw / 2, gy + gh / 2))
        step._phase = 1
    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            runner._advance()

def _handle_click_menu(runner: DemoRunner, step: ClickMenu, dt: float):
    runner._action_desc = f"Menu: {step.menu_name} > {step.item_text}"
    editor = _get_editor(runner)
    if step._phase == 0:
        # Find menu label rect on MenuBar
        if not editor or not editor._menu_bar:
            runner._advance()
            return
        bar = editor._menu_bar
        rects = bar._menu_rects()
        for i, (name, _popup) in enumerate(bar.menus):
            if name == step.menu_name:
                lx, ly, lw, lh = rects[i]
                _init_click(step, runner, (lx + lw / 2, ly + lh / 2))
                step._phase = 1
                return
        runner._advance()  # menu not found
    elif step._phase == 1:
        # Click to open menu
        if _drive_click(runner, step, dt):
            step._phase = 2
            step._mc_t = 0.0
    elif step._phase == 2:
        # Find item in the open popup
        step._mc_t += dt
        if step._mc_t < 0.05:
            return  # brief wait for popup
        editor = _get_editor(runner)
        if not editor or not editor._menu_bar:
            runner._advance()
            return
        bar = editor._menu_bar
        if bar._open_index < 0:
            runner._advance()
            return
        _name, popup = bar.menus[bar._open_index]
        gx, gy, _gw, _gh = popup.get_global_rect()
        for i, item in enumerate(popup.items):
            if item.text == step.item_text:
                row_y = gy + i * 28 + 14  # approximate row center
                _init_click(step, runner, (gx + popup.size.x / 2, row_y))
                step._phase = 3
                return
        # Item not found, close menu and advance
        bar._close_all()
        runner._advance()
    elif step._phase == 3:
        # Click the menu item
        if _drive_click(runner, step, dt):
            runner._advance()

def _handle_new_scene_select(runner: DemoRunner, step: NewSceneSelect, dt: float):
    runner._action_desc = f"NewScene: {step.root_type}"
    editor = _get_editor(runner)
    if step._phase == 0:
        # Wait for NewSceneDialog to appear
        if not editor:
            runner._advance()
            return
        dialog = getattr(editor, "_new_scene_dialog", None)
        if not dialog or not dialog.visible:
            # In headless mode, the dialog may not exist yet, call new_scene directly
            state = editor.state
            type_map = {
                "3D Scene (Default)": (_resolve_root_type("Node3D"), True),
                "3D Scene": (_resolve_root_type("Node3D"), False),
                "2D Scene": (_resolve_root_type("Node2D"), False),
                "UI Scene": (_resolve_root_type("Control"), False),
                "Empty": (Node, False),
            }
            rt, pop = type_map.get(step.root_type, (Node, False))
            state.new_scene(root_type=rt, populate=pop)
            runner._advance()
            return
        # Find the matching row button
        inner = dialog._dialog_panel
        if not inner:
            runner._advance()
            return
        from simvx.core import Button as _Button

        for child in inner.children:
            if isinstance(child, _Button) and child.text == step.root_type:
                gx, gy, gw, gh = child.get_global_rect()
                _init_click(step, runner, (gx + gw / 2, gy + gh / 2))
                step._phase = 1
                return
        runner._advance()
    elif step._phase == 1:
        if _drive_click(runner, step, dt):
            runner._advance()