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 blocking overlay with a dim scrim. The router gates input to # this subtree; outside-click and Escape both fire ``cancel_requested``. self.set_anchor_preset(AnchorPreset.FULL_RECT) 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_overlay("blocking")
[docs] def hide(self): if self.visible: self.close_overlay() self.visible = False self._buttons.clear()
def _on_cancel(self): if self.visible: self.close_overlay() self.visible = False 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 / 16.0 # The "blocking" overlay's dim scrim is painted by the collector; 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