Skip to content
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: ...
66 changes: 66 additions & 0 deletions docs/reST/ref/event.rst
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,72 @@ 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 printed to stderr and otherwise ignored.

.. versionaddedold:: todo

.. ## pygame.event.add_event_watcher ##

.. function:: remove_event_watcher

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

Removes a already existing event watcher.

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

.. versionaddedold:: todo

.. ## pygame.event.remove_event_watcher ##

.. function:: add_event_filter

| :sl:`add an event filter`
| :sg:`add_event_filter[T: (Event) -> Any](T) -> T`

Adds an event filter
The provided function should take a ``Event`` and should return a truthy value to skip the event.

For convenience this function returns its parameter.

WARNING: The passed function may be called in a separate thread.
The filter is called before events are added to the queue.
This function is not called if the event is blocked.

If an error is generated in this function it is printed to stderr and otherwise ignored.

.. versionaddedold:: todo

.. ## pygame.event.add_event_filter ##

.. function:: remove_event_filter

| :sl:`remove an event filter`
| :sg:`remove_event_filter[T: (Event) -> Any](T) -> None`

Removes an already existing event filter.
The object passed to this function should be the same as was passed to ``add_event_filter``.

.. versionaddedold:: todo

.. ## pygame.event.remove_event_filter ##

.. class:: Event

| :sl:`pygame object for representing events`
Expand Down
4 changes: 4 additions & 0 deletions src_c/doc/event_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
#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) -> None\nremove an event watcher"
#define DOC_EVENT_ADDEVENTFILTER "add_event_filter[T: (Event) -> Any](T) -> T\nadd an event filter"
#define DOC_EVENT_REMOVEEVENTFILTER "remove_event_filter[T: (Event) -> Any](T) -> None\nremove an event filter"
#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"
232 changes: 231 additions & 1 deletion src_c/event.c
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ static char released_keys[SDL_NUM_SCANCODES] = {0};
static char pressed_mouse_buttons[5] = {0};
static char released_mouse_buttons[5] = {0};

/*The filters for events added by the user*/
static PyObject *userFilters;

#ifdef __EMSCRIPTEN__
/* these macros are no-op here */
#define PG_LOCK_EVFILTER_MUTEX
Expand Down Expand Up @@ -544,6 +547,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 @@ -732,6 +738,7 @@ pg_event_filter(void *_, SDL_Event *event)
return RAISE(pgExc_SDLError, SDL_GetError()), 0;
*/
}
// If the event has been disabled, skip filtering it
/* TODO:
* Any event that gets blocked here will not be visible to the event
* watchers. So things like WINDOWEVENT should never be blocked here.
Expand All @@ -740,7 +747,45 @@ pg_event_filter(void *_, SDL_Event *event)
* If the user requests a block on WINDOWEVENTs we are going to handle
* it specially and call it a "pseudo-block", where the filtering will
* happen in a _pg_filter_blocked_events call. */
return PG_EventEnabled(_pg_pgevent_proxify(event->type));
if (!PG_EventEnabled(_pg_pgevent_proxify(event->type))) {
return false;
}

PG_LOCK_EVFILTER_MUTEX

PyGILState_STATE gstate = PyGILState_Ensure();

Py_ssize_t totalUserFilters = PyList_Size(userFilters);
for (Py_ssize_t i = 0; i < totalUserFilters; i++) {
PyObject *filter = PyList_GetItem(userFilters, i);
PyObject *eventObj = pg_eventNew_no_free_proxy(event);
if (!eventObj) {
PyErr_Print();
PyErr_Clear();
break;
}
PyObject *returnValue = PyObject_CallOneArg(filter, eventObj);
Py_DECREF(eventObj);
if (!returnValue) {
PyGILState_Release(gstate);
PG_UNLOCK_EVFILTER_MUTEX

PyErr_Print();
PyErr_Clear();
return true;
}
int skip = PyObject_IsTrue(returnValue);
Py_DECREF(returnValue);
if (skip) {
PyGILState_Release(gstate);
PG_UNLOCK_EVFILTER_MUTEX
return false;
}
}
PyGILState_Release(gstate);
PG_UNLOCK_EVFILTER_MUTEX

return true;
}

/* The two keyrepeat functions below modify state accessed by the event filter,
Expand Down Expand Up @@ -804,6 +849,10 @@ pgEvent_AutoInit(PyObject *self, PyObject *_null)
}
}
#endif
userFilters = PyList_New(0);
if (!userFilters) {
return RAISE(pgExc_SDLError, "Failed to initialize event filters");
}
SDL_SetEventFilter(pg_event_filter, NULL);
}
_pg_event_is_init = 1;
Expand Down Expand Up @@ -2528,6 +2577,179 @@ 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
*/
static int
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_translate_windowevent(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);
PyErr_Print();
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)) {
Py_INCREF(arg);
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();

// 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);
Py_DECREF(arg);
#else
SDL_DelEventWatch(pg_watcher_wrapper, arg);
Py_DECREF(arg);
#endif
Py_RETURN_NONE;
}

/*
These two function access state used in the filter, so they use the mutex lock
*/
static PyObject *
pg_event_add_filter(PyObject *self, PyObject *arg)
{
VIDEO_INIT_CHECK();

if (PyCallable_Check(arg)) {
PG_LOCK_EVFILTER_MUTEX
if (PyList_Append(userFilters, arg) < 0) {
PG_UNLOCK_EVFILTER_MUTEX
return NULL; // PyErr already set
}
PG_UNLOCK_EVFILTER_MUTEX
}
else {
PyErr_SetString(PyExc_ValueError,
"event filters must be callable objects");
return NULL;
}
Py_INCREF(arg);
return arg;
}

static PyObject *
pg_event_remove_filter(PyObject *self, PyObject *arg)
{
VIDEO_INIT_CHECK();

PG_LOCK_EVFILTER_MUTEX
Py_ssize_t index = PySequence_Index(userFilters, arg);
if (index == -1) {
PG_UNLOCK_EVFILTER_MUTEX
if (PyErr_Occurred()) {
// An error occurred during the search
return NULL;
}
return RAISE(PyExc_ValueError, "event filter not found");
}
if (PySequence_DelItem(userFilters, index) < 0) {
PG_UNLOCK_EVFILTER_MUTEX
return NULL; // PyErr already set
}
PG_UNLOCK_EVFILTER_MUTEX
Py_RETURN_NONE;
}

static PyMethodDef _event_methods[] = {
{"_internal_mod_init", (PyCFunction)pgEvent_AutoInit, METH_NOARGS,
"auto initialize for event module"},
Expand Down Expand Up @@ -2557,6 +2779,14 @@ 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},
{"add_event_filter", (PyCFunction)pg_event_add_filter, METH_O,
DOC_EVENT_ADDEVENTFILTER},
{"remove_event_filter", (PyCFunction)pg_event_remove_filter, METH_O,
DOC_EVENT_REMOVEEVENTFILTER},
{"custom_type", (PyCFunction)pg_event_custom_type, METH_NOARGS,
DOC_EVENT_CUSTOMTYPE},

Expand Down
Loading