diff --git a/doc/modules/ext/mouse.rst b/doc/modules/ext/mouse.rst new file mode 100644 index 00000000..5b058647 --- /dev/null +++ b/doc/modules/ext/mouse.rst @@ -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: diff --git a/doc/modules/sdl2ext.rst b/doc/modules/sdl2ext.rst index 0e3aeaf9..989039dc 100644 --- a/doc/modules/sdl2ext.rst +++ b/doc/modules/sdl2ext.rst @@ -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 diff --git a/doc/news.rst b/doc/news.rst index 1f9791b2..5068cde5 100644 --- a/doc/news.rst +++ b/doc/news.rst @@ -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 ------ diff --git a/examples/particles.py b/examples/particles.py index 7d6eb6ec..269c2d85 100644 --- a/examples/particles.py +++ b/examples/particles.py @@ -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 @@ -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) diff --git a/sdl2/ext/__init__.py b/sdl2/ext/__init__.py index 081260c3..c0d62a46 100644 --- a/sdl2/ext/__init__.py +++ b/sdl2/ext/__init__.py @@ -25,4 +25,5 @@ from .spritesystem import * from .surface import * from .window import * +from .mouse import * from .displays import * diff --git a/sdl2/ext/mouse.py b/sdl2/ext/mouse.py new file mode 100644 index 00000000..5f6e4f7d --- /dev/null +++ b/sdl2/ext/mouse.py @@ -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) diff --git a/sdl2/ext/window.py b/sdl2/ext/window.py index c24c6bad..148aaa4c 100644 --- a/sdl2/ext/window.py +++ b/sdl2/ext/window.py @@ -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. diff --git a/sdl2/test/sdl2ext_mouse_test.py b/sdl2/test/sdl2ext_mouse_test.py new file mode 100644 index 00000000..21246006 --- /dev/null +++ b/sdl2/test/sdl2ext_mouse_test.py @@ -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