"""GotoLineDialog -- small floating dialog for line number navigation (Ctrl+G)."""
import logging
from typing import TYPE_CHECKING
from simvx.core import PointerFilter
from simvx.core.descriptors import Property
from simvx.core.input import Key, KeyEvent, TextEvent
from simvx.core.ui.core import Control, FocusMode
from simvx.core.ui.theme import get_theme
if TYPE_CHECKING:
from ..state import State
log = logging.getLogger(__name__)
_WIDTH = 300.0
_HEIGHT = 60.0
_PAD = 8.0
_FONT_SIZE = 14.0
_OVERLAY_BG = (0.0, 0.0, 0.0, 0.25)
[docs]
class GotoLineDialog(Control):
"""Small centered dialog for entering a line number.
Enter navigates to the line, Escape dismisses.
"""
pointer_filter = Property(PointerFilter.STOP, hint="How this control takes part in the pointer hit test")
top_level = Property(True, on_change="_invalidate_transform", hint="Placed on the screen, not by its parent")
visible = Property(
False,
coerce=bool,
hint="The goto-line prompt is up only between show() and its answer",
on_change="_on_visible_changed",
)
# Above every panel: a prompt is what the user is looking at.
z_index = Property(2000, range=(-4096, 4096), hint="Draw order (higher = on top)", on_change="_invalidate_z_cache")
# Zero because this dialog lays itself out against the parent rect in
# ``on_draw`` and never reads its own size.
size_x = Property(0.0, range=(0, 10000), hint="Control width", on_change="_on_size_changed")
size_y = Property(0.0, range=(0, 10000), hint="Control height", on_change="_on_size_changed")
# The prompt takes the keyboard while it is up: it focuses itself from
# show(), is never a Tab stop, and takes typing.
focus_mode = Property(FocusMode.CLICK, hint="Whether this control can take keyboard focus")
[docs]
@property
def wants_text_input(self) -> bool:
return True
def __init__(self, state: State, **kwargs):
super().__init__(**kwargs)
self._state = state
self._input: str = ""
self._max_line: int = 1
self._cursor_blink: float = 0.0
[docs]
def show(self, max_line: int = 1):
self.visible = True
self._input = ""
self._max_line = max(max_line, 1)
self._cursor_blink = 0.0
self.grab_focus()
[docs]
def hide(self):
self.visible = False
self._input = ""
self.release_focus()
def _navigate(self):
if not self._input:
self.hide()
return
try:
line = int(self._input)
except ValueError:
self.hide()
return
line = max(1, min(line, self._max_line)) - 1 # Convert to 0-indexed
path = self._state.active_file
self.hide()
self._state.goto(path, line, 0)
[docs]
def on_update(self, dt: float):
if self.visible:
self._cursor_blink += dt
if self._cursor_blink > 1.0:
self._cursor_blink = 0.0
[docs]
def on_draw(self, renderer):
if not self.visible:
return
theme = get_theme()
ss = self._get_parent_size()
sw, sh = ss.x, ss.y
scale = _FONT_SIZE / 16.0
# Semi-transparent overlay
renderer.rect((0, 0), (sw, sh), fill=_OVERLAY_BG)
# Dialog centered
dx = (sw - _WIDTH) / 2
dy = (sh - _HEIGHT) / 2
# Background + border
renderer.rect((dx, dy), (_WIDTH, _HEIGHT), fill=theme.popup_bg)
renderer.rect((dx, dy), (_WIDTH, _HEIGHT), stroke=theme.accent)
# Label
label = f"Go to Line (1-{self._max_line}):"
label_y = dy + 6
renderer.text(label, (dx + _PAD, label_y), fill=theme.text_dim, scale=scale * 0.85)
# Input field
input_y = dy + 26
input_h = 24
renderer.rect((dx + _PAD, input_y), (_WIDTH - _PAD * 2, input_h), fill=theme.bg_input)
renderer.rect((dx + _PAD, input_y), (_WIDTH - _PAD * 2, input_h), stroke=theme.accent)
# Input text
text_y = input_y + (input_h - _FONT_SIZE) / 2
renderer.text(self._input, (dx + _PAD + 4, text_y), fill=theme.text, scale=scale)
# Cursor
if self._cursor_blink < 0.5:
cx = dx + _PAD + 4 + renderer.text_width(self._input, scale)
renderer.line((cx, input_y + 4), (cx, input_y + input_h - 4), stroke=theme.text)