Source code for simvx.editor.rename_class_dialog
"""Modal dialog for renaming a user-defined class project-wide.
Wraps :func:`simvx.editor.project_classes.rename_class` with a small
text input + "Also rename file" checkbox. Visible only when the
selected node's class is user-defined (lives outside ``simvx.core``);
the entry points (scene-tree right-click + Inspector toolbar button)
gate visibility before the dialog is opened.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from simvx.core import (
Button,
CheckBox,
HBoxContainer,
Label,
Node,
Panel,
TextEdit,
VBoxContainer,
Vec2,
)
from .project_classes import RenameResult, rename_class
if TYPE_CHECKING:
from .project_classes import ProjectClassIndex
log = logging.getLogger(__name__)
[docs]
class RenameClassDialog(Panel):
"""Project-wide rename for a user-defined class."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size = Vec2(420, 180)
self.visible = False
body = VBoxContainer(name="Body")
body.size = Vec2(400, 160)
body.position = Vec2(10, 10)
body.gap = 8
self.add_child(body)
self._heading = Label(text="Rename class", name="Heading")
self._heading.font_size = 14
body.add_child(self._heading)
self._old_name_label = Label(text="", name="OldName")
body.add_child(self._old_name_label)
self._name_edit = TextEdit(name="NewName")
self._name_edit.size = Vec2(380, 28)
body.add_child(self._name_edit)
self._also_rename_file = CheckBox(text="Also rename file to match", name="RenameFile")
body.add_child(self._also_rename_file)
self._error_label = Label(text="", name="Error")
self._error_label.colour = (0.85, 0.30, 0.30, 1.0)
body.add_child(self._error_label)
actions = HBoxContainer(name="Actions")
actions.gap = 8
body.add_child(actions)
self._cancel_btn = Button(text="Cancel", name="Cancel")
self._cancel_btn.size = Vec2(80, 28)
self._cancel_btn.pressed.connect(self.dismiss)
actions.add_child(self._cancel_btn)
self._submit_btn = Button(text="Rename", name="Submit")
self._submit_btn.size = Vec2(100, 28)
self._submit_btn.pressed.connect(self._on_submit)
actions.add_child(self._submit_btn)
# Per-call wiring populated by show_for.
self._project_index: ProjectClassIndex | None = None
self._old_name: str = ""
self._on_renamed: list = []
# ------------------------------------------------------------------ public
[docs]
def show_for(
self,
node: Node,
*,
project_index: ProjectClassIndex,
on_renamed=None,
) -> None:
"""Open for ``node``'s class. ``on_renamed`` fires after a
successful rename with the :class:`RenameResult`."""
cls_name = type(node).__name__
self._old_name = cls_name
self._project_index = project_index
self._on_renamed = [on_renamed] if on_renamed is not None else []
self._old_name_label.text = f"Renaming class {cls_name!r}"
self._name_edit.text = cls_name
self._also_rename_file.checked = False
self._error_label.text = ""
self.visible = True
# Position centred-ish if a parent gives us screen size info; otherwise
# leave layout to the caller.
if self._tree:
self._tree.push_popup(self)
[docs]
def dismiss(self) -> None:
if not self.visible:
return
self.visible = False
if self._tree:
self._tree.pop_popup(self)
# ------------------------------------------------------------------ internals
def _on_submit(self) -> None:
new_name = self._name_edit.text.strip()
if not new_name:
self._error_label.text = "Class name cannot be empty"
return
if not new_name.isidentifier():
self._error_label.text = "Not a valid Python identifier"
return
if new_name == self._old_name:
self.dismiss()
return
if self._project_index is None:
self._error_label.text = "No project context"
return
try:
result: RenameResult = rename_class(
self._project_index,
self._old_name,
new_name,
rename_file=bool(self._also_rename_file.checked),
)
except ValueError as e:
self._error_label.text = str(e)
return
for cb in self._on_renamed:
try:
cb(result)
except Exception:
log.exception("rename_class on_renamed callback failed")
self.dismiss()