Source code for pygame_menu.widgets.widget.selector

"""
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
import pygame_menu.controls as ctrl

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, \
    Literal, ColorType, ColorInputType, Tuple2IntType, Tuple3IntType, EventVectorType, \
    Tuple2NumberType, Callable

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
    """
    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_sformat(self, sformat: str) -> 'Selector': """ Set sformat for classic style. This receives a string which is later formatted with {0}: title and {1}: the current selected item. :param sformat: String. Must contain {0} and {1} :return: Self reference """ assert isinstance(sformat, str) assert '{0}' in sformat and '{1}' in sformat and '{2}' not in sformat, \ 'sformat must contain {0} and {1}' self._sformat = sformat return self
[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 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
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 button 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 when changing the selector :param onreturn: Callback executed when pressing return button :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