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

UIDropDownMenu support for passing a dictionary of callables #257

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions pygame_gui/elements/ui_drop_down_menu.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Union, List, Tuple, Dict
from typing import Union, List, Tuple, Dict, Callable

import pygame

Expand All @@ -23,6 +23,7 @@ class UIExpandedDropDownState:

:param drop_down_menu_ui: The UIDropDownElement this state belongs to.
:param options_list: The list of options in this drop down.
:param options_dict: Optional dictionary of methods to call when an option is chosen.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this parameter should have a different name to avoid confusion with options_list and to better represent what it is. Something like per_option_callbacks but perhaps snappier - if you can think of anything?

:param selected_option: The currently selected option.
:param base_position_rect: Position and dimensions rectangle.
:param close_button_width: Width of close button.
Expand All @@ -36,6 +37,7 @@ class UIExpandedDropDownState:
def __init__(self,
drop_down_menu_ui: 'UIDropDownMenu',
options_list: List[str],
options_dict: Union[Dict[str, Callable[..., None]], None],
selected_option: str,
base_position_rect: Union[pygame.Rect, None],
close_button_width: int,
Expand All @@ -47,6 +49,7 @@ def __init__(self,

self.drop_down_menu_ui = drop_down_menu_ui
self.options_list = options_list
self.options_dict = options_dict
self.selected_option = selected_option
self.base_position_rect = base_position_rect

Expand Down Expand Up @@ -257,15 +260,21 @@ def process_event(self, event: pygame.event.Event) -> bool:
self.drop_down_menu_ui.selected_option = selection
self.should_transition = True

# If they provided a dictionary with a callable, use that.
selected_text = self.drop_down_menu_ui.selected_option
if selected_text and self.options_dict and self.options_dict[selected_text]:
self.options_dict[selected_text]()
return True # In this case, consume the event.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be consistent in consuming the UI_SELECTION_LIST_NEW_SELECTION event whether we post the UI_DROP_DOWN_MENU_CHANGED event or not.

I actually think your approach is correct and we should be consuming the UI_SELECTION_LIST_NEW_SELECTION event here. There is no real loss in available functionality as users will still have their call backs or the UI_DROP_DOWN_MENU_CHANGED events if they want to ping a sound effect or something here. This is really an internal event for the drop down and not consuming it could potentially lead to confusion or double event triggering.


# old event - to be removed in 0.8.0
event_data = {'user_type': OldType(UI_DROP_DOWN_MENU_CHANGED),
'text': self.drop_down_menu_ui.selected_option,
'text': selected_text,
'ui_element': self.drop_down_menu_ui,
'ui_object_id': self.drop_down_menu_ui.most_specific_combined_id}
pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_data))

# new event
event_data = {'text': self.drop_down_menu_ui.selected_option,
event_data = {'text': selected_text,
'ui_element': self.drop_down_menu_ui,
'ui_object_id': self.drop_down_menu_ui.most_specific_combined_id}
pygame.event.post(pygame.event.Event(UI_DROP_DOWN_MENU_CHANGED, event_data))
Expand Down Expand Up @@ -627,7 +636,16 @@ class UIDropDownMenu(UIContainer):
The drop down is implemented through two states, one representing the 'closed' menu state
and one for when it has been 'expanded'.

:param options_list: The list of of options to choose from. They must be strings.
:param options_list: The list of options to choose from. They must be strings.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shame I didn't name this parameter just 'options' eh? But I guess it is a bit too late now.


Alternatively, you can provide a dictionary with the strings as keys and
method pointers (callables) as the values; if you provide a dictionary and
a method pointer is specified for an option, that method will be called
directly instead of an event being sent. To specify a method pointer, just
refer to the method without parenthesis::

options_list = {'flour': self.flour_option_chosen}

:param starting_option: The starting option, selected when the menu is first created.
:param relative_rect: The size and position of the element when not expanded.
:param manager: The UIManager that manages this element.
Expand All @@ -643,7 +661,7 @@ class UIDropDownMenu(UIContainer):
"""

def __init__(self,
options_list: List[str],
options_list: Union[List[str], Dict[str, Callable[..., None]]],
starting_option: str,
relative_rect: pygame.Rect,
manager: IUIManagerInterface,
Expand All @@ -665,7 +683,16 @@ def __init__(self,
object_id=object_id,
element_id='drop_down_menu')

self.options_list = options_list
# options_list may be a list of strings, or a dictionary of strings and callables.
self.options_list = None
self.options_dict = None

if isinstance(options_list, dict):
self.options_list = list(options_list.keys())
self.options_dict = options_list
else:
self.options_list = options_list

self.selected_option = starting_option
self.open_button_width = 20

Expand Down Expand Up @@ -700,6 +727,7 @@ def __init__(self,
self.visible),
'expanded': UIExpandedDropDownState(self,
self.options_list,
self.options_dict,
self.selected_option,
self.background_rect,
self.open_button_width,
Expand Down
42 changes: 42 additions & 0 deletions tests/test_elements/test_ui_drop_down_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,48 @@ def test_select_option_from_drop_down(self, _init_pygame, default_ui_manager,

assert menu.selected_option == 'flour'

def test_options_dictionary(self, _init_pygame, default_ui_manager,
_display_surface_return_none):
# Inline function can modify a mutable type in the enclosure, such as a list.
menu_items_called = []

def flour_menu_item():
menu_items_called.append('flour')

test_container = UIContainer(relative_rect=pygame.Rect(0, 0, 300, 300),
manager=default_ui_manager)

menu = UIDropDownMenu(options_list={'eggs': None, 'flour': flour_menu_item},
starting_option='eggs',
relative_rect=pygame.Rect(100, 100, 200, 30),
manager=default_ui_manager,
container=test_container)

menu.current_state.should_transition = True
menu.update(0.01)

menu.process_event(pygame.event.Event(
pygame_gui.UI_SELECTION_LIST_NEW_SELECTION,
{'ui_element': menu.menu_states['expanded'].options_selection_list}))

flour_button = menu.current_state.options_selection_list.item_list_container.elements[1]

flour_button.process_event(pygame.event.Event(pygame.MOUSEBUTTONDOWN,
{'button': pygame.BUTTON_LEFT,
'pos': flour_button.rect.center}))

flour_button.process_event(pygame.event.Event(pygame.MOUSEBUTTONUP,
{'button': pygame.BUTTON_LEFT,
'pos': flour_button.rect.center}))

for event in pygame.event.get():
default_ui_manager.process_events(event)

for event in pygame.event.get():
default_ui_manager.process_events(event)

assert menu_items_called[0] == 'flour'

def test_disable(self, _init_pygame, default_ui_manager, _display_surface_return_none):
test_container = UIContainer(relative_rect=pygame.Rect(0, 0, 300, 300),
manager=default_ui_manager)
Expand Down