"""
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',
'SelectorManager',
# Constants
'SELECTOR_STYLE_CLASSIC',
'SELECTOR_STYLE_FANCY',
# Utils
'check_selector_items',
# Types
'SelectorStyleType'
]
import pygame
import pygame_menu
from abc import ABC
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, AbstractWidgetManager
from pygame_menu._types import Tuple, Union, List, Any, Optional, CallbackType, \
ColorType, ColorInputType, Tuple2IntType, Tuple3IntType, EventVectorType, \
Tuple2NumberType, Callable
SELECTOR_STYLE_CLASSIC = 'classic'
SELECTOR_STYLE_FANCY = 'fancy'
SelectorStyleType = str
def check_selector_items(items: Union[Tuple, List]) -> None:
"""
Check the items list.
:param items: Items list
"""
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 (apply) 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 (apply) 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.
"""
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.
"""
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
"""
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 from 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...)]``
"""
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():
self._readonly_check_mouseover(events)
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 self._ctrl.left(event, self) or
joy_hatmotion and self._ctrl.joy_left(event, self) or
joy_axismotion and self._ctrl.joy_axis_x_left(event, self)
):
self._left()
return True
# Right button
elif (
keydown and self._ctrl.right(event, self) or
joy_hatmotion and self._ctrl.joy_right(event, self) or
joy_axismotion and self._ctrl.joy_axis_x_right(event, self)
):
self._right()
return True
# Press enter
elif (
keydown and self._ctrl.apply(event, self) or
joy_button_down and self._ctrl.joy_select(event, self)
):
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
class SelectorManager(AbstractWidgetManager, ABC):
"""
Selector manager.
"""
def selector(
self,
title: Any,
items: Union[List[Tuple[Any, ...]], List[str]],
default: int = 0,
onchange: CallbackType = None,
onreturn: CallbackType = None,
onselect: Optional[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any]] = None,
selector_id: str = '',
style: SelectorStyleType = SELECTOR_STYLE_CLASSIC,
**kwargs
) -> 'pygame_menu.widgets.Selector':
"""
Add a selector to the Menu: several items and two functions that are
executed when changing the selector (left/right) and pressing return
(apply) 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...)``.
If ``onselect`` is defined, the callback is executed as follows, where
``selected`` is a boolean representing the selected status:
.. code-block:: python
onselect(selected, widget, menu)
kwargs (Optional)
- ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_
- ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color
- ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px
- ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color
- ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px
- ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals`
- ``border_width`` (int) – Border width in px. If ``0`` disables the border
- ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over
- ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows
- ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating
- ``font_background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Widget font background color
- ``font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget font color
- ``font_name`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Widget font path
- ``font_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Font shadow color
- ``font_shadow_offset`` (int) – Font shadow offset in px
- ``font_shadow_position`` (str) – Font shadow position, see locals for position
- ``font_shadow`` (bool) – Font shadow is enabled or disabled
- ``font_size`` (int) – Font size of the widget
- ``margin`` (tuple, list) – Widget (left, bottom) margin in px
- ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left)
- ``readonly_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode
- ``readonly_selected_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode and is selected
- ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color
- ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect
- ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow
- ``shadow_radius`` (int) - Border radius of the shadow
- ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'``
- ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled
- ``style_fancy_arrow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Arrow color of fancy style
- ``style_fancy_arrow_margin`` (tuple, list) – Margin of arrows on x-axis and y-axis in px; format: (left, right, vertical)
- ``style_fancy_bgcolor`` (tuple, list, str, int, :py:class:`pygame.Color`) – Background color of fancy style
- ``style_fancy_bordercolor`` (tuple, list, str, int, :py:class:`pygame.Color`) – Border color of fancy style
- ``style_fancy_borderwidth`` (int) – Border width of fancy style; ``1`` by default
- ``style_fancy_box_inflate`` (tuple, list) – Box inflate of fancy style on x-axis and y-axis (x, y) in px
- ``style_fancy_box_margin`` (tuple, list) – Box margin on x-axis and y-axis (x, y) in fancy style from title in px
- ``tab_size`` (int) – Width of a tab character
.. note::
All theme-related optional kwargs use the default Menu theme if not
defined.
.. note::
This is applied only to the base Menu (not the currently displayed,
stored in ``_current`` pointer); for such behaviour apply to
:py:meth:`pygame_menu.menu.Menu.get_current` object.
.. warning::
Be careful with kwargs collision. Consider that all optional documented
kwargs keys are removed from the object.
:param title: Title of the selector
:param items: Item list of the selector; format ``[('Item1', a, b, c...), ('Item2', d, e, f...)]``
:param default: Index of default item to display
:param onchange: Callback executed when changing the selector
:param onreturn: Callback executed when pressing return (apply)
:param onselect: Callback executed when selecting the widget
:param selector_id: ID of the selector
:param style: Selector style (visual)
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.Selector`
"""
# Filter widget attributes to avoid passing them to the callbacks
attributes = self._filter_widget_attributes(kwargs)
# Get fancy style attributes
style_fancy_arrow_color = kwargs.pop('style_fancy_arrow_color', self._theme.widget_box_arrow_color)
style_fancy_arrow_margin = kwargs.pop('style_fancy_arrow_margin', self._theme.widget_box_arrow_margin)
style_fancy_bgcolor = kwargs.pop('style_fancy_bgcolor', self._theme.widget_box_background_color)
style_fancy_bordercolor = kwargs.pop('style_fancy_bordercolor', self._theme.widget_box_border_color)
style_fancy_borderwidth = kwargs.pop('style_fancy_borderwidth', self._theme.widget_box_border_width)
style_fancy_box_inflate = kwargs.pop('style_fancy_box_inflate', self._theme.widget_box_inflate)
style_fancy_box_margin = kwargs.pop('style_fancy_box_margin', self._theme.widget_box_margin)
widget = Selector(
default=default,
items=items,
onchange=onchange,
onreturn=onreturn,
onselect=onselect,
selector_id=selector_id,
style=style,
style_fancy_arrow_color=style_fancy_arrow_color,
style_fancy_arrow_margin=style_fancy_arrow_margin,
style_fancy_bgcolor=style_fancy_bgcolor,
style_fancy_bordercolor=style_fancy_bordercolor,
style_fancy_borderwidth=style_fancy_borderwidth,
style_fancy_box_inflate=style_fancy_box_inflate,
style_fancy_box_margin=style_fancy_box_margin,
title=title,
**kwargs
)
self._configure_widget(widget=widget, **attributes)
self._append_widget(widget)
return widget