|
1 | 1 | """User interface elements.""" |
2 | | -from .compat import isiterable, stringify |
| 2 | +from ctypes import byref, c_int, POINTER |
| 3 | +from .color import Color |
| 4 | +from .compat import isiterable, stringify, utf8 |
| 5 | +from .common import SDLError |
3 | 6 | from .ebs import System, World |
4 | 7 | from .events import EventHandler |
5 | 8 | from .sprite import Sprite |
6 | | -from .. import events, mouse, keyboard, rect |
| 9 | +from .window import Window |
| 10 | +from .. import (events, dll, mouse, keyboard, rect, error, SDL_PumpEvents, |
| 11 | + SDL_Window) |
| 12 | +from .. import messagebox as mb |
7 | 13 |
|
8 | | -__all__ = ["RELEASED", "HOVERED", "PRESSED", "BUTTON", "CHECKBUTTON", |
9 | | - "TEXTENTRY", "UIProcessor", "UIFactory" |
10 | | - ] |
| 14 | +__all__ = [ |
| 15 | + "RELEASED", "HOVERED", "PRESSED", "BUTTON", "CHECKBUTTON", "TEXTENTRY", |
| 16 | + "MessageBoxTheme", "MessageBox", "show_messagebox", "show_alert", |
| 17 | + "UIProcessor", "UIFactory" |
| 18 | +] |
11 | 19 |
|
12 | 20 |
|
13 | 21 | RELEASED = 0x0000 |
|
20 | 28 | TEXTENTRY = 0x0004 |
21 | 29 |
|
22 | 30 |
|
| 31 | +class MessageBoxTheme(object): |
| 32 | + """Initializes a color scheme for use with :obj:`MessageBox` objects. |
| 33 | +
|
| 34 | + This is used to define the background, text, and various button colors |
| 35 | + to use when presenting dialog boxes to users. All colors must be defined |
| 36 | + as either :obj:`sdl2.ext.Color` objects or 8-bit ``(r, g, b)`` tuples. |
| 37 | +
|
| 38 | + .. note: SDL2 only supports MessageBox themes on a few platforms, including |
| 39 | + Linux/BSD (if using X11) and Haiku. MessageBox themes will have no effect |
| 40 | + on Windows, macOS, or Linux if using Wayland. |
| 41 | +
|
| 42 | + Args: |
| 43 | + bg (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use for the |
| 44 | + background of the dialog box. Defaults to ``(56, 54, 53)``. |
| 45 | + text (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use for the |
| 46 | + text of the dialog box. Defaults to ``(209, 207, 205)``. |
| 47 | + btn (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use for the |
| 48 | + backgrounds of buttons. Defaults to ``(140, 135, 129)``. |
| 49 | + btn_border (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use |
| 50 | + for the borders of buttons. Defaults to ``(105, 102, 99)``. |
| 51 | + btn_selected (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use |
| 52 | + for selected buttons. Defaults to ``(205, 202, 53)``. |
| 53 | +
|
| 54 | + """ |
| 55 | + def __init__( |
| 56 | + self, bg=None, text=None, btn=None, btn_border=None, btn_selected=None |
| 57 | + ): |
| 58 | + # NOTE: Default colors taken from SDL_x11messagebox.c |
| 59 | + self._theme = [ |
| 60 | + (56, 54, 53), # Background color |
| 61 | + (209, 207, 205), # Text color |
| 62 | + (140, 135, 129), # Button border color |
| 63 | + (105, 102, 99), # Button background color |
| 64 | + (205, 202, 53) # Selected button color |
| 65 | + ] |
| 66 | + # Update default theme colors based on provided values |
| 67 | + elements = [bg, text, btn_border, btn, btn_selected] |
| 68 | + for i in range(len(elements)): |
| 69 | + if elements[i] is not None: |
| 70 | + self._theme[i] = self._validate_color(elements[i]) |
| 71 | + |
| 72 | + def _validate_color(self, col): |
| 73 | + if not isinstance(col, Color): |
| 74 | + if not isiterable(col) or len(col) != 3: |
| 75 | + e = "MessageBox colors must be specified as (r, g, b) tuples." |
| 76 | + raise TypeError(e) |
| 77 | + for val in col: |
| 78 | + if int(val) != float(val): |
| 79 | + e = "All RGB values must be integers between 0 and 255." |
| 80 | + raise ValueError(e) |
| 81 | + col = Color(col[0], col[1], col[2]) |
| 82 | + return (col.r, col.g, col.b) |
| 83 | + |
| 84 | + def _get_theme(self): |
| 85 | + sdl_colors = [] |
| 86 | + for col in self._theme: |
| 87 | + sdl_colors.append(mb.SDL_MessageBoxColor(*col)) |
| 88 | + col_array = (mb.SDL_MessageBoxColor * 5)(*sdl_colors) |
| 89 | + return mb.SDL_MessageBoxColorScheme(col_array) |
| 90 | + |
| 91 | + |
| 92 | +class MessageBox(object): |
| 93 | + """Creates a prototype for a dialog box that can be presented to the user. |
| 94 | +
|
| 95 | + The `MessageBox` class is for designing a dialog box in the style of the |
| 96 | + system's window manager, containing a title, a message to present, and |
| 97 | + one or more response buttons. |
| 98 | +
|
| 99 | + Args: |
| 100 | + title (str): The title to use for the dialog box. All UTF-8 characters |
| 101 | + are supported. |
| 102 | + msg (str): The main body of text to display in the dialog box. All UTF-8 |
| 103 | + characters are supported. |
| 104 | + buttons (list): A list of strings, containing the labels of the buttons |
| 105 | + to place at the bottom of the dialog box (e.g. ``["No", "Yes"]``). |
| 106 | + Buttons will be placed in left-to-right order. |
| 107 | + default (str, optional): The label of the button to highlight as the |
| 108 | + default option (e.g. ``"Yes"``). Must match one of the labels in |
| 109 | + ``buttons``. This option will be accepted if the Return/Enter key |
| 110 | + is pressed on the keyboard. |
| 111 | + msgtype (str, optional): The type of dialog box to create, if supported |
| 112 | + by the system. On most window managers, this changes the icon used |
| 113 | + in the dialog box. Must be one of 'error', 'warning', or 'info', or |
| 114 | + None (the default). |
| 115 | + theme (:obj:`MessageBoxTheme`, optional): The color scheme to use for |
| 116 | + the dialog box, if supported by the window manager. Defaults to the |
| 117 | + system default theme. |
| 118 | +
|
| 119 | + """ |
| 120 | + def __init__(self, title, msg, buttons, default=None, msgtype=None, theme=None): |
| 121 | + self._title = utf8(title).encode('utf-8') |
| 122 | + self._text = utf8(msg).encode('utf-8') |
| 123 | + self._validate_buttons(buttons) |
| 124 | + self._buttons = buttons |
| 125 | + self._sdlbuttons = self._init_buttons(buttons, default) |
| 126 | + self._type = self._set_msgtype(msgtype) if msgtype else 0 |
| 127 | + self._theme = theme._get_theme() if theme else None |
| 128 | + |
| 129 | + def _set_msgtype(self, msgtype): |
| 130 | + _flagmap = { |
| 131 | + 'error': mb.SDL_MESSAGEBOX_ERROR, |
| 132 | + 'warning': mb.SDL_MESSAGEBOX_WARNING, |
| 133 | + 'info': mb.SDL_MESSAGEBOX_INFORMATION, |
| 134 | + } |
| 135 | + if msgtype.lower() not in _flagmap.keys(): |
| 136 | + raise ValueError( |
| 137 | + "MessageBox type must be 'error', 'warning', 'info', or None." |
| 138 | + ) |
| 139 | + return _flagmap[msgtype] |
| 140 | + |
| 141 | + def _validate_buttons(self, buttons): |
| 142 | + if not isiterable(buttons): |
| 143 | + raise TypeError("Buttons must be provided as a list.") |
| 144 | + elif len(buttons) == 0: |
| 145 | + raise ValueError("MessageBox must have at least one button.") |
| 146 | + |
| 147 | + def _init_buttons(self, buttons, default): |
| 148 | + default_flag = mb.SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT |
| 149 | + buttonset = [] |
| 150 | + for i in range(len(buttons)): |
| 151 | + b = mb.SDL_MessageBoxButtonData( |
| 152 | + flags = (default_flag if buttons[i] == default else 0), |
| 153 | + buttonid = i, |
| 154 | + text = utf8(buttons[i]).encode('utf-8'), |
| 155 | + ) |
| 156 | + buttonset.append(b) |
| 157 | + return (mb.SDL_MessageBoxButtonData * len(buttons))(*buttonset) |
| 158 | + |
| 159 | + def _get_window_pointer(self, win): |
| 160 | + if isinstance(win, Window): |
| 161 | + win = win.window |
| 162 | + if isinstance(win, SDL_Window): |
| 163 | + win = dll.get_pointer(win) |
| 164 | + if hasattr(win, "contents") and isinstance(win.contents, SDL_Window): |
| 165 | + return win |
| 166 | + else: |
| 167 | + e = "'window' must be a Window or SDL_Window object (got {0})" |
| 168 | + raise ValueError(e.format(str(type(win)))) |
| 169 | + |
| 170 | + def _get_msgbox(self, window=None): |
| 171 | + if window: |
| 172 | + window = self._get_window_pointer(window) |
| 173 | + return mb.SDL_MessageBoxData( |
| 174 | + flags = self._type | mb.SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT, |
| 175 | + window = window, |
| 176 | + title = self._title, |
| 177 | + message = self._text, |
| 178 | + numbuttons = len(self._buttons), |
| 179 | + buttons = self._sdlbuttons, |
| 180 | + colorScheme = dll.get_pointer(self._theme) if self._theme else None, |
| 181 | + ) |
| 182 | + |
| 183 | + |
| 184 | +def show_messagebox(msgbox, window=None): |
| 185 | + """Displays a dialog box to the user and waits for a response. |
| 186 | +
|
| 187 | + By default message boxes are presented independently of any window, but |
| 188 | + they can optionally be attached explicitly to a specific SDL window. This |
| 189 | + prevents that window from regaining focus until a response to the dialog |
| 190 | + box is made. |
| 191 | +
|
| 192 | + Args: |
| 193 | + msgbox (:obj:`~sdl2.ext.MessageBox`): The dialog box to display |
| 194 | + on-screen. |
| 195 | + window (:obj:`~sdl2.SDL_Window`, :obj:`~sdl2.ext.Window`, optional): The |
| 196 | + window to associate with the dialog box. Defaults to None. |
| 197 | +
|
| 198 | + Returns: |
| 199 | + str: The label of the button selected by the user. |
| 200 | +
|
| 201 | + """ |
| 202 | + resp = c_int(-1) |
| 203 | + ret = mb.SDL_ShowMessageBox( |
| 204 | + msgbox._get_msgbox(window), |
| 205 | + byref(resp) |
| 206 | + ) |
| 207 | + SDL_PumpEvents() |
| 208 | + if ret == 0: |
| 209 | + return msgbox._buttons[resp.value] |
| 210 | + else: |
| 211 | + errmsg = error.SDL_GetError().decode('utf-8') |
| 212 | + error.SDL_ClearError() |
| 213 | + e = "Error encountered displaying message box" |
| 214 | + if len(errmsg): |
| 215 | + e += ": {0}".format(errmsg) |
| 216 | + raise SDLError(e) |
| 217 | + |
| 218 | + |
| 219 | +def show_alert(title, msg, msgtype=None, window=None): |
| 220 | + """Displays a simple alert to the user and waits for a response. |
| 221 | +
|
| 222 | + This function is a simplified version of :func:`show_messagebox` for cases |
| 223 | + where only one response button ("OK") is needed and a custom color scheme |
| 224 | + is not necessary. |
| 225 | +
|
| 226 | + By default message boxes are presented independently of any window, but |
| 227 | + they can optionally be attached explicitly to a specific SDL window. This |
| 228 | + prevents that window from regaining focus until a response to the dialog |
| 229 | + box is made. |
| 230 | +
|
| 231 | + Args: |
| 232 | + msgbox (:obj:`~sdl2.ext.MessageBox`): The dialog box to display |
| 233 | + on-screen. |
| 234 | + window (:obj:`~sdl2.SDL_Window`, :obj:`~sdl2.ext.Window`, optional): The |
| 235 | + window to associate with the dialog box. Defaults to ``None``. |
| 236 | +
|
| 237 | + """ |
| 238 | + box = MessageBox(title, msg, ["OK"], msgtype=msgtype) |
| 239 | + if window: |
| 240 | + window = box._get_window_pointer(window) |
| 241 | + ret = mb.SDL_ShowSimpleMessageBox( |
| 242 | + box._type, |
| 243 | + box._title, |
| 244 | + box._text, |
| 245 | + window |
| 246 | + ) |
| 247 | + SDL_PumpEvents() |
| 248 | + if ret != 0: |
| 249 | + errmsg = error.SDL_GetError().decode('utf-8') |
| 250 | + error.SDL_ClearError() |
| 251 | + e = "Error encountered displaying message box" |
| 252 | + if len(errmsg): |
| 253 | + e += ": {0}".format(errmsg) |
| 254 | + raise SDLError(e) |
| 255 | + |
| 256 | + |
23 | 257 | def _compose_button(obj): |
24 | 258 | """Binds button attributes to the object, so it can be properly |
25 | 259 | processed by the UIProcessor. |
|
0 commit comments