Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion buildconfig/stubs/pygame/event.pyi
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, ClassVar, TypeAlias, final
from typing import Any, Callable, ClassVar, Optional, TypeAlias, Union, final

from pygame.typing import SequenceLike
from typing_extensions import deprecated # added in 3.13
Expand Down Expand Up @@ -53,3 +53,7 @@ def set_grab(grab: bool, /) -> None: ...
def get_grab() -> bool: ...
def post(event: Event, /) -> bool: ...
def custom_type() -> int: ...
def add_event_watcher[T: Callable[[Event], Any]](watcher: T, /) -> T: ...
def remove_event_watcher[T: Callable[[Event], Any]](watcher: T, /) -> None: ...
def add_event_filter[T: Callable[[Event], Any]](filter: T, /) -> T: ...
def remove_event_filter[T: Callable[[Event], Any]](filter: T, /) -> None: ...
40 changes: 40 additions & 0 deletions docs/reST/ref/event.rst
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,46 @@ On Android, the following events can be generated

.. ## pygame.event.custom_type ##

.. function:: add_event_watcher

| :sl:`add an event watcher`
| :sg:`add_event_watcher[T: Callable[[Event], Any]](T) -> T`

Adds an event watcher
The provided function should take a ``Event`` and may return any value.

For convenience this function returns its parameter.

WARNING: The passed function may be called in a separate thread.
The watcher is called when events are added to the queue.
The queue processing may occur immediately or when ``pygame.event.poll`` / ``pygame.event.get`` is called.
This function is not called if the event is blocked or filtered out.

If an error is generated in this function it is caught
and re-raised in the main loop of the program
when ``pygame.event.poll``, ``pygame.event.get``,
``pygame.event.pump``, etc. are called.


.. versionadded:: 2.5.7

.. ## pygame.event.add_event_watcher ##

.. function:: remove_event_watcher

| :sl:`remove an event watcher`
| :sg:`remove_event_watcher[T: (Event) -> Any](T) -> bool`

Removes a already existing event watcher.

The object passed to this function should be the same as was passed to ``add_event_watcher``.

This returns a boolean on whether the watcher could be removed or not.

.. versionadded:: 2.5.7

.. ## pygame.event.remove_event_watcher ##

.. class:: Event

| :sl:`pygame object for representing events`
Expand Down
2 changes: 2 additions & 0 deletions src_c/doc/event_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
#define DOC_EVENT_GETGRAB "get_grab() -> bool\ntest if the program is sharing input devices"
#define DOC_EVENT_POST "post(event, /) -> bool\nplace a new event on the queue"
#define DOC_EVENT_CUSTOMTYPE "custom_type() -> int\nmake custom user event type"
#define DOC_EVENT_ADDEVENTWATCHER "add_event_watcher[T: Callable[[Event], Any]](T) -> T\nadd an event watcher"
#define DOC_EVENT_REMOVEEVENTWATCHER "remove_event_watcher[T: (Event) -> Any](T) -> bool\nremove an event watcher"
#define DOC_EVENT_EVENT "Event(type, dict) -> Event\nEvent(type, **attributes) -> Event\npygame object for representing events"
#define DOC_EVENT_EVENT_TYPE "type -> int\nevent type identifier."
#define DOC_EVENT_EVENT_DICT "__dict__ -> dict\nevent attribute dictionary"
186 changes: 186 additions & 0 deletions src_c/event.c
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,25 @@ _pg_repeat_callback(Uint32 interval, void *param)
return repeat_interval_copy;
}

/* This variable and macro allow for errors raised outside
* of the normal execution of python to be caught and re-raised
* in the main loop of the program.
*/

static PyObject *_pg_deferred_error = NULL;

#define CHECK_AND_RAISE_DEFERRED_ERROR \
if (_pg_deferred_error) { \
PyErr_SetRaisedException(_pg_deferred_error); \
_pg_deferred_error = NULL; \
return NULL; \
}

/* A set containing all active event_watchers
* Used to ensure that the reference counting of the watchers stays stable
*/
static PyObject *_pg_event_watcher_list = NULL;

/* This function attempts to determine the unicode attribute from
* the keydown/keyup event. This is used as a last-resort, in case we
* could not determine the unicode from TEXTINPUT field. Why?
Expand Down Expand Up @@ -554,6 +573,9 @@ _pg_remove_pending_VIDEOEXPOSE(void *userdata, SDL_Event *event)
}
return 1;
}
// Forward declare pg_eventNew_no_free_proxy to be used in pg_event_filter
static PyObject *
pg_eventNew_no_free_proxy(SDL_Event *event);

/* SDL 2 to SDL 1.2 event mapping and SDL 1.2 key repeat emulation,
* this can alter events in-place.
Expand Down Expand Up @@ -798,6 +820,7 @@ pgEvent_AutoQuit(PyObject *self, PyObject *_null)
* stops returning new types when they are finished, without that
* test preventing further tests from getting a custom event type.*/
_custom_event = _PGE_CUSTOM_EVENT_INIT;
Py_DECREF(_pg_event_watcher_list);
}
_pg_event_is_init = 0;
Py_RETURN_NONE;
Expand All @@ -819,6 +842,11 @@ pgEvent_AutoInit(PyObject *self, PyObject *_null)
}
#endif
SDL_SetEventFilter(pg_event_filter, NULL);

_pg_event_watcher_list = PyList_New(0);
if (!_pg_event_watcher_list) {
return NULL;
}
#if SDL_VERSION_ATLEAST(3, 0, 0)
if (Gesture_Init() != 0) {
return RAISE(pgExc_SDLError, SDL_GetError());
Expand Down Expand Up @@ -1948,6 +1976,7 @@ _pg_event_wait(SDL_Event *event, int timeout)
static PyObject *
pg_event_pump(PyObject *self, PyObject *_null)
{
CHECK_AND_RAISE_DEFERRED_ERROR;
VIDEO_INIT_CHECK();
_pg_event_pump(1);
Py_RETURN_NONE;
Expand All @@ -1956,6 +1985,7 @@ pg_event_pump(PyObject *self, PyObject *_null)
static PyObject *
pg_event_poll(PyObject *self, PyObject *_null)
{
CHECK_AND_RAISE_DEFERRED_ERROR;
SDL_Event event;
VIDEO_INIT_CHECK();

Expand All @@ -1969,6 +1999,7 @@ pg_event_poll(PyObject *self, PyObject *_null)
static PyObject *
pg_event_wait(PyObject *self, PyObject *args, PyObject *kwargs)
{
CHECK_AND_RAISE_DEFERRED_ERROR;
SDL_Event event;
int status, timeout = 0;
static char *kwids[] = {"timeout", NULL};
Expand Down Expand Up @@ -2330,6 +2361,7 @@ _pg_get_seq_events(PyObject *obj)
static PyObject *
pg_event_get(PyObject *self, PyObject *args, PyObject *kwargs)
{
CHECK_AND_RAISE_DEFERRED_ERROR;
PyObject *obj_evtype = NULL;
PyObject *obj_exclude = NULL;
int dopump = 1;
Expand Down Expand Up @@ -2364,6 +2396,7 @@ pg_event_get(PyObject *self, PyObject *args, PyObject *kwargs)
static PyObject *
pg_event_peek(PyObject *self, PyObject *args, PyObject *kwargs)
{
CHECK_AND_RAISE_DEFERRED_ERROR;
SDL_Event event;
Py_ssize_t len;
int type, loop, res;
Expand Down Expand Up @@ -2433,6 +2466,7 @@ pg_event_peek(PyObject *self, PyObject *args, PyObject *kwargs)
static PyObject *
pg_event_post(PyObject *self, PyObject *obj)
{
CHECK_AND_RAISE_DEFERRED_ERROR;
VIDEO_INIT_CHECK();
if (!pgEvent_Check(obj)) {
return RAISE(PyExc_TypeError, "argument must be an Event object");
Expand Down Expand Up @@ -2562,6 +2596,154 @@ pg_event_custom_type(PyObject *self, PyObject *_null)
}
}

/*
Constructs a Event object but if it is using a dict proxy it doesn't free the
proxy Used when other pygame functions will also be using the event to avoid it
being freed
*/
static PyObject *
pg_eventNew_no_free_proxy(SDL_Event *event)
{
if (event->type >= PGPOST_EVENTBEGIN) {
// Since the dict_proxy has a counter for how many are on the queue, we
// need to increase that counter
pgEventDictProxy *dict_proxy = (pgEventDictProxy *)event->user.data1;
// We only need to do anything if the dict_proxy exists since if it
// doesn't that implies an empty dict
if (dict_proxy) {
SDL_AtomicLock(&dict_proxy->lock);
// So when it gets decremented in pg_EventNew it goes back to where
// it was
dict_proxy->num_on_queue++;
// So that if it gets dropped to zero it wont get freed and risk a
// double free later on
int proxy_frees_on_end = dict_proxy->do_free_at_end;
dict_proxy->do_free_at_end = false;
SDL_AtomicUnlock(&dict_proxy->lock);
// Contruct the event object
PyObject *eventObj = pgEvent_New(event);
// Restore the state of do_free_at_end
dict_proxy->do_free_at_end = proxy_frees_on_end;
return eventObj;
}
}
// If the event is not a posted event than there is no dict_proxy to risk
// issues so the event object constructer can be called as-is
return pgEvent_New(event);
}

/*
A wrapper around a callable python object to be used by SDL
*/
#if !SDL_VERSION_ATLEAST(3, 0, 0)
static int
#else
static bool
#endif
pg_watcher_wrapper(void *userdata, SDL_Event *event)
{
PyObject *callable = (PyObject *)userdata;
PyGILState_STATE gstate = PyGILState_Ensure();
/* WINDOWEVENT translation needed only on SDL2 */
#if !SDL_VERSION_ATLEAST(3, 0, 0)
/* 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
* here */
// We make a local copy of the event on the stack and use that so that the
// original event is not modified
SDL_Event localEvent = *event;
// _pg_pgevent_type(NULL, &localEvent);

PyObject *eventObj = pg_eventNew_no_free_proxy(&localEvent);
#else
PyObject *eventObj = pg_eventNew_no_free_proxy(event);
#endif
if (PyErr_Occurred()) {
Py_XDECREF(eventObj);
PyGILState_Release(gstate);
PyErr_Print();
PyErr_Clear();
return 0;
}
if (!eventObj) {
PyGILState_Release(gstate);
return 0;
}
PyObject *returnValue = PyObject_CallOneArg(callable, eventObj);
Py_DECREF(eventObj);
if (PyErr_Occurred()) {
Py_XDECREF(returnValue);
PyGILState_Release(gstate);
_pg_deferred_error = PyErr_GetRaisedException();
Py_INCREF(_pg_deferred_error);
PyErr_Clear();
return 0;
}
Py_DECREF(returnValue);
PyGILState_Release(gstate);
return 0;
}

/*
Add a function as an event watcher

*/
static PyObject *
pg_event_add_watcher(PyObject *self, PyObject *arg)
{
VIDEO_INIT_CHECK();

if (PyCallable_Check(arg)) {
int result = PyList_Append(_pg_event_watcher_list, arg);
if (result == -1) {
return NULL;
}
SDL_AddEventWatch(pg_watcher_wrapper, arg);
}
else {
PyErr_SetString(PyExc_ValueError,
"event watchers must be callable objects");
return NULL;
}
// Increase reference count of param and return it so that this could be
// used as a decorator
Py_INCREF(arg);
return arg;
}

static PyObject *
pg_event_remove_watcher(PyObject *self, PyObject *arg)
{
VIDEO_INIT_CHECK();
int registered = PySequence_Contains(_pg_event_watcher_list, arg);
if (registered == 0) {
Py_RETURN_FALSE;
}
if (registered == -1) {
return NULL;
}

// This function does nothing if arg is not currently set as a watch
#if SDL_VERSION_ATLEAST(3, 0, 0)
// SDL 3 renamed DelEventWatch to RemoveEventWatch
SDL_RemoveEventWatch(pg_watcher_wrapper, arg);
#else
SDL_DelEventWatch(pg_watcher_wrapper, arg);
#endif

Py_ssize_t index = PySequence_Index(_pg_event_watcher_list, arg);
if (index == -1) {
return NULL;
}
int removalResult = PySequence_DelItem(_pg_event_watcher_list, index);
if (removalResult == -1) {
return NULL;
}

Py_RETURN_TRUE;
}

static PyMethodDef _event_methods[] = {
{"_internal_mod_init", (PyCFunction)pgEvent_AutoInit, METH_NOARGS,
"auto initialize for event module"},
Expand Down Expand Up @@ -2591,6 +2773,10 @@ static PyMethodDef _event_methods[] = {
DOC_EVENT_SETBLOCKED},
{"get_blocked", (PyCFunction)pg_event_get_blocked, METH_O,
DOC_EVENT_GETBLOCKED},
{"add_event_watcher", (PyCFunction)pg_event_add_watcher, METH_O,
DOC_EVENT_ADDEVENTWATCHER},
{"remove_event_watcher", (PyCFunction)pg_event_remove_watcher, METH_O,
DOC_EVENT_REMOVEEVENTWATCHER},
{"custom_type", (PyCFunction)pg_event_custom_type, METH_NOARGS,
DOC_EVENT_CUSTOMTYPE},

Expand Down
Loading