Skip to content

Commit

Permalink
Merge pull request #508 from LondonClass/button-modification
Browse files Browse the repository at this point in the history
Add parameter command to Button
  • Loading branch information
MyreMylar authored Feb 14, 2024
2 parents c9df688 + d0b8c98 commit 38291c9
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 39 deletions.
106 changes: 67 additions & 39 deletions pygame_gui/elements/ui_button.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Union, Tuple, Dict, Iterable, Optional
from typing import Union, Tuple, Dict, Iterable, Callable, Optional, Any
from inspect import signature

import pygame

Expand Down Expand Up @@ -41,6 +42,7 @@ class UIButton(UIElement):
unique event.
:param visible: Whether the element is visible by default. Warning - container visibility may
override this.
:param command: Functions to be called when an event is triggered by this element.
:param text_kwargs: a dictionary of variable arguments to pass to the translated string
useful when you have multiple translations that need variables inserted
in the middle.
Expand All @@ -59,6 +61,7 @@ def __init__(self, relative_rect: Union[pygame.Rect, Tuple[float, float], pygame
generate_click_events_from: Iterable[int] = frozenset([pygame.BUTTON_LEFT]),
visible: int = 1,
*,
command: Union[Callable, Dict[int, Callable]] = None,
tool_tip_object_id: Optional[ObjectID] = None,
text_kwargs: Optional[Dict[str, str]] = None,
tool_tip_text_kwargs: Optional[Dict[str, str]] = None,
Expand Down Expand Up @@ -131,6 +134,17 @@ def __init__(self, relative_rect: Union[pygame.Rect, Tuple[float, float], pygame
self.text_shadow_offset = (0, 0)

self.state_transitions = {}

self._handler = {}
if command is not None:
if callable(command):
self.bind(UI_BUTTON_PRESSED, command)
else:
for key, value in command.items():
self.bind(key, value)

if UI_BUTTON_DOUBLE_CLICKED in self._handler:
self.allow_double_clicks = True

self.rebuild_from_changed_theme_data()

Expand Down Expand Up @@ -315,9 +329,9 @@ def process_event(self, event: pygame.event.Event) -> bool:
if self.is_enabled:
if (self.allow_double_clicks and self.last_click_button == event.button and
self.double_click_timer <= self.ui_manager.get_double_click_time()):
self.on_double_clicked(event.button)
self.on_self_event(UI_BUTTON_DOUBLE_CLICKED, {'mouse_button':event.button})
else:
self.on_start_press(event.button)
self.on_self_event(UI_BUTTON_START_PRESS, {'mouse_button':event.button})
self.double_click_timer = 0.0
self.last_click_button = event.button
self.held = True
Expand All @@ -335,54 +349,68 @@ def process_event(self, event: pygame.event.Event) -> bool:
self._set_inactive()
consumed_event = True
self.pressed_event = True
self.on_pressed(event.button)
self.on_self_event(UI_BUTTON_PRESSED, {'mouse_button':event.button})

if self.is_enabled and self.held:
self.held = False
self._set_inactive()
consumed_event = True

return consumed_event

def bind(self, event:int, function:Callable = None):
"""
Bind a function to an element event.
def on_start_press(self, button: int):
# old event to remove in 0.8.0
event_data = {'user_type': OldType(UI_BUTTON_START_PRESS),
'ui_element': self,
'ui_object_id': self.most_specific_combined_id}
pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))
:param event: The event to bind.
# new event
event_data = {'ui_element': self,
'ui_object_id': self.most_specific_combined_id,
'mouse_button': button}
pygame.event.post(pygame.event.Event(UI_BUTTON_START_PRESS, event_data))

def on_pressed(self, button: int):
# old event
event_data = {'user_type': OldType(UI_BUTTON_PRESSED),
'ui_element': self,
'ui_object_id': self.most_specific_combined_id}
pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))
:param function: The function to bind. None to unbind.
# new event
event_data = {'ui_element': self,
'ui_object_id': self.most_specific_combined_id,
'mouse_button': button}
pygame.event.post(pygame.event.Event(UI_BUTTON_PRESSED, event_data))
"""
if function is None:
self._handler.pop(event, None)
return

def on_double_clicked(self, button: int):
# old event to remove in 0.8.0
event_data = {'user_type': OldType(UI_BUTTON_DOUBLE_CLICKED),
'ui_element': self,
'ui_object_id': self.most_specific_combined_id}
pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))
if callable(function):
num_params = len(signature(function).parameters)
if num_params == 1:
self._handler[event] = function
elif num_params == 0:
self._handler[event] = lambda _:function()
else:
raise ValueError("Command function signatures can have 0 or 1 parameter. "
"If one parameter is set it will contain data for the id of the mouse button used "
"to trigger this click event.")
else:
raise TypeError("Command function must be callable")

def on_self_event(self, event:int, data:Dict[str, Any]=None):
"""
Called when an event is triggered by this element. Handles these events either by posting the event back
to the event queue, or by running a function supplied by the user.
# new event
event_data = {'ui_element': self,
'ui_object_id': self.most_specific_combined_id,
'mouse_button': button}
pygame.event.post(pygame.event.Event(UI_BUTTON_DOUBLE_CLICKED, event_data))

:param event: The event triggered.
:param data: event data
"""
if data is None:
data = {}

if event in self._handler:
self._handler[event](data)
else:
# old event to remove in 0.8.0
event_data = {'user_type': OldType(event),
'ui_element': self,
'ui_object_id': self.most_specific_combined_id}
pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))

# new event
event_data = data
event_data.update({'ui_element': self,
'ui_object_id': self.most_specific_combined_id})
pygame.event.post(pygame.event.Event(event, event_data))

def check_pressed(self) -> bool:
"""
Expand Down
132 changes: 132 additions & 0 deletions tests/test_elements/test_ui_button.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,138 @@ def test_enable(self, _init_pygame: None, default_ui_manager: UIManager,
button.update(0.01)

assert button.check_pressed() is True and button.is_enabled is True

def test_command(self, _init_pygame: None, default_ui_manager: UIManager,
_display_surface_return_none):
button_clicked = False

def test_function(data):
nonlocal button_clicked
button_clicked = True

button = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
text="Test Button",
tool_tip_text="This is a test of the button's tool tip functionality.",
manager=default_ui_manager,
command=test_function)

assert button._handler[pygame_gui.UI_BUTTON_PRESSED] == test_function

assert not button_clicked
# process a mouse button down event
button.process_event(
pygame.event.Event(pygame.MOUSEBUTTONDOWN, {'button': 1, 'pos': (50, 25)}))

# process a mouse button up event
button.process_event(
pygame.event.Event(pygame.MOUSEBUTTONUP, {'button': 1, 'pos': (50, 25)}))

button.update(0.01)
assert button_clicked

def test_command_bad_value(self, _init_pygame: None, default_ui_manager: UIManager,
_display_surface_return_none):
with pytest.raises(TypeError, match="Command function must be callable"):
button = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
text="Test Button",
tool_tip_text="This is a test of the button's tool tip functionality.",
manager=default_ui_manager,
command={pygame_gui.UI_BUTTON_PRESSED:5})

def test_bind(self, _init_pygame: None, default_ui_manager: UIManager,
_display_surface_return_none):
def test_function(data):
pass

button = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
text="Test Button",
tool_tip_text="This is a test of the button's tool tip functionality.",
manager=default_ui_manager)

assert pygame_gui.UI_BUTTON_PRESSED not in button._handler

button.bind(pygame_gui.UI_BUTTON_PRESSED, test_function)
assert button._handler[pygame_gui.UI_BUTTON_PRESSED] == test_function

# test unbind
button.bind(pygame_gui.UI_BUTTON_PRESSED, None)
assert pygame_gui.UI_BUTTON_PRESSED not in button._handler

button.bind(pygame_gui.UI_BUTTON_PRESSED, None)
assert pygame_gui.UI_BUTTON_PRESSED not in button._handler

with pytest.raises(TypeError, match="Command function must be callable"):
button.bind(pygame_gui.UI_BUTTON_PRESSED, "non-callable")

def function_with_3_params(x, y, z):
pass

with pytest.raises(ValueError):
button.bind(pygame_gui.UI_BUTTON_PRESSED, function_with_3_params)

def test_on_self_event(self, _init_pygame: None, default_ui_manager: UIManager,
_display_surface_return_none):
button_start_press = False

def test_function(data):
nonlocal button_start_press
button_start_press = True

pressed_button = 0
def test_function2(data):
nonlocal pressed_button
pressed_button = data["mouse_button"]

command_dict ={pygame_gui.UI_BUTTON_START_PRESS:test_function,
pygame_gui.UI_BUTTON_PRESSED:test_function2}

button = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
text="Test Button",
tool_tip_text="This is a test of the button's tool tip functionality.",
manager=default_ui_manager,
command=command_dict)

assert button._handler[pygame_gui.UI_BUTTON_START_PRESS] == test_function # not
assert button._handler[pygame_gui.UI_BUTTON_PRESSED] == test_function2
assert pygame_gui.UI_BUTTON_DOUBLE_CLICKED not in button._handler

assert not button_start_press
button.on_self_event(pygame_gui.UI_BUTTON_START_PRESS, {'mouse_button':1})
assert button_start_press

assert pressed_button == 0
button.on_self_event(pygame_gui.UI_BUTTON_PRESSED, {'mouse_button':3})
assert pressed_button == 3

button.on_self_event(pygame_gui.UI_BUTTON_DOUBLE_CLICKED, {'mouse_button':1})

confirm_double_click_event_fired = False
for event in pygame.event.get():
if (event.type == pygame_gui.UI_BUTTON_DOUBLE_CLICKED and
event.ui_element == button):
confirm_double_click_event_fired = True
assert confirm_double_click_event_fired

def test_on_self_event_no_params(self, _init_pygame: None, default_ui_manager: UIManager,
_display_surface_return_none):
button_start_press = False

def test_function():
nonlocal button_start_press
button_start_press = True

command_dict ={pygame_gui.UI_BUTTON_START_PRESS:test_function}

button = UIButton(relative_rect=pygame.Rect(10, 10, 150, 30),
text="Test Button",
tool_tip_text="This is a test of the button's tool tip functionality.",
manager=default_ui_manager,
command=command_dict)

assert not button_start_press
button.on_self_event(pygame_gui.UI_BUTTON_START_PRESS, {'mouse_button':1})
assert button_start_press


def test_set_active(self, _init_pygame: None, default_ui_manager: UIManager,
_display_surface_return_none):
Expand Down

0 comments on commit 38291c9

Please sign in to comment.