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

Added ability to set default value(s) for UISelectionList #213

Merged
merged 17 commits into from Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
64 changes: 63 additions & 1 deletion pygame_gui/elements/ui_selection_list.py
Expand Up @@ -24,6 +24,9 @@ class UISelectionList(UIElement):
for details.
:param item_list: A list of items as strings (item name only), or tuples of two strings (name,
theme_object_id).
:param default_selection: Default item(s) that should be selected: a string or a (str, str)
tuple for single-selection lists or a list of strings or list of
(str, str) tuples for multi-selection lists.
:param manager: The GUI manager that handles drawing and updating the UI and interactions
between elements.
:param allow_multi_select: True if we are allowed to pick multiple things from the selection
Expand Down Expand Up @@ -55,7 +58,12 @@ def __init__(self,
parent_element: UIElement = None,
object_id: Union[ObjectID, str, None] = None,
anchors: Dict[str, str] = None,
visible: int = 1
visible: int = 1,
default_selection: Union[
str, Tuple[str, str], # Single-selection lists
List[str], List[Tuple[str, str]], # Multi-selection lists
None
] = None,
):

super().__init__(relative_rect,
Expand All @@ -75,6 +83,7 @@ def __init__(self,
self.list_and_scroll_bar_container = None
self.item_list_container = None
self._raw_item_list = item_list
self._default_selection = default_selection
self.item_list = []
self.allow_multi_select = allow_multi_select
self.allow_double_clicks = allow_double_clicks
Expand All @@ -96,6 +105,9 @@ def __init__(self,

self.rebuild_from_changed_theme_data()

if self._default_selection is not None:
self.set_default_selection()

def get_single_selection(self) -> Union[str, None]:
"""
Get the selected item in a list, if any. Only works if this is a single-selection list.
Expand Down Expand Up @@ -295,6 +307,56 @@ def set_item_list(self, new_item_list: Union[List[str], List[Tuple[str, str]]]):
else:
break

def set_default_selection(self):
"""
Set the default selection of the list. The default selection type must be a string or (str,
str) tuple for single selection lists. For multi-selection lists, they can be a single
string, an (str, str) tuple, a list of strings, or a list of (str, str) tuples.

For foregivess' sake, a single-item list MAY be used to specify the default value for a
single-selection list.

Tuples should be arranged like so:

(list_text, object_ID)

- list_text: displayed in the UI
- object_ID: used for theming and events

:raise ValueError: Throw an exception if a list is used for the default for a
single-selection list, or if the default value(s) requested is/are not
present in the options list.

:raise TypeError: Throw an exception if anything other than a string or a (str, str) tuple
is encountered in the requested defaults.

"""
default = self._default_selection

if isinstance(default, list) and self.allow_multi_select is not True:
raise ValueError('Multiple default values specified for single-selection list.')
elif not isinstance(default, list):
default = [default]

# Sanity check: return if any values - even not requested defaults - are already selected.
for item in self.item_list:
if item['selected']:
return

for d in default:
if isinstance(d, str):
idx = next((i for i, item in enumerate(self.item_list) if item['text'] == d), None)
elif isinstance(d, tuple):
idx = next((i for i, item in enumerate(self.item_list) if item['text'] == d[0] and
item['object_id'] == d[1]), None)
else:
raise TypeError(f'Requested default {d} is not a string or (str, str) tuple.')

if idx is None:
raise ValueError(f'Requested default {d} not found in selection list {self.item_list}.')
self.item_list[idx]['selected'] = True
self.item_list[idx]['button_element'].select()

def process_event(self, event: pygame.event.Event) -> bool:
"""
Can be overridden, also handle resizing windows. Gives UI Windows access to pygame events.
Expand Down
160 changes: 160 additions & 0 deletions tests/test_elements/test_ui_selection_list.py
Expand Up @@ -783,3 +783,163 @@ def test_show_hide_rendering(self, _init_pygame, default_ui_manager, _display_su
manager.update(0.01)
manager.draw_ui(surface)
assert compare_surfaces(empty_surface, surface)

def test_default_selection_exceptions(self):
"""
Test that all exceptions throw appropriately:
1. ValueError if a list of strings/string tuples is passed to a single-selection list.
2. TypeError is a completely invalid type (like an object) is passed.
3. ValueError if ANY of the requested default values are not actually present in the item
list.
"""
resolution = (400, 400)
manager = UIManager(resolution)
lst = [f'item {n}' for n in range(20)]

# Test Case 1
test_case_1_throws = False
try:
UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection=['item 1', 'item 2'],
manager=manager
)
except ValueError as e:
assert 'Multiple default values' in str(e)
test_case_1_throws = True

assert test_case_1_throws is True

# Test Case 2
test_case_2_throws = False
try:
UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection=4,
manager=manager
)
except TypeError as e:
assert 'is not a string or (str, str) tuple.' in str(e)
test_case_2_throws = True

assert test_case_2_throws is True

# Test Case 3 for single-selection lists
test_case_3_single_throws = False
try:
UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection='I am not in the list',
manager=manager
)
except ValueError as e:
assert 'not found in selection list' in str(e)
test_case_3_single_throws = True

assert test_case_3_single_throws is True

# Test Case 4 for multi-select lists.
test_case_3_multiple_throws = False
try:
UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection=['item 1', 'I am not in the list'],
allow_multi_select=True,
manager=manager
)
except ValueError as e:
assert 'not found in selection list' in str(e)
test_case_3_multiple_throws = True

assert test_case_3_multiple_throws is True

def test_default_selection_sets_correctly(self):
"""
Test that the default selection parameter ACTUALLY sets the values.
"""
resolution = (400, 400)
manager = UIManager(resolution)
lst = [f'item {i}' for i in range(20)]
selection = 'item 10'

# Single-selection list default.
single_list = UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection=selection,
manager=manager
)

assert selection == single_list.get_single_selection()

# Multi-selection list defaults.
selection = ['item 3', 'item 10', 'item 15']
multi_list = UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection=selection,
allow_multi_select=True,
manager=manager
)

assert selection == multi_list.get_multi_selection()

def test_default_selection_changes(
self,
_init_pygame,
default_ui_manager,
default_display_surface,
_display_surface_return_none
):
"""
Test that the default selection parameter does not permanently fix the list selection.
"""
resolution = (400, 400)
manager = UIManager(resolution)
lst = [f'item {i}' for i in range(0, 20)]
default = 10
new_selection = 5

# Test single selection list
single_list = UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection=lst[default],
manager=manager
)

event_data = {'user_type': pygame_gui.UI_BUTTON_PRESSED,
'ui_element': single_list.item_list_container.elements[new_selection]}
select_event = pygame.event.Event(pygame.USEREVENT, event_data)
single_list.process_event(select_event)

assert lst[new_selection] == single_list.get_single_selection()

# Test multi-selection list
default = [lst[10], lst[17]]
selections = [4, 10, 15]

multi_list = UISelectionList(
relative_rect=pygame.Rect(100, 100, 400, 400),
item_list=lst,
default_selection=default,
allow_multi_select=True,
manager=manager
)

for selection in selections:
event_data = {'user_type': pygame_gui.UI_BUTTON_PRESSED,
'ui_element': multi_list.item_list_container.elements[selection]}
select_event = pygame.event.Event(pygame.USEREVENT, event_data)
multi_list.process_event(select_event)

final_vals = multi_list.get_multi_selection()
assert len(final_vals) == 3
assert lst[10] not in final_vals
assert lst[4] in final_vals
assert lst[15] in final_vals
assert lst[17] in final_vals