Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add parameter command to Button #508

Merged
merged 14 commits into from
Feb 14, 2024
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 @@
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 triggered by this element.
MyreMylar marked this conversation as resolved.
Show resolved Hide resolved
: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 @@
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 @@
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

Check warning on line 147 in pygame_gui/elements/ui_button.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/elements/ui_button.py#L147

Added line #L147 was not covered by tests

self.rebuild_from_changed_theme_data()

Expand Down Expand Up @@ -315,9 +329,9 @@
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 @@
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 a event 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.
MyreMylar marked this conversation as resolved.
Show resolved Hide resolved

# 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 = {}

Check warning on line 398 in pygame_gui/elements/ui_button.py

View check run for this annotation

Codecov / codecov/patch

pygame_gui/elements/ui_button.py#L398

Added line #L398 was not covered by tests

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
Loading