Skip to content

Commit

Permalink
Add sdl2.ext.mouse module (#247)
Browse files Browse the repository at this point in the history
* First untested draft of mouse API

* Add unit tests for mouse ext module

* Add new mouse module to news.rst

* Update particles example to use mouse ext

* Minor docs cleanup
  • Loading branch information
a-hurst committed Oct 24, 2022
1 parent ac3d8ee commit 9274538
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 20 deletions.
16 changes: 16 additions & 0 deletions doc/modules/ext/mouse.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
`sdl2.ext.mouse` - Configuring and Handling Mouse Input
=======================================================

This module provides a number of functions to make it easier to configure and
retrieve mouse input in PySDL2.

The :func:`show_cursor`, :func:`hide_cursor`, and :func:`cursor_hidden`
functions allow you to easily show, hide, and check the visibility of the mouse
cursor. Additionally, you can check the cursor's absolute or relative location
with the :func:`mouse_coords` and :func:`mouse_delta` functions (respectively),
or obtain the current state of the mouse buttons with
:func:`mouse_button_state`. The location of the mouse cursor can be changed
programatically using :func:`warp_mouse`.

.. automodule:: sdl2.ext.mouse
:members:
1 change: 1 addition & 0 deletions doc/modules/sdl2ext.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and/or unpleasant parts of the SDL2 API. At present, these modules include:

ext/common.rst
ext/window.rst
ext/mouse.rst
ext/displays.rst
ext/renderer.rst
ext/msgbox.rst
Expand Down
14 changes: 14 additions & 0 deletions doc/news.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ This describes the latest changes between the PySDL2 releases.
0.9.15 (Unreleased)
-------------------

New Features:

* Added a series of new functions :func:`~sdl2.ext.show_cursor`,
:func:`~sdl2.ext.hide_cursor`, and :func:`~sdl2.ext.cursor_hidden` for
changing and querying the visibility of the mouse cursor.
* Added new functions :func:`~sdl2.ext.mouse_coords` and
:func:`~sdl2.ext.warp_mouse` for getting and setting the current position of
the mouse cursor.
* 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
of the mouse buttons.


0.9.14
------
Expand Down
30 changes: 10 additions & 20 deletions examples/particles.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,14 +185,12 @@ def run():
factory.from_image(RESOURCES.get_path("star.png"))
)

# Center the mouse on the window. We use the SDL2 functions directly
# here. Since the SDL2 functions do not know anything about the
# sdl2.ext.Window class, we have to pass the window's SDL_Window to it.
sdl2.SDL_WarpMouseInWindow(window.window, world.mousex, world.mousey)
# Center the mouse on the window.
sdl2.ext.warp_mouse(world.mousex, world.mousey, window=window)

# Hide the mouse cursor, so it does not show up - just show the
# particles.
sdl2.SDL_ShowCursor(0)
sdl2.ext.hide_cursor()

# Create the rendering system for the particles. This is somewhat
# similar to the SoftSpriteRenderSystem, but since we only operate with
Expand All @@ -204,26 +202,18 @@ def run():
# The almighty event loop. You already know several parts of it.
running = True
while running:

# Check for any quit events
for event in sdl2.ext.get_events():
if event.type == sdl2.SDL_QUIT:
running = False
break

if event.type == sdl2.SDL_MOUSEMOTION:
# Take care of the mouse motions here. Every time the
# mouse is moved, we will make that information globally
# available to our application environment by updating
# the world attributes created earlier.
world.mousex = event.motion.x
world.mousey = event.motion.y
# We updated the mouse coordinates once, ditch all the
# other ones. Since world.process() might take several
# milliseconds, new motion events can occur on the event
# queue (10ths to 100ths!), and we do not want to handle
# each of them. For this example, it is enough to handle
# one per update cycle.
sdl2.SDL_FlushEvent(sdl2.SDL_MOUSEMOTION)
break
# Once per loop, update the world with the current mouse position
x, y = sdl2.ext.mouse_coords()
world.mousex = x
world.mousey = y

world.process()
sdl2.SDL_Delay(1)

Expand Down
1 change: 1 addition & 0 deletions sdl2/ext/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@
from .spritesystem import *
from .surface import *
from .window import *
from .mouse import *
from .displays import *
199 changes: 199 additions & 0 deletions sdl2/ext/mouse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
"""Window routines to manage on-screen windows."""
from ctypes import c_int, byref
from collections import namedtuple
from .compat import stringify, utf8
from .err import SDLError, raise_sdl_err
from .window import _get_sdl_window
from .. import mouse
from ..events import SDL_ENABLE, SDL_DISABLE, SDL_QUERY

__all__ = [
"show_cursor", "hide_cursor", "cursor_hidden", "mouse_coords", "mouse_delta",
"warp_mouse", "mouse_button_state", "ButtonState",
]


class ButtonState(object):
"""A class representing the state of the mouse buttons.
Args:
buttonmask (int): The raw SDL button mask to parse.
Attributes:
raw (int): The raw SDL button mask representing the button state.
"""
def __init__(self, buttonmask):
self.raw = buttonmask

def __repr__(self):
s = "ButtonState(l={0}, r={1}, m={2})"
return s.format(self.left, self.right, self.middle)

def __eq__(self, s2):
return self.raw == s2.raw

def __ne__(self, s2):
return self.raw != s2.raw

def _check_button(self, bmask):
return int(bool(self.raw & bmask))

@property
def any_pressed(self):
"""bool: True if any buttons are currently pressed, otherwise False.
"""
return self.raw != 0

@property
def left(self):
"""int: The state of the left mouse button (0 = up, 1 = down).
"""
return self._check_button(mouse.SDL_BUTTON_LMASK)

@property
def right(self):
"""int: The state of the right mouse button (0 = up, 1 = down).
"""
return self._check_button(mouse.SDL_BUTTON_RMASK)

@property
def middle(self):
"""int: The state of the middle mouse button (0 = up, 1 = down).
"""
return self._check_button(mouse.SDL_BUTTON_MMASK)

@property
def x1(self):
"""int: The state of the first extra mouse button (0 = up, 1 = down).
"""
return self._check_button(mouse.SDL_BUTTON_X1MASK)

@property
def x2(self):
"""int: The state of the second extra mouse button (0 = up, 1 = down).
"""
return self._check_button(mouse.SDL_BUTTON_X2MASK)


def show_cursor():
"""Unhides the mouse cursor if it is currently hidden.
"""
ret = mouse.SDL_ShowCursor(SDL_ENABLE)
if ret < 0:
raise_sdl_err("showing the mouse cursor")

def hide_cursor():
"""Hides the mouse cursor if it is currently visible.
"""
ret = mouse.SDL_ShowCursor(SDL_DISABLE)
if ret < 0:
raise_sdl_err("hiding the mouse cursor")

def cursor_hidden():
"""Checks whether the mouse cursor is currently visible.
Returns:
bool: True if the cursor is hidden, otherwise False.
"""
return mouse.SDL_ShowCursor(SDL_QUERY) == SDL_DISABLE

def mouse_coords(desktop=False):
"""Get the current x/y coordinates of the mouse cursor.
By default, this function reports the coordinates relative to the top-left
corner of the SDL window that currently has focus. To obtain the mouse
coordinates relative to the top-right corner of the full desktop, this
function can optionally be called with ``desktop`` argument set to True.
Args:
desktop (bool, optional): If True, reports the mouse coordinates
relative to the full desktop instead of the currently-focused SDL
window. Defaults to False.
Returns:
tuple: The current (x, y) coordinates of the mouse cursor.
"""
x, y = c_int(0), c_int(0)
if desktop:
mouse.SDL_GetGlobalMouseState(byref(x), byref(y))
else:
mouse.SDL_GetMouseState(byref(x), byref(y))
return (int(x.value), int(y.value))

def mouse_button_state():
"""Gets the current state of each button of the mouse.
Mice in SDL are currently able to have up to 5 buttons: left, right, middle,
and two extras (x1 and x2). You can check each of these individually, or
alternatively check whether any buttons have been pressed::
bstate = mouse_button_state()
if bstate.any_pressed:
if bstate.left == 1:
print("left button down!")
if bstate.right == 1:
print("right button down!")
Returns:
:obj:`ButtonState`: A representation of the current button state of the
mouse.
"""
x, y = c_int(0), c_int(0)
bmask = mouse.SDL_GetMouseState(byref(x), byref(y))
return ButtonState(bmask)

def mouse_delta():
"""Get the relative change in cursor position since last checked.
The first time this function is called, it will report the (x, y) change in
cursor position since the SDL video or event system was initialized.
Subsequent calls to this function report the change in position since the
previous time the function was called.
Returns:
tuple: The (x, y) change in cursor coordinates since the function was
last called.
"""
x, y = c_int(0), c_int(0)
mouse.SDL_GetRelativeMouseState(byref(x), byref(y))
return (int(x.value), int(y.value))

def warp_mouse(x, y, window=None, desktop=False):
"""Warps the mouse cursor to a given location on the screen.
By default, this warps the mouse cursor relative to the top-left corner of
whatever SDL window currently has mouse focus. For example,::
warp_mouse(400, 300)
would warp the mouse to the middle of a 800x600 SDL window. Alternatively,
the cursor can be warped within a specific SDL window or relative to the
full desktop.
Args:
x (int): The new X position for the mouse cursor.
y (int): The new Y position for the mouse cursor.
window (:obj:`SDL_Window` or :obj:`~sdl2.ext.Window`, optional): The
SDL window within which to warp the mouse cursor. If not specified
(the default), the cursor will be warped within the SDL window that
currently has mouse focus.
desktop (bool, optional): If True, the mouse cursor will be warped
relative to the full desktop instead of the current SDL window.
Defaults to False.
"""
if desktop:
ret = mouse.SDL_WarpMouseGlobal(x, y)
if ret < 0:
raise_sdl_err("warping the mouse cursor")
else:
if window is not None:
window = _get_sdl_window(window)
mouse.SDL_WarpMouseInWindow(window, x, y)
11 changes: 11 additions & 0 deletions sdl2/ext/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
__all__ = ["Window"]


def _get_sdl_window(w, argname="window"):
if isinstance(w, Window):
w = w.window
elif hasattr(w, "contents"):
w = w.contents
if not isinstance(w, video.SDL_Window):
err = "'{0}' is not a valid SDL window.".format(argname)
raise ValueError(err)
return w


class Window(object):
"""Creates a visible window with an optional border and title text.
Expand Down
77 changes: 77 additions & 0 deletions sdl2/test/sdl2ext_mouse_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import sys
import pytest
import sdl2
from sdl2 import SDL_Window, SDL_ClearError
from sdl2 import ext as sdl2ext

from .conftest import SKIP_ANNOYING


@pytest.fixture(scope="module")
def with_ext_window(with_sdl):
win = sdl2ext.Window("Test", (100, 100))
win.show()
yield win
win.close()


def test_ButtonState():
test1 = sdl2.SDL_BUTTON_LMASK | sdl2.SDL_BUTTON_RMASK | sdl2.SDL_BUTTON_MMASK
test2 = sdl2.SDL_BUTTON_X1MASK | sdl2.SDL_BUTTON_X2MASK

b1 = sdl2ext.ButtonState(test1)
assert b1.raw == test1
assert b1.left and b1.right and b1.middle
assert not (b1.x1 or b1.x2)
assert b1.any_pressed

b2 = sdl2ext.ButtonState(test2)
assert not (b2.left or b2.right or b2.middle)
assert b2.x1 and b2.x2
assert b2.any_pressed

b3 = sdl2ext.ButtonState(0)
assert not b3.any_pressed

def test_showhide_cursor(with_ext_window):
sdl2ext.hide_cursor()
assert sdl2ext.cursor_hidden()
sdl2ext.show_cursor()
assert not sdl2ext.cursor_hidden()

def test_mouse_button_state(with_ext_window):
bstate = sdl2ext.mouse_button_state()
assert isinstance(bstate, sdl2ext.ButtonState)

def test_mouse_coords(with_ext_window):
# Get mouse positon within the window
pos = sdl2ext.mouse_coords()
assert 0 <= pos[0] <= 100
assert 0 <= pos[1] <= 100
# Get mouse positon relative to the desktop
pos = sdl2ext.mouse_coords(desktop=True)
assert 0 <= pos[0]
assert 0 <= pos[1]

def test_mouse_delta(with_ext_window):
# NOTE: Can't test properly with warp_mouse, since warping the mouse in SDL
# doesn't affect the mouse xdelta/ydelta properties
dx, dy = sdl2ext.mouse_delta()
assert type(dx) == int and type(dy) == int

@pytest.mark.skipif(SKIP_ANNOYING, reason="Skip unless requested")
def test_warp_mouse(with_ext_window):
x_orig, y_orig = sdl2ext.mouse_coords(desktop=True)
# Test warping within a specific window
win = with_ext_window
sdl2ext.warp_mouse(20, 30, window=win)
x, y = sdl2ext.mouse_coords()
assert x == 20 and y == 30
# Test warping in the window that currently has focus
sdl2ext.warp_mouse(50, 50)
x, y = sdl2ext.mouse_coords()
assert x == 50 and y == 50
# Test warping relative to the desktop
sdl2ext.warp_mouse(x_orig, y_orig, desktop=True)
x, y = sdl2ext.mouse_coords(desktop=True)
assert x == x_orig and y == y_orig

0 comments on commit 9274538

Please sign in to comment.