"""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) --
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