"""
pygame-menu
https://github.com/ppizarror/pygame-menu
SELECTOR
Selector class, contains several items that can be changed in a horizontal way
(left/right). Items are solely displayed.
"""
__all__ = [
# Main class
'Selector',
# Constants
'SELECTOR_STYLE_CLASSIC',
'SELECTOR_STYLE_FANCY',
# Utils
'check_selector_items',
# Types
'SelectorStyleType'
]
import pygame
import pygame_menu.controls as ctrl
from pygame_menu.locals import FINGERUP
from pygame_menu.utils import check_key_pressed_valid, assert_color, assert_vector, \
make_surface, get_finger_pos
from pygame_menu.widgets.core.widget import Widget
from pygame_menu._types import Tuple, Union, List, Any, Optional, CallbackType, \
Literal, ColorType, ColorInputType, Tuple2IntType, Tuple3IntType, EventVectorType, \
Tuple2NumberType
SELECTOR_STYLE_CLASSIC = 'classic'
SELECTOR_STYLE_FANCY = 'fancy'
SelectorStyleType = Literal[SELECTOR_STYLE_CLASSIC, SELECTOR_STYLE_FANCY]
def check_selector_items(items: Union[Tuple, List]) -> None:
"""
Check the items list.
:param items: Items list
:return: None
"""
assert len(items) > 0, 'item list cannot be empty'
for e in items:
assert len(e) >= 1, \
'length of each item on item list must be equal or greater than 1 ' \
'(i.e. cannot be empty)'
assert isinstance(e[0], (str, bytes)), \
f'first element of each item on list must be a string ' \
f'(the title of each item), but received "{e[0]}"'
# noinspection PyMissingOrEmptyDocstring
[docs]class Selector(Widget):
"""
Selector widget: several items and two functions that are executed when changing
the selector (left/right) and pressing return key on the selected item.
The items of the selector are like:
.. code-block:: python
items = [('Item1', a, b, c...), ('Item2', d, e, f...)]
The callbacks receive the current selected item, its index in the list,
the associated arguments, and all unknown keyword arguments, where
``selected_item=widget.get_value()`` and ``selected_index=widget.get_index()``:
.. code-block:: python
onchange((selected_item, selected_index), a, b, c..., **kwargs)
onreturn((selected_item, selected_index), a, b, c..., **kwargs)
For example, if ``selected_index=0`` then ``selected_item=('Item1', a, b, c...)``.
.. note::
Selector accepts all transformations.
:param title: Selector title
:param items: Items of the selector
:param selector_id: ID of the selector
:param default: Index of default item to display
:param onchange: Callback when changing the selector
:param onreturn: Callback when pressing return on the selector
:param onselect: Function when selecting the widget
:param style: Selector style (visual)
:param style_fancy_arrow_color: Arrow color of fancy style
:param style_fancy_arrow_margin: Margin of arrows on x-axis and y-axis (x, y) in px; format: (left, right, vertical)
:param style_fancy_bgcolor: Background color of fancy style
:param style_fancy_bordercolor: Border color of fancy style
:param style_fancy_borderwidth: Border width of fancy style
:param style_fancy_box_inflate: Box inflate of fancy style on x-axis and y-axis (x, y) in px
:param style_fancy_box_margin: Box margin on x-axis and y-axis (x, y) in fancy style from title in px
:param kwargs: Optional keyword arguments
"""
_index: int
_items: Union[List[Tuple[Any, ...]], List[str]]
_sformat: str
_style: SelectorStyleType
_style_fancy_arrow_color: ColorType
_style_fancy_arrow_margin: Tuple3IntType
_style_fancy_bgcolor: ColorType
_style_fancy_bordercolor: ColorType
_style_fancy_borderwidth: int
_style_fancy_box_inflate: Tuple2IntType
_style_fancy_box_margin: Tuple2IntType # Box (left, top)
_title_size: int
def __init__(
self,
title: Any,
items: Union[List[Tuple[Any, ...]], List[str]],
selector_id: str = '',
default: int = 0,
onchange: CallbackType = None,
onreturn: CallbackType = None,
onselect: CallbackType = None,
style: SelectorStyleType = SELECTOR_STYLE_CLASSIC,
style_fancy_arrow_color: ColorInputType = (160, 160, 160),
style_fancy_arrow_margin: Tuple3IntType = (5, 5, 0),
style_fancy_bgcolor: ColorInputType = (180, 180, 180),
style_fancy_bordercolor: ColorInputType = (0, 0, 0),
style_fancy_borderwidth: int = 1,
style_fancy_box_inflate: Tuple2IntType = (0, 0),
style_fancy_box_margin: Tuple2NumberType = (25, 0),
*args,
**kwargs
) -> None:
assert isinstance(items, list)
assert isinstance(selector_id, str)
assert isinstance(default, int)
assert style in (SELECTOR_STYLE_CLASSIC, SELECTOR_STYLE_FANCY), \
'invalid selector style'
# Check items list
check_selector_items(items)
assert default >= 0, \
'default position must be equal or greater than zero'
assert default < len(items), \
'default position should be lower than number of values'
assert isinstance(selector_id, str), 'id must be a string'
assert isinstance(default, int), 'default must be an integer'
# Check fancy style
style_fancy_arrow_color = assert_color(style_fancy_arrow_color)
assert_vector(style_fancy_arrow_margin, 3, int)
assert_vector(style_fancy_box_margin, 2)
style_fancy_bgcolor = assert_color(style_fancy_bgcolor)
style_fancy_bordercolor = assert_color(style_fancy_bordercolor)
assert isinstance(style_fancy_borderwidth, int) and style_fancy_borderwidth >= 0
assert_vector(style_fancy_box_inflate, 2, int)
assert style_fancy_box_inflate[0] >= 0 and style_fancy_box_inflate[1] >= 0, \
'box inflate must be equal or greater than zero on both axis'
super(Selector, self).__init__(
onchange=onchange,
onreturn=onreturn,
onselect=onselect,
title=title,
widget_id=selector_id,
args=args,
kwargs=kwargs
)
self._accept_events = True
self._index = 0
self._items = items.copy()
self._sformat = ''
self._style = style
self._title_size = 0
# Store fancy style
self._style_fancy_arrow_color = style_fancy_arrow_color
self._style_fancy_arrow_margin = style_fancy_arrow_margin
self._style_fancy_bgcolor = style_fancy_bgcolor
self._style_fancy_bordercolor = style_fancy_bordercolor
self._style_fancy_borderwidth = style_fancy_borderwidth
self._style_fancy_box_inflate = style_fancy_box_inflate
self._style_fancy_box_margin = (int(style_fancy_box_margin[0]),
int(style_fancy_box_margin[1]))
# Apply default item
default %= len(self._items)
for k in range(0, default):
self._right()
# Last configs
self.set_sformat('{0}< {1} >')
self.set_default_value(default)
[docs] def set_default_value(self, index: int) -> 'Selector':
self._default_value = index
return self
def _apply_font(self) -> None:
self._title_size = self._font.size(self._title)[0]
if self._style == SELECTOR_STYLE_FANCY:
self._title_size += self._style_fancy_box_margin[0] \
- self._style_fancy_box_inflate[0] / 2 \
+ self._style_fancy_borderwidth
self._title_size = int(self._title_size)
def _draw(self, surface: 'pygame.Surface') -> None:
surface.blit(self._surface, self._rect.topleft)
def _render(self) -> Optional[bool]:
current_selected = self.get_value()[0][0]
if not self._render_hash_changed(
current_selected, self._selected, self._visible, self._index,
self.readonly):
return True
color = self.get_font_color_status()
# Render from different styles
if self._style == SELECTOR_STYLE_CLASSIC:
string = self._sformat.format(self._title, current_selected)
self._surface = self._render_string(string, color)
else:
title = self._render_string(self._title, color)
current = self._render_string(
current_selected, self.get_font_color_status(check_selection=False)
)
# Create arrows
arrow_left = pygame.Rect(
int(title.get_width() + self._style_fancy_arrow_margin[0]
+ self._style_fancy_box_margin[0]),
int(self._style_fancy_arrow_margin[2]
+ self._style_fancy_box_inflate[1] / 2),
title.get_height(),
title.get_height()
)
arrow_left_pos = (
(arrow_left.left + 5, arrow_left.centery),
(arrow_left.centerx, arrow_left.top + 5),
(arrow_left.centerx, arrow_left.centery - 2),
(arrow_left.right - 5, arrow_left.centery - 2),
(arrow_left.right - 5, arrow_left.centery + 2),
(arrow_left.centerx, arrow_left.centery + 2),
(arrow_left.centerx, arrow_left.bottom - 5),
(arrow_left.left + 5, arrow_left.centery)
)
arrow_right = pygame.Rect(
int(title.get_width()
+ 2 * self._style_fancy_arrow_margin[0]
+ self._style_fancy_box_margin[0]
+ self._style_fancy_arrow_margin[1] + current.get_width()
),
int(self._style_fancy_arrow_margin[2]
+ self._style_fancy_box_inflate[1] / 2
+ self._style_fancy_box_margin[1]),
title.get_height(),
title.get_height()
)
arrow_right_pos = (
(2 * arrow_right.right - (arrow_right.left + 5), arrow_right.centery),
(2 * arrow_right.right - arrow_right.centerx, arrow_right.top + 5),
(2 * arrow_right.right - arrow_right.centerx, arrow_right.centery - 2),
(2 * arrow_right.right - (arrow_right.right - 5), arrow_right.centery - 2),
(2 * arrow_right.right - (arrow_right.right - 5), arrow_right.centery + 2),
(2 * arrow_right.right - arrow_right.centerx, arrow_right.centery + 2),
(2 * arrow_right.right - arrow_right.centerx, arrow_right.bottom - 5),
(2 * arrow_right.right - (arrow_right.left + 5), arrow_right.centery)
)
self._surface = make_surface(
title.get_width()
+ 2 * self._style_fancy_arrow_margin[0]
+ 2 * self._style_fancy_arrow_margin[1]
+ self._style_fancy_box_margin[0]
+ current.get_width()
+ 2 * arrow_left.width
+ self._style_fancy_borderwidth
+ self._style_fancy_box_inflate[0] / 2,
title.get_height() + self._style_fancy_box_inflate[1])
self._surface.blit(title, (0, int(self._style_fancy_box_inflate[1] / 2)))
current_rect_bg = current.get_rect()
current_rect_bg.x += title.get_width() + self._style_fancy_box_margin[0]
current_rect_bg.y += int(self._style_fancy_box_inflate[1] / 2
+ self._style_fancy_box_margin[1])
current_rect_bg.width += 2 * (self._style_fancy_arrow_margin[0]
+ self._style_fancy_arrow_margin[1]
+ arrow_left.width)
current_rect_bg = current_rect_bg.inflate(self._style_fancy_box_inflate)
pygame.draw.rect(self._surface, self._style_fancy_bgcolor, current_rect_bg)
pygame.draw.rect(self._surface, self._style_fancy_bordercolor, current_rect_bg,
self._style_fancy_borderwidth)
self._surface.blit(
current, (int(title.get_width()
+ arrow_left.width
+ self._style_fancy_arrow_margin[0]
+ self._style_fancy_arrow_margin[1]
+ self._style_fancy_box_margin[0]),
int(self._style_fancy_box_inflate[1] / 2
+ self._style_fancy_box_margin[1])))
pygame.draw.polygon(self._surface, self._style_fancy_arrow_color, arrow_left_pos)
pygame.draw.polygon(self._surface, self._style_fancy_arrow_color, arrow_right_pos)
self._apply_transforms()
self._rect.width, self._rect.height = self._surface.get_size()
self.force_menu_surface_update()
[docs] def get_index(self) -> int:
"""
Get selected index.
:return: Selected index
"""
return self._index
[docs] def get_items(self) -> Union[List[Tuple[Any, ...]], List[str]]:
"""
Return a copy of the select items.
:return: Select items list
"""
return self._items.copy()
[docs] def get_value(self) -> Tuple[Union[Tuple[Any, ...], str], int]:
"""
Return the current value of the selected index.
:return: Value and index as a tuple, (value, index)
"""
return self._items[self._index], self._index
[docs] def value_changed(self) -> bool:
return self._index != self._default_value
def _left(self) -> None:
"""
Move selector to left.
:return: None
"""
if self.readonly:
return
self._index = (self._index - 1) % len(self._items)
self.change(*self._items[self._index][1:])
self._sound.play_key_add()
def _right(self) -> None:
"""
Move selector to right.
:return: None
"""
if self.readonly:
return
self._index = (self._index + 1) % len(self._items)
self.change(*self._items[self._index][1:])
self._sound.play_key_add()
[docs] def set_value(self, item: Union[str, int]) -> None:
"""
Set the current value of the widget, selecting the item that matches
the text if ``item`` is a string, or the index if ``item`` is an integer.
This method raises ``ValueError`` if no item found.
For example, if widget item list is ``[['a', 0], ['b', 1], ['a', 2]]``:
- *widget*.set_value('a') -> Widget selects the first item (index 0)
- *widget*.set_value(2) -> Widget selects the third item (index 2)
.. note::
This method does not trigger any event (change).
:param item: Item to select, can be a string or an integer
:return: None
"""
assert isinstance(item, (str, int)), 'item must be a string or an integer'
if isinstance(item, str):
found = False
for i in self._items:
if i[0] == item:
self._index = self._items.index(i)
found = True
break
if not found:
raise ValueError(f'no value "{item}" found in selector')
elif isinstance(item, int):
assert 0 <= item < len(self._items), \
'item index must be greater than zero and lower than the number ' \
'of items on the selector'
self._index = item
self._render()
[docs] def update_items(self, items: Union[List[Tuple[Any, ...]], List[str]]) -> None:
"""
Update selector items.
.. note::
If the length of the list is different than the previous one,
the new index of the selector will be the first item of the list.
:param items: New selector items; format ``[('Item1', a, b, c...), ('Item2', d, e, f...)]``
:return: None
"""
check_selector_items(items)
selected_item = self._items[self._index]
self._items = items
try:
self._index = self._items.index(selected_item)
except ValueError:
if self._index >= len(self._items):
self._index = 0
self._default_value = 0
[docs] def update(self, events: EventVectorType) -> bool:
self.apply_update_callbacks(events)
if self.readonly or not self.is_visible():
return False
for event in events:
if event.type == pygame.KEYDOWN: # Check key is valid
if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
continue
# Check mouse over
self._check_mouseover(event)
# Events
keydown = self._keyboard_enabled and event.type == pygame.KEYDOWN
joy_hatmotion = self._joystick_enabled and event.type == pygame.JOYHATMOTION
joy_axismotion = self._joystick_enabled and event.type == pygame.JOYAXISMOTION
joy_button_down = self._joystick_enabled and event.type == pygame.JOYBUTTONDOWN
# Left button
if keydown and event.key == ctrl.KEY_LEFT or \
joy_hatmotion and event.value == ctrl.JOY_LEFT or \
joy_axismotion and event.axis == ctrl.JOY_AXIS_X and event.value < ctrl.JOY_DEADZONE:
self._left()
return True
# Right button
elif keydown and event.key == ctrl.KEY_RIGHT or \
joy_hatmotion and event.value == ctrl.JOY_RIGHT or \
joy_axismotion and event.axis == ctrl.JOY_AXIS_X and event.value > -ctrl.JOY_DEADZONE:
self._right()
return True
# Press enter
elif keydown and event.key == ctrl.KEY_APPLY or \
joy_button_down and event.button == ctrl.JOY_BUTTON_SELECT:
self._sound.play_key_add()
self.apply(*self._items[self._index][1:])
return True
# Click on selector; don't consider the mouse wheel (button 4 & 5)
elif event.type == pygame.MOUSEBUTTONUP and self._mouse_enabled and \
event.button in (1, 2, 3) or \
event.type == FINGERUP and self._touchscreen_enabled and \
self._menu is not None:
event_pos = get_finger_pos(self._menu, event)
# If collides
rect = self.get_rect(to_real_position=True, apply_padding=False)
if rect.collidepoint(*event_pos):
# Check if mouse collides left or right as percentage, use only X coordinate
mouse_x, _ = event_pos
topleft, _ = rect.topleft
topright, _ = rect.topright
dist = mouse_x - (topleft + self._title_size) # Distance from title
if dist > 0: # User clicked the options, not title
# Position in percentage, if <0.5 user clicked left
pos = dist / float(topright - topleft - self._title_size)
if pos <= 0.5:
self._left()
else:
self._right()
return True
return False