Skip to content

Commit ab78467

Browse files
authored
Add sdl2.ext wrapper for MessageBox API (#188)
* Add utility functions useful for MessageBox * Add draft of MessageBox API and tests * Make ext.Window title allow bytes in Py3 * Add prototype tests for untested msgbox functions * Updated news.rst * Fix MessageBoxTheme usage & add test
1 parent 2148bf2 commit ab78467

File tree

6 files changed

+339
-11
lines changed

6 files changed

+339
-11
lines changed

doc/news.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ New Features:
1919
* Added support for passing ``SDL_Surface`` pointers directly to many
2020
``sdl2.ext`` functions, removing the need to explicitly use the ``.contents``
2121
attribute.
22+
* Added :obj:`sdl2.ext.MessageBox`, :func:`sdl2.ext.show_messagebox`, and
23+
:func:`sdl2.ext.show_alert` as Pythonic wrappers around the SDL2 MessageBox
24+
API (PR #188)
2225

2326
Fixed bugs:
2427

sdl2/dll.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import os
33
import sys
44
import warnings
5-
from ctypes import CDLL, POINTER, Structure, c_uint8
5+
from ctypes import CDLL, POINTER, Structure, c_uint8, cast, addressof
66
from ctypes.util import find_library
77

88
# Prints warning without stack or line info
@@ -29,6 +29,12 @@ def _pretty_fmt(message, category, filename, lineno, line=None):
2929
__all__ = ["DLL", "nullfunc"]
3030

3131

32+
# Gets a usable pointer from an SDL2 ctypes object
33+
def get_pointer(ctypes_obj):
34+
pointer_type = POINTER(type(ctypes_obj))
35+
return cast(addressof(ctypes_obj), pointer_type)
36+
37+
3238
# For determining DLL version on load
3339
class SDL_version(Structure):
3440
_fields_ = [("major", c_uint8),

sdl2/ext/compat.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,26 @@
3232
ISPYTHON3 = True
3333
unicode = str
3434

35-
isiterable = lambda x: isinstance(x, Iterable)
35+
36+
def isiterable(x):
37+
"""Determines if an object is iterable and not a string."""
38+
return hasattr(x, "__iter__") and not hasattr(x, "upper")
39+
40+
41+
def utf8(x):
42+
"""Converts input to a unicode string in a Python 2/3 agnostic manner.
43+
44+
"""
45+
if ISPYTHON2:
46+
if type(x) in (str, bytes):
47+
return x.decode('utf-8')
48+
else:
49+
return unicode(x)
50+
else:
51+
if type(x) == bytes:
52+
return x.decode('utf-8')
53+
else:
54+
return str(x)
3655

3756

3857
def platform_is_64bit():

sdl2/ext/gui.py

Lines changed: 239 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
"""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
36
from .ebs import System, World
47
from .events import EventHandler
58
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
713

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+
]
1119

1220

1321
RELEASED = 0x0000
@@ -20,6 +28,232 @@
2028
TEXTENTRY = 0x0004
2129

2230

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+
23257
def _compose_button(obj):
24258
"""Binds button attributes to the object, so it can be properly
25259
processed by the UIProcessor.

sdl2/ext/window.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Window routines to manage on-screen windows."""
22
from ctypes import c_int, byref
3-
from .compat import byteify, stringify
3+
from .compat import stringify, utf8
44
from .common import SDLError
55
from .. import video
66

@@ -130,7 +130,8 @@ def title(self):
130130

131131
@title.setter
132132
def title(self, value):
133-
video.SDL_SetWindowTitle(self.window, byteify(value, "utf-8"))
133+
title_bytes = utf8(value).encode('utf-8')
134+
video.SDL_SetWindowTitle(self.window, title_bytes)
134135
self._title = value
135136

136137
@property
@@ -152,7 +153,7 @@ def create(self):
152153
"""Creates the window if it does not already exist."""
153154
if self.window != None:
154155
return
155-
window = video.SDL_CreateWindow(byteify(self._title, "utf-8"),
156+
window = video.SDL_CreateWindow(utf8(self._title).encode('utf-8'),
156157
self._position[0], self._position[1],
157158
self._size[0], self._size[1],
158159
self._flags)

0 commit comments

Comments
 (0)