Skip to content

Commit

Permalink
display.message_box() (#2427)
Browse files Browse the repository at this point in the history
* Add message_box to display module
  • Loading branch information
yunline authored Oct 15, 2023
1 parent 0076d72 commit a917d3f
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 9 deletions.
13 changes: 12 additions & 1 deletion buildconfig/stubs/pygame/display.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from typing import Dict, List, Optional, Tuple, Union, overload
from typing import Dict, List, Optional, Tuple, Union, overload, Literal

from pygame.constants import FULLSCREEN
from pygame.surface import Surface

from pygame._sdl2 import Window

from ._common import (
ColorValue,
Coordinate,
Expand Down Expand Up @@ -87,3 +89,12 @@ def is_fullscreen() -> bool: ...
def is_vsync() -> bool: ...
def get_current_refresh_rate() -> int: ...
def get_desktop_refresh_rates() -> List[int]: ...
def message_box(
title: str,
message: Optional[str] = None,
message_type: Literal["info", "warn", "error"] = "info",
parent_window: Optional[Window] = None,
buttons: Sequence[str] = ("OK",),
return_button: int = 0,
escape_button: Optional[int] = None,
) -> int: ...
30 changes: 30 additions & 0 deletions docs/reST/ref/display.rst
Original file line number Diff line number Diff line change
Expand Up @@ -802,4 +802,34 @@ required).
.. versionadded:: 2.2.0
.. ## pygame.display.set_allow_screensaver ##
.. function:: message_box

| :sl:`Create a native GUI message box`
| :sg:`message_box(title, message=None, message_type='info', parent_window=None, buttons=('OK',), return_button=0, escape_button=None) -> int`
:param str title: A title string.
:param str message: A message string. If this parameter is set to ``None``, the message will be the title.
:param str message_type: Set the type of message_box, could be ``"info"``, ``"warn"`` or ``"error"``.
:param tuple buttons: An optional sequence of button name strings to show to the user.
:param int return_button: Button index to use if the return key is hit, ``0`` by default.
:param int escape_button: Button index to use if the escape key is hit, ``None`` for no button linked by default.
..
(Uncomment this after the window API is published)
:param Window parent_window: The parent window of the message_box
..
:return: The index of the button that was pushed.

This function should be called on the thread that ``set_mode()`` is called.
It will block execution of that thread until the user clicks a button or
closes the message_box.

This function may be called at any time, even before ``pygame.init()``.

Negative values of ``return_button`` and ``escape_button`` are allowed
just like standard Python list indexing.

.. versionadded:: 2.4.0


.. ## pygame.display ##
205 changes: 204 additions & 1 deletion src_c/display.c
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ _display_state_cleanup(_DisplayState *state)
}
}

// prevent this code block from being linked twice
// (this code block is copied by window.c)
#ifndef BUILD_STATIC

#if !defined(__APPLE__)
static char *icon_defaultname = "pygame_icon.bmp";
static int icon_colorkey = 0;
Expand Down Expand Up @@ -176,6 +180,8 @@ pg_display_resource(char *filename)
return result;
}

#endif // BUILD_STATIC

/* init routines */
static PyObject *
pg_display_quit(PyObject *self, PyObject *_null)
Expand Down Expand Up @@ -2686,6 +2692,198 @@ pg_set_allow_screensaver(PyObject *self, PyObject *arg, PyObject *kwargs)
Py_RETURN_NONE;
}

static PyObject *
pg_message_box(PyObject *self, PyObject *arg, PyObject *kwargs)
{
const char *title = NULL;
PyObject *message = Py_None, *parent_window = Py_None;
const char *msgbox_type = "info";
PyObject *buttons = NULL;
PyObject *escape_button_index_obj = Py_None;

int return_button_index = 0;

static char *keywords[] = {"title", "message", "message_type",
"parent_window", "buttons", "return_button",
"escape_button", NULL};

if (!PyArg_ParseTupleAndKeywords(
arg, kwargs, "s|OsO!OiO", keywords, &title, &message, &msgbox_type,
&pgWindow_Type, &parent_window, &buttons, &return_button_index,
&escape_button_index_obj)) {
return NULL;
}

int escape_button_index = 0;
SDL_bool escape_button_used = SDL_FALSE;
if (escape_button_index_obj != Py_None) {
escape_button_index = PyLong_AsLong(escape_button_index_obj);
if (escape_button_index == -1 && PyErr_Occurred())
return NULL;
escape_button_used = SDL_TRUE;
}

SDL_MessageBoxData msgbox_data;

msgbox_data.flags = 0;
if (!strcmp(msgbox_type, "info")) {
msgbox_data.flags |= SDL_MESSAGEBOX_INFORMATION;
}
else if (!strcmp(msgbox_type, "warn")) {
msgbox_data.flags |= SDL_MESSAGEBOX_WARNING;
}
else if (!strcmp(msgbox_type, "error")) {
msgbox_data.flags |= SDL_MESSAGEBOX_ERROR;
}
else {
PyErr_Format(PyExc_ValueError,
"type should be 'info', 'warn' or 'error', "
"got '%s'",
msgbox_type);
return NULL;
}

#if SDL_VERSION_ATLEAST(2, 0, 12)
msgbox_data.flags |= SDL_MESSAGEBOX_BUTTONS_LEFT_TO_RIGHT;
#endif

if (parent_window == Py_None)
msgbox_data.window = NULL;
else
msgbox_data.window = ((pgWindowObject *)parent_window)->_win;

msgbox_data.colorScheme = NULL; // use system color scheme settings

msgbox_data.title = title;
if (PyUnicode_Check(message)) {
msgbox_data.message = PyUnicode_AsUTF8(message);
if (!msgbox_data.message)
return NULL;
}
else if (message == Py_None) {
msgbox_data.message = title;
}
else {
PyErr_Format(PyExc_TypeError, "'message' must be str, not '%s'",
message->ob_type->tp_name);
return NULL;
}

SDL_MessageBoxButtonData *buttons_data = NULL;

if (buttons == NULL) {
buttons_data = malloc(sizeof(SDL_MessageBoxButtonData));
buttons_data->flags = 0;
buttons_data->buttonid = 0;
buttons_data->text = "OK";

msgbox_data.numbuttons = 1;

if (-1 > return_button_index || return_button_index >= 1) {
PyErr_SetString(PyExc_IndexError,
"return_button index out of range");
goto error;
}
buttons_data->flags |= SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;

if (escape_button_used) {
if (-1 > escape_button_index || escape_button_index >= 1) {
PyErr_SetString(PyExc_IndexError,
"escape_button index out of range");
goto error;
}
buttons_data->flags |= SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
}
}
else {
if (!PySequence_Check(buttons) || PyUnicode_Check(buttons)) {
PyErr_Format(PyExc_TypeError,
"'buttons' should be a sequence of string, got '%s'",
buttons->ob_type->tp_name);
return NULL;
}
Py_ssize_t num_buttons = PySequence_Size(buttons);
msgbox_data.numbuttons = (int)num_buttons;
if (num_buttons < 0) {
return NULL;
}
else if (num_buttons == 0) {
return RAISE(PyExc_TypeError,
"'buttons' should contain at least 1 button");
}

if (return_button_index < 0) {
return_button_index = (int)num_buttons + return_button_index;
}
if (0 > return_button_index || return_button_index >= num_buttons) {
return RAISE(PyExc_IndexError, "return_button index out of range");
}
if (escape_button_used) {
if (escape_button_index < 0) {
escape_button_index = (int)num_buttons + escape_button_index;
}
if (0 > escape_button_index ||
escape_button_index >= num_buttons) {
return RAISE(PyExc_IndexError,
"escape_button index out of range");
}
}

buttons_data = malloc(sizeof(SDL_MessageBoxButtonData) * num_buttons);
for (Py_ssize_t i = 0; i < num_buttons; i++) {
#if SDL_VERSION_ATLEAST(2, 0, 12)
PyObject *btn_name_obj = PySequence_GetItem(buttons, i);
#else
PyObject *btn_name_obj =
PySequence_GetItem(buttons, num_buttons - i - 1);
#endif
if (!btn_name_obj)
goto error;

if (!PyUnicode_Check(btn_name_obj)) {
PyErr_SetString(PyExc_TypeError,
"'buttons' should be a sequence of string");
goto error;
}

const char *btn_name = PyUnicode_AsUTF8(btn_name_obj);
if (!btn_name)
goto error;

buttons_data[i].text = btn_name;
#if SDL_VERSION_ATLEAST(2, 0, 12)
buttons_data[i].buttonid = (int)i;
#else
buttons_data[i].buttonid = (int)(num_buttons - i - 1);
#endif
buttons_data[i].flags = 0;
if (return_button_index == buttons_data[i].buttonid)
buttons_data[i].flags |=
SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT;
if (escape_button_used &&
escape_button_index == buttons_data[i].buttonid)
buttons_data[i].flags |=
SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT;
}
}

msgbox_data.buttons = buttons_data;

int clicked_button_id;

if (SDL_ShowMessageBox(&msgbox_data, &clicked_button_id)) {
PyErr_SetString(pgExc_SDLError, SDL_GetError());
goto error;
}

free(buttons_data);
return PyLong_FromLong(clicked_button_id);

error:
free(buttons_data);
return NULL;
}

static PyMethodDef _pg_display_methods[] = {
{"init", (PyCFunction)pg_display_init, METH_NOARGS, DOC_DISPLAY_INIT},
{"quit", (PyCFunction)pg_display_quit, METH_NOARGS, DOC_DISPLAY_QUIT},
Expand Down Expand Up @@ -2757,7 +2955,8 @@ static PyMethodDef _pg_display_methods[] = {
METH_NOARGS, DOC_DISPLAY_GETALLOWSCREENSAVER},
{"set_allow_screensaver", (PyCFunction)pg_set_allow_screensaver,
METH_VARARGS | METH_KEYWORDS, DOC_DISPLAY_SETALLOWSCREENSAVER},

{"message_box", (PyCFunction)pg_message_box, METH_VARARGS | METH_KEYWORDS,
DOC_DISPLAY_MESSAGEBOX},
{NULL, NULL, 0, NULL}};

#ifndef PYPY_VERSION
Expand Down Expand Up @@ -2798,6 +2997,10 @@ MODINIT_DEFINE(display)
if (PyErr_Occurred()) {
return NULL;
}
import_pygame_window();
if (PyErr_Occurred()) {
return NULL;
}

/* type preparation */
if (PyType_Ready(&pgVidInfo_Type) < 0) {
Expand Down
1 change: 1 addition & 0 deletions src_c/doc/display_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@
#define DOC_DISPLAY_ISVSYNC "is_vsync() -> bool\nReturns True if vertical synchronisation for pygame.display.flip() and pygame.display.update() is enabled"
#define DOC_DISPLAY_GETCURRENTREFRESHRATE "get_current_refresh_rate() -> int\nReturns the screen refresh rate or 0 if unknown"
#define DOC_DISPLAY_GETDESKTOPREFRESHRATES "get_desktop_refresh_rates() -> list\nReturns the screen refresh rates for all displays (in windowed mode)."
#define DOC_DISPLAY_MESSAGEBOX "message_box(title, message=None, message_type='info', parent_window=None, buttons=('OK',), return_button=0, escape_button=None) -> int\nCreate a native GUI message box"
9 changes: 7 additions & 2 deletions src_c/static.c
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ import_pygame_joystick(void)
{
}

void
import_pygame_window(void)
{
}

PyMODINIT_FUNC
PyInit_base(void);
PyMODINIT_FUNC
Expand Down Expand Up @@ -349,6 +354,8 @@ PyInit_pygame_static()
#include "simd_blitters_avx2.c"
#include "simd_blitters_sse2.c"

#include "window.c"

#undef pgVidInfo_Type
#undef pgVidInfo_New

Expand Down Expand Up @@ -422,5 +429,3 @@ PyInit_pygame_static()
#undef MAX
#undef MIN
#include "scale2x.c"

#include "window.c"
5 changes: 0 additions & 5 deletions src_c/window.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@

#include "doc/sdl2_video_doc.h"

// prevent that code block copied from display.c from being linked twice
#ifndef BUILD_STATIC

#if !defined(__APPLE__)
static char *icon_defaultname = "pygame_icon.bmp";
static int icon_colorkey = 0;
Expand Down Expand Up @@ -94,8 +91,6 @@ pg_display_resource(char *filename)
return result;
}

#endif // BUILD_STATIC

static PyTypeObject pgWindow_Type;

#define pgWindow_Check(x) \
Expand Down
Loading

0 comments on commit a917d3f

Please sign in to comment.