Source code for simvx.ide.widgets.confirm_dialog

"""ConfirmDialog -- modal dialog with message and action buttons."""


from __future__ import annotations

import logging

from simvx.core import Signal, Vec2
from simvx.core.ui.core import Control, UIInputEvent
from simvx.core.ui.theme import get_theme

log = logging.getLogger(__name__)

_WIDTH = 420.0
_HEIGHT = 120.0
_PAD = 12.0
_FONT_SIZE = 14.0
_BTN_H = 28.0
_BTN_PAD = 8.0

_OVERLAY_BG = (0.0, 0.0, 0.0, 0.4)


[docs] class ConfirmDialog(Control): """Modal dialog with a message and up to 3 buttons. Usage:: dlg = ConfirmDialog() dlg.show("Unsaved changes", "Save changes before closing?", buttons=[("Save", "save"), ("Don't Save", "discard"), ("Cancel", "cancel")]) dlg.button_pressed.connect(lambda action: print(action)) """ def __init__(self, **kwargs): super().__init__(**kwargs) self.visible = False self.z_index = 3000 self.button_pressed = Signal() self._title: str = "" self._message: str = "" self._buttons: list[tuple[str, str]] = [] # (label, action_id) self._hovered_btn: int = -1 self.size = Vec2(0, 0)
[docs] def show(self, title: str, message: str, buttons: list[tuple[str, str]] | None = None): """Show the dialog with given title, message, and buttons. Each button is (label, action_id). The action_id is emitted via button_pressed. """ self._title = title self._message = message self._buttons = buttons or [("OK", "ok")] self._hovered_btn = -1 self.visible = True self.set_focus() if self._tree: self._tree.push_popup(self)
[docs] def hide(self): was_visible = self.visible self.visible = False self._buttons.clear() self.release_focus() if was_visible and self._tree: self._tree.pop_popup(self)
# -- Popup overlay API (required for SceneTree popup system) --
[docs] def is_popup_point_inside(self, point) -> bool: """Any click while dialog is visible is inside the popup (modal).""" return self.visible
[docs] def popup_input(self, event): """Route click events from the popup system to our handler.""" self._on_gui_input(event)
[docs] def dismiss_popup(self): """Dismiss from popup system = cancel.""" if self.visible: self.hide() self.button_pressed.emit("cancel")
def _on_gui_input(self, event: UIInputEvent): if not self.visible: return if event.key == "escape" and event.pressed: self.hide() self.button_pressed.emit("cancel") return if event.key == "enter" and event.pressed: # Enter triggers the first (primary) button if self._buttons: action = self._buttons[0][1] self.hide() self.button_pressed.emit(action) return # Track hover over buttons if event.position and event.button == 0: self._hovered_btn = self._btn_index_at(event.position) # Click on button if event.button == 1 and event.pressed and event.position: idx = self._btn_index_at(event.position) if idx >= 0: action = self._buttons[idx][1] self.hide() self.button_pressed.emit(action) def _btn_index_at(self, pos) -> int: """Return button index under the position, or -1.""" ss = self._get_parent_size() sw, sh = ss.x, ss.y dx = (sw - _WIDTH) / 2 dy = (sh - _HEIGHT) / 2 px = pos.x if hasattr(pos, 'x') else pos[0] py = pos.y if hasattr(pos, 'y') else pos[1] btn_y = dy + _HEIGHT - _BTN_H - _PAD if not (btn_y <= py <= btn_y + _BTN_H): return -1 # Buttons are right-aligned bx = dx + _WIDTH - _PAD for i in range(len(self._buttons) - 1, -1, -1): label = self._buttons[i][0] bw = max(80.0, len(label) * _FONT_SIZE * 0.6 + 24) bx -= bw if bx <= px <= bx + bw: return i bx -= _BTN_PAD return -1
[docs] def 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 / 14.0 # Overlay renderer.draw_filled_rect(0, 0, sw, sh, _OVERLAY_BG) # Dialog box dx = (sw - _WIDTH) / 2 dy = (sh - _HEIGHT) / 2 renderer.draw_filled_rect(dx, dy, _WIDTH, _HEIGHT, theme.popup_bg) renderer.draw_rect_coloured(dx, dy, _WIDTH, _HEIGHT, theme.border_light) # Title renderer.draw_text_coloured(self._title, dx + _PAD, dy + _PAD, scale, theme.text) # Message msg_y = dy + _PAD + _FONT_SIZE * 1.6 renderer.draw_text_coloured(self._message, dx + _PAD, msg_y, scale * 0.9, theme.text_dim) # Buttons (right-aligned, first button is "primary") btn_y = dy + _HEIGHT - _BTN_H - _PAD bx = dx + _WIDTH - _PAD for i in range(len(self._buttons) - 1, -1, -1): label = self._buttons[i][0] bw = max(80.0, len(label) * _FONT_SIZE * 0.6 + 24) bx -= bw # Button background if i == 0: bg = theme.accent # primary action elif self._buttons[i][1] == "discard": bg = theme.btn_danger elif i == self._hovered_btn: bg = theme.btn_hover else: bg = theme.btn_bg if i == self._hovered_btn and i != 0: bg = theme.btn_hover renderer.draw_filled_rect(bx, btn_y, bw, _BTN_H, bg) renderer.draw_rect_coloured(bx, btn_y, bw, _BTN_H, theme.border_light) # Button label (centered) tw = renderer.text_width(label, scale * 0.9) tx = bx + (bw - tw) / 2 ty = btn_y + (_BTN_H - _FONT_SIZE) / 2 renderer.draw_text_coloured(label, tx, ty, scale * 0.9, theme.text) bx -= _BTN_PAD