# McRogueFace Cookbook - Modal Widget """ Overlay popup that blocks background input. Example: from lib.modal import Modal def on_confirm(): print("Confirmed!") modal.hide() modal = Modal( title="Confirm Action", message="Are you sure you want to proceed?", buttons=[ ("Cancel", modal.hide), ("Confirm", on_confirm) ] ) modal.show(scene) """ import mcrfpy class Modal: """Overlay popup that blocks background input. Args: title: Modal title text message: Modal body message (optional) content_frame: Custom content frame (overrides message) buttons: List of (label, callback) tuples width: Modal width (default: 400) height: Modal height (default: auto-calculated) overlay_color: Semi-transparent overlay color bg_color: Modal background color title_color: Title text color message_color: Message text color button_spacing: Space between buttons Attributes: overlay: The overlay frame (add to scene to show) modal_frame: The modal content frame visible: Whether modal is currently visible """ DEFAULT_OVERLAY = mcrfpy.Color(0, 0, 0, 180) DEFAULT_BG = mcrfpy.Color(40, 40, 50) DEFAULT_TITLE_COLOR = mcrfpy.Color(255, 255, 255) DEFAULT_MESSAGE_COLOR = mcrfpy.Color(200, 200, 200) DEFAULT_BUTTON_BG = mcrfpy.Color(60, 60, 75) DEFAULT_BUTTON_HOVER = mcrfpy.Color(80, 80, 100) def __init__(self, title, message=None, content_frame=None, buttons=None, width=400, height=None, overlay_color=None, bg_color=None, title_color=None, message_color=None, button_spacing=10): self.title = title self.message = message self.width = width self.button_spacing = button_spacing self._on_close = None # Colors self.overlay_color = overlay_color or self.DEFAULT_OVERLAY self.bg_color = bg_color or self.DEFAULT_BG self.title_color = title_color or self.DEFAULT_TITLE_COLOR self.message_color = message_color or self.DEFAULT_MESSAGE_COLOR # State self.visible = False self._scene = None self._buttons = buttons or [] # Calculate height if not specified if height is None: height = 60 # Title if message: # Rough estimate of message height lines = len(message) // 40 + message.count('\n') + 1 height += lines * 20 + 20 if content_frame: height += 150 # Default content height if buttons: height += 50 # Button row height = max(150, height) self.height = height # Create overlay (fullscreen semi-transparent) self.overlay = mcrfpy.Frame( pos=(0, 0), size=(1024, 768), # Will be adjusted on show fill_color=self.overlay_color, outline=0 ) # Block clicks on overlay from reaching elements behind self.overlay.on_click = self._on_overlay_click # Create modal frame (centered) modal_x = (1024 - width) // 2 modal_y = (768 - height) // 2 self.modal_frame = mcrfpy.Frame( pos=(modal_x, modal_y), size=(width, height), fill_color=self.bg_color, outline_color=mcrfpy.Color(100, 100, 120), outline=2 ) self.overlay.children.append(self.modal_frame) # Add title self._title_caption = mcrfpy.Caption( text=title, pos=(width // 2, 15), fill_color=self.title_color, font_size=18 ) self._title_caption.outline = 1 self._title_caption.outline_color = mcrfpy.Color(0, 0, 0) self.modal_frame.children.append(self._title_caption) # Add separator sep = mcrfpy.Frame( pos=(20, 45), size=(width - 40, 2), fill_color=mcrfpy.Color(80, 80, 100), outline=0 ) self.modal_frame.children.append(sep) # Content area starts at y=55 content_y = 55 # Add message if provided if message and not content_frame: self._message_caption = mcrfpy.Caption( text=message, pos=(20, content_y), fill_color=self.message_color, font_size=14 ) self.modal_frame.children.append(self._message_caption) elif content_frame: content_frame.x = 20 content_frame.y = content_y self.modal_frame.children.append(content_frame) # Add buttons at bottom if buttons: self._create_buttons(buttons) def _create_buttons(self, buttons): """Create button row at bottom of modal.""" button_width = 100 button_height = 35 total_width = len(buttons) * button_width + (len(buttons) - 1) * self.button_spacing start_x = (self.width - total_width) // 2 button_y = self.height - button_height - 15 self._button_frames = [] for i, (label, callback) in enumerate(buttons): x = start_x + i * (button_width + self.button_spacing) btn_frame = mcrfpy.Frame( pos=(x, button_y), size=(button_width, button_height), fill_color=self.DEFAULT_BUTTON_BG, outline_color=mcrfpy.Color(120, 120, 140), outline=1 ) btn_label = mcrfpy.Caption( text=label, pos=(button_width // 2, (button_height - 14) // 2), fill_color=mcrfpy.Color(220, 220, 220), font_size=14 ) btn_frame.children.append(btn_label) # Hover effect def make_enter(frame): def handler(pos, button, action): frame.fill_color = self.DEFAULT_BUTTON_HOVER return handler def make_exit(frame): def handler(pos, button, action): frame.fill_color = self.DEFAULT_BUTTON_BG return handler def make_click(cb): def handler(pos, button, action): if button == "left" and action == "end" and cb: cb() return handler btn_frame.on_enter = make_enter(btn_frame) btn_frame.on_exit = make_exit(btn_frame) btn_frame.on_click = make_click(callback) self._button_frames.append(btn_frame) self.modal_frame.children.append(btn_frame) def _on_overlay_click(self, pos, button, action): """Handle clicks on overlay (outside modal).""" # Check if click is outside modal if button == "left" and action == "end": mx, my = self.modal_frame.x, self.modal_frame.y mw, mh = self.modal_frame.w, self.modal_frame.h px, py = pos.x, pos.y if not (mx <= px <= mx + mw and my <= py <= my + mh): # Click outside modal - close if allowed if self._on_close: self._on_close() @property def on_close(self): """Callback when modal is closed by clicking outside.""" return self._on_close @on_close.setter def on_close(self, callback): """Set close callback.""" self._on_close = callback def show(self, scene=None): """Show the modal. Args: scene: Scene to add modal to (uses stored scene if not provided) """ if scene: self._scene = scene if self._scene and not self.visible: # Adjust overlay size to match scene # Note: Assumes 1024x768 for now self._scene.children.append(self.overlay) self.visible = True def hide(self): """Hide the modal.""" if self._scene and self.visible: # Remove overlay from scene try: # Find and remove overlay for i in range(len(self._scene.children)): if self._scene.children[i] is self.overlay: self._scene.children.pop() break except Exception: pass self.visible = False def set_message(self, message): """Update the modal message.""" self.message = message if hasattr(self, '_message_caption'): self._message_caption.text = message def set_title(self, title): """Update the modal title.""" self.title = title self._title_caption.text = title class ConfirmModal(Modal): """Pre-configured confirmation modal with Yes/No buttons. Args: title: Modal title message: Confirmation message on_confirm: Callback when confirmed on_cancel: Callback when cancelled (optional) confirm_text: Text for confirm button (default: "Confirm") cancel_text: Text for cancel button (default: "Cancel") """ def __init__(self, title, message, on_confirm, on_cancel=None, confirm_text="Confirm", cancel_text="Cancel", **kwargs): self._confirm_callback = on_confirm self._cancel_callback = on_cancel buttons = [ (cancel_text, self._on_cancel), (confirm_text, self._on_confirm) ] super().__init__(title, message=message, buttons=buttons, **kwargs) def _on_confirm(self): """Handle confirm button.""" self.hide() if self._confirm_callback: self._confirm_callback() def _on_cancel(self): """Handle cancel button.""" self.hide() if self._cancel_callback: self._cancel_callback() class AlertModal(Modal): """Pre-configured alert modal with single OK button. Args: title: Modal title message: Alert message on_dismiss: Callback when dismissed (optional) button_text: Text for button (default: "OK") """ def __init__(self, title, message, on_dismiss=None, button_text="OK", **kwargs): self._dismiss_callback = on_dismiss buttons = [(button_text, self._on_dismiss)] super().__init__(title, message=message, buttons=buttons, width=kwargs.pop('width', 350), **kwargs) def _on_dismiss(self): """Handle dismiss button.""" self.hide() if self._dismiss_callback: self._dismiss_callback()