diff --git a/buildconfig/stubs/pygame/key.pyi b/buildconfig/stubs/pygame/key.pyi index fa75a16495..e41720df7c 100644 --- a/buildconfig/stubs/pygame/key.pyi +++ b/buildconfig/stubs/pygame/key.pyi @@ -6,6 +6,8 @@ class ScancodeWrapper(Tuple[bool, ...]): ... def get_focused() -> bool: ... def get_pressed() -> ScancodeWrapper: ... +def get_just_pressed() -> ScancodeWrapper: ... +def get_just_released() -> ScancodeWrapper: ... def get_mods() -> int: ... def set_mods(mods: int) -> None: ... def set_repeat(delay: int = 0, interval: int = 0) -> None: ... diff --git a/docs/reST/ref/key.rst b/docs/reST/ref/key.rst index d25a603982..d0f8d36886 100644 --- a/docs/reST/ref/key.rst +++ b/docs/reST/ref/key.rst @@ -274,6 +274,65 @@ for ``KMOD_NONE``, which should be compared using equals ``==``). For example: .. ## pygame.key.get_pressed ## +.. function:: get_just_pressed + + | :sl:`returns a pygame.key.ScancodeWrapper containing the most recent key presses` + | :sg:`get_just_pressed() -> bools` + + Returns a mapping from key codes to booleans indicating which keys were + newly pressed as of the last time events were processed. This can be used + as a convenience function to detect keys that were pressed "this frame." + + The result of this function is updated when new events are processed, + e.g. in :func:`pygame.event.get()` or :func:`pygame.event.pump()`. + + A key can be marked as "just pressed" even if it is not currently pressed + according to :func:`pygame.key.get_pressed()`, if it was pressed and released + again during the same frame. Multiple presses and releases of the same key + are not distinguished from a single press with this function. + + .. seealso:: :func:`pygame.key.get_just_released()` + + .. note:: + If you require getting the key presses in order, use the event queue + ``KEYDOWN`` events + + :: + + if pygame.key.get_just_pressed()[pygame.K_b]: + print("B key just pressed") + + .. versionadded:: 2.4.0 + + .. ## pygame.key.get_just_pressed ## + +.. function:: get_just_released + + | :sl:`returns a pygame.key.ScancodeWrapper containing the most recent key releases` + | :sg:`get_just_pressed() -> bools` + + Returns a mapping from key codes to booleans indicating which keys were + newly released as of the last time events were processed. This can be used + as a convenience function to detect keys that were released "this frame." + + The result of this function is updated when new events are processed, + e.g. in :func:`pygame.event.get()` or :func:`pygame.event.pump()`. + + .. seealso:: :func:`pygame.key.get_just_pressed()` + + .. note:: + If you require getting the key releases in order, use the event queue + ``KEYUP`` events. + + :: + + if pygame.key.get_just_released()[pygame.K_b]: + print("B key just released") + + .. versionadded:: 2.4.0 + + .. ## pygame.key.get_just_released ## + .. function:: get_mods | :sl:`determine which modifier keys are being held` diff --git a/src_c/_pygame.h b/src_c/_pygame.h index 6069a51f98..860c90bfde 100644 --- a/src_c/_pygame.h +++ b/src_c/_pygame.h @@ -423,7 +423,7 @@ typedef enum { #define PYGAMEAPI_COLOR_NUMSLOTS 5 #define PYGAMEAPI_MATH_NUMSLOTS 2 #define PYGAMEAPI_BASE_NUMSLOTS 24 -#define PYGAMEAPI_EVENT_NUMSLOTS 6 +#define PYGAMEAPI_EVENT_NUMSLOTS 8 #define PYGAMEAPI_WINDOW_NUMSLOTS 1 #endif /* _PYGAME_INTERNAL_H */ diff --git a/src_c/doc/key_doc.h b/src_c/doc/key_doc.h index 277eb7d086..341ea20350 100644 --- a/src_c/doc/key_doc.h +++ b/src_c/doc/key_doc.h @@ -2,6 +2,8 @@ #define DOC_KEY "pygame module to work with the keyboard" #define DOC_KEY_GETFOCUSED "get_focused() -> bool\ntrue if the display is receiving keyboard input from the system" #define DOC_KEY_GETPRESSED "get_pressed() -> bools\nget the state of all keyboard buttons" +#define DOC_KEY_GETJUSTPRESSED "get_just_pressed() -> bools\nreturns a pygame.key.ScancodeWrapper containing the most recent key presses" +#define DOC_KEY_GETJUSTRELEASED "get_just_pressed() -> bools\nreturns a pygame.key.ScancodeWrapper containing the most recent key releases" #define DOC_KEY_GETMODS "get_mods() -> int\ndetermine which modifier keys are being held" #define DOC_KEY_SETMODS "set_mods(int) -> None\ntemporarily set which modifier keys are pressed" #define DOC_KEY_SETREPEAT "set_repeat() -> None\nset_repeat(delay) -> None\nset_repeat(delay, interval) -> None\ncontrol how held keys are repeated" diff --git a/src_c/event.c b/src_c/event.c index cd381f95d7..20abb5b919 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -92,6 +92,10 @@ static SDL_TimerID _pg_repeat_timer = 0; static SDL_Event _pg_repeat_event; static SDL_Event _pg_last_keydown_event = {0}; +/* Not used as text, acts as an array of bools */ +static char pressed_keys[SDL_NUM_SCANCODES] = {0}; +static char released_keys[SDL_NUM_SCANCODES] = {0}; + #ifdef __EMSCRIPTEN__ /* these macros are no-op here */ #define PG_LOCK_EVFILTER_MUTEX @@ -492,6 +496,7 @@ pg_event_filter(void *_, SDL_Event *event) return 0; PG_LOCK_EVFILTER_MUTEX + pressed_keys[event->key.keysym.scancode] = 1; if (pg_key_repeat_delay > 0) { if (_pg_repeat_timer) SDL_RemoveTimer(_pg_repeat_timer); @@ -521,6 +526,7 @@ pg_event_filter(void *_, SDL_Event *event) else if (event->type == SDL_KEYUP) { PG_LOCK_EVFILTER_MUTEX + released_keys[event->key.keysym.scancode] = 1; if (_pg_repeat_timer && _pg_repeat_event.key.keysym.scancode == event->key.keysym.scancode) { SDL_RemoveTimer(_pg_repeat_timer); @@ -1587,8 +1593,14 @@ static void _pg_event_pump(int dopump) { if (dopump) { + /* This needs to be reset just before calling pump, e.g. on calls to + * pygame.event.get(), but not on pygame.event.get(pump=False). */ + memset(pressed_keys, 0, sizeof(pressed_keys)); + memset(released_keys, 0, sizeof(released_keys)); + SDL_PumpEvents(); } + /* We need to translate WINDOWEVENTS. But if we do that from the * from event filter, internal SDL stuff that rely on WINDOWEVENT * might break. So after every event pump, we translate events from @@ -1768,6 +1780,18 @@ _pg_event_append_to_list(PyObject *list, SDL_Event *event) return 1; } +char * +pgEvent_GetKeyDownInfo(void) +{ + return pressed_keys; +} + +char * +pgEvent_GetKeyUpInfo(void) +{ + return released_keys; +} + static PyObject * _pg_get_all_events_except(PyObject *obj) { @@ -2274,13 +2298,15 @@ MODINIT_DEFINE(event) } /* export the c api */ - assert(PYGAMEAPI_EVENT_NUMSLOTS == 6); + assert(PYGAMEAPI_EVENT_NUMSLOTS == 8); c_api[0] = &pgEvent_Type; c_api[1] = pgEvent_New; c_api[2] = pg_post_event; c_api[3] = pg_post_event_dictproxy; c_api[4] = pg_EnableKeyRepeat; c_api[5] = pg_GetKeyRepeat; + c_api[6] = pgEvent_GetKeyDownInfo; + c_api[7] = pgEvent_GetKeyUpInfo; apiobj = encapsulate_api(c_api, "event"); if (PyModule_AddObject(module, PYGAMEAPI_LOCAL_ENTRY, apiobj)) { diff --git a/src_c/include/_pygame.h b/src_c/include/_pygame.h index bec945b998..88d4fc0db0 100644 --- a/src_c/include/_pygame.h +++ b/src_c/include/_pygame.h @@ -376,6 +376,10 @@ typedef struct pgEventObject pgEventObject; #define pg_GetKeyRepeat (*(void (*)(int *, int *))PYGAMEAPI_GET_SLOT(event, 5)) +#define pgEvent_GetKeyDownInfo (*(char *(*)(void))PYGAMEAPI_GET_SLOT(event, 6)) + +#define pgEvent_GetKeyUpInfo (*(char *(*)(void))PYGAMEAPI_GET_SLOT(event, 7)) + #define import_pygame_event() IMPORT_PYGAME_MODULE(event) #endif diff --git a/src_c/key.c b/src_c/key.c index e7f28e744e..27c211a2aa 100644 --- a/src_c/key.c +++ b/src_c/key.c @@ -194,6 +194,62 @@ key_get_pressed(PyObject *self, PyObject *_null) return ret_obj; } +static PyObject * +get_just_pressed(PyObject *self, PyObject *_null) +{ + VIDEO_INIT_CHECK(); + + char *pressed_keys = pgEvent_GetKeyDownInfo(); + PyObject *key_tuple = NULL; + PyObject *ret_obj = NULL; + + if (!(key_tuple = PyTuple_New(SDL_NUM_SCANCODES))) + return NULL; + + int i; + for (i = 0; i < SDL_NUM_SCANCODES; i++) { + PyObject *key_elem; + key_elem = PyBool_FromLong(pressed_keys[i]); + if (!key_elem) { + Py_DECREF(key_tuple); + return NULL; + } + PyTuple_SET_ITEM(key_tuple, i, key_elem); + } + ret_obj = PyObject_CallFunctionObjArgs((PyObject *)&pgScancodeWrapper_Type, + key_tuple, NULL); + Py_DECREF(key_tuple); + return ret_obj; +} + +static PyObject * +get_just_released(PyObject *self, PyObject *_null) +{ + VIDEO_INIT_CHECK(); + + char *released_keys = pgEvent_GetKeyUpInfo(); + PyObject *key_tuple = NULL; + PyObject *ret_obj = NULL; + + if (!(key_tuple = PyTuple_New(SDL_NUM_SCANCODES))) + return NULL; + + int i; + for (i = 0; i < SDL_NUM_SCANCODES; i++) { + PyObject *key_elem; + key_elem = PyBool_FromLong(released_keys[i]); + if (!key_elem) { + Py_DECREF(key_tuple); + return NULL; + } + PyTuple_SET_ITEM(key_tuple, i, key_elem); + } + ret_obj = PyObject_CallFunctionObjArgs((PyObject *)&pgScancodeWrapper_Type, + key_tuple, NULL); + Py_DECREF(key_tuple); + return ret_obj; +} + /* Keep our own key-name table for backwards compatibility. * This has to be kept updated (only new things can be added, existing records * in this must not be changed). @@ -522,6 +578,10 @@ static PyMethodDef _key_methods[] = { DOC_KEY_STOPTEXTINPUT}, {"set_text_input_rect", key_set_text_input_rect, METH_O, DOC_KEY_SETTEXTINPUTRECT}, + {"get_just_pressed", (PyCFunction)get_just_pressed, METH_NOARGS, + DOC_KEY_GETJUSTPRESSED}, + {"get_just_released", (PyCFunction)get_just_released, METH_NOARGS, + DOC_KEY_GETJUSTRELEASED}, {NULL, NULL, 0, NULL}}; diff --git a/test/key_test.py b/test/key_test.py index ddcec61e0a..f09c59edd8 100644 --- a/test/key_test.py +++ b/test/key_test.py @@ -234,6 +234,14 @@ def test_get_pressed(self): states = pygame.key.get_pressed() self.assertEqual(states[pygame.K_RIGHT], 0) + def test_get_just_pressed(self): + pressed_keys = pygame.key.get_just_pressed() + self.assertEqual(pressed_keys[pygame.K_RIGHT], 0) + + def test_get_just_released(self): + released_keys = pygame.key.get_just_released() + self.assertEqual(released_keys[pygame.K_RIGHT], 0) + def test_get_pressed_not_iter(self): states = pygame.key.get_pressed() with self.assertRaises(TypeError):