Source code for simvx.ide.widgets.confirm_dialog

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

import logging

from simvx.core import Signal, Vec2
from simvx.core.input import MouseButton
from simvx.core.ui.core import Control, UIInputEvent
from simvx.core.ui.enums import AnchorPreset
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


[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) # Full-screen modal with a dim backdrop. The router gates input to this # subtree; outside-click and Escape both fire ``cancel_requested``. self.set_anchor_preset(AnchorPreset.FULL_RECT) self.modal = True self.dismiss_on_outside_click = True self.pause_tree_when_modal = True self.top_level = True 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) self.cancel_requested.connect(self._on_cancel)
[docs] def show(self, title: str, message: str, buttons: list[tuple[str, str]] | None = None): """Show the dialog. Each button is ``(label, action_id)``.""" self._title = title self._message = message self._buttons = buttons or [("OK", "ok")] self._hovered_btn = -1 self.show_modal()
[docs] def hide(self): if self.visible: self.close_modal() self._buttons.clear()
def _on_cancel(self): if self.visible: self.close_modal() self.button_pressed.emit("cancel") def _on_gui_input(self, event: UIInputEvent): if not self.visible: 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) event.handled = True return # Track hover over buttons if event.position and event.button is None: self._hovered_btn = self._btn_index_at(event.position) # Click on button if event.button == MouseButton.LEFT 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) event.handled = True 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 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 / 14.0 # The auto-injected ``DimBackdrop`` already paints the overlay; we only # render the dialog box on top. dx = (sw - _WIDTH) / 2 dy = (sh - _HEIGHT) / 2 renderer.draw_rect((dx, dy), (_WIDTH, _HEIGHT), colour=theme.popup_bg, filled=True) renderer.draw_rect((dx, dy), (_WIDTH, _HEIGHT), colour=theme.border_light) # Title renderer.draw_text(self._title, (dx + _PAD, dy + _PAD), colour=theme.text, scale=scale) # Message msg_y = dy + _PAD + _FONT_SIZE * 1.6 renderer.draw_text( self._message, (dx + _PAD, msg_y), colour=theme.text_dim, scale=scale * 0.9 ) # 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_rect((bx, btn_y), (bw, _BTN_H), colour=bg, filled=True) renderer.draw_rect((bx, btn_y), (bw, _BTN_H), colour=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(label, (tx, ty), colour=theme.text, scale=scale * 0.9) bx -= _BTN_PAD