Skip to content

Commit

Permalink
Add new sdl2.ext module for handling SDL input events (#261)
Browse files Browse the repository at this point in the history
* Initial commit of new input module

* Update TTF example to use ext.input

* Break loops on matching input in ext.input

* Added a function for checking quit requests

* Add quit_requested to some examples

* Add test for quit_requested

* Update news.rst

* Improve displays.rst title

* Additional docs tweaks

* Fix tests with Python 2.7
  • Loading branch information
a-hurst committed May 8, 2023
1 parent 5b6a783 commit 00dd449
Show file tree
Hide file tree
Showing 11 changed files with 703 additions and 87 deletions.
4 changes: 2 additions & 2 deletions doc/modules/ext/displays.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
`sdl2.ext.displays` - Getting info about the connected displays
===============================================================
`sdl2.ext.displays` - Querying the Connected Displays
=====================================================

This module provides the ``get_displays`` function, which returns a list of
``DisplayInfo`` objects containing information about the names, supported
Expand Down
21 changes: 21 additions & 0 deletions doc/modules/ext/input.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
`sdl2.ext.input` - Handling SDL2 Input Events
=============================================

This module provides a range of Pythonic functions for handling and processing
SDL input events (e.g. key presses, mouse clicks, unicode text input) as
retrieved from :func:`~sdl2.ext.get_events`.

The :func:`key_pressed` function allows for easily checking whether a given key
has been pressed (or released). Likewise, :func:`mouse_clicked` lets you handle
mouse button press and release events. If you want to check the `locations` of
mouse clicks, :func:`get_clicks` returns the pixel coordinates for all clicks
(if any) in a given list of events.

For handling text entry in PySDL2 (including unicode characters),
:func:`get_text_input` returns all text input in a given list of events as a
unicode string. Note that text input events are disabled by default in SDL, but
can easily be enabled/disabled using :func:`start_text_input` and
:func:`stop_text_input`.

.. automodule:: sdl2.ext.input
:members:
7 changes: 4 additions & 3 deletions doc/modules/sdl2ext.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@ SDL2-based extensions
---------------------

In addition to simple Pythonic wrappers for SDL2 functions and structures, the
:mod:`sdl2.ext` module also offers a number of high-level classes and functions
that use SDL2 internally to provide APIs for font rendering, building GUIs,
importing images, and more:
:mod:`sdl2.ext` module also offers a number of high-level classes and functions
that use SDL2 internally to provide APIs for font rendering, handling input
events, importing images, and more:

.. toctree::
:maxdepth: 1

ext/input.rst
ext/surface.rst
ext/draw.rst
ext/image.rst
Expand Down
18 changes: 17 additions & 1 deletion doc/news.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ This describes the latest changes between the PySDL2 releases.
0.9.16 (Unreleased)
-------------------

New Features:

* Added a new function :func:`~sdl2.ext.key_pressed` for easily checking
if a given key has been pressed (or released).
* Added a new function :func:`~sdl2.ext.mouse_clicked` for easily checking
if a mouse button has been pressed (or released), as well as a
:func:`~sdl2.ext.get_clicks` function for retrieving the pixel coordinates
of any mouse clicks.
* Added a new function :func:`~sdl2.ext.get_text_input` for returning
text input as a unicode string, as well as :func:`~sdl2.ext.start_text_input`,
:func:`~sdl2.ext.stop_text_input`, and :func:`~sdl2.ext.text_input_enabled`
functions for toggling and querying whether SDL text input events are enabled.
* Added a new function :func:`~sdl2.ext.quit_requested` for easily checking
:func:`~sdl2.ext.get_events` output for quit requests.


0.9.15
------

Expand All @@ -21,7 +37,7 @@ New Features:
* Added a new function :func:`~sdl2.ext.mouse_delta` for checking the relative
movement of the mouse cursor since last checked.
* Added a new function :func:`~sdl2.ext.mouse_button_state` and corresponding
class :class:`~sdl2.ext.ButtonState` for easily checking the current state
class :class:`~sdl2.ext.ButtonState` for easily checking the current state
of the mouse buttons.
* Added indexing support for :class:`sdl2.SDL_Point` and :class:`sdl2.SDL_Rect`
to allow easier unpacking in Python (e.g. ``x, y, w, h = rect``).
Expand Down
38 changes: 19 additions & 19 deletions examples/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from random import randint
import sdl2
import sdl2.ext
from sdl2.ext import get_events, quit_requested, mouse_clicked

# Draws random lines on the passed surface
def draw_lines(surface, width, height):
Expand Down Expand Up @@ -71,27 +72,26 @@ def run():
curindex = 0
draw_lines(windowsurface, 800, 600)

# The event loop is nearly the same as we used in colorpalettes.py. If you
# do not know, what happens here, take a look at colorpalettes.py for a
# detailled description.
# The event loop is nearly the same as we used in colorpalettes.py, except
# that here we use some functions from sdl2.ext for handling mouse clicks
# and quit requests to make the code more Pythonic.
running = True
while running:
events = sdl2.ext.get_events()
for event in events:
if event.type == sdl2.SDL_QUIT:
running = False
break
if event.type == sdl2.SDL_MOUSEBUTTONDOWN:
curindex += 1
if curindex >= len(functions):
curindex = 0
# In contrast to colorpalettes.py, our mapping table consists
# of functions and their arguments. Thus, we get the currently
# requested function and argument tuple and execute the
# function with the arguments.
func, args = functions[curindex]
func(*args)
break
events = get_events()
# Handle any quit requests
if quit_requested(events):
running = False
# If the mouse is clicked, switch to the next drawing function
if mouse_clicked(events):
curindex += 1
if curindex >= len(functions):
curindex = 0
# In contrast to colorpalettes.py, our mapping table consists
# of functions and their arguments. Thus, we get the currently
# requested function and argument tuple and execute the
# function with the arguments.
func, args = functions[curindex]
func(*args)
window.refresh()
sdl2.ext.quit()
return 0
Expand Down
88 changes: 43 additions & 45 deletions examples/ttf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import sys
import sdl2.ext
from sdl2.sdlttf import TTF_FontLineSkip
from sdl2.ext import FontTTF
from sdl2.ext import FontTTF, key_pressed, get_text_input, quit_requested

filepath = os.path.abspath(os.path.dirname(__file__))
RESOURCES = sdl2.ext.Resources(filepath, "resources")
Expand Down Expand Up @@ -64,55 +64,53 @@ def run():

# Tell SDL2 to start reading Text Editing events. This allows for proper
# handling of unicode characters and modifier keys.
sdl2.SDL_StartTextInput()
sdl2.ext.start_text_input()

# Create a simple event loop and wait for keydown, text editing, and quit events.
running = True
while running:
update_txt = False
events = sdl2.ext.get_events()
for event in events:
update_txt = False
if event.type == sdl2.SDL_QUIT:
running = False
break

# Handle non-standard keyboard events
elif event.type == sdl2.SDL_KEYDOWN:
update_txt = True
sdl_keysym = event.key.keysym.sym
# If backspace pressed, remove last character (if any) from txt
if sdl_keysym == sdl2.SDLK_BACKSPACE:
txt = txt[:-1]
# If enter/return pressed, insert a newline
elif sdl_keysym == sdl2.SDLK_RETURN:
txt = txt + u"\n"
# If left or right arrow pressed, change text alignment mode
elif sdl_keysym == sdl2.SDLK_LEFT:
align_idx = (align_idx - 1) % 3
elif sdl_keysym == sdl2.SDLK_RIGHT:
align_idx = (align_idx + 1) % 3
elif sdl_keysym == sdl2.SDLK_UP:
line_height += 1
elif sdl_keysym == sdl2.SDLK_DOWN:
if line_height > 1:
line_height -= 1
# If tab pressed, cycle through the different font styles
elif sdl_keysym == sdl2.SDLK_TAB:
style_idx = (style_idx + 1) % len(styles)

# Handle text input events
elif event.type == sdl2.SDL_TEXTINPUT:
update_txt = True
txt += event.text.text.decode("utf-8")

# If txt has changed since the start of the loop, update the renderer
if update_txt:
align = alignments[align_idx]
style = styles[style_idx]
txt_rendered = font.render_text(
txt, style, width=780, line_h=line_height, align=align
)
update_text(renderer, txt_rendered)

if quit_requested(events):
running = False
break

# Handle special keyboard events
if key_pressed(events):
# If any key pressed, re-render text
update_txt = True
# If backspace pressed, remove last character (if any) from txt
if key_pressed(events, sdl2.SDLK_BACKSPACE):
txt = txt[:-1]
# If enter/return pressed, insert a newline
elif key_pressed(events, sdl2.SDLK_RETURN):
txt = txt + u"\n"
# If left or right arrow pressed, change text alignment mode
elif key_pressed(events, "left"):
align_idx = (align_idx - 1) % 3
elif key_pressed(events, "right"):
align_idx = (align_idx + 1) % 3
elif key_pressed(events, "up"):
line_height += 1
elif key_pressed(events, "down"):
if line_height > 1:
line_height -= 1
# If tab pressed, cycle through the different font styles
elif key_pressed(events, "tab"):
style_idx = (style_idx + 1) % len(styles)

# Handle text input events
txt += get_text_input(events)

# If txt has changed since the start of the loop, update the renderer
if update_txt:
align = alignments[align_idx]
style = styles[style_idx]
txt_rendered = font.render_text(
txt, style, width=780, line_h=line_height, align=align
)
update_text(renderer, txt_rendered)

# Now that we're done, close everything down and quit SDL2
font.close()
Expand Down
1 change: 1 addition & 0 deletions sdl2/ext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@
from .window import *
from .mouse import *
from .displays import *
from .input import *
66 changes: 51 additions & 15 deletions sdl2/ext/common.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import ctypes
from .. import (events, timer, error, dll,
from .. import (timer, error, dll,
SDL_Init, SDL_InitSubSystem, SDL_Quit, SDL_QuitSubSystem, SDL_WasInit,
SDL_INIT_VIDEO, SDL_INIT_AUDIO, SDL_INIT_TIMER, SDL_INIT_HAPTIC,
SDL_INIT_JOYSTICK, SDL_INIT_GAMECONTROLLER, SDL_INIT_SENSOR, SDL_INIT_EVENTS,
)
from ..events import (
SDL_Event, SDL_PumpEvents, SDL_PeepEvents, SDL_PollEvent,
SDL_QUIT, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT,
)
from .compat import isiterable
from .err import raise_sdl_err

_HASSDLTTF = True
Expand All @@ -17,7 +22,7 @@
except ImportError:
_HASSDLIMAGE = False

__all__ = ["init", "quit", "get_events", "TestEventProcessor"]
__all__ = ["init", "quit", "get_events", "quit_requested", "TestEventProcessor"]


_sdl_subsystems = {
Expand Down Expand Up @@ -125,19 +130,15 @@ def get_events():
the event queue.
"""
events.SDL_PumpEvents()
SDL_PumpEvents()

evlist = []
SDL_PeepEvents = events.SDL_PeepEvents

op = events.SDL_GETEVENT
first = events.SDL_FIRSTEVENT
last = events.SDL_LASTEVENT

while True:
evarray = (events.SDL_Event * 10)()
ptr = ctypes.cast(evarray, ctypes.POINTER(events.SDL_Event))
ret = SDL_PeepEvents(ptr, 10, op, first, last)
evarray = (SDL_Event * 10)()
ptr = ctypes.cast(evarray, ctypes.POINTER(SDL_Event))
ret = SDL_PeepEvents(
ptr, 10, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT
)
if ret <= 0:
break
evlist += list(evarray)[:ret]
Expand All @@ -147,6 +148,41 @@ def get_events():
return evlist


def quit_requested(events):
"""Checks for quit requests in a given event queue.
Quit requests occur when an SDL recieves a system-level quit signal (e.g
clicking the 'close' button on an SDL window, pressing Command-Q on macOS).
This function makes it easier to handle these events in your code::
running = True
while running:
events = sdl2.ext.get_events()
if quit_requested(events):
running = False
Args:
events (list of :obj:`sdl2.SDL_Event`): A list of SDL events to check
for quit requests.
Returns:
bool: True if a quit request has occurred, otherwise False.
"""
# Ensure 'events' is iterable
if not isiterable(events):
events = [events]

# Check for any quit events in the queue
requested = False
for e in events:
if e.type == SDL_QUIT:
requested = True
break

return requested


class TestEventProcessor(object):
"""A simple event processor for testing purposes."""

Expand All @@ -160,12 +196,12 @@ def run(self, window):
the test event loop.
"""
event = events.SDL_Event()
event = SDL_Event()
running = True
while running:
ret = events.SDL_PollEvent(ctypes.byref(event), 1)
ret = SDL_PollEvent(ctypes.byref(event), 1)
if ret == 1:
if event.type == events.SDL_QUIT:
if event.type == SDL_QUIT:
running = False
break
window.refresh()
Expand Down

0 comments on commit 00dd449

Please sign in to comment.