diff --git a/pygame_gui/elements/ui_button.py b/pygame_gui/elements/ui_button.py index 09bd78cd..1553ec07 100644 --- a/pygame_gui/elements/ui_button.py +++ b/pygame_gui/elements/ui_button.py @@ -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 @@ -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. @@ -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, @@ -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() @@ -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 @@ -335,7 +349,7 @@ 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 @@ -343,46 +357,60 @@ def process_event(self, event: pygame.event.Event) -> bool: 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: """ diff --git a/tests/test_elements/test_ui_button.py b/tests/test_elements/test_ui_button.py index 8e62d6dc..7f2ac4c1 100644 --- a/tests/test_elements/test_ui_button.py +++ b/tests/test_elements/test_ui_button.py @@ -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):