diff --git a/buildconfig/stubs/pygame/event.pyi b/buildconfig/stubs/pygame/event.pyi index dc9a1c852f..809c8fee2c 100644 --- a/buildconfig/stubs/pygame/event.pyi +++ b/buildconfig/stubs/pygame/event.pyi @@ -2,27 +2,434 @@ from typing import ( Any, Dict, List, + Tuple, Optional, Union, - final, + Literal, + overload, + Type, ) -from ._common import Sequence +from pygame._common import Sequence +from pygame.window import Window -@final -class Event: +class _EventMeta(type): ... + +class Event(metaclass=_EventMeta): type: int dict: Dict[str, Any] __dict__: Dict[str, Any] __hash__: None # type: ignore + @overload + def __init__( + self, type: int, dict: Dict[str, Any] = ..., /, **kwargs: Any + ) -> None: ... + @overload def __init__( - self, type: int, dict: Dict[str, Any] = ..., **kwargs: Any + self, dict: Dict[str, Any] = ..., /, **kwargs: Any ) -> None: ... def __getattribute__(self, name: str) -> Any: ... def __setattr__(self, name: str, value: Any) -> None: ... def __delattr__(self, name: str) -> None: ... def __bool__(self) -> bool: ... +class ActiveEvent(Event): + type: Literal[32768] = 32768 + gain: int + state: int + +class AppTerminating(Event): + type: Literal[257] = 257 + +class AppLowMemory(Event): + type: Literal[258] = 258 + +class AppWillEnterBackground(Event): + type: Literal[259] = 259 + +class AppDidEnterBackground(Event): + type: Literal[260] = 260 + +class AppWillEnterForeground(Event): + type: Literal[261] = 261 + +class AppDidEnterForeground(Event): + type: Literal[262] = 262 + +class ClipboardUpdate(Event): + type: Literal[2304] = 2304 + +class KeyDown(Event): + type: Literal[768] = 768 + unicode: str + key: int + mod: int + scancode: int + window: Optional[Window] + +class KeyUp(Event): + type: Literal[769] = 769 + unicode: str + key: int + mod: int + scancode: int + window: Optional[Window] + +class KeyMapChanged(Event): + type: Literal[772] = 772 + +class LocaleChanged(Event): + """Only for SDL 2.0.14+""" + type: Literal[263] = 263 + +class MouseMotion(Event): + type: Literal[1024] = 1024 + pos: Tuple[int, int] + rel: Tuple[int, int] + buttons: tuple[int, int, int] + touch: bool + window: Optional[Window] + +class MouseButtonDown(Event): + type: Literal[1025] = 1025 + pos: Tuple[int, int] + button: int + touch: bool + window: Optional[Window] + +class MouseButtonUp(Event): + type: Literal[1026] = 1026 + pos: Tuple[int, int] + button: int + touch: bool + window: Optional[Window] + +class JoyAxisMotion(Event): + """Attribute "joy" is depracated, use "instance_id".""" + type: Literal[1536] = 1536 + joy: int + instance_id: int + axis: int + value: float + +class JoyBallMotion(Event): + """Attribute "joy" is depracated, use "instance_id".""" + type: Literal[1537] = 1537 + joy: int + instance_id: int + ball: int + rel: Tuple[int, int] + +class JoyHatMotion(Event): + """Attribute "joy" is depracated, use "instance_id".""" + type: Literal[1538] = 1538 + joy: int + instance_id: int + hat: int + value: Tuple[int, int] + +class JoyButtonUp(Event): + """Attribute "joy" is depracated, use "instance_id".""" + type: Literal[1540] = 1540 + joy: int + instance_id: int + button: int + +class JoyButtonDown(Event): + """Attribute "joy" is depracated, use "instance_id".""" + type: Literal[1539] = 1539 + joy: int + instance_id: int + button: int + +class Quit(Event): + type: Literal[256] = 256 + +class SysWMEvent(Event): + """ + Attributes are OS-depended: + hwnd, msg, wparam, lparam - Windows. + event - Unix / OpenBSD + For other OSes and in some cases for Unix / OpenBSD + this event won't have any attributes. + """ + type: Literal[513] = 513 + hwnd: int + msg: int + wparam: int + lparam: int + event: bytes + +class VideoResize(Event): + type: Literal[32769] = 32769 + size: Tuple[int, int] + w: int + h: int + +class VideoExpose(Event): + type: Literal[32770] = 32770 + +class MidiIn(Event): + type: Literal[32771] = 32771 + +class MidiOut(Event): + type: Literal[32772] = 32772 + +class NoEvent(Event): + type: Literal[0] = 0 + +class FingerMotion(Event): + """Attribute "window" avalible only for SDL 2.0.14+""" + type: Literal[1794] = 1794 + touch_id: int + finger_id: int + x: float + y: float + dx: float + dy: float + pressure: float + window: Optional[Window] + +class FingerDown(Event): + """Attribute "window" avalible only for SDL 2.0.14+""" + type: Literal[1792] = 1792 + touch_id: int + finger_id: int + x: float + y: float + dx: float + dy: float + pressure: float + window: Optional[Window] + +class FingerUp(Event): + """Attribute "window" avalible only for SDL 2.0.14+""" + type: Literal[1793] = 1793 + touch_id: int + finger_id: int + x: float + y: float + dx: float + dy: float + pressure: float + window: Optional[Window] + +class MultiGesture(Event): + type: Literal[2050] = 2050 + touch_id: int + x: float + y: float + rotated: float + pinched: float + num_fingers: int + +class MouseWheel(Event): + type: Literal[1027] = 1027 + flipped: bool + x: int + y: int + precise_x: float + precise_y: float + touch: bool + window: Optional[Window] + +class TextInput(Event): + type: Literal[771] = 771 + text: str + window: Optional[Window] + +class TextEditing(Event): + type: Literal[770] = 770 + text: str + start: int + length: int + window: Optional[Window] + +class DropFile(Event): + type: Literal[4096] = 4096 + file: str + window: Optional[Window] + +class DropText(Event): + type: Literal[4097] = 4097 + text: str + window: Optional[Window] + +class DropBegin(Event): + type: Literal[4098] = 4098 + window: Optional[Window] + +class DropComplete(Event): + type: Literal[4099] = 4099 + window: Optional[Window] + +class ControllerAxisMotion(Event): + type: Literal[1616] = 1616 + instance_id: int + axis: int + value: int + +class ControllerButtonDown(Event): + type: Literal[1617] = 1617 + instance_id: int + button: int + +class ControllerButtonUp(Event): + type: Literal[1618] = 1618 + instance_id: int + button: int + +class ControllerDeviceAdded(Event): + type: Literal[1619] = 1619 + device_index: int + guid: str + +class ControllerDeviceRemoved(Event): + type: Literal[1620] = 1620 + instance_id: int + +class ControllerDeviceMapped(Event): + type: Literal[1621] = 1621 + instance_id: int + +class JoyDeviceAdded(Event): + type: Literal[1541] = 1541 + device_index: int + guid: str + +class JoyDeviceRemoved(Event): + type: Literal[1542] = 1542 + instance_id: int + +class ControllerTouchpadDown(Event): + """Only for SDL 2.0.14+""" + type: Literal[1622] = 1622 + instance_id: int + touch_id: int + finger_id: int + x: float + y: float + pressure: float + +class ControllerTouchpadMotion(Event): + """Only for SDL 2.0.14+""" + type: Literal[1623] = 1623 + instance_id: int + touch_id: int + finger_id: int + x: float + y: float + pressure: float + +class ControllerTouchpadUp(Event): + """Only for SDL 2.0.14+""" + type: Literal[1624] = 1624 + instance_id: int + touch_id: int + finger_id: int + x: float + y: float + pressure: float + +class ControllerSensorUpdate(Event): + """Only for SDL 2.0.14+""" + type: Literal[1625] = 1625 + +class AudioDeviceAdded(Event): + type: Literal[4352] = 4352 + which: int + iscapture: int + +class AudioDeviceRemoved(Event): + type: Literal[4353] = 4353 + which: int + iscapture: int + +class RenderTargetsReset(Event): + type: Literal[8192] = 8192 + +class RenderDeviceReset(Event): + type: Literal[8193] = 8193 + +class WindowShown(Event): + type: Literal[32774] = 32774 + window: Optional[Window] + +class WindowHidden(Event): + type: Literal[32775] = 32775 + window: Optional[Window] + +class WindowExposed(Event): + type: Literal[32776] = 32776 + window: Optional[Window] + +class WindowMoved(Event): + type: Literal[32777] = 32777 + x: int + y: int + window: Optional[Window] + +class WindowResized(Event): + type: Literal[32778] = 32778 + x: int + y: int + window: Optional[Window] + +class WindowSizeChanged(Event): + type: Literal[32779] = 32779 + x: int + y: int + window: Optional[Window] + +class WindowMinimized(Event): + type: Literal[32780] = 32780 + window: Optional[Window] + +class WindowMaximized(Event): + type: Literal[32781] = 32781 + window: Optional[Window] + +class WindowRestored(Event): + type: Literal[32782] = 32782 + window: Optional[Window] + +class WindowEnter(Event): + type: Literal[32783] = 32783 + window: Optional[Window] + +class WindowLeave(Event): + type: Literal[32784] = 32784 + window: Optional[Window] + +class WindowFocusGained(Event): + type: Literal[32785] = 32785 + window: Optional[Window] + +class WindowFocusLost(Event): + type: Literal[32786] = 32786 + window: Optional[Window] + +class WindowClose(Event): + type: Literal[32787] = 32787 + window: Optional[Window] + +class WindowTakeFocus(Event): + type: Literal[32788] = 32788 + window: Optional[Window] + +class WindowHitTest(Event): + type: Literal[32789] = 32789 + window: Optional[Window] + +class WindowICCProfChanged(Event): + type: Literal[32790] = 32790 + window: Optional[Window] + +class WindowDisplayChanged(Event): + type: Literal[32791] = 32791 + display_index: int + window: Optional[Window] + _EventTypes = Union[int, Sequence[int]] def pump() -> None: ... @@ -43,5 +450,8 @@ def set_grab(grab: bool, /) -> None: ... def get_grab() -> bool: ... def post(event: Event, /) -> bool: ... def custom_type() -> int: ... +def event_class(type: int, /) -> Type[Event]: ... +def __dir__() -> List[str]: ... +def __getattr__(name: str, /) -> object: ... EventType = Event diff --git a/docs/reST/c_api/event.rst b/docs/reST/c_api/event.rst index 7f053faf3d..278a43b240 100644 --- a/docs/reST/c_api/event.rst +++ b/docs/reST/c_api/event.rst @@ -59,10 +59,10 @@ Header file: src_c/include/pygame.h Return an array of bools (using char) of length 5 with the most recent button releases. -.. c:function:: int pg_post_event(Uint32 type, PyObject *dict) +.. c:function:: int pg_post_event(Uint32 type, PyObject *obj) Posts a pygame event that is an ``SDL_USEREVENT`` on the SDL side. This - function takes a python dict, which can be NULL too. + function takes a python dict/event object, which can be NULL too. This function does not need GIL to be held if dict is NULL, but needs GIL otherwise. Just like the SDL ``SDL_PushEvent`` function, returns 1 on success, 0 if the event was not posted due to it being blocked, and -1 on @@ -79,3 +79,9 @@ Header file: src_c/include/pygame.h creation of the dictproxy instance, and when it is freed. Just like the SDL ``SDL_PushEvent`` function, returns 1 on success, 0 if the event was not posted due to it being blocked, and -1 on failure. + +.. c:function:: PyObject* pgEvent_GetClass(Uint32 type) + + Returns a python class object correlated with the given event type - object is returned + as a new reference. On error returns NULL and sets python exception. + Same as calling ``pygame.event.event_class(type)`` in python. diff --git a/docs/reST/ref/event.rst b/docs/reST/ref/event.rst index 89562572a9..989baa24e1 100644 --- a/docs/reST/ref/event.rst +++ b/docs/reST/ref/event.rst @@ -463,6 +463,19 @@ On Android, the following events can be generated .. ## pygame.event.custom_type ## +.. function:: event_class + + | :sl:`returns related event class to event type` + | :sg:`event_class(type: int, /) -> type[Event]` + + Returns an event class that is correlated with the given event type. If the class to a given event type is not found, + but the type is within the range of valid values for the event type, instead of a ``pygame.event.Event`` subclass, + ``pygame.event.Event`` itself will be returned, so don't rely on the retuned class having ``type`` attribute equal to a number. + This happens for example, with user event types that weren't created by subclassing ``pygame.event.Event``. + + .. versionadded:: 2.5.0 + .. ## pygame.event.event_class ## + .. class:: Event | :sl:`pygame object for representing events` @@ -477,6 +490,8 @@ On Android, the following events can be generated .. versionchanged:: 2.1.4 This class is also available through the ``pygame.Event`` alias. + + .. versionchanged:: 2.5.0 This class can be subclassed to create user-defined event types. .. note:: From version 2.1.3 ``EventType`` is an alias for ``Event``. Beforehand, @@ -515,4 +530,84 @@ On Android, the following events can be generated .. ## pygame.event.Event ## +.. table:: List of Event subclasses available as aliases for event types importable as ``pygame.event.{Class name}``. + :widths: auto + + ============================= ============================= ========================== + Class name Alias to Notes + ============================= ============================= ========================== + ``ActiveEvent`` ``ACTIVEEVENT`` + ``AppTerminating`` ``APP_TERMINATING`` + ``AppLowMemory`` ``APP_LOWMEMORY`` + ``AppWillEnterBackground`` ``APP_WILLENTERBACKGROUND`` + ``AppDidEnterBackground`` ``APP_DIDENTERBACKGROUND`` + ``AppWillEnterForeground`` ``APP_WILLENTERFOREGROUND`` + ``AppDidEnterForeground`` ``APP_DIDENTERFOREGROUND`` + ``ClipboardUpdate`` ``CLIPBOARDUPDATE`` + ``KeyDown`` ``KEYDOWN`` + ``KeyUp`` ``KEYUP`` + ``KeyMapChanged`` ``KEYMAPCHANGED`` + ``LocaleChanged`` ``LOCALECHANGED`` Only for SDL 2.0.14+ + ``MouseMotion`` ``MOUSEMOTION`` + ``MouseButtonDown`` ``MOUSEBUTTONDOWN`` + ``MouseButtonUp`` ``MOUSEBUTTONUP`` + ``JoyAxisMotion`` ``JOYAXISMOTION`` + ``JoyBallMotion`` ``JOYBALLMOTION`` + ``JoyHatMotion`` ``JOYHATMOTION`` + ``JoyButtonUp`` ``JOYBUTTONUP`` + ``JoyButtonDown`` ``JOYBUTTONDOWN`` + ``Quit`` ``QUIT`` + ``SysWMEvent`` ``SYSWMEVENT`` + ``VideoResize`` ``VIDEORESIZE`` + ``VideoExpose`` ``VIDEOEXPOSE`` + ``MidiIn`` ``MIDIIN`` + ``MidiOut`` ``MIDIOUT`` + ``NoEvent`` ``NOEVENT`` + ``FingerMotion`` ``FINGERMOTION`` + ``FingerDown`` ``FINGERDOWN`` + ``FingerUp`` ``FINGERUP`` + ``MultiGesture`` ``MULTIGESTURE`` + ``MouseWheel`` ``MOUSEWHEEL`` + ``TextInput`` ``TEXTINPUT`` + ``TextEditing`` ``TEXTEDITING`` + ``DropFile`` ``DROPFILE`` + ``DropText`` ``DROPTEXT`` + ``DropBegin`` ``DROPBEGIN`` + ``DropComplete`` ``DROPCOMPLETE`` + ``ControllerAxisMotion`` ``CONTROLLERAXISMOTION`` + ``ControllerButtonDown`` ``CONTROLLERBUTTONDOWN`` + ``ControllerButtonUp`` ``CONTROLLERBUTTONUP`` + ``ControllerDeviceAdded`` ``CONTROLLERDEVICEADDED`` + ``ControllerDeviceRemoved`` ``CONTROLLERDEVICEREMOVED`` + ``ControllerDeviceMapped`` ``CONTROLLERDEVICEREMAPPED`` + ``JoyDeviceAdded`` ``JOYDEVICEADDED`` + ``JoyDeviceRemoved`` ``JOYDEVICEREMOVED`` + ``ControllerTouchpadDown`` ``CONTROLLERTOUCHPADDOWN`` Only for SDL 2.0.14+ + ``ControllerTouchpadMotion`` ``CONTROLLERTOUCHPADMOTION`` Only for SDL 2.0.14+ + ``ControllerTouchpadUp`` ``CONTROLLERTOUCHPADUP`` Only for SDL 2.0.14+ + ``ControllerSensorUpdate`` ``CONTROLLERSENSORUPDATE`` Only for SDL 2.0.14+ + ``AudioDeviceAdded`` ``AUDIODEVICEADDED`` + ``AudioDeviceRemoved`` ``AUDIODEVICEREMOVED`` + ``RenderTargetsReset`` ``RENDER_TARGETS_RESET`` + ``RenderDeviceReset`` ``RENDER_DEVICE_RESET`` + ``WindowShown`` ``WINDOWSHOWN`` + ``WindowHidden`` ``WINDOWHIDDEN`` + ``WindowExposed`` ``WINDOWEXPOSED`` + ``WindowMoved`` ``WINDOWMOVED`` + ``WindowResized`` ``WINDOWRESIZED`` + ``WindowSizeChanged`` ``WINDOWSIZECHANGED`` + ``WindowMinimized`` ``WINDOWMINIMIZED`` + ``WindowMaximized`` ``WINDOWMAXIMIZED`` + ``WindowRestored`` ``WINDOWRESTORED`` + ``WindowEnter`` ``WINDOWENTER`` + ``WindowLeave`` ``WINDOWLEAVE`` + ``WindowFocusGained`` ``WINDOWFOCUSGAINED`` + ``WindowFocusLost`` ``WINDOWFOCUSLOST`` + ``WindowClose`` ``WINDOWCLOSE`` + ``WindowTakeFocus`` ``WINDOWTAKEFOCUS`` + ``WindowHitTest`` ``WINDOWHITTEST`` + ``WindowICCProfChanged`` ``WINDOWICCPROFCHANGED`` + ``WindowDisplayChanged`` ``WINDOWDISPLAYCHANGED`` + ============================= ============================= ========================== + .. ## pygame.event ## diff --git a/meson_options.txt b/meson_options.txt index c34007090a..fc5228d30f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -24,12 +24,12 @@ option('midi', type: 'feature', value: 'enabled') # Controls whether to make a "stripped" pygame install. Enabling this disables # the bundling of docs/examples/tests/stubs in the wheels. # The default behaviour is to bundle all of these. -option('stripped', type: 'boolean', value: 'false') +option('stripped', type: 'boolean', value: false) # Controls whether to compile with -Werror (or its msvc equivalent). The default # behaviour is to not do this by default -option('error_on_warns', type: 'boolean', value: 'false') +option('error_on_warns', type: 'boolean', value: false) # Controls whether to error on build if generated docs are missing. Defaults to # false. -option('error_docs_missing', type: 'boolean', value: 'false') +option('error_docs_missing', type: 'boolean', value: false) diff --git a/src_c/_pygame.h b/src_c/_pygame.h index aa13a70bb0..a8762c4249 100644 --- a/src_c/_pygame.h +++ b/src_c/_pygame.h @@ -196,14 +196,14 @@ PG_SurfaceHasRLE(SDL_Surface *surface); #endif -/* DictProxy is useful for event posting with an arbitrary dict. Maintains - * state of number of events on queue and whether the owner of this struct - * wants this dict freed. This DictProxy is only to be freed when there are no - * more instances of this DictProxy on the event queue. Access to this is - * safeguarded with a per-proxy spinlock, which is more optimal than having - * to hold GIL in case of event timers */ +/* DictProxy is useful for event posting with an arbitrary dict/event object. + * Maintains state of number of events on queue and whether the owner of this + * struct wants this dict/event instance freed. This DictProxy is only to be + * freed when there are no more instances of this DictProxy on the event queue. + * Access to this is safeguarded with a per-proxy spinlock, which is more + * optimal than having to hold GIL in case of event timers */ typedef struct _pgEventDictProxy { - PyObject *dict; + PyObject *obj; // Either dict or event object. SDL_SpinLock lock; int num_on_queue; Uint8 do_free_at_end; @@ -533,7 +533,7 @@ typedef enum { #define PYGAMEAPI_COLOR_NUMSLOTS 5 #define PYGAMEAPI_MATH_NUMSLOTS 2 #define PYGAMEAPI_BASE_NUMSLOTS 29 -#define PYGAMEAPI_EVENT_NUMSLOTS 10 +#define PYGAMEAPI_EVENT_NUMSLOTS 11 #define PYGAMEAPI_WINDOW_NUMSLOTS 1 #define PYGAMEAPI_GEOMETRY_NUMSLOTS 1 diff --git a/src_c/base.c b/src_c/base.c index 2a609e6601..03f58868e7 100644 --- a/src_c/base.c +++ b/src_c/base.c @@ -296,6 +296,10 @@ pg_mod_autoquit(const char *modname) funcobj = PyObject_GetAttrString(module, "_internal_mod_quit"); + /* Silence errors */ + if (PyErr_Occurred()) + PyErr_Clear(); + /* If we could not load _internal_mod_quit, load quit function */ if (!funcobj) funcobj = PyObject_GetAttrString(module, "quit"); diff --git a/src_c/doc/event_doc.h b/src_c/doc/event_doc.h index b23e5dce05..ff5290161c 100644 --- a/src_c/doc/event_doc.h +++ b/src_c/doc/event_doc.h @@ -14,6 +14,7 @@ #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_EVENTCLASS "event_class(type: int, /) -> type[Event]\nreturns related event class to event type" #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" diff --git a/src_c/event.c b/src_c/event.c index aa0c95b39e..754760a3c1 100644 --- a/src_c/event.c +++ b/src_c/event.c @@ -97,6 +97,7 @@ static char pressed_keys[SDL_NUM_SCANCODES] = {0}; static char released_keys[SDL_NUM_SCANCODES] = {0}; static char pressed_mouse_buttons[5] = {0}; static char released_mouse_buttons[5] = {0}; +static PyObject *pg_event_lookup; #ifdef __EMSCRIPTEN__ /* these macros are no-op here */ @@ -309,6 +310,15 @@ _pg_get_event_unicode(SDL_Event *event) case PGPOST_##name: \ return proxify ? PGPOST_##name : PGE_##name +static PyTypeObject pgEvent_Type; +static PyTypeObject pgEventMeta_Type; +#define pgEvent_CheckExact(x) ((x)->ob_type == &pgEvent_Type) +#define pgEvent_Check(x) (PyObject_IsInstance(x, (PyObject *)&pgEvent_Type)) +#define pgEvent_IsSubclass(x) \ + (PyObject_IsSubclass(x, (PyObject *)&pgEvent_Type) * \ + (x != (PyObject *)&pgEvent_Type)) +#define OFF(x) offsetof(pgEventObject, x) + /* The next three functions are used for proxying SDL events to and from * PGPOST_* events. * @@ -698,13 +708,13 @@ pg_post_event_dictproxy(Uint32 type, pgEventDictProxy *dict_proxy) } /* This function posts an SDL "UserEvent" event, can also optionally take a - * python dict. This function does not need GIL to be held if dict is NULL, but - * needs GIL otherwise */ + * python dict/event object. This function does not need GIL to be held if dict + * is NULL, but needs GIL otherwise */ static int -pg_post_event(Uint32 type, PyObject *dict) +pg_post_event(Uint32 type, PyObject *obj) { int ret; - if (!dict) { + if (!obj) { return pg_post_event_dictproxy(type, NULL); } @@ -714,8 +724,8 @@ pg_post_event(Uint32 type, PyObject *dict) return SDL_SetError("insufficient memory (internal malloc failed)"); } - Py_INCREF(dict); - dict_proxy->dict = dict; + Py_INCREF(obj); + dict_proxy->obj = obj; /* initially set to 0 - unlocked state */ dict_proxy->lock = 0; dict_proxy->num_on_queue = 0; @@ -724,7 +734,7 @@ pg_post_event(Uint32 type, PyObject *dict) ret = pg_post_event_dictproxy(type, dict_proxy); if (ret != 1) { - Py_DECREF(dict); + Py_DECREF(obj); free(dict_proxy); } return ret; @@ -888,6 +898,396 @@ _pg_name_from_eventtype(int type) return "Unknown"; } +// Madeup function, but it works. +static int +_very_bad_hash(const char *str) +{ + // Rarely works, but for existing names no collisions! + unsigned int sum = 0; + int size = (int)strlen(str); + int off = 1; + + for (int i = 0; i < size; i++) { + sum += str[i]; + sum = sum ^ (sum >> off); + off += 1; + if (off > 10) { + off = 1; + } + } + return sum; +} + +static int +_pg_eventtype_from_name_hash(const char *name) +{ + /* + This was generated with these helper files (rerun if events table + modified): + + parse.py: + import subprocess as sp + + text = "[Content of the _pg_name_from_eventtype switch]" + + def parse(text): + res = [] + groups = [] + + ev_id = "" + + for line in text.split("\n"): + if line.startswith(" case"): + ev_id = line[len(' case '):-len(':')] + elif line.startswith(" "): + name = line[len(' return "'):-len('";')] + res.append((name, ev_id)) + groups.append((name, ev_id)) + else: + res.append(line) + return res, groups + + + def do_hash(groups): + inp = "\n".join(map(lambda x: x[0], groups)) + p = sp.run(["./a.out"], stdout=sp.PIPE, input=inp, + encoding="ascii") + codes = list(map(int, p.stdout.split("\n"))) + ret = [] + for idx in range(len(groups)): + ret.append((*groups[idx], codes[idx])) + return ret + + + def collides(hashes): + hdb = {} + hashes_db = {} + for name, _, h in hashes: + if h in hdb: + print(f"collision: {hdb[h]}, {name}") + exit() + else: + hdb[h] = name + hashes_db[name] = h + return hashes_db + + + def convert(lines, hashes_db): + text = "" + for line in lines: + if isinstance(line, str): + text += line + "\n" + else: + text += f" case {hashes_db[line[0]]}" \ + f": //{line[0]}\n " \ + f"return {line[1]};\n" + + return text + + + if __name__ == "__main__": + lines, groups = parse(text) + hashes = do_hash(groups) + hashes_db = collides(hashes) + print(convert(lines, hashes_db)) + + parse.c: + #include + #include + + unsigned int _very_bad_hash(char *txt) { + unsigned int sum = 0; + int off = 1; + + for (int i = 0; i < strlen(txt); i++) { + sum += txt[i]; + sum = sum ^ (sum >> off); + off += 1; + if (off > 10) { + off = 1; + } + } + return sum; + } + + int main() { + char *line = NULL; + size_t s; + getline(&line, &s, stdin); + line[strcspn(line, "\n")] = 0; + printf("%u", _very_bad_hash(line)); + while (1) { + if (getline(&line, &s, stdin) == -1) { + return 0; + }; + line[strcspn(line, "\n")] = 0; + printf("\n%u", _very_bad_hash(line)); + } + return 0; + } + + And of course, run something like: "gcc parse.c", before running + "python parse.py". + + */ + switch (_very_bad_hash(name)) { + case 1784: // ActiveEvent + return SDL_ACTIVEEVENT; + case 1791: // AppTerminating + return SDL_APP_TERMINATING; + case 1757: // AppLowMemory + return SDL_APP_LOWMEMORY; + case 3391: // AppWillEnterBackground + return SDL_APP_WILLENTERBACKGROUND; + case 3729: // AppDidEnterBackground + return SDL_APP_DIDENTERBACKGROUND; + case 2275: // AppWillEnterForeground + return SDL_APP_WILLENTERFOREGROUND; + case 3967: // AppDidEnterForeground + return SDL_APP_DIDENTERFOREGROUND; + case 2377: // ClipboardUpdate + return SDL_CLIPBOARDUPDATE; + case 720: // KeyDown + return SDL_KEYDOWN; + case 570: // KeyUp + return SDL_KEYUP; + case 1917: // KeyMapChanged + return SDL_KEYMAPCHANGED; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case 1940: // LocaleChanged + return SDL_LOCALECHANGED; +#endif + case 1726: // MouseMotion + return SDL_MOUSEMOTION; + case 2417: // MouseButtonDown + return SDL_MOUSEBUTTONDOWN; + case 1967: // MouseButtonUp + return SDL_MOUSEBUTTONUP; + case 1972: // JoyAxisMotion + return SDL_JOYAXISMOTION; + case 1844: // JoyBallMotion + return SDL_JOYBALLMOTION; + case 1765: // JoyHatMotion + return SDL_JOYHATMOTION; + case 1740: // JoyButtonUp + return SDL_JOYBUTTONUP; + case 1907: // JoyButtonDown + return SDL_JOYBUTTONDOWN; + case 405: // Quit + return SDL_QUIT; + case 1098: // SysWMEvent + return SDL_SYSWMEVENT; + case 1751: // VideoResize + return SDL_VIDEORESIZE; + case 1785: // VideoExpose + return SDL_VIDEOEXPOSE; + case 635: // MidiIn + return PGE_MIDIIN; + case 751: // MidiOut + return PGE_MIDIOUT; + case 741: // NoEvent + return SDL_NOEVENT; + case 1821: // FingerMotion + return SDL_FINGERMOTION; + case 1043: // FingerDown + return SDL_FINGERDOWN; + case 826: // FingerUp + return SDL_FINGERUP; + case 1758: // MultiGesture + return SDL_MULTIGESTURE; + case 1089: // MouseWheel + return SDL_MOUSEWHEEL; + case 1010: // TextInput + return SDL_TEXTINPUT; + case 1788: // TextEditing + return SDL_TEXTEDITING; + case 867: // DropFile + return SDL_DROPFILE; + case 953: // DropText + return SDL_DROPTEXT; + case 992: // DropBegin + return SDL_DROPBEGIN; + case 1691: // DropComplete + return SDL_DROPCOMPLETE; + case 2897: // ControllerAxisMotion + return SDL_CONTROLLERAXISMOTION; + case 2914: // ControllerButtonDown + return SDL_CONTROLLERBUTTONDOWN; + case 2734: // ControllerButtonUp + return SDL_CONTROLLERBUTTONUP; + case 3623: // ControllerDeviceAdded + return SDL_CONTROLLERDEVICEADDED; + case 3116: // ControllerDeviceRemoved + return SDL_CONTROLLERDEVICEREMOVED; + case 3380: // ControllerDeviceMapped + return SDL_CONTROLLERDEVICEREMAPPED; + case 2030: // JoyDeviceAdded + return SDL_JOYDEVICEADDED; + case 2302: // JoyDeviceRemoved + return SDL_JOYDEVICEREMOVED; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case 5155: // ControllerTouchpadDown + return SDL_CONTROLLERTOUCHPADDOWN; + case 5915: // ControllerTouchpadMotion + return SDL_CONTROLLERTOUCHPADMOTION; + case 2665: // ControllerTouchpadUp + return SDL_CONTROLLERTOUCHPADUP; + case 3160: // ControllerSensorUpdate + return SDL_CONTROLLERSENSORUPDATE; +#endif /*SDL_VERSION_ATLEAST(2, 0, 14)*/ + case 2199: // AudioDeviceAdded + return SDL_AUDIODEVICEADDED; + case 2301: // AudioDeviceRemoved + return SDL_AUDIODEVICEREMOVED; + case 2048: // RenderTargetsReset + return SDL_RENDER_TARGETS_RESET; + case 2625: // RenderDeviceReset + return SDL_RENDER_DEVICE_RESET; + case 1706: // WindowShown + return PGE_WINDOWSHOWN; + case 1681: // WindowHidden + return PGE_WINDOWHIDDEN; + case 1963: // WindowExposed + return PGE_WINDOWEXPOSED; + case 1699: // WindowMoved + return PGE_WINDOWMOVED; + case 2003: // WindowResized + return PGE_WINDOWRESIZED; + case 2528: // WindowSizeChanged + return PGE_WINDOWSIZECHANGED; + case 2231: // WindowMinimized + return PGE_WINDOWMINIMIZED; + case 2221: // WindowMaximized + return PGE_WINDOWMAXIMIZED; + case 2188: // WindowRestored + return PGE_WINDOWRESTORED; + case 1710: // WindowEnter + return PGE_WINDOWENTER; + case 1774: // WindowLeave + return PGE_WINDOWLEAVE; + case 2233: // WindowFocusGained + return PGE_WINDOWFOCUSGAINED; + case 2174: // WindowFocusLost + return PGE_WINDOWFOCUSLOST; + case 1770: // WindowClose + return PGE_WINDOWCLOSE; + case 1886: // WindowTakeFocus + return PGE_WINDOWTAKEFOCUS; + case 2026: // WindowHitTest + return PGE_WINDOWHITTEST; + case 2548: // WindowICCProfChanged + return PGE_WINDOWICCPROFCHANGED; + case 2889: // WindowDisplayChanged + return PGE_WINDOWDISPLAYCHANGED; + } + return -1; +} + +static int +_pg_eventtype_from_name(const char *name) +{ + const int guessed_type = _pg_eventtype_from_name_hash(name); + + if (guessed_type == -1) + return -1; + + if (strcmp(name, _pg_name_from_eventtype(guessed_type)) != 0) + return -1; + + return guessed_type; +} + +/* + To generate this, see the python code in _pg_eventtype_from_name_hash, + but replace its '__name__ == "__main__: [...]" with: + if __name__ == "__main__": + lines, groups = parse(text) + print("{") + for line in lines: + if isinstance(line, str): + print(line) + else: + print(f" \"{line[0]}\",") + print(" NULL\n};") +*/ +static const char *_event_names[] = {"ActiveEvent", + "AppTerminating", + "AppLowMemory", + "AppWillEnterBackground", + "AppDidEnterBackground", + "AppWillEnterForeground", + "AppDidEnterForeground", + "ClipboardUpdate", + "KeyDown", + "KeyUp", + "KeyMapChanged", +#if SDL_VERSION_ATLEAST(2, 0, 14) + "LocaleChanged", +#endif + "MouseMotion", + "MouseButtonDown", + "MouseButtonUp", + "JoyAxisMotion", + "JoyBallMotion", + "JoyHatMotion", + "JoyButtonUp", + "JoyButtonDown", + "Quit", + "SysWMEvent", + "VideoResize", + "VideoExpose", + "MidiIn", + "MidiOut", + "NoEvent", + "FingerMotion", + "FingerDown", + "FingerUp", + "MultiGesture", + "MouseWheel", + "TextInput", + "TextEditing", + "DropFile", + "DropText", + "DropBegin", + "DropComplete", + "ControllerAxisMotion", + "ControllerButtonDown", + "ControllerButtonUp", + "ControllerDeviceAdded", + "ControllerDeviceRemoved", + "ControllerDeviceMapped", + "JoyDeviceAdded", + "JoyDeviceRemoved", +#if SDL_VERSION_ATLEAST(2, 0, 14) + "ControllerTouchpadDown", + "ControllerTouchpadMotion", + "ControllerTouchpadUp", + "ControllerSensorUpdate", +#endif /*SDL_VERSION_ATLEAST(2, 0, 14)*/ + "AudioDeviceAdded", + "AudioDeviceRemoved", + "RenderTargetsReset", + "RenderDeviceReset", + "WindowShown", + "WindowHidden", + "WindowExposed", + "WindowMoved", + "WindowResized", + "WindowSizeChanged", + "WindowMinimized", + "WindowMaximized", + "WindowRestored", + "WindowEnter", + "WindowLeave", + "WindowFocusGained", + "WindowFocusLost", + "WindowClose", + "WindowTakeFocus", + "WindowHitTest", + "WindowICCProfChanged", + "WindowDisplayChanged", + NULL}; + /* Helper for adding objects to dictionaries. Check for errors with PyErr_Occurred() */ static void @@ -917,7 +1317,7 @@ get_joy_device_index(int instance_id) } static PyObject * -dict_from_event(SDL_Event *event) +obj_from_event(SDL_Event *event) { PyObject *dict = NULL, *tuple, *obj; int hx, hy; @@ -935,7 +1335,7 @@ dict_from_event(SDL_Event *event) /* spinlocks must be held and released as quickly as possible */ SDL_AtomicLock(&dict_proxy->lock); - dict = dict_proxy->dict; + dict = dict_proxy->obj; dict_proxy->num_on_queue--; to_free = dict_proxy->num_on_queue <= 0 && dict_proxy->do_free_at_end; SDL_AtomicUnlock(&dict_proxy->lock); @@ -1348,6 +1748,19 @@ pg_event_dealloc(PyObject *self) Py_TYPE(self)->tp_free(self); } +static PyObject * +_pg_EventGetAttr(PyObject *o, PyObject *attr_name) +{ + const char *attr = PyUnicode_AsUTF8(attr_name); + if (!attr) + return NULL; + + if (strcmp(attr, "type") == 0) { + return PyLong_FromLong(((pgEventObject *)o)->type); + } + return PyObject_GenericGetAttr(o, attr_name); +} + #ifdef PYPY_VERSION /* Because pypy does not work with the __dict__ tp_dictoffset. */ PyObject * @@ -1356,7 +1769,10 @@ pg_EventGetAttr(PyObject *o, PyObject *attr_name) /* Try e->dict first, if not try the generic attribute. */ PyObject *result = PyDict_GetItem(((pgEventObject *)o)->dict, attr_name); if (!result) { - return PyObject_GenericGetAttr(o, attr_name); + return _pg_EventGetAttr(o, attr_name); + } + else { + Py_INCREF(result); } return result; } @@ -1396,14 +1812,6 @@ pg_EventSetAttr(PyObject *o, PyObject *name, PyObject *value) } #endif -PyObject * -pg_event_str(PyObject *self) -{ - pgEventObject *e = (pgEventObject *)self; - return PyUnicode_FromFormat("", e->type, - _pg_name_from_eventtype(e->type), e->dict); -} - static int _pg_event_nonzero(pgEventObject *self) { @@ -1414,9 +1822,35 @@ static PyNumberMethods pg_event_as_number = { .nb_bool = (inquiry)_pg_event_nonzero, }; -static PyTypeObject pgEvent_Type; -#define pgEvent_Check(x) ((x)->ob_type == &pgEvent_Type) -#define OFF(x) offsetof(pgEventObject, x) +PyObject * +pg_event_str(PyObject *self) +{ + pgEventObject *e = (pgEventObject *)self; + if (!pgEvent_CheckExact(self)) { + PyObject *e_type = (PyObject *)Py_TYPE(self); + PyObject *e_name = PyObject_GetAttrString(e_type, "__name__"); + PyObject *e_module = PyObject_GetAttrString(e_type, "__module__"); + + if (PyObject_IsTrue(e->dict)) + return PyUnicode_FromFormat("%S.%S(%S)", e_module, e_name, + e->dict); + return PyUnicode_FromFormat("%S.%S()", e_module, e_name); + } + + char *event_name = _pg_name_from_eventtype(e->type); + ; + + if (strcmp(event_name, "UserEvent") == 0 || + strcmp(event_name, "Unknown") == 0) { + if (PyObject_IsTrue(e->dict)) + return PyUnicode_FromFormat("Event(%d, %S)", e->type, e->dict); + return PyUnicode_FromFormat("Event(%d)", e->type); + } + if (PyObject_IsTrue(e->dict)) + return PyUnicode_FromFormat("%s(%d, %S)", event_name, e->type, + e->dict); + return PyUnicode_FromFormat("%s(%d)", event_name, e->type); +} static PyMemberDef pg_event_members[] = { {"__dict__", T_OBJECT, OFF(dict), READONLY}, @@ -1461,10 +1895,25 @@ pg_event_richcompare(PyObject *o1, PyObject *o2, int opid) static int pg_event_init(pgEventObject *self, PyObject *args, PyObject *kwargs) { - int type; + int type = 0; PyObject *dict = NULL; + PyObject *self_t = (PyObject *)Py_TYPE(self); + if (self_t != (PyObject *)&pgEvent_Type) { + PyObject *type_o = PyObject_GetAttrString(self_t, "type"); + if (!type_o) + return -1; - if (!PyArg_ParseTuple(args, "i|O!", &type, &PyDict_Type, &dict)) { + type = PyLong_AsLong(type_o); + Py_DECREF(type_o); + + if (PyErr_Occurred()) + return -1; + + if (!PyArg_ParseTuple(args, "|O!", &PyDict_Type, &dict)) { + return -1; + } + } + else if (!PyArg_ParseTuple(args, "i|O!", &type, &PyDict_Type, &dict)) { return -1; } @@ -1498,7 +1947,7 @@ pg_event_init(pgEventObject *self, PyObject *args, PyObject *kwargs) if (PyDict_GetItemString(dict, "type")) { PyErr_SetString(PyExc_ValueError, - "redundant type field in event dict"); + "'type' field in event dict is not allowed"); Py_DECREF(dict); return -1; } @@ -1508,9 +1957,242 @@ pg_event_init(pgEventObject *self, PyObject *args, PyObject *kwargs) return 0; } +/* Return a new reference. */ +static PyObject * +_pgEvent_CreateSubclass(Uint32 ev_type) +{ + const char *name = _pg_name_from_eventtype(ev_type); + + if (strcmp(name, "Unknown") == 0 || strcmp(name, "UserEvent") == 0) { + Py_INCREF((PyObject *)&pgEvent_Type); + return (PyObject *)&pgEvent_Type; + } + + PyObject *dict = PyDict_New(); + if (!dict) + return NULL; + + PyObject *args = + Py_BuildValue("s(O)N", name, (PyObject *)&pgEvent_Type, dict); + if (!args) + return NULL; + + PyObject *cls = + PyType_Type.tp_call((PyObject *)&pgEventMeta_Type, args, NULL); + Py_DECREF(args); + + if (!cls) { + return NULL; + } + + PyObject *value = PyLong_FromLong(ev_type); + if (!value) { + return NULL; + } + + if (PyObject_SetAttrString(cls, "type", value) < 0) { + Py_DECREF(value); + return NULL; + } + + Py_DECREF(value); + + PyObject *mod_name = PyUnicode_FromString("pygame.event"); + if (!mod_name) { + return NULL; + } + + if (PyObject_SetAttrString(cls, "__module__", mod_name) < 0) { + Py_DECREF(mod_name); + return NULL; + } + + Py_DECREF(mod_name); + + return cls; +} + +static PyObject * +pgEvent_GetClass(Uint32 type) +{ + PyObject *e_typeo, *e_class; + + e_typeo = PyLong_FromLong(type); + + if (!e_typeo) + return NULL; + + e_class = PyDict_GetItem(pg_event_lookup, e_typeo); + Py_XINCREF(e_class); // Claiming borrowed reference. + + if (e_class) { + Py_DECREF(e_typeo); + return e_class; + } + + e_class = _pgEvent_CreateSubclass(type); + + if (!e_class) { + Py_DECREF(e_typeo); + return NULL; + } + + if (PyDict_SetItem(pg_event_lookup, e_typeo, e_class) < 0) { + Py_DECREF(e_typeo); + Py_DECREF(e_class); + return NULL; + } + + Py_DECREF(e_typeo); + return e_class; +} + +static int +_register_user_event_class(PyObject *e_class) +{ + int e_type = -1; + PyObject *e_typeo = NULL; + + if (_custom_event < PG_NUMEVENTS) { + e_type = _custom_event; + e_typeo = PyLong_FromLong(_custom_event++); + } + else { + PyErr_SetString( + pgExc_SDLError, + "Exceeded maximimum number of allowed user-defined types."); + return -1; + } + + if (!e_typeo) + return -1; + + if (PyDict_SetItem(pg_event_lookup, e_typeo, e_class) < 0) { + Py_DECREF(e_typeo); + return -1; + } + + Py_DECREF(e_typeo); + return e_type; +} + +static PyObject * +pg_event_init_subclass(PyTypeObject *cls, PyObject *args, PyObject **kwargs) +{ + if (!(cls->tp_flags & Py_TPFLAGS_HEAPTYPE)) { + // Only heaptypes. + Py_RETURN_NONE; + } + + int e_type = _register_user_event_class((PyObject *)cls); + if (e_type < 0) + return NULL; + + PyObject *value = PyLong_FromLong(e_type); + if (!value) + return NULL; + + if (PyObject_SetAttrString((PyObject *)cls, "type", value) < 0) { + Py_DECREF(value); + return NULL; + } + + Py_DECREF(value); + + Py_RETURN_NONE; +} + +static PyMethodDef eventobj_methods[] = { + {"__init_subclass__", (PyCFunction)(void (*)(void))pg_event_init_subclass, + METH_VARARGS | METH_KEYWORDS | METH_CLASS}, + {NULL, NULL, 0, NULL}}; + +/* event metaclass */ +static PyObject * +_pg_event_instantiate_class(Uint32 e_type, PyObject *e_dict) +{ + PyObject *e_typeo = pgEvent_GetClass(e_type); + + if (!e_typeo) { + return NULL; + } + else if (e_typeo == (PyObject *)&pgEvent_Type) { + Py_DECREF(e_typeo); + PyObject *args = Py_BuildValue("(iO)", e_type, e_dict); + + if (!args) { + Py_DECREF(e_typeo); + return NULL; + } + + PyObject *ret = + PyType_Type.tp_call((PyObject *)&pgEvent_Type, args, NULL); + return ret; + } + + PyObject *args = Py_BuildValue("(O)", e_dict); + + if (!args) { + Py_DECREF(e_typeo); + return NULL; + } + + PyObject *ret = PyType_Type.tp_call(e_typeo, args, NULL); + Py_DECREF(e_typeo); + return ret; +} + +static PyObject * +pgEventMeta_Call(PyObject *type, PyObject *args, PyObject *kwds) +{ + if (type == (PyObject *)&pgEvent_Type) { + int e_type = 0; + PyObject *e_dict = NULL; + + if (!PyArg_ParseTuple(args, "i|O!", &e_type, &PyDict_Type, &e_dict)) { + return NULL; + } + + if (e_type < 0 || e_type >= PG_NUMEVENTS) { + return RAISE(PyExc_ValueError, "event type out of range"); + } + + if (e_dict) + Py_INCREF(e_dict); + + if (!e_dict) { + e_dict = PyDict_New(); + if (!e_dict) + return NULL; + } + + if (kwds && PyDict_Update(e_dict, kwds) < 0) { + Py_DECREF(e_dict); + return NULL; + } + + PyObject *ret = _pg_event_instantiate_class(e_type, e_dict); + Py_XDECREF(e_dict); + + return ret; + } + + return PyType_Type.tp_call(type, args, kwds); +} + +static PyTypeObject pgEventMeta_Type = { + PyVarObject_HEAD_INIT(NULL, 0).tp_name = "pygame.event._EventMeta", + .tp_basicsize = 0, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_call = pgEventMeta_Call, +}; + +/* event type declaration */ + static PyTypeObject pgEvent_Type = { - PyVarObject_HEAD_INIT(NULL, 0).tp_name = "pygame.event.Event", + PyVarObject_HEAD_INIT(&pgEventMeta_Type, 0).tp_name = "pygame.event.Event", .tp_basicsize = sizeof(pgEventObject), + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_dealloc = pg_event_dealloc, .tp_repr = pg_event_str, .tp_as_number = &pg_event_as_number, @@ -1518,7 +2200,7 @@ static PyTypeObject pgEvent_Type = { .tp_getattro = pg_EventGetAttr, .tp_setattro = pg_EventSetAttr, #else - .tp_getattro = PyObject_GenericGetAttr, + .tp_getattro = _pg_EventGetAttr, .tp_setattro = PyObject_GenericSetAttr, #endif .tp_doc = DOC_EVENT_EVENT, @@ -1527,29 +2209,85 @@ static PyTypeObject pgEvent_Type = { .tp_dictoffset = offsetof(pgEventObject, dict), .tp_init = (initproc)pg_event_init, .tp_new = PyType_GenericNew, + .tp_methods = eventobj_methods, }; +static PyObject * +pg_event_class(PyObject *self, PyObject *args) +{ + Uint32 e_type; + + if (!PyArg_ParseTuple(args, "I", &e_type)) + return NULL; + + return pgEvent_GetClass(e_type); +} + static PyObject * pgEvent_New(SDL_Event *event) { pgEventObject *e; - e = PyObject_New(pgEventObject, &pgEvent_Type); - if (!e) - return PyErr_NoMemory(); + PyObject *e_obj, *e_typeo, *obj; + Uint32 e_type; if (event) { - e->type = _pg_pgevent_deproxify(event->type); - e->dict = dict_from_event(event); + e_type = _pg_pgevent_deproxify(event->type); + obj = obj_from_event(event); } else { - e->type = SDL_NOEVENT; - e->dict = PyDict_New(); + e_type = SDL_NOEVENT; + obj = PyDict_New(); } - if (!e->dict) { - Py_TYPE(e)->tp_free(e); + if (!obj) { return PyErr_NoMemory(); } - return (PyObject *)e; + if (pgEvent_Check(obj)) + return obj; + + e_typeo = pgEvent_GetClass(e_type); + + if (!e_typeo) { + Py_DECREF(obj); + return NULL; + } + + if (e_typeo != (PyObject *)&pgEvent_Type) { + // PyTuple_New(0) returns an immortal object and should always succeed. + e_obj = PyObject_Call(e_typeo, PyTuple_New(0), obj); + Py_DECREF(e_typeo); + Py_DECREF(obj); + if (!e_obj) + return NULL; + return e_obj; + } + else { + // Plain event object - for unknown classes. + Py_DECREF(e_typeo); + + PyObject *e_type_num = PyLong_FromLong(e_type); + if (!e_type_num) + return NULL; + + PyObject *args = PyTuple_New(1); + if (!args) { + Py_DECREF(e_typeo); + Py_DECREF(e_type_num); + return NULL; + } + + PyTuple_SET_ITEM(args, 0, e_type_num); + e_obj = PyObject_Call(e_typeo, args, obj); + Py_DECREF(args); + + if (!e_obj) { + return NULL; + } + + e = (pgEventObject *)e_obj; + e->type = e_type; + e->dict = obj; + return e_obj; + } } /* event module functions */ @@ -2114,14 +2852,9 @@ pg_event_peek(PyObject *self, PyObject *args, PyObject *kwargs) * event is filtered after that */ static PyObject * -pg_event_post(PyObject *self, PyObject *obj) +_post_event(Uint32 e_type, PyObject *obj) { - VIDEO_INIT_CHECK(); - if (!pgEvent_Check(obj)) - return RAISE(PyExc_TypeError, "argument must be an Event object"); - - pgEventObject *e = (pgEventObject *)obj; - switch (pg_post_event(e->type, e->dict)) { + switch (pg_post_event(e_type, obj)) { case 0: Py_RETURN_FALSE; case 1: @@ -2131,6 +2864,17 @@ pg_event_post(PyObject *self, PyObject *obj) } } +static PyObject * +pg_event_post(PyObject *self, PyObject *obj) +{ + VIDEO_INIT_CHECK(); + if (!pgEvent_Check(obj)) + return RAISE(PyExc_TypeError, "argument must be an Event object"); + + pgEventObject *e = (pgEventObject *)obj; + return _post_event(e->type, obj); +} + static PyObject * pg_event_set_allowed(PyObject *self, PyObject *obj) { @@ -2240,6 +2984,95 @@ pg_event_custom_type(PyObject *self, PyObject *_null) "pygame.event.custom_type made too many event types."); } +static PyObject * +pg_event_name_to_id(PyObject *self, PyObject *attr) +{ + if (!PyUnicode_Check(attr)) + return NULL; + + const char *attr_name = PyUnicode_AsUTF8(attr); + + if (PyErr_Occurred()) + return NULL; + + int e_type = _pg_eventtype_from_name(attr_name); + return PyLong_FromLong(e_type); +} + +static PyObject * +pg_event__gettattr__(PyObject *self, PyObject *attr) +{ + if (!PyUnicode_Check(attr)) + return NULL; + + const char *attr_name = PyUnicode_AsUTF8(attr); + + if (PyErr_Occurred()) + return NULL; + + int e_type = _pg_eventtype_from_name(attr_name); + + if (e_type < 0) + return PyErr_Format(PyExc_AttributeError, + "module 'pygame.event' has no attribute '%s'", + attr_name); + return pgEvent_GetClass(e_type); +} + +static PyObject * +pg_event__dir__(PyObject *self, PyObject *args) +{ + size_t length = 0; + + while (_event_names[length] != NULL) { + length++; + } + + PyObject *dict = PyObject_GetAttrString(self, "__dict__"); + if (!dict) { + return NULL; + } + + if (!PyDict_Check(dict)) { + Py_DECREF(dict); + PyErr_Format(PyExc_TypeError, ".__dict__ is not a dictionary"); + return NULL; + } + + PyObject *result = PyDict_Keys(dict); + Py_ssize_t res_size = PyList_Size(result); + Py_DECREF(dict); + + if (!result) { + return NULL; + } + + PyObject *dir = PyList_New(res_size + length); + + if (!dir) { + Py_DECREF(result); + return NULL; + } + + for (Py_ssize_t idx = 0; idx < res_size; idx++) { + PyObject *item = PyList_GetItem(result, idx); + Py_INCREF(item); + PyList_SetItem(dir, idx, item); + } + Py_DECREF(result); + + for (size_t idx = 0; idx < length; idx++) { + PyObject *item = PyUnicode_FromString(_event_names[idx]); + if (!item) { + Py_DECREF(dir); + return NULL; + } + PyList_SetItem(dir, idx + res_size, item); + } + + return dir; +} + static PyMethodDef _event_methods[] = { {"_internal_mod_init", (PyCFunction)pgEvent_AutoInit, METH_NOARGS, "auto initialize for event module"}, @@ -2271,6 +3104,11 @@ static PyMethodDef _event_methods[] = { DOC_EVENT_GETBLOCKED}, {"custom_type", (PyCFunction)pg_event_custom_type, METH_NOARGS, DOC_EVENT_CUSTOMTYPE}, + {"event_class", (PyCFunction)pg_event_class, METH_VARARGS, + DOC_EVENT_EVENTCLASS}, + {"_name_to_id", (PyCFunction)pg_event_name_to_id, METH_O}, + {"__getattr__", (PyCFunction)pg_event__gettattr__, METH_O}, + {"__dir__", (PyCFunction)pg_event__dir__, METH_NOARGS}, {NULL, NULL, 0, NULL}}; @@ -2303,6 +3141,11 @@ MODINIT_DEFINE(event) } /* type preparation */ + pgEventMeta_Type.tp_base = &PyType_Type; + if (PyType_Ready(&pgEventMeta_Type) < 0) { + return NULL; + } + if (PyType_Ready(&pgEvent_Type) < 0) { return NULL; } @@ -2313,6 +3156,14 @@ MODINIT_DEFINE(event) return NULL; } + Py_INCREF(&pgEventMeta_Type); + if (PyModule_AddObject(module, "_InternalEventMeta", + (PyObject *)&pgEventMeta_Type)) { + Py_DECREF(&pgEventMeta_Type); + Py_DECREF(module); + return NULL; + } + Py_INCREF(&pgEvent_Type); if (PyModule_AddObject(module, "EventType", (PyObject *)&pgEvent_Type)) { Py_DECREF(&pgEvent_Type); @@ -2338,6 +3189,7 @@ MODINIT_DEFINE(event) c_api[7] = pgEvent_GetKeyUpInfo; c_api[8] = pgEvent_GetMouseButtonDownInfo; c_api[9] = pgEvent_GetMouseButtonUpInfo; + c_api[10] = pgEvent_GetClass; apiobj = encapsulate_api(c_api, "event"); if (PyModule_AddObject(module, PYGAMEAPI_LOCAL_ENTRY, apiobj)) { @@ -2346,6 +3198,27 @@ MODINIT_DEFINE(event) return NULL; } + if (!pg_event_lookup) { + pg_event_lookup = PyDict_New(); + } + + if (!pg_event_lookup) { + Py_DECREF(module); + return NULL; + } + + PyObject *proxy = PyDictProxy_New(pg_event_lookup); + if (!proxy) { + Py_DECREF(module); + return NULL; + } + + if (PyModule_AddObject(module, "_event_classes", proxy)) { + Py_DECREF(proxy); + Py_DECREF(module); + return NULL; + } + SDL_RegisterEvents(PG_NUMEVENTS - SDL_USEREVENT); return module; } diff --git a/src_c/include/_pygame.h b/src_c/include/_pygame.h index 508d89fbd1..7e93f037a9 100644 --- a/src_c/include/_pygame.h +++ b/src_c/include/_pygame.h @@ -415,6 +415,9 @@ typedef struct pgEventObject pgEventObject; #define pgEvent_GetMouseButtonUpInfo \ (*(char *(*)(void))PYGAMEAPI_GET_SLOT(event, 9)) +#define pgEvent_GetClass \ + (*(PyObject * (*)(Uint32)) PYGAMEAPI_GET_SLOT(event, 10)) + #define import_pygame_event() IMPORT_PYGAME_MODULE(event) #endif diff --git a/src_c/time.c b/src_c/time.c index 383782d9b3..8eae0e3cac 100644 --- a/src_c/time.c +++ b/src_c/time.c @@ -154,7 +154,7 @@ _pg_timer_free(pgEventTimer *timer) if (is_fully_freed) { PyGILState_STATE gstate = PyGILState_Ensure(); - Py_DECREF(timer->dict_proxy->dict); + Py_DECREF(timer->dict_proxy->obj); PyGILState_Release(gstate); free(timer->dict_proxy); } @@ -213,7 +213,7 @@ _pg_add_event_timer(int ev_type, PyObject *ev_dict, int repeat) PyGILState_STATE gstate = PyGILState_Ensure(); Py_INCREF(ev_dict); PyGILState_Release(gstate); - new->dict_proxy->dict = ev_dict; + new->dict_proxy->obj = ev_dict; new->dict_proxy->lock = 0; new->dict_proxy->num_on_queue = 0; new->dict_proxy->do_free_at_end = 0; diff --git a/test/event_test.py b/test/event_test.py index c3a3d5e1c3..3f27c63308 100644 --- a/test/event_test.py +++ b/test/event_test.py @@ -293,6 +293,21 @@ def test_custom_type(self): self.assertEqual(len(queue), 1) self.assertEqual(queue[0].type, atype) + def test_subclass(self): + class MyEvent(pygame.event.Event): + pass + + self.assertIn("type", MyEvent.__dict__) + self.assertEqual(MyEvent.type, pygame.event.custom_type() - 1) + + event = MyEvent() + self.assertEqual(event.type, MyEvent.type) + self.assertIs(MyEvent, pygame.event.event_class(MyEvent.type)) + + d = {"arg": "val"} + event = MyEvent(d) + self.assertIs(event.dict, d) + def test_custom_type__end_boundary(self): """Ensure custom_type() raises error when no more custom types. @@ -484,6 +499,17 @@ def test_post_same_reference(self): self.assertEqual(e.type, event.type) self.assertIs(e.dict, event.dict) + def test_post_same_object(self): + pygame.event.clear() + + for ev_type in EVENT_TYPES: + event = pygame.event.Event(ev_type, EVENT_TEST_PARAMS[ev_type]) + pygame.event.post(event) + + self.assertIs( + pygame.event.get(ev_type)[0], event, race_condition_notification + ) + def test_post_blocked(self): """ Test blocked events are not posted. Also test whether post() @@ -922,6 +948,23 @@ def test_poll(self): self.assertEqual(pygame.event.poll().type, e3.type) self.assertEqual(pygame.event.poll().type, pygame.NOEVENT) + def test_event_class(self): + for ev_type in EVENT_TYPES: + if ev_type == pygame.USEREVENT: + self.assertIs(pygame.event.event_class(ev_type), pygame.event.Event) + else: + self.assertEqual(pygame.event.event_class(ev_type).type, ev_type) + + classes = [ + (pygame.KEYDOWN, pygame.event.KeyDown), + (pygame.KEYUP, pygame.event.KeyUp), + (pygame.NOEVENT, pygame.event.NoEvent), + ] + + for ev_id, ev_cls in classes: + self.assertIs(ev_cls, pygame.event.event_class(ev_id)) + self.assertEqual(ev_cls.type, ev_id) + class EventModuleTestsWithTiming(unittest.TestCase): __tags__ = ["timing"] @@ -965,7 +1008,8 @@ def test_event_wait(self): (70, pygame.NOEVENT, 70), ): start_time = time.perf_counter() - self.assertEqual(pygame.event.wait(wait_time).type, expected_type) + ev = pygame.event.wait(wait_time) + self.assertEqual(ev.type, expected_type) self.assertAlmostEqual( time.perf_counter() - start_time, expected_time / 1000, delta=0.01 ) diff --git a/test/midi_test.py b/test/midi_test.py index f4189a2351..ab0ea74f81 100644 --- a/test/midi_test.py +++ b/test/midi_test.py @@ -398,10 +398,7 @@ def test_midis2events(self): midi_event = midi_events[i] midi_event_data = midi_event[MIDI_DATA] - # Can't directly check event instance as pygame.event.Event is - # a function. - # self.assertIsInstance(pg_event, pygame.event.Event) - self.assertEqual(pg_event.__class__.__name__, "Event") + self.assertIsInstance(pg_event, pygame.Event) self.assertEqual(pg_event.type, pygame.MIDIIN) self.assertEqual(pg_event.status, midi_event_data[MD_STATUS]) self.assertEqual(pg_event.data1, midi_event_data[MD_DATA1])