From e3d34b4fc5815545211e233bdb88f84929c18dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=91line?= <31395137+yunline@users.noreply.github.com> Date: Sun, 15 Oct 2023 20:53:04 +0800 Subject: [PATCH] Window class input grab API rework (#2379) * Window grab API rework. Grab split to mouse & keyboard --- buildconfig/stubs/pygame/_window.pyi | 9 ++- docs/reST/ref/sdl2_video.rst | 86 +++++++++++++++++++++--- src_c/doc/sdl2_video_doc.h | 5 +- src_c/window.c | 98 ++++++++++++++++++++++++++-- test/window_test.py | 36 ++++++---- 5 files changed, 206 insertions(+), 28 deletions(-) diff --git a/buildconfig/stubs/pygame/_window.pyi b/buildconfig/stubs/pygame/_window.pyi index bc17adc3d5..44e2b7a780 100644 --- a/buildconfig/stubs/pygame/_window.pyi +++ b/buildconfig/stubs/pygame/_window.pyi @@ -25,8 +25,9 @@ class Window: def minimize(self) -> None: ... def set_modal_for(self, parent: Window) -> None: ... def set_icon(self, icon: Surface) -> None: ... - - grab: bool + + grab_mouse: bool + grab_keyboard: bool title: str resizable: bool borderless: bool @@ -34,6 +35,10 @@ class Window: relative_mouse: bool opacity: float + @property + def mouse_grabbed(self) -> bool: ... + @property + def keyboard_grabbed(self) -> bool: ... @property def id(self) -> int: ... @property diff --git a/docs/reST/ref/sdl2_video.rst b/docs/reST/ref/sdl2_video.rst index 939cf3c665..4ef89eff0e 100644 --- a/docs/reST/ref/sdl2_video.rst +++ b/docs/reST/ref/sdl2_video.rst @@ -80,7 +80,8 @@ :param bool resizable: Create a resizable window. :param bool minimized: Create a mimized window. :param bool maximized: Create a maximized window. - :param bool input_grabbed: Create a window with a grabbed input focus. + :param bool mouse_grabbed: Create a window with grabbed mouse input. + :param bool keyboard_grabbed: Create a window with grabbed keyboard input. :param bool input_focus: Create a window with input focus. :param bool mouse_focus: Create a window with mouse focus. :param bool foreign: Marks a window not created by SDL. @@ -99,15 +100,84 @@ (X11 only). - .. attribute:: grab + .. attribute:: grab_mouse - | :sl:`Get or set the window's input grab state` - | :sg:`grab -> bool` + | :sl:`Get or set the window's mouse grab mode` + | :sg:`grab_mouse -> bool` - Gets or sets the window's input grab state. - When input is grabbed, the mouse is confined to the window. - If the caller enables a grab while another window is currently grabbed, - the other window loses its grab in favor of the caller's window. + When this attribute is set to ``True``, the window will try to confine the mouse + cursor to itself. + + Note this only set the "mode" of grab. The mouse may be confined to another window + depending on the window focus. To get if the mouse is currently restricted to this + window, please use :attr:`mouse_grabbed`. + + .. seealso:: :attr:`mouse_grabbed` + + .. versionadded:: 2.4.0 + + .. attribute:: grab_keyboard + + | :sl:`Get or set the window's keyboard grab mode` + | :sg:`grab_keyboard -> bool` + + When this attribute is set to ``True``, the window will try to capture system + keyboard shortcuts like ``Alt+Tab`` or the ``Meta/Super`` key. + + This attribute only set the "mode" of grab. The keyboard may be captured by + another window depending on the window focus. To get if keyboard is currently + captured by this window, please use :attr:`keyboard_grabbed`. + + Note that not all system keyboard shortcuts can be captured by applications + (one example is ``Ctrl+Alt+Del`` on Windows). + + When keyboard grab is enabled, pygame will continue to handle ``Alt+Tab`` when + the window is full-screen to ensure the user is not trapped in your application. + If you have a custom keyboard shortcut to exit fullscreen mode, you may suppress + this behavior with an environment variable, e.g. + ``os.environ["SDL_ALLOW_ALT_TAB_WHILE_GRABBED"] = "0"``. + + This attribute requires SDL 2.0.16+. + + .. seealso:: :attr:`keyboard_grabbed` + + .. versionadded:: 2.4.0 + + .. attribute:: mouse_grabbed + + | :sl:`Get if the mouse cursor is confined to the window (**read-only**)` + | :sg:`mouse_grabbed -> bool` + + Get if the mouse cursor is currently grabbed and confined to the window. + + Roughly equivalent to this expression: + + :: + + win.grab_mouse and (win is get_grabbed_window()) + + .. seealso:: :attr:`grab_mouse` + + .. versionadded:: 2.4.0 + + .. attribute:: keyboard_grabbed + + | :sl:`Get if the keyboard shortcuts are captured by the window (**read-only**)` + | :sg:`keyboard_grabbed -> bool` + + Get if the keyboard shortcuts are currently grabbed and captured by the window. + + Roughly equivalent to this expression: + + :: + + win.grab_keyboard and (win is get_grabbed_window()) + + This attribute requires SDL 2.0.16+. + + .. seealso:: :attr:`grab_keyboard` + + .. versionadded:: 2.4.0 .. attribute:: relative_mouse diff --git a/src_c/doc/sdl2_video_doc.h b/src_c/doc/sdl2_video_doc.h index 04448e7506..e335cb946e 100644 --- a/src_c/doc/sdl2_video_doc.h +++ b/src_c/doc/sdl2_video_doc.h @@ -5,7 +5,10 @@ #define DOC_SDL2_VIDEO_GETDRIVERS "get_drivers() -> Iterator[RendererDriverInfo]\nYield info about the rendering drivers available for Renderer objects" #define DOC_SDL2_VIDEO_GETGRABBEDWINDOW "get_grabbed_window() -> Window or None\nGet the window with input grab enabled" #define DOC_SDL2_VIDEO_WINDOW "Window(title='pygame window', size=(640, 480), position=None, fullscreen=False, fullscreen_desktop=False, **kwargs) -> Window\npygame object that represents a window" -#define DOC_SDL2_VIDEO_WINDOW_GRAB "grab -> bool\nGet or set the window's input grab state" +#define DOC_SDL2_VIDEO_WINDOW_GRABMOUSE "grab_mouse -> bool\nGet or set the window's mouse grab mode" +#define DOC_SDL2_VIDEO_WINDOW_GRABKEYBOARD "grab_keyboard -> bool\nGet or set the window's keyboard grab mode" +#define DOC_SDL2_VIDEO_WINDOW_MOUSEGRABBED "mouse_grabbed -> bool\nGet if the mouse cursor is confined to the window (**read-only**)" +#define DOC_SDL2_VIDEO_WINDOW_KEYBOARDGRABBED "keyboard_grabbed -> bool\nGet if the keyboard shortcuts are captured by the window (**read-only**)" #define DOC_SDL2_VIDEO_WINDOW_RELATIVEMOUSE "relative_mouse -> bool\nGet or set the window's relative mouse mode state" #define DOC_SDL2_VIDEO_WINDOW_TITLE "title -> str\nGet or set the window title" #define DOC_SDL2_VIDEO_WINDOW_RESIZABLE "resizable -> bool\nGet or set whether the window is resizable" diff --git a/src_c/window.c b/src_c/window.c index 4788388733..cd99c19117 100644 --- a/src_c/window.c +++ b/src_c/window.c @@ -228,21 +228,88 @@ window_set_icon(pgWindowObject *self, PyObject *arg) } static int -window_set_grab(pgWindowObject *self, PyObject *arg, void *v) +window_set_grab_mouse(pgWindowObject *self, PyObject *arg, void *v) { int enable = PyObject_IsTrue(arg); if (enable == -1) return -1; +#if SDL_VERSION_ATLEAST(2, 0, 16) + SDL_SetWindowMouseGrab(self->_win, enable); +#else SDL_SetWindowGrab(self->_win, enable); +#endif return 0; } static PyObject * -window_get_grab(pgWindowObject *self, void *v) +window_get_grab_mouse(pgWindowObject *self, void *v) { +#if SDL_VERSION_ATLEAST(2, 0, 16) + return PyBool_FromLong(SDL_GetWindowFlags(self->_win) & + SDL_WINDOW_MOUSE_GRABBED); +#else + return PyBool_FromLong(SDL_GetWindowFlags(self->_win) & + SDL_WINDOW_INPUT_GRABBED); +#endif +} + +static PyObject * +window_get_mouse_grabbed(pgWindowObject *self, void *v) +{ +#if SDL_VERSION_ATLEAST(2, 0, 16) + return PyBool_FromLong(SDL_GetWindowMouseGrab(self->_win)); +#else return PyBool_FromLong(SDL_GetWindowGrab(self->_win)); +#endif +} + +static int +window_set_grab_keyboard(pgWindowObject *self, PyObject *arg, void *v) +{ +#if SDL_VERSION_ATLEAST(2, 0, 16) + int enable = PyObject_IsTrue(arg); + if (enable == -1) + return -1; + + SDL_SetWindowKeyboardGrab(self->_win, enable); +#else + if (PyErr_WarnEx(PyExc_Warning, "'grab_keyboard' requires SDL 2.0.16+", + 1) == -1) { + return -1; + } +#endif + return 0; +} + +static PyObject * +window_get_grab_keyboard(pgWindowObject *self, void *v) +{ +#if SDL_VERSION_ATLEAST(2, 0, 16) + return PyBool_FromLong(SDL_GetWindowFlags(self->_win) & + SDL_WINDOW_KEYBOARD_GRABBED); +#else + if (PyErr_WarnEx(PyExc_Warning, "'grab_keyboard' requires SDL 2.0.16+", + 1) == -1) { + return NULL; + } + return PyBool_FromLong(SDL_FALSE); +#endif +} + +static PyObject * +window_get_keyboard_grabbed(pgWindowObject *self, void *v) +{ +#if SDL_VERSION_ATLEAST(2, 0, 16) + return PyBool_FromLong(SDL_GetWindowKeyboardGrab(self->_win)); +#else + if (PyErr_WarnEx(PyExc_Warning, "'keyboard_captured' requires SDL 2.0.16+", + 1) == -1) { + return NULL; + } + return PyBool_FromLong(SDL_FALSE); +#endif } static int @@ -626,7 +693,7 @@ window_init(pgWindowObject *self, PyObject *args, PyObject *kwargs) if (_value_bool) flags |= SDL_WINDOW_MAXIMIZED; } - else if (!strcmp(_key_str, "input_grabbed")) { + else if (!strcmp(_key_str, "mouse_grabbed")) { if (_value_bool) #if SDL_VERSION_ATLEAST(2, 0, 16) flags |= SDL_WINDOW_MOUSE_GRABBED; @@ -634,6 +701,20 @@ window_init(pgWindowObject *self, PyObject *args, PyObject *kwargs) flags |= SDL_WINDOW_INPUT_GRABBED; #endif } + else if (!strcmp(_key_str, "keyboard_grabbed")) { + if (_value_bool) { +#if SDL_VERSION_ATLEAST(2, 0, 16) + flags |= SDL_WINDOW_KEYBOARD_GRABBED; +#else + if (PyErr_WarnEx(PyExc_Warning, + "Keyword 'keyboard_grabbed' requires " + "SDL 2.0.16+", + 1) == -1) { + return -1; + } +#endif + } + } else if (!strcmp(_key_str, "input_focus")) { if (_value_bool) { flags |= SDL_WINDOW_INPUT_FOCUS; @@ -827,8 +908,15 @@ static PyMethodDef window_methods[] = { {NULL, NULL, 0, NULL}}; static PyGetSetDef _window_getset[] = { - {"grab", (getter)window_get_grab, (setter)window_set_grab, - DOC_SDL2_VIDEO_WINDOW_GRAB, NULL}, + {"grab_mouse", (getter)window_get_grab_mouse, + (setter)window_set_grab_mouse, DOC_SDL2_VIDEO_WINDOW_GRABMOUSE, NULL}, + {"grab_keyboard", (getter)window_get_grab_keyboard, + (setter)window_set_grab_keyboard, DOC_SDL2_VIDEO_WINDOW_GRABKEYBOARD, + NULL}, + {"mouse_grabbed", (getter)window_get_mouse_grabbed, NULL, + DOC_SDL2_VIDEO_WINDOW_MOUSEGRABBED, NULL}, + {"keyboard_grabbed", (getter)window_get_keyboard_grabbed, NULL, + DOC_SDL2_VIDEO_WINDOW_KEYBOARDGRABBED, NULL}, {"title", (getter)window_get_title, (setter)window_set_title, DOC_SDL2_VIDEO_WINDOW_TITLE, NULL}, {"resizable", (getter)window_get_resizable, (setter)window_set_resizable, diff --git a/test/window_test.py b/test/window_test.py index 089b54424d..5eb045e9d4 100644 --- a/test/window_test.py +++ b/test/window_test.py @@ -5,7 +5,7 @@ from pygame._sdl2.video import Window from pygame.version import SDL -# os.environ["SDL_VIDEODRIVER"] = "dummy" +os.environ["SDL_VIDEODRIVER"] = "dummy" pygame.init() @@ -27,18 +27,30 @@ def bool_attr_test(self, attr): setattr(self.win, attr, 17) self.assertIsInstance(getattr(self.win, attr), bool) - def test_grab(self): - self.bool_attr_test("grab") + def test_grab_mouse_keyboard(self): + self.bool_attr_test("grab_mouse") + self.bool_attr_test("grab_keyboard") - @unittest.skipIf( - os.environ.get("SDL_VIDEODRIVER") == "dummy", - "requires the SDL_VIDEODRIVER to be a non dummy value", - ) - def test_grab_set(self): - self.win.grab = True - self.assertTrue(self.win.grab) - self.win.grab = False - self.assertFalse(self.win.grab) + self.win.grab_mouse = True + self.assertTrue(self.win.grab_mouse) + self.win.grab_mouse = False + self.assertFalse(self.win.grab_mouse) + + if SDL >= (2, 0, 16): + self.win.grab_keyboard = True + self.assertTrue(self.win.grab_keyboard) + self.win.grab_keyboard = False + self.assertFalse(self.win.grab_keyboard) + + def test_mouse_keyboard_grabbed(self): + self.assertIsInstance(getattr(self.win, "mouse_grabbed"), bool) + self.assertIsInstance(getattr(self.win, "keyboard_grabbed"), bool) + self.assertRaises( + AttributeError, lambda: setattr(self.win, "mouse_grabbed", False) + ) + self.assertRaises( + AttributeError, lambda: setattr(self.win, "keyboard_grabbed", False) + ) def test_title(self): self.assertEqual(self.win.title, self.DEFAULT_TITLE)