From 05e0d6c471cfc2df442e0acdd4b1463275afb910 Mon Sep 17 00:00:00 2001 From: Mambouna Date: Thu, 3 Apr 2025 19:29:50 +0200 Subject: [PATCH 01/11] FIX #334 - Expanded mouse functionality Removes usage of old mouse enum. Introduces new mouse built-in object that is backwards compatible with old mouse enum. Old projects can stay the same and should continue working. Functions of new mouse: - `mouse.pressed` to get a tuple of all button states. - `mouse.pressed_left` to get just the state of the left mouse button. `pressed_right` and `pressed_middle` are also available. - `mouse.pos` gives the current position. Can also be set to change the mouse position. - `mouse.rel` gives the last relative position change of the mouse. - `mouse.visible` gives the boolean of the visibility state. Can also be set. - `mouse.focused` gives the boolean of whether the window is focused. - `mouse.cursor` gives the current cursor name if it is of type `system` or a custom cursor image. Can also be set to a few different system cursors or a custom image. If a custom image is used, a hotspot can be given as well. - `mouse.cursor_name` gives just the name of the cursor. Can't be set. Same goes for `mouse.cursor_hotspot`. Functionality of a deque of recent positions as well as updated documentation will be added in further commits. --- src/pgzero/builtins.py | 5 +- src/pgzero/game.py | 2 +- src/pgzero/mouse.py | 184 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/pgzero/mouse.py diff --git a/src/pgzero/builtins.py b/src/pgzero/builtins.py index 6d6e54b0..b90b420e 100644 --- a/src/pgzero/builtins.py +++ b/src/pgzero/builtins.py @@ -8,11 +8,14 @@ from .animation import animate from .rect import Rect, ZRect from .loaders import images, sounds -from .constants import mouse, keys, keymods +# Removed the former mouse enum import +from .constants import keys, keymods from .game import exit # The actual screen will be installed here from .screen import screen_instance as screen +# Make mouse globally available +from .mouse import mouse_instance as mouse __all__ = [ diff --git a/src/pgzero/game.py b/src/pgzero/game.py index 4f242a85..2b2df224 100644 --- a/src/pgzero/game.py +++ b/src/pgzero/game.py @@ -9,7 +9,6 @@ import pgzero.keyboard import pgzero.screen import pgzero.loaders -import pgzero.screen from . import constants @@ -64,6 +63,7 @@ def __init__( self.fps = fps self.keyboard = pgzero.keyboard.keyboard self.handlers = {} + global mouse def reinit_screen(self) -> bool: """Reinitialise the window. diff --git a/src/pgzero/mouse.py b/src/pgzero/mouse.py new file mode 100644 index 00000000..f725ce84 --- /dev/null +++ b/src/pgzero/mouse.py @@ -0,0 +1,184 @@ +import pygame +import pygame.mouse +from collections import deque +from . import loaders + + +class Mouse: + """Interface to the pygame mouse. Also integrates former + enum properties of mouse to retain all previous functionality + and be backwards compatible. + """ + + @property + def LEFT(self): + return 1 + + @property + def MIDDLE(self): + return 2 + + @property + def RIGHT(self): + return 3 + + @property + def WHEEL_UP(self): + return 4 + + @property + def WHEEL_DOWN(self): + return 5 + + # TODO: Clean up the return value of this to make it easier + # to understand for users. + @property + def pressed(self): + return pygame.mouse.get_pressed() + + @property + def pressed_left(self): + return pygame.mouse.get_pressed()[0] + + @property + def pressed_middle(self): + return pygame.mouse.get_pressed()[1] + + @property + def pressed_right(self): + return pygame.mouse.get_pressed()[2] + + @property + def pos(self): + return pygame.mouse.get_pos() + + @pos.setter + def pos(self, pos, pos_y = None): + # Setting mouse visibility is a workaround to allow + # setting the mouse position under Wayland, which + # normally prevents user applications from doing this. + # switch_back is necessary in case the pointer is + # moved while already set invisible by the user. + switch_back = True if self.visible else False + pygame.mouse.set_visible(False) + if pos_y: + pygame.mouse.set_pos([pos, pos_y]) + # TODO: Nicer way to do this? Does it have to be so strict + # in checking? + elif isinstance(pos, (tuple, list)) and len(pos) == 2 and\ + isinstance(pos[0], int) and isinstance(pos[1], int): + pygame.mouse.set_pos(pos) + else: + raise ValueError("Setting the mouse position requires either" + " one tuple with two integers or two integers" + " as individual parameters.") + if switch_back: + pygame.mouse.set_visible(True) + + @property + def rel(self): + return pygame.mouse.get_rel() + + @property + def recent_rel(self): + pass + + @property + def visible(self): + return pygame.mouse.get_visible() + + @visible.setter + def visible(self, val): + match val: + case bool(): + vis = val + case 0: + vis = False + case 1: + vis = True + case _: + raise ValueError("Value to set mouse visibility must be" + " a boolean or either 1 or 0.") + pygame.mouse.set_visible(vis) + + @property + def focused(self): + return pygame.mouse.get_focused() + + # TODO: Better way to separate cursor, name and hotspot getting + # without doubling up on the code? cursor could just call + # name and hotspot, but then we would run pygames get_cursor() + # twice. Neither is ideal. + # Another option would be to expose an explicit getter function + # and then turn it into a property the old fashioned way. This + # works, but it rather ugly and terrible practise... + @property + def cursor(self): + c = pygame.mouse.get_cursor() + if c.type == "system": + return c.__repr__().split("_")[-1][:-2], None + elif c.type == "color": + name = self._cursor_image_name + if name and loaders.images._cache[name] == c.data[1]: + return name, c.data[0] + else: + return "UNKNOWN", c.data[0] + else: + return "BITMAP", None + + @cursor.setter + def cursor(self, args): + # Separate arguments if both a cursor and hotspot were given. + if isinstance(args, tuple) and len(args) > 1: + c_string = args[0] + hotspot = args[1] + else: + c_string = args + hotspot = None + system_cursors = ["ARROW", "IBEAM", "WAIT", "CROSSHAIR", "HAND"] + if c_string in system_cursors: + self._cursor_image_name = None + exec("pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_" + + c_string + ")") + if hotspot: + print("WARNING: System cursors can't be given a hotspot" + " as they define their own. The given hotspo was" + " ignored.") + else: + if not hotspot: + hotspot = (0, 0) + surface = loaders.images.load(c_string) + self._cursor_image_name = c_string + pygame.mouse.set_cursor(hotspot, surface) + + @property + def cursor_name(self): + c = pygame.mouse.get_cursor() + # As pygame doesn't have an exposed parameter to get + # the kind of system cursor applied, we get it here + # in a kind of dirty but consistent way. + if c.type == "system": + return c.__repr__().split("_")[-1][:-2] + elif c.type == "color": + name = self._cursor_image_name + if name and loaders.images._cache[name] == c.data[1]: + return name + else: + return "UNKNOWN" + else: + return "BITMAP" + + @property + def cursor_hotspot(self): + c = pygame.mouse.get_cursor() + # For color cursors, the hotspot is easy to get. + if c.type == "color": + return c.data[0] + # Pygame doesn't have a way to give us the hotspot + # for system cursors and bitmap cursors aren't + # supported for PGZero. + else: + return None + + +mouse_instance = Mouse() From e2e6a0b9386983afc727278465018fe6377f0bed Mon Sep 17 00:00:00 2001 From: Mambouna Date: Thu, 3 Apr 2025 20:07:34 +0200 Subject: [PATCH 02/11] flake8 adjustments --- src/pgzero/game.py | 1 - src/pgzero/mouse.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pgzero/game.py b/src/pgzero/game.py index 2b2df224..48af7d2b 100644 --- a/src/pgzero/game.py +++ b/src/pgzero/game.py @@ -63,7 +63,6 @@ def __init__( self.fps = fps self.keyboard = pgzero.keyboard.keyboard self.handlers = {} - global mouse def reinit_screen(self) -> bool: """Reinitialise the window. diff --git a/src/pgzero/mouse.py b/src/pgzero/mouse.py index f725ce84..3285fe80 100644 --- a/src/pgzero/mouse.py +++ b/src/pgzero/mouse.py @@ -5,8 +5,8 @@ class Mouse: - """Interface to the pygame mouse. Also integrates former - enum properties of mouse to retain all previous functionality + """Interface to the pygame mouse. Also integrates former + enum properties of mouse to retain all previous functionality and be backwards compatible. """ @@ -17,7 +17,7 @@ def LEFT(self): @property def MIDDLE(self): return 2 - + @property def RIGHT(self): return 3 @@ -53,7 +53,7 @@ def pos(self): return pygame.mouse.get_pos() @pos.setter - def pos(self, pos, pos_y = None): + def pos(self, pos, pos_y=None): # Setting mouse visibility is a workaround to allow # setting the mouse position under Wayland, which # normally prevents user applications from doing this. @@ -63,7 +63,7 @@ def pos(self, pos, pos_y = None): pygame.mouse.set_visible(False) if pos_y: pygame.mouse.set_pos([pos, pos_y]) - # TODO: Nicer way to do this? Does it have to be so strict + # TODO: Nicer way to do this? Does it have to be so strict # in checking? elif isinstance(pos, (tuple, list)) and len(pos) == 2 and\ isinstance(pos[0], int) and isinstance(pos[1], int): @@ -167,7 +167,7 @@ def cursor_name(self): return "UNKNOWN" else: return "BITMAP" - + @property def cursor_hotspot(self): c = pygame.mouse.get_cursor() From 2b972eb765392586b4843c6c7d8195d4b9c51734 Mon Sep 17 00:00:00 2001 From: Mambouna Date: Fri, 4 Apr 2025 13:35:18 +0200 Subject: [PATCH 03/11] FIX #334 - deques of last pos and rel added Added deques of the last relative position changes by mouse-move events as well as the absolute positions after all those movements. - `mouse.recent_rel` and `mouse.recent_pos` to access a tuple of those movements. - `mouse.recent_rel_max` and `mouse.recent_pos_max` control how many move events back are recorded. Reworked the majority of attributes to perform better and more consistently. - `mouse.pressed` is now not recalculated every call. Same goes for the individual buttons. - Same for `mouse.pos` and `mouse.rel`. Rel additionally sums the relative movement of all move events over one frame which makes it consistent with calling `pygame.mouse.get_rel()` every frame. - To retain the function of `get_rel()` separately, `mouse.last_called_rel` and `mouse.last_called_pos` were added. These give the position and relative change from when they were last accessed. --- src/pgzero/game.py | 29 +++++++++++++++ src/pgzero/mouse.py | 90 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 112 insertions(+), 7 deletions(-) diff --git a/src/pgzero/game.py b/src/pgzero/game.py index 48af7d2b..7eeef6c1 100644 --- a/src/pgzero/game.py +++ b/src/pgzero/game.py @@ -62,6 +62,7 @@ def __init__( self.icon = None self.fps = fps self.keyboard = pgzero.keyboard.keyboard + self.mouse = pgzero.mouse.mouse_instance self.handlers = {} def reinit_screen(self) -> bool: @@ -250,6 +251,9 @@ def inject_global_handlers(self): user_key_down = self.handlers.get(pygame.KEYDOWN) user_key_up = self.handlers.get(pygame.KEYUP) + user_mouse_down = self.handlers.get(pygame.MOUSEBUTTONDOWN) + user_mouse_up = self.handlers.get(pygame.MOUSEBUTTONUP) + user_mouse_move = self.handlers.get(pygame.MOUSEMOTION) def key_down(event): if event.key == pygame.K_q and \ @@ -264,8 +268,27 @@ def key_up(event): if user_key_up: return user_key_up(event) + def mouse_down(event): + self.mouse._press(event.button) + if user_mouse_down: + return user_mouse_down(event) + + def mouse_up(event): + self.mouse._release(event.button) + if user_mouse_up: + return user_mouse_up(event) + + def mouse_move(event): + self.mouse._set_pos(event.pos) + self.mouse._add_rel(event.rel) + if user_mouse_move: + return user_mouse_move(event) + self.handlers[pygame.KEYDOWN] = key_down self.handlers[pygame.KEYUP] = key_up + self.handlers[pygame.MOUSEBUTTONDOWN] = mouse_down + self.handlers[pygame.MOUSEBUTTONUP] = mouse_up + self.handlers[pygame.MOUSEMOTION] = mouse_move def handle_events(self, dt, update) -> bool: """Handle all events for the current frame. @@ -274,6 +297,12 @@ def handle_events(self, dt, update) -> bool: """ updated = False + # If no mouse movement occured, the rel state of the + # mouse is set to (0, 0). This is necessary since it + # otherwise retains the rel of the last movement + # even if the mouse hasn't been moved. + self.mouse._null_rel() + for event in pygame.event.get(): handler = self.handlers.get(event.type) if handler: diff --git a/src/pgzero/mouse.py b/src/pgzero/mouse.py index 3285fe80..11511437 100644 --- a/src/pgzero/mouse.py +++ b/src/pgzero/mouse.py @@ -1,6 +1,7 @@ import pygame import pygame.mouse from collections import deque +from itertools import islice from . import loaders @@ -10,6 +11,35 @@ class Mouse: and be backwards compatible. """ + def __init__(self): + self._pressed = [False, False, False] + self._pos = None + self._rel = (0, 0) + self._last_pos = (0, 0) + self._last_rel = (0, 0) + self._recent_pos = deque(maxlen=60) + self._recent_rel = deque(maxlen=60) + + def _press(self, button): + self._pressed[button - 1] = True + + def _release(self, button): + self._pressed[button - 1] = False + + def _set_pos(self, pos): + self._pos = pos + self._recent_pos.appendleft(pos) + + def _add_rel(self, rel): + self._rel = self._rel[0] + rel[0], self._rel[1] + rel[1] + lrx = self._last_rel[0] + rel[0] + lry = self._last_rel[1] + rel[1] + self._last_rel = lrx, lry + self._recent_rel.appendleft(rel) + + def _null_rel(self): + self._rel = (0, 0) + @property def LEFT(self): return 1 @@ -34,23 +64,23 @@ def WHEEL_DOWN(self): # to understand for users. @property def pressed(self): - return pygame.mouse.get_pressed() + return tuple(self._pressed) @property def pressed_left(self): - return pygame.mouse.get_pressed()[0] + return self._pressed[0] @property def pressed_middle(self): - return pygame.mouse.get_pressed()[1] + return self._pressed[1] @property def pressed_right(self): - return pygame.mouse.get_pressed()[2] + return self._pressed[2] @property def pos(self): - return pygame.mouse.get_pos() + return self._pos @pos.setter def pos(self, pos, pos_y=None): @@ -74,14 +104,58 @@ def pos(self, pos, pos_y=None): " as individual parameters.") if switch_back: pygame.mouse.set_visible(True) + # Note: Setting self._pos manually here isn't necessary + # because set_pos() triggers a new MOUSEMOTION event. + + @property + def last_called_pos(self): + p = self._last_pos + self._last_pos = self._pos + return p + + @property + def recent_pos(self): + return tuple(self._recent_pos) + + @property + def recent_pos_max(self): + return self._recent_pos.maxlen + + @recent_pos_max.setter + def recent_pos_max(self, maxl): + c_max = self._recent_pos.maxlen + if maxl > c_max: + self._recent_pos = deque(self._recent_pos, maxlen=maxl) + elif maxl < c_max: + elems = islice(self._recent_pos, maxl) + self._recent_pos = deque(elems, maxlen=maxl) @property def rel(self): - return pygame.mouse.get_rel() + return self._rel + + @property + def last_called_rel(self): + r = self._last_rel + self._last_rel = (0, 0) + return r @property def recent_rel(self): - pass + return tuple(self._recent_rel) + + @property + def recent_rel_max(self): + return self._recent_rel.maxlen + + @recent_rel_max.setter + def recent_rel_max(self, maxl): + c_max = self._recent_rel.maxlen + if maxl > c_max: + self._recent_rel = deque(self._recent_rel, maxlen=maxl) + elif maxl < c_max: + elems = islice(self._recent_rel, maxl) + self._recent_rel = deque(elems, maxlen=maxl) @property def visible(self): @@ -112,6 +186,8 @@ def focused(self): # Another option would be to expose an explicit getter function # and then turn it into a property the old fashioned way. This # works, but it rather ugly and terrible practise... + + # TODO: Should cursor also be incorporated into an attribute? @property def cursor(self): c = pygame.mouse.get_cursor() From b8fed00f99b4cb0e8938967fd51d912dfdd10fdd Mon Sep 17 00:00:00 2001 From: Mambouna Date: Sat, 5 Apr 2025 11:46:17 +0200 Subject: [PATCH 04/11] FIX #334 - Added documentation --- doc/builtins.rst | 211 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/doc/builtins.rst b/doc/builtins.rst index 4ac0e0fd..dbdd5ba0 100644 --- a/doc/builtins.rst +++ b/doc/builtins.rst @@ -374,6 +374,217 @@ can use the :func:`on_music_end() hook ` to do something when the music ends - for example, to pick another track at random. +.. _mouse: + +Mouse +----- + +The built-in mouse object can be used to get information of various current +states of the mouse in the game: where it is, what buttons are pressed, +whether it is visible and many more... + +It can also be used to manipulate the mouse, for example changing its position +or the displayed mouse cursor. + + +.. class:: Mouse + + .. attribute:: pressed + + Returns a tuple of three booleans for the left, middle and right + mouse buttons in order. True means the button is currently pressed. + + .. attribute:: pressed_left + + Returns True if the left mouse button is currently pressed, + False else. + + .. attribute:: pressed_middle + + Returns True if the middle mouse button is currently pressed, + False else. + + .. attribute:: pressed_right + + Returns True if the right mouse button is currently pressed, + False else. + + .. attribute:: pos + + Returns the current position of the mouse. + + Setting this will teleport the mouse to the supplied position + inside the game window. + + .. attribute:: last_called_pos + + Returns the position of the mouse at the time this value + was last read. + + .. attribute:: recent_pos + + Returns a tuple of the n last positions the mouse had moved + to starting from the most recent. Default for n is 60 and + can be changed via recent_pos_max. + + .. attribute:: recent_pos_max + + Returns the total number of positions tracked via recent_pos. + + Increasing this value simply extends the length of the queue + of tracked positions. Decreasing it also cuts the queue to the + new maximum number of elements. + + .. attribute:: rel + + Returns the relative position change of the mouse over the last + frame. + + .. attribute:: last_called_rel + + Returns the relative position change of the mouse since this + value was last read. + + .. attribute:: recent_rel + + Returns a tuple of the n last position changes the mouse had, + starting from the most recent. Default for n is 60 and + can be changed via recent_rel_max. + + .. attribute:: recent_rel_max + + Returns the total number of position changed tracked via + recent_pos. + + Increasing this value simply extends the length of the queue + of tracked positions. Decreasing it also cuts the queue to the + new maximum number of elements. + + .. attribute:: visible + + Returns a boolean of whether the mouse cursor is currently + visible. + + Setting this to either a boolean or 0 or 1 makes the mouse + visible or invisible. + + .. attribute:: focused + + Returns a boolean of whether the game window is currently + focused. + + .. attribute:: cursor + + Returns a tuple with two elements: the name string of the + current cursor and the hotspot of the cursor if it is known. + + Setting this loads a new cursor and applies it to the mouse. + This can be done with either one string, or one string and + a hotspot position tuple at the same time. + + For a more detailed explanation, see below. + + .. attribute:: cursor_name + + Returns the current name string of the mouse cursor. + + .. attribute:: cursor_hotspot + + Returns the hotspot tuple of the current cursor or None + if the hotspot is unknown (because it is a system cursor). + + +Recent pos and rel +'''''''''''''''''' + +The ``mouse.recent_pos`` and ``mouse.recent_rel`` attributes allow +you to get the last recorded positions and relative movements from +any mouse movement event. This can be used among other things to +make visual effects of things "following" the mouse. Here is an +example that draws a trail of circles behind the mouse:: + + def draw(): + for p in mouse.recent_pos: + screen.draw.circle(p, 5, "red") + +How many events back are recorded is controlled by ``recent_pos_max`` +and ``recent_rel_max``. It's important to note here that these +attributes don't record the position and relative movement for each +frame, like ``mouse.rel`` for example does, but rather the positions +and changes for each individual mouse movement event. Since multiple +mouse movement events can happen in a single frame, there is no fixed +relationship between the number of frames passed and the recorded +positions and relative changes. In general though, a higher maximum +value means positions and changes from longer before will still be +recorded. + +Cursors +''''''' + +Many games change how the mouse cursor looks. PGZero lets you control +this by setting the ``mouse.cursor`` attribute. There are two options +here: system cursors or custom cursors. + +Windows, MacOS and Linux systems all come with a variety of pre-made +cursor shapes ready to use. To change to any of them, simply use +``mouse.cursor = "ARROW"`` where *"ARROW"* is usually the default cursor +on your system. To use something else, simply change the string to one +of these options: *"ARROW", "IBEAM", "WAIT", "CROSSHAIR", "HAND"*. + +If you want to use a custom cursor from an image you have in the +``images`` folder, just call ``mouse.cursor = "image_name"`` and the +cursor will automatically be loaded from the image resource. + +When using a custom cursor, you might also want to tell PGZero where +in the image the spot is, where an actual mouse click should occur. +The default cursor of most systems has this in the top left corner of +the cursor image but yours could have it in the center (think shooter +crosshairs), in the top middle (a pointing hand maybe) or anywhere else. + +To tell PGZero where to put the actual point, you can also give a hotspot +tuple when setting the cursor: ``mouse.cursor = "image_name", (12, 0)``. + +This tells the game that when you press a mouse button, the click should +happen not at the top left corner of the cursor image, but 12 pixels to +the right of it. This might be the top middle for a cursor with an image +size of 24x24 pixels. The center for a 40x40 pixels cursor would be +``(20, 20)``. + +This tuple is simply a position tuple like the kind we already know about. +The only difference is that it looks for the position to put mouse clicks +not in relation to the top left corner of the game window, but just the +cursor image. + +System cursors define their own hotspot, so you don't have to worry about +them. This also means you can't manually check the hotspot of system +cursors with ``mouse.cursor_hotspot`` however. This will return ``None`` +when a system cursor is in use. + +LEFT or pressed_left +'''''''''''''''''''' + +While both are accessed via ``mouse``, ``mouse.LEFT`` and +``mouse.pressed_left`` are different things. ``mouse.LEFT`` is used +in your custom functions that handle mouse events, e.g. +``if button == mouse.LEFT`` whereas ``mouse.pressed`` and its variants +can be used outside of these, for example in the ``update()`` function to +check whether a mouse button is pressed at that moment. + +Example:: + + def on_mouse_down(button): + if button == mouse.LEFT: + play_tune() + + def update(): + if mouse.pressed_right: + fireworks() + + +``mouse.WHEELUP`` and ``mouse.WHEELDOWN`` have no equivalent outside of +``on_mouse`` functions since these can't be held down. + + .. _clock: Clock From 71119f6a666b3436e64cdbc6a2b589e23a991934 Mon Sep 17 00:00:00 2001 From: Mambouna Date: Sun, 13 Apr 2025 19:03:58 +0200 Subject: [PATCH 05/11] Added pgzero.mouse to relevant test case --- test/test_event_dispatch.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_event_dispatch.py b/test/test_event_dispatch.py index 5697a242..a9ceaf60 100644 --- a/test/test_event_dispatch.py +++ b/test/test_event_dispatch.py @@ -2,6 +2,7 @@ from unittest.mock import Mock from pgzero.game import PGZeroGame from pgzero.constants import mouse +import pgzero.mouse class Event: From 3b23a69559fa61557e6024702d4d9ae24c0af5ef Mon Sep 17 00:00:00 2001 From: Mambouna Date: Mon, 23 Jun 2025 13:37:02 +0200 Subject: [PATCH 06/11] Added unittests --- src/pgzero/mouse.py | 4 +- test/test_mouse.py | 125 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 test/test_mouse.py diff --git a/src/pgzero/mouse.py b/src/pgzero/mouse.py index 11511437..78b77f8d 100644 --- a/src/pgzero/mouse.py +++ b/src/pgzero/mouse.py @@ -186,8 +186,6 @@ def focused(self): # Another option would be to expose an explicit getter function # and then turn it into a property the old fashioned way. This # works, but it rather ugly and terrible practise... - - # TODO: Should cursor also be incorporated into an attribute? @property def cursor(self): c = pygame.mouse.get_cursor() @@ -218,7 +216,7 @@ def cursor(self, args): + c_string + ")") if hotspot: print("WARNING: System cursors can't be given a hotspot" - " as they define their own. The given hotspo was" + " as they define their own. The given hotspot was" " ignored.") else: if not hotspot: diff --git a/test/test_mouse.py b/test/test_mouse.py new file mode 100644 index 00000000..0ebcc7e1 --- /dev/null +++ b/test/test_mouse.py @@ -0,0 +1,125 @@ +import unittest + +import pygame + +from pgzero.mouse import mouse + + +class MouseTest(unittest.TestCase): + def setUp(self): + pygame.init() + cls.surf = pygame.display.set_mode((200, 200)) + set_root(__file__) + mouse._press(mouse.LEFT) + mouse._press(mouse.MIDDLE) + mouse._set_pos((10, 10)) + pygame.mouse.set_visible(False) + pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_HAND) + + def test_never_pressed(self): + """A button value is false if never pressed.""" + self.assertFalse(mouse.pressed_right) + + def test_press(self): + """We can check for depressed buttons by property check.""" + self.assertTrue(mouse.pressed_left) + + def test_release(self): + """We can release a previously pressed key.""" + mouse._release(mouse.LEFT) + self.assertFalse(mouse.pressed_left) + + def test_get_pressed(self): + """We can get all button states together.""" + self.assertEqual(mouse.pressed, (True, True, False)) + + def test_already_pressed(self): + """Pressing an already pressed button still works.""" + mouse._press(mouse.LEFT) + self.assertTrue(mouse.pressed_left) + + def test_already_released(self): + """Releasing an unpressed button still works.""" + mouse._release(mouse.RIGHT) + self.assertFalse(mouse.pressed_right) + + def test_release_other(self): + """Releasing one key does not release any others that are pressed.""" + mouse._release(mouse.LEFT) + self.assertTrue(mouse.pressed_middle) + + def test_uppercase_constants(self): + """The uppercase attribute names from earlier in the project still + work. This is important for backwards compatibility.""" + self.assertEqual(mouse.LEFT, 1) + self.assertEqual(mouse.MIDDLE, 2) + self.assertEqual(mouse.RIGHT, 3) + self.assertEqual(mouse.WHEEL_UP, 4) + self.assertEqual(mouse.WHEEL_DOWN, 5) + + def test_get_position(self): + """We get the correct current position of the mouse.""" + self.assertEqual(mouse.pos, (10, 10)) + + def test_get_lc_position(self): + """We can get the last called position.""" + self.assertEqual(mouse.last_called_pos, (0, 0)) + + def test_set_position(self): + """We can change the mouse position.""" + mouse.pos = (25, 25) + self.assertEqual(mouse.pos, (25, 25)) + + def test_recent_pos(self): + """Recent positions can be gotten.""" + mouse.pos = (1, 1) + mouse.pos = (2, 2) + self.assertEqual(mouse.recent_pos, ((0,0), (10,10), (1,1), (2,2))) + + def test_recent_pos_max(self): + """We can change the number of recent positions.""" + mouse.recent_pos_max = 120 + self.assertEqual(mouse.recent_pos.maxlen, 120) + + def test_get_relative(self): + """We can get the last position change, starting frm (10,10).""" + mouse.pos = (5, 5) + self.assertEqual(mouse.rel, (5, 5)) + + def test_get_lc_relative(self): + """We can get the last called position change, startig from (0, 0).""" + mouse.pos = (25, 25) + self.assertEqual(mouse.last_called_rel, (25, 25)) + + def test_recent_rel(self): + """Recent position changes can be gotten.""" + mouse.pos = (1, 1) + mouse.pos = (2, 2) + self.assertEqual(mouse.recent_pos, ((0,0), (10,10), (-9,-9), (1,1))) + + def test_recent_rel_max(self): + """We can change the number of recent position changes.""" + mouse.recent_rel_max = 120 + self.assertEqual(mouse.recent_rel.maxlen, 120) + + def test_get_visibility(self): + """We can get whether the mouse cursor is visible.""" + self.assertFalse(mouse.visible) + + def test_change_visibility(self): + """We can change mouse visibility.""" + mouse.visible = True + self.assertTrue(pygame.mouse.get_visible()) + + def test_get_cursor_name(self): + """We can check the cursor name.""" + self.assertEqual(mouse.cursor_name, "HAND") + + def test_change_cursor(self): + """We can change the cursor.""" + mouse.cursor = "IBEAM" + self.assertTrue("IBEAM" in pygame.mouse.get_cursor().__repr__()) + + def test_cursor_hotspot(self): + """We can check the hotspot of the cursor.""" + self.assertNone(mouse.cursor_hotspot) From 4170e38c76d313dad2de7e812f2e33f73bcbab3f Mon Sep 17 00:00:00 2001 From: Mambouna Date: Mon, 23 Jun 2025 18:05:39 +0200 Subject: [PATCH 07/11] Fixed tests, some tests are still suboptimal. --- test/test_mouse.py | 63 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/test/test_mouse.py b/test/test_mouse.py index 0ebcc7e1..5864b15f 100644 --- a/test/test_mouse.py +++ b/test/test_mouse.py @@ -1,21 +1,35 @@ import unittest +from collections import deque import pygame -from pgzero.mouse import mouse +from pgzero.loaders import set_root +from pgzero.mouse import mouse_instance as mouse class MouseTest(unittest.TestCase): - def setUp(self): + @classmethod + def setUpClass(cls): + """Initialise the display and set loaders to target the current dir.""" pygame.init() cls.surf = pygame.display.set_mode((200, 200)) set_root(__file__) + + @classmethod + def tearDownClass(cls): + """Shut down the display.""" + pygame.display.quit() + + def setUp(self): mouse._press(mouse.LEFT) mouse._press(mouse.MIDDLE) mouse._set_pos((10, 10)) pygame.mouse.set_visible(False) pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_HAND) + def tearDown(self): + mouse._null_rel() + def test_never_pressed(self): """A button value is false if never pressed.""" self.assertFalse(mouse.pressed_right) @@ -67,40 +81,57 @@ def test_get_lc_position(self): def test_set_position(self): """We can change the mouse position.""" - mouse.pos = (25, 25) + # TODO: Can this be done via mouse.pos = (25, 25)? Problem is that a + # normal Pygame event would have to be gotten before changes take + # effect. + mouse._set_pos((25, 25)) self.assertEqual(mouse.pos, (25, 25)) def test_recent_pos(self): """Recent positions can be gotten.""" - mouse.pos = (1, 1) - mouse.pos = (2, 2) - self.assertEqual(mouse.recent_pos, ((0,0), (10,10), (1,1), (2,2))) + mouse._recent_pos = deque(maxlen=60) + # TODO: Can this be done via mouse.pos = (1, 1) etc.? Problem is that a + # normal Pygame event would have to be gotten before changes take + # effect. + mouse._set_pos((1, 1)) + mouse._set_pos((2, 2)) + self.assertEqual(mouse.recent_pos, ((2,2), (1,1))) def test_recent_pos_max(self): """We can change the number of recent positions.""" mouse.recent_pos_max = 120 - self.assertEqual(mouse.recent_pos.maxlen, 120) + self.assertEqual(mouse._recent_pos.maxlen, 120) def test_get_relative(self): - """We can get the last position change, starting frm (10,10).""" - mouse.pos = (5, 5) - self.assertEqual(mouse.rel, (5, 5)) + """We can get the last position change, starting from (10,10).""" + # TODO: Can this be done via mouse.pos = (5, 5)? Problem is that a + # normal Pygame event would have to be gotten before changes take + # effect. + mouse._add_rel((-5, -5)) + self.assertEqual(mouse.rel, (-5, -5)) def test_get_lc_relative(self): """We can get the last called position change, startig from (0, 0).""" - mouse.pos = (25, 25) + # TODO: Can this be done via mouse.pos = (25, 25)? Problem is that a + # normal Pygame event would have to be gotten before changes take + # effect. + mouse._add_rel((25, 25)) self.assertEqual(mouse.last_called_rel, (25, 25)) def test_recent_rel(self): """Recent position changes can be gotten.""" - mouse.pos = (1, 1) - mouse.pos = (2, 2) - self.assertEqual(mouse.recent_pos, ((0,0), (10,10), (-9,-9), (1,1))) + mouse._recent_rel = deque(maxlen=60) + # TODO: Can this be done via mouse.pos = (1, 1) etc.? Problem is that a + # normal Pygame event would have to be gotten before changes take + # effect. + mouse._add_rel((2, 2)) + mouse._add_rel((-1, -1)) + self.assertEqual(mouse.recent_rel, ((-1, -1), (2, 2))) def test_recent_rel_max(self): """We can change the number of recent position changes.""" mouse.recent_rel_max = 120 - self.assertEqual(mouse.recent_rel.maxlen, 120) + self.assertEqual(mouse._recent_rel.maxlen, 120) def test_get_visibility(self): """We can get whether the mouse cursor is visible.""" @@ -122,4 +153,4 @@ def test_change_cursor(self): def test_cursor_hotspot(self): """We can check the hotspot of the cursor.""" - self.assertNone(mouse.cursor_hotspot) + self.assertIsNone(mouse.cursor_hotspot) From 9ad8774fbd1559bf60cec615e16d9ef7aa09d9e3 Mon Sep 17 00:00:00 2001 From: Mambouna Date: Mon, 23 Jun 2025 18:17:21 +0200 Subject: [PATCH 08/11] Fixes for flake8 --- test/test_event_dispatch.py | 1 - test/test_mouse.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/test/test_event_dispatch.py b/test/test_event_dispatch.py index a9ceaf60..5697a242 100644 --- a/test/test_event_dispatch.py +++ b/test/test_event_dispatch.py @@ -2,7 +2,6 @@ from unittest.mock import Mock from pgzero.game import PGZeroGame from pgzero.constants import mouse -import pgzero.mouse class Event: diff --git a/test/test_mouse.py b/test/test_mouse.py index 5864b15f..e7173792 100644 --- a/test/test_mouse.py +++ b/test/test_mouse.py @@ -56,7 +56,7 @@ def test_already_released(self): """Releasing an unpressed button still works.""" mouse._release(mouse.RIGHT) self.assertFalse(mouse.pressed_right) - + def test_release_other(self): """Releasing one key does not release any others that are pressed.""" mouse._release(mouse.LEFT) @@ -95,7 +95,7 @@ def test_recent_pos(self): # effect. mouse._set_pos((1, 1)) mouse._set_pos((2, 2)) - self.assertEqual(mouse.recent_pos, ((2,2), (1,1))) + self.assertEqual(mouse.recent_pos, ((2, 2), (1, 1))) def test_recent_pos_max(self): """We can change the number of recent positions.""" From 9a2687fc3cf7c6920c0dd11362b7c0e69436d14b Mon Sep 17 00:00:00 2001 From: Mambouna Date: Sat, 20 Sep 2025 20:24:57 +0200 Subject: [PATCH 09/11] Removed trailing whitespace --- doc/builtins.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/builtins.rst b/doc/builtins.rst index dbdd5ba0..4761fe7c 100644 --- a/doc/builtins.rst +++ b/doc/builtins.rst @@ -401,7 +401,7 @@ or the displayed mouse cursor. .. attribute:: pressed_middle - Returns True if the middle mouse button is currently pressed, + Returns True if the middle mouse button is currently pressed, False else. .. attribute:: pressed_right @@ -423,14 +423,14 @@ or the displayed mouse cursor. .. attribute:: recent_pos - Returns a tuple of the n last positions the mouse had moved - to starting from the most recent. Default for n is 60 and + Returns a tuple of the n last positions the mouse had moved + to starting from the most recent. Default for n is 60 and can be changed via recent_pos_max. .. attribute:: recent_pos_max Returns the total number of positions tracked via recent_pos. - + Increasing this value simply extends the length of the queue of tracked positions. Decreasing it also cuts the queue to the new maximum number of elements. @@ -448,14 +448,14 @@ or the displayed mouse cursor. .. attribute:: recent_rel Returns a tuple of the n last position changes the mouse had, - starting from the most recent. Default for n is 60 and + starting from the most recent. Default for n is 60 and can be changed via recent_rel_max. .. attribute:: recent_rel_max Returns the total number of position changed tracked via recent_pos. - + Increasing this value simply extends the length of the queue of tracked positions. Decreasing it also cuts the queue to the new maximum number of elements. @@ -492,7 +492,7 @@ or the displayed mouse cursor. Returns the hotspot tuple of the current cursor or None if the hotspot is unknown (because it is a system cursor). - + Recent pos and rel '''''''''''''''''' @@ -515,7 +515,7 @@ and changes for each individual mouse movement event. Since multiple mouse movement events can happen in a single frame, there is no fixed relationship between the number of frames passed and the recorded positions and relative changes. In general though, a higher maximum -value means positions and changes from longer before will still be +value means positions and changes from longer before will still be recorded. Cursors @@ -563,9 +563,9 @@ when a system cursor is in use. LEFT or pressed_left '''''''''''''''''''' -While both are accessed via ``mouse``, ``mouse.LEFT`` and +While both are accessed via ``mouse``, ``mouse.LEFT`` and ``mouse.pressed_left`` are different things. ``mouse.LEFT`` is used -in your custom functions that handle mouse events, e.g. +in your custom functions that handle mouse events, e.g. ``if button == mouse.LEFT`` whereas ``mouse.pressed`` and its variants can be used outside of these, for example in the ``update()`` function to check whether a mouse button is pressed at that moment. From 115fbe1afa11de17f62e553bc385f914781f5a82 Mon Sep 17 00:00:00 2001 From: Mambouna Date: Mon, 29 Sep 2025 11:42:32 +0200 Subject: [PATCH 10/11] Removed cursor functionality --- doc/builtins.rst | 64 +------------------------------------- src/pgzero/mouse.py | 75 --------------------------------------------- test/test_mouse.py | 14 --------- 3 files changed, 1 insertion(+), 152 deletions(-) diff --git a/doc/builtins.rst b/doc/builtins.rst index 4761fe7c..642d7320 100644 --- a/doc/builtins.rst +++ b/doc/builtins.rst @@ -383,8 +383,7 @@ The built-in mouse object can be used to get information of various current states of the mouse in the game: where it is, what buttons are pressed, whether it is visible and many more... -It can also be used to manipulate the mouse, for example changing its position -or the displayed mouse cursor. +It can also be used to manipulate the mouse, for example changing its position. .. class:: Mouse @@ -473,26 +472,6 @@ or the displayed mouse cursor. Returns a boolean of whether the game window is currently focused. - .. attribute:: cursor - - Returns a tuple with two elements: the name string of the - current cursor and the hotspot of the cursor if it is known. - - Setting this loads a new cursor and applies it to the mouse. - This can be done with either one string, or one string and - a hotspot position tuple at the same time. - - For a more detailed explanation, see below. - - .. attribute:: cursor_name - - Returns the current name string of the mouse cursor. - - .. attribute:: cursor_hotspot - - Returns the hotspot tuple of the current cursor or None - if the hotspot is unknown (because it is a system cursor). - Recent pos and rel '''''''''''''''''' @@ -518,47 +497,6 @@ positions and relative changes. In general though, a higher maximum value means positions and changes from longer before will still be recorded. -Cursors -''''''' - -Many games change how the mouse cursor looks. PGZero lets you control -this by setting the ``mouse.cursor`` attribute. There are two options -here: system cursors or custom cursors. - -Windows, MacOS and Linux systems all come with a variety of pre-made -cursor shapes ready to use. To change to any of them, simply use -``mouse.cursor = "ARROW"`` where *"ARROW"* is usually the default cursor -on your system. To use something else, simply change the string to one -of these options: *"ARROW", "IBEAM", "WAIT", "CROSSHAIR", "HAND"*. - -If you want to use a custom cursor from an image you have in the -``images`` folder, just call ``mouse.cursor = "image_name"`` and the -cursor will automatically be loaded from the image resource. - -When using a custom cursor, you might also want to tell PGZero where -in the image the spot is, where an actual mouse click should occur. -The default cursor of most systems has this in the top left corner of -the cursor image but yours could have it in the center (think shooter -crosshairs), in the top middle (a pointing hand maybe) or anywhere else. - -To tell PGZero where to put the actual point, you can also give a hotspot -tuple when setting the cursor: ``mouse.cursor = "image_name", (12, 0)``. - -This tells the game that when you press a mouse button, the click should -happen not at the top left corner of the cursor image, but 12 pixels to -the right of it. This might be the top middle for a cursor with an image -size of 24x24 pixels. The center for a 40x40 pixels cursor would be -``(20, 20)``. - -This tuple is simply a position tuple like the kind we already know about. -The only difference is that it looks for the position to put mouse clicks -not in relation to the top left corner of the game window, but just the -cursor image. - -System cursors define their own hotspot, so you don't have to worry about -them. This also means you can't manually check the hotspot of system -cursors with ``mouse.cursor_hotspot`` however. This will return ``None`` -when a system cursor is in use. LEFT or pressed_left '''''''''''''''''''' diff --git a/src/pgzero/mouse.py b/src/pgzero/mouse.py index 78b77f8d..7e16a9bc 100644 --- a/src/pgzero/mouse.py +++ b/src/pgzero/mouse.py @@ -179,80 +179,5 @@ def visible(self, val): def focused(self): return pygame.mouse.get_focused() - # TODO: Better way to separate cursor, name and hotspot getting - # without doubling up on the code? cursor could just call - # name and hotspot, but then we would run pygames get_cursor() - # twice. Neither is ideal. - # Another option would be to expose an explicit getter function - # and then turn it into a property the old fashioned way. This - # works, but it rather ugly and terrible practise... - @property - def cursor(self): - c = pygame.mouse.get_cursor() - if c.type == "system": - return c.__repr__().split("_")[-1][:-2], None - elif c.type == "color": - name = self._cursor_image_name - if name and loaders.images._cache[name] == c.data[1]: - return name, c.data[0] - else: - return "UNKNOWN", c.data[0] - else: - return "BITMAP", None - - @cursor.setter - def cursor(self, args): - # Separate arguments if both a cursor and hotspot were given. - if isinstance(args, tuple) and len(args) > 1: - c_string = args[0] - hotspot = args[1] - else: - c_string = args - hotspot = None - system_cursors = ["ARROW", "IBEAM", "WAIT", "CROSSHAIR", "HAND"] - if c_string in system_cursors: - self._cursor_image_name = None - exec("pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_" - + c_string + ")") - if hotspot: - print("WARNING: System cursors can't be given a hotspot" - " as they define their own. The given hotspot was" - " ignored.") - else: - if not hotspot: - hotspot = (0, 0) - surface = loaders.images.load(c_string) - self._cursor_image_name = c_string - pygame.mouse.set_cursor(hotspot, surface) - - @property - def cursor_name(self): - c = pygame.mouse.get_cursor() - # As pygame doesn't have an exposed parameter to get - # the kind of system cursor applied, we get it here - # in a kind of dirty but consistent way. - if c.type == "system": - return c.__repr__().split("_")[-1][:-2] - elif c.type == "color": - name = self._cursor_image_name - if name and loaders.images._cache[name] == c.data[1]: - return name - else: - return "UNKNOWN" - else: - return "BITMAP" - - @property - def cursor_hotspot(self): - c = pygame.mouse.get_cursor() - # For color cursors, the hotspot is easy to get. - if c.type == "color": - return c.data[0] - # Pygame doesn't have a way to give us the hotspot - # for system cursors and bitmap cursors aren't - # supported for PGZero. - else: - return None - mouse_instance = Mouse() diff --git a/test/test_mouse.py b/test/test_mouse.py index e7173792..ec996eef 100644 --- a/test/test_mouse.py +++ b/test/test_mouse.py @@ -25,7 +25,6 @@ def setUp(self): mouse._press(mouse.MIDDLE) mouse._set_pos((10, 10)) pygame.mouse.set_visible(False) - pygame.mouse.set_cursor(pygame.SYSTEM_CURSOR_HAND) def tearDown(self): mouse._null_rel() @@ -141,16 +140,3 @@ def test_change_visibility(self): """We can change mouse visibility.""" mouse.visible = True self.assertTrue(pygame.mouse.get_visible()) - - def test_get_cursor_name(self): - """We can check the cursor name.""" - self.assertEqual(mouse.cursor_name, "HAND") - - def test_change_cursor(self): - """We can change the cursor.""" - mouse.cursor = "IBEAM" - self.assertTrue("IBEAM" in pygame.mouse.get_cursor().__repr__()) - - def test_cursor_hotspot(self): - """We can check the hotspot of the cursor.""" - self.assertIsNone(mouse.cursor_hotspot) From 987cc199201e3b2b64042e4fa463dc1930fcd68b Mon Sep 17 00:00:00 2001 From: Mambouna Date: Mon, 29 Sep 2025 11:48:26 +0200 Subject: [PATCH 11/11] Removed unnecessary import --- src/pgzero/mouse.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pgzero/mouse.py b/src/pgzero/mouse.py index 7e16a9bc..b317b7c8 100644 --- a/src/pgzero/mouse.py +++ b/src/pgzero/mouse.py @@ -2,7 +2,6 @@ import pygame.mouse from collections import deque from itertools import islice -from . import loaders class Mouse: