Source code for pygame_menu.widgets.core.widget

# coding=utf-8
"""
pygame-menu
https://github.com/ppizarror/pygame-menu

WIDGET
Base class for widgets.

NOTE: pygame-menu v3 will not provide new widgets or functionalities, consider
upgrading to the latest version.

License:
-------------------------------------------------------------------------------
The MIT License (MIT)
Copyright 2017-2021 Pablo Pizarro R. @ppizarror

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-------------------------------------------------------------------------------
"""

import pygame
import pygame_menu.baseimage as _baseimage
import pygame_menu.font as _fonts
import pygame_menu.locals as _locals
from pygame_menu.widgets.core.selection import Selection
from pygame_menu.sound import Sound
from pygame_menu.utils import make_surface, assert_alignment, assert_color, assert_position, assert_vector2, \
    to_string, is_callable

from uuid import uuid4
import time
import warnings


[docs]class Widget(object): """ Widget abstract class. :param title: Widget title :type title: str :param widget_id: Widget identifier :type widget_id: str :param onchange: Callback when updating the status of the widget, executed in ``widget.change()`` :type onchange: callable, None :param onreturn: Callback when applying on the widget (return), executed in ``widget.apply()`` :type onreturn: callable, None :param args: Optional arguments for callbacks :type args: any :param kwargs: Optional keyword arguments :type kwargs: dict, any """ def __init__(self, title='', widget_id='', onchange=None, onreturn=None, args=None, kwargs=None ): assert isinstance(widget_id, str), 'widget id must be a string' if onchange: assert is_callable(onchange), 'onchange must be callable or None' if onreturn: assert is_callable(onreturn), 'onreturn must be callable or None' # Store id, if None or empty create new ID based on UUID if widget_id is None or len(widget_id) == 0: widget_id = uuid4() self._alignment = _locals.ALIGN_CENTER self._attributes = {} # Stores widget attributes self._background_color = None self._background_inflate = (0, 0) self._events = [] # type: list self._id = str(widget_id) self._margin = (0.0, 0.0) # type: tuple self._max_width = None # type: (int,float) self._padding = (0, 0, 0, 0) # top, right, bottom, left self._position_set = False self._selected_rect = None # type: (pygame.rect.Rect,None) self._selection_time = 0 # type: float self._title = to_string(title) # Widget transforms self._angle = 0 # Rotation angle (degrees) self._flip = (False, False) # x, y self._scale = [False, 1, 1, False, False] # do_scale, x, y, smooth, use_same_xy self._translate = (0.0, 0.0) # type: tuple # Widget rect. This object does not contain padding. For getting the widget+padding # use .get_rect() Widget method instead self._rect = pygame.Rect(0, 0, 0, 0) # type: pygame.Rect # Callbacks self._draw_callbacks = {} # type: dict self._update_callbacks = {} # type: dict self._args = args or [] # type: list self._kwargs = kwargs or {} # type: dict self._on_change = onchange # type: callable self._on_return = onreturn # type: callable # Surface of the widget self._surface = None # type: (pygame.Surface,None) # Menu reference self._menu = None # If this is True then the widget forces the Menu to update because the # widget render has changed self._menu_surface_needs_update = False # Modified in set_font() method self._font = None # type: (pygame.font.Font,None) self._font_antialias = True # type: bool self._font_background_color = None # type: (tuple, None) self._font_color = (0, 0, 0) # type: tuple self._font_name = '' # type: str self._font_selected_color = (255, 255, 255) # type: tuple self._font_size = 0 # type: int # Text shadow self._shadow = False # type: bool self._shadow_color = (0, 0, 0) # type: tuple self._shadow_offset = 2.0 # type: float self._shadow_position = _locals.POSITION_NORTHWEST self._shadow_tuple = None # (x px offset, y px offset) self._create_shadow_tuple() # Rendering, this variable may be used by render() method # If the hash of the variables change respect to the last render hash # (hash computed using self._hash_variables() method) # then the widget should render and update the hash self._last_render_hash = 0 # type: int # Selection effect, for avoiding exception while getting object rect, NullSelection # was created. Initially it was None self._selection_effect = _NullSelection() # type: Selection # Public attributes self.active = False # Widget requests focus self.is_selectable = True # Some widgets cannot be selected like labels self.joystick_enabled = True self.lock_position = False # If True, locks position after first call to .set_position(x,y) method self.mouse_enabled = True self.selected = False self.selection_effect_enabled = True # Some widgets cannot have selection effect self.selection_expand_background = False # If True, the widget background will inflate to match selection margin if selected self.sound = Sound() # type: Sound self.touchscreen_enabled = True self.visible = True # Use .show() or .hide() to modify this status
[docs] def set_attribute(self, key, value): """ Set widget attribute. :param key: Key of the attribute :type key: str :param value: Value of the attribute :type value: any :return: None """ assert isinstance(key, str) self._attributes[key] = value
[docs] def get_attribute(self, key, default=None): """ Get attribute value. :param key: Key of the attribute :type key: str :param default: Value if does not exists :type default: any :return: Attribute data :rtype: any """ assert isinstance(key, str) if not self.has_attribute(key): return default return self._attributes[key]
[docs] def has_attribute(self, key): """ Returns true if widget has the given attribute. :param key: Key of the attribute :type key: str :return: True if exists :rtype: bool """ assert isinstance(key, str) return key in self._attributes.keys()
[docs] def remove_attribute(self, key): """ Removes the given attribute from the widget. Throws ``IndexError`` if given key does not exist. :param key: Key of the attribute :type key: str :return: None """ if not self.has_attribute(key): raise IndexError('attribute "{0}" does not exists on widget'.format(key)) del self._attributes[key]
@staticmethod def _hash_variables(*args): """ Compute hash from a series of variables. :param args: Variables to compute hash :type args: any :return: Hash data :rtype: int """ return hash(args) def _render_hash_changed(self, *args): """ This method checks if the widget must render because the inner variables changed. This method should include all the variables used by the render method, for example, visibility, selected, etc. :param args: Variables to check the hash :type args: any :return: ``True`` if render has changed the widget :rtype: bool """ _hash = self._hash_variables(*args) if _hash != self._last_render_hash: self._last_render_hash = _hash return True return False
[docs] def set_title(self, title): # lgtm [py/inheritance/incorrect-overridden-signature] """ Update the widget title. :param title: New title :type title: str :return: None """ self._title = to_string(title) self._apply_font() self._last_render_hash = 0 # Force widget render self._render()
[docs] def get_title(self): """ Return the widget title. :return: Widget title :rtype: str """ return self._title
[docs] def set_background_color(self, color, inflate=(0, 0)): """ Set widget background color. :param color: Widget background color :type color: tuple, list, :py:class:`pygame_menu.baseimage.BaseImage`, None :param inflate: Inflate background in *(x,y)*. If ``None``, the widget value is not updated :type inflate: tuple, list, None :return: None """ if color is not None: if isinstance(color, _baseimage.BaseImage): assert color.get_drawing_mode() == _baseimage.IMAGE_MODE_FILL, \ 'currently widget only supports IMAGE_MODE_FILL drawing mode' else: assert_color(color) if inflate is None: inflate = self._background_inflate assert_vector2(inflate) assert inflate[0] >= 0 and inflate[1] >= 0, \ 'widget background inflate must be equal or greater than zero in both axis' self._background_color = color self._background_inflate = tuple(inflate) self._last_render_hash = 0 # Force widget render
[docs] def expand_background_inflate_to_selection_effect(self): """ Expand background inflate to match the selection effect (the widget don't require to be selected). This is a permanent change; for dynamic purpuoses, depending if the widget is selected or not, setting ``widget.selection_expand_background`` to ``True`` may help. .. note:: This method may have unexpected results with certain selection effects. :return: None """ self._background_inflate = self._selection_effect.get_xy_margin()
def _fill_background_color(self, surface): """ Fill a surface with the widget background color. :param surface: Surface to fill :type surface: :py:class:`pygame.Surface` :return: None """ if self._background_color is None: return if not (self.selection_expand_background and self.selected): inflate = self._background_inflate else: inflate = self._selection_effect.get_xy_margin() rect = self.get_rect(inflate=inflate) if isinstance(self._background_color, _baseimage.BaseImage): self._background_color.draw( surface=surface, area=rect, position=(rect.x, rect.y) ) else: surface.fill(self._background_color, rect)
[docs] def get_selection_effect(self): """ Return the selection effect. .. warning:: Use with caution. :return: Selection effect :rtype: :py:class:`pygame_menu.widgets.core.Selection` """ return self._selection_effect
[docs] def set_selection_effect(self, selection): """ Set the selection effect handler. :param selection: Selection effect class :type selection: :py:class:`pygame_menu.widgets.core.Selection` :return: None """ assert isinstance(selection, Selection) self._selection_effect = selection self._last_render_hash = 0 # Force widget render
[docs] def apply(self, *args): """ Run ``on_return`` callback when return event. A callback function receives the following arguments: .. code-block:: python callback_func(value, *args, *widget._args, **widget._kwargs) with: - ``value`` (if something is returned by ``get_value()``) - ``args`` given to this method - ``args`` of the widget - ``kwargs`` of the widget :param args: Extra arguments passed to the callback :param args: any :return: None """ if self._on_return: args = list(args) + list(self._args) try: args.insert(0, self.get_value()) except ValueError: pass return self._on_return(*args, **self._kwargs)
[docs] def change(self, *args): """ Run ``on_change`` callback after change event is triggered. A callback function receives the following arguments: .. code-block:: python callback_func(value, *args, *widget._args, **widget._kwargs) with: - ``value`` (if something is returned by ``get_value()``) - ``args`` given to this method - ``args`` of the widget - ``kwargs`` of the widget :param args: Extra arguments passed to the callback :param args: any :return: None """ if self._on_change: args = list(args) + list(self._args) try: args.insert(0, self.get_value()) except ValueError: pass return self._on_change(*args, **self._kwargs)
[docs] def draw(self, surface): """ Draw the widget shape. :param surface: Surface to draw :type surface: :py:class:`pygame.Surface` :return: None """ raise NotImplementedError('override is mandatory')
[docs] def draw_selection(self, surface): """ Draw selection effect on widget. :param surface: Surface to draw :type surface: :py:class:`pygame.Surface` :return: None """ if not self.is_selectable or self._selection_effect is None or not self.selection_effect_enabled: return self._selection_effect.draw(surface, self)
[docs] def set_max_width(self, width): """ Set widget max width (column support) if ``force_fit_text`` is enabled. :param width: Width in px, None if max width is disabled :type width: int, float, None :return: None """ if width is not None: assert isinstance(width, (int, float)), 'width must be numeric' self._max_width = width
[docs] def get_margin(self): """ Return the widget margin. :return: Widget margin *(left,bottom)* :rtype: tuple """ return self._margin
[docs] def set_margin(self, x, y): """ Set Widget margin (left, bottom). :param x: Margin on x axis (left) :type x: int, float :param y: Margin on y axis (bottom) :type y: int, float :return: None """ assert isinstance(x, (int, float)) assert isinstance(y, (int, float)) self._margin = (x, y) self._last_render_hash = 0 # Force widget render
[docs] def get_padding(self): """ Return the widget padding. :return: Widget padding *(top,right,bottom,left)* :rtype: tuple """ return self._padding
[docs] def set_padding(self, padding): """ Set the Widget padding according to CSS rules. - If an integer or float is provided: top, right, bottom and left values will be the same - If 2-item tuple is provided: top and bottom takes the first value, left and right the second - If 3-item tuple is provided: top will take the first value, left and right the second, and bottom the third - If 4-item tuple is provided: padding will be (top, right, bottom, left) .. note:: See `CSS W3Schools <https://www.w3schools.com/css/css_padding.asp>`_ for more info about padding. :param padding: Can be a single number, or a tuple of 2, 3 or 4 elements following CSS style :type padding: int, float, tuple, list :return: None """ assert isinstance(padding, (int, float, tuple, list)) if isinstance(padding, (int, float)): assert padding >= 0, 'padding cant be a negative number' self._padding = (padding, padding, padding, padding) else: assert 1 <= len(padding) <= 4, 'padding must be a tuple of 2, 3 or 4 elements' for i in range(len(padding)): assert isinstance(padding[i], (int, float)), 'all padding elements must be integers or floats' assert padding[i] >= 0, 'all padding elements must be equal or greater than zero' if len(padding) == 1: self._padding = (padding[0], padding[0], padding[0], padding[0]) elif len(padding) == 2: self._padding = (padding[0], padding[1], padding[0], padding[1]) elif len(padding) == 3: self._padding = (padding[0], padding[1], padding[2], padding[1]) else: self._padding = (padding[0], padding[1], padding[2], padding[3]) self._last_render_hash = 0 # Force widget render
[docs] def get_rect(self, apply_padding=True, inflate=None): """ Return the Rect object, this forces the widget rendering. .. note:: This is the only method that returns the rect with the padding applied. If widget._rect is used, the padding has not been applied. :param apply_padding: Apply widget padding :type apply_padding: bool :param inflate: Inflate rect *(x,y)* in px :type inflate: None, tuple, list :return: Widget rect :rtype: :py:class:`pygame.Rect` """ self._render() # Padding + inflate if inflate is None: inflate = (0, 0) pad_top = self._padding[0] * apply_padding + inflate[1] / 2 pad_right = self._padding[1] * apply_padding + inflate[0] / 2 pad_bottom = self._padding[2] * apply_padding + inflate[1] / 2 pad_left = self._padding[3] * apply_padding + inflate[0] / 2 return pygame.Rect(int(self._rect.x - pad_left), int(self._rect.y - pad_top), int(self._rect.width + pad_left + pad_right), int(self._rect.height + pad_bottom + pad_top))
[docs] def get_value(self): """ Return the value. If exception ``ValueError`` is raised, no value will be passed to the callbacks. :return: Widget data value :rtype: Object """ raise ValueError('{}({}) does not accept value'.format(self.__class__.__name__, self.get_id()))
[docs] def get_id(self): """ Return the widget ID. :return: Widget ID :rtype: str """ return self._id
[docs] def change_id(self, widget_id): """ Change widget id. :param widget_id: Widget ID :type widget_id: str :return: None """ assert isinstance(widget_id, str) if self._menu is not None: # noinspection PyProtectedMember self._menu._check_id_duplicated(widget_id) self._id = widget_id
def _render(self): """ Render the widget surface. This method shall update the attribute ``_surface`` with a :py:class:`pygame.Surface` representing the outer borders of the widget. :return: True if widget has rendered a new state, None if the widget has not changed, so render used a cache :rtype: bool, None """ raise NotImplementedError('override is mandatory') def _font_render_string(self, text, color=(0, 0, 0), use_background_color=True): """ Render text. If the font is not defined returns a zero-width surface. :param text: Text to render :type text: str, None :param color: Text color :type color: tuple :param use_background_color: Use default background color :type use_background_color: bool :return: Text surface :rtype: :py:class:`pygame.Surface` """ # assert isinstance(text, str) assert isinstance(color, tuple), 'invalid color' assert isinstance(use_background_color, bool), 'use_background_color must be boolean' bgcolor = self._font_background_color # Disable if not use_background_color: bgcolor = None if self._font is None: return make_surface(0, 0) surface = self._font.render(text, self._font_antialias, color, bgcolor) return surface def _render_string(self, string, color): """ Render text and turn it into a surface. :param string: Text to render :type string: str :param color: Text color :type color: tuple :return: Text surface :rtype: :py:class:`pygame.Surface` """ text = self._font_render_string(string, color) # Create surface surface = make_surface( width=text.get_width(), height=text.get_height(), alpha=True ) # Draw shadow first if self._shadow: text_bg = self._font_render_string(string, self._shadow_color) surface.blit(text_bg, self._shadow_tuple) surface.blit(text, (0, 0)) new_width = surface.get_size()[0] new_height = surface.get_size()[1] if self._max_width is not None and new_width > self._max_width: surface = pygame.transform.smoothscale(surface, (self._max_width, new_height)) return surface def _apply_surface_transforms(self): """ Apply surface transforms. :return: None """ if self._angle != 0: self._surface = pygame.transform.rotate(self._surface, self._angle) if self._flip[0] or self._flip[1]: self._surface = pygame.transform.flip(self._surface, self._flip[0], self._flip[1]) if self._scale[0] and (self._scale[1] != 1 or self._scale[2] != 1): w = self._scale[1] if w != 1: warnings.warn('widget _max_width is not None, scalling factor in x-axes should be equal to 1') h = self._scale[2] width = self._surface.get_width() height = self._surface.get_height() use_same_xy = self._scale[4] if not use_same_xy: w = int(w * width) h = int(h * height) if not self._scale[3]: # smooth self._surface = pygame.transform.scale(self._surface, (w, h)) else: self._surface = pygame.transform.smoothscale(self._surface, (w, h))
[docs] def surface_needs_update(self): """ Checks if the widget width/height has changed because events. If so, return true and set the status of the widget (menu widget position needs update) as false. This method is used by ``Menu.update()``. :return: True if the widget position has changed by events after the rendering. :rtype: bool """ if self._menu_surface_needs_update: self._menu_surface_needs_update = False return True return False
[docs] def set_font(self, font, font_size, color, selected_color, background_color, antialias=True): """ Set the text font. :param font: Font name (see :py:class:`pygame.font.match_font` for precise format) :type font: str :param font_size: Size of font in pixels :type font_size: int :param color: Text color :type color: tuple :param selected_color: Text color when widget is selected :type selected_color: tuple :param background_color: Font background color :type background_color: tuple, None :param antialias: Determines if antialias is applied to font (uses more processing power) :type antialias: bool :return: None """ assert isinstance(font, str) assert isinstance(font_size, (int, float)) assert isinstance(antialias, bool) assert_color(color) assert_color(selected_color) if background_color is not None: assert_color(background_color) # If background is a color and it's transparent raise a warning # Font background color must be opaque, otherwise the results are quite bad if len(background_color) == 4 and background_color[3] != 255: background_color = None msg = 'font background color must be opaque, alpha channel must be 255' warnings.warn(msg) font_size = int(font_size) self._font = _fonts.get_font(font, font_size) self._font_antialias = antialias self._font_background_color = background_color self._font_color = color self._font_name = font self._font_selected_color = selected_color self._font_size = font_size self._last_render_hash = 0 # Force widget render self._apply_font()
[docs] def update_font(self, style): """ Updates font. This method receives a style dict (non empty) containing the following keys: - ``antialias`` Font antialias (bool) - ``background_color`` Background color (tuple) - ``color`` Font color (tuple) - ``name`` Name of the font (str) - ``selected_color`` Selected color (tuple) - ``size`` Size of the font (int) .. note:: If a key is not defined it will be rewritten using current font style from ``Widget.get_font_info()`` method. :param style: Font style dict :type style: dict :return: None """ assert isinstance(style, dict) assert 1 <= len(style.keys()) <= 6 current_font = self.get_font_info() for k in current_font.keys(): if k not in style.keys(): style[k] = current_font[k] self.set_font( font=style['name'], font_size=style['size'], color=style['color'], selected_color=style['selected_color'], background_color=style['background_color'], antialias=style['antialias'] )
[docs] def get_font_info(self): """ Return a dict with the information of the widget font. Dict values: - ``antialias`` Font antialias (bool) - ``background_color`` Background color (tuple) - ``color`` Font color (tuple) - ``name`` Name of the font (str) - ``selected_color`` Selected color (tuple) - ``size`` Size of the font (int) :return: Dict :rtype: dict """ return { 'antialias': self._font_antialias, 'background_color': self._font_background_color, 'color': self._font_color, 'name': self._font_name, 'selected_color': self._font_selected_color, 'size': self._font_size }
[docs] def set_menu(self, menu): """ Set the menu reference. :param menu: Menu object :type menu: :py:class:`pygame_menu.Menu`, None :return: None """ self._menu = menu
[docs] def get_menu(self): """ Return the menu reference (if exists). .. warning:: Use with caution. :return: Menu reference :rtype: :py:class:`pygame_menu.Menu`, None """ return self._menu
def _apply_font(self): """ Function triggered after a font is applied to the widget. :return: None """ raise NotImplementedError('override is mandatory')
[docs] def set_position(self, posx, posy): """ Set the widget position. :param posx: X position :type posx: int, float :param posy: Y position :type posy: int, float :return: None """ if self._position_set and self.lock_position: return self._rect.x = posx + self._translate[0] self._rect.y = posy + self._translate[1] self._position_set = True
[docs] def get_position(self): """ Return the widget position tuple *(x, y)*. :return: Widget position :rtype: tuple """ return self._rect.x, self._rect.y
[docs] def get_width(self, apply_padding=True, apply_selection=False): """ Return the widget width. .. warning:: If the widget is not rendered, this method will return ``0``. :param apply_padding: Apply padding :type apply_selection: bool :param apply_selection: Apply selection :type apply_padding: bool :return: Widget width (px) :rtype: float """ assert isinstance(apply_padding, bool) assert isinstance(apply_selection, bool) rect = self.get_rect(apply_padding=apply_padding) width = rect.width if apply_selection: width += self._selection_effect.get_width() return float(width)
[docs] def get_height(self, apply_padding=True, apply_selection=False): """ Return the widget height. .. warning:: If the widget is not rendered, this method will return ``0``. :param apply_padding: Apply padding :type apply_selection: bool :param apply_selection: Apply selection :type apply_padding: bool :return: Widget height (px) :rtype: float """ assert isinstance(apply_padding, bool) assert isinstance(apply_selection, bool) rect = self.get_rect(apply_padding=apply_padding) height = rect.height if apply_selection: height += self._selection_effect.get_height() return float(height)
[docs] def get_size(self, apply_padding=True, apply_selection=False): """ Return the widget size. .. warning:: If the widget is not rendered this method might return ``(0,0)``. :param apply_padding: Apply padding :type apply_selection: bool :param apply_selection: Apply selection :type apply_padding: bool :return: Widget *(width, height)* :rtype: tuple """ return self.get_width(apply_padding=apply_padding, apply_selection=apply_selection), \ self.get_height(apply_padding=apply_padding, apply_selection=apply_selection)
[docs] def flip(self, x, y): """ This method can flip the widget either vertically, horizontally, or both. Flipping a widget is non-destructive and does not change the dimensions. :param x: Flip in x axis :type x: bool :param y: Flip on y axis :type y: bool :return: None """ assert isinstance(x, bool) assert isinstance(y, bool) self._flip = (x, y) self._last_render_hash = 0 # Force widget render
[docs] def scale(self, width, height, smooth=True): """ Scale the widget to a desired width and height factor. .. note:: Not all widgets are affected by scale. :param width: Scale factor of the width :type width: int, float :param height: Scale factor of the height :type height: int, float :param smooth: Smooth scaling :type smooth: bool :return: None """ assert isinstance(width, (int, float)) assert isinstance(height, (int, float)) assert isinstance(smooth, bool) assert width > 0 and height > 0, 'width and height must be greater than zero' self._scale = [True, width, height, smooth, False] if width == 1 and height == 1: # Disables scalling self._scale[0] = False self._last_render_hash = 0 # Force widget render
[docs] def resize(self, width, height, smooth=False): """ Set the widget size to another size. .. note :: This method calls ``widget.scale`` method; thus, some widgets may not support this transformation. :param width: New width of the widget in px :type width: int, float :param height: New height of the widget in px :type height: int, float :param smooth: Smooth scaling :type smooth: bool :return: None """ self.scale(int(width), int(height), smooth) self._scale[4] = True # enables use_same_xy
[docs] def translate(self, x, y): """ Translate to *(+x,+y)* according to default position. .. note:: To revert changes, only set to *(0,0)*. :param x: +X in px :type x: int, float :param y: +Y in px :type y: int, float :return: None """ assert isinstance(x, (int, float)) assert isinstance(y, (int, float)) self._translate = (x, y) self._menu_surface_needs_update = True
[docs] def rotate(self, angle): """ Unfiltered counterclockwise rotation. The angle argument represents degrees and can be any floating point value. Negative angle amounts will rotate clockwise. .. note:: Not all widgets accepts rotation. Also this rotation only affects the text or images, the selection or background is not rotated. :param angle: Rotation angle (degrees 0-360) :type angle: int, float :return: None """ assert isinstance(angle, (int, float)) self._angle = angle self._last_render_hash = 0 # Force widget render
[docs] def set_alignment(self, align): """ Set the alignment of the widget. :param align: Widget align, see locals :type align: str :return: None """ assert_alignment(align) self._alignment = align
[docs] def get_alignment(self): """ Return the widget alignment. :return: Widget align, see locals :rtype: str """ return self._alignment
[docs] def set_selected(self, selected=True): """ Mark the widget as selected. :param selected: Set item as selected :type selected: bool :return: None """ self.selected = selected self.active = False self._last_render_hash = 0 # Force widget render if selected: self._focus() self._selection_time = time.time() else: self._blur() self._events = [] # Remove events
[docs] def get_selected_time(self): """ Return time the widget has been selected in miliseconds. If the widget is not currently selected, return 0. :return: Time in ms :rtype: float """ if not self.selected: return 0 return (time.time() - self._selection_time) * 1000
[docs] def get_surface(self): """ Return widget surface. .. warning:: Use with caution. :return: Widget surface :rtype: :py:class:`pygame.Surface` """ return self._surface
def _focus(self): """ Function that is executed when the widget receives a focus (is selected). :return: None """ pass def _blur(self): """ Function that is executed when the widget loses the focus. :return: None """ pass
[docs] def set_shadow(self, enabled=True, color=None, position=None, offset=2): """ Show text shadow. :param enabled: Shadow is enabled or not :type enabled: bool :param color: Shadow color :type color: list, None :param position: Shadow position :type position: str, None :param offset: Shadow offset :type offset: int, float, None :return: None """ self._shadow = enabled if color is not None: assert_color(color) self._shadow_color = color if position is not None: assert_position(position) self._shadow_position = position assert isinstance(offset, (int, float)) if offset <= 0: raise ValueError('shadow offset must be greater than zero') self._shadow_offset = offset self._create_shadow_tuple() # Create shadow tuple position self._last_render_hash = 0 # Force widget render
[docs] def set_sound(self, sound): """ Set sound engine to the widget. :param sound: Sound object :type sound: :py:class:`pygame_menu.sound.Sound` :return: None """ self.sound = sound
def _create_shadow_tuple(self): """ Create shadow position tuple. :return: None """ x = 0 y = 0 if self._shadow_position == _locals.POSITION_NORTHWEST: x = -1 y = -1 elif self._shadow_position == _locals.POSITION_NORTH: y = -1 elif self._shadow_position == _locals.POSITION_NORTHEAST: x = 1 y = -1 elif self._shadow_position == _locals.POSITION_EAST: x = 1 elif self._shadow_position == _locals.POSITION_SOUTHEAST: x = 1 y = 1 elif self._shadow_position == _locals.POSITION_SOUTH: y = 1 elif self._shadow_position == _locals.POSITION_SOUTHWEST: x = -1 y = 1 elif self._shadow_position == _locals.POSITION_WEST: x = -1 self._shadow_tuple = (x * self._shadow_offset, y * self._shadow_offset)
[docs] def set_controls(self, joystick=True, mouse=True, touchscreen=True): """ Enable interfaces to control the widget. :param joystick: Use joystick :type joystick: bool :param mouse: Use mouse :type mouse: bool :param touchscreen: Use touchscreen :type touchscreen: bool :return: None """ self.joystick_enabled = joystick self.mouse_enabled = mouse self.touchscreen_enabled = touchscreen
[docs] def set_value(self, value): """ Set the widget value. .. warning:: This method does not fire the callbacks as it is called programmatically. This behavior is deliberately chosen to avoid infinite loops. :param value: Value to be set on the widget :type value: Object :return: None """ raise ValueError('{}({}) does not accept value'.format(self.__class__.__name__, self.get_id()))
[docs] def update(self, events): """ Update internal variable according to the given events list and fire the callbacks. :param events: List/Tuple of pygame events :type events: list[:py:class:`pygame.event.Event`], tuple[:py:class:`pygame.event.Event`] :return: True if updated :rtype: bool """ raise NotImplementedError('override is mandatory')
[docs] def add_draw_callback(self, draw_callback): """ Adds a function to the widget to be executed each time the widget is drawn. The function that this method receives receives two objects: the widget itself and the menu reference. .. code-block:: python import math def draw_update_function(widget, menu): t = widget.get_attribute('t', 0) t += menu.get_clock().get_time() widget.set_padding(10*(1 + math.sin(t)))) # Oscillating padding button = menu.add_button('This button updates its padding', None) button.set_draw_callback(draw_update_function) After creating a new callback, this functions returns the ID of the call. It can be removed anytime using ``widget.remove_draw_callback(id)``. :param draw_callback: Function :type draw_callback: callable :return: Callback ID :rtype: str """ assert is_callable(draw_callback), 'draw callback must be a function type' callback_id = str(uuid4()) self._draw_callbacks[callback_id] = draw_callback return callback_id
[docs] def remove_draw_callback(self, callback_id): """ Removes draw callback from ID. :param callback_id: Callback ID :type callback_id: str :return: None """ assert isinstance(callback_id, str) if callback_id not in self._draw_callbacks.keys(): raise IndexError('callback ID "{0}" does not exist'.format(callback_id)) del self._draw_callbacks[callback_id]
[docs] def apply_draw_callbacks(self): """ Apply callbacks on widget draw. :return: None """ if len(self._draw_callbacks) == 0: return for callback in self._draw_callbacks.values(): callback(self, self._menu)
[docs] def add_update_callback(self, update_callback): """ Adds a function to the widget to be executed each time the widget is updated. The function that this method receives receives two objects: the widget itself and the menu reference. It is similar to ``add_draw_callback``. After creating a new callback, this functions returns the ID of the call. It can be removed anytime using ``widget.remove_update_callback(id)``. .. note:: Not all widgets are updated, so the provided function may never be executed. :param update_callback: Function :type update_callback: callable :return: Callback ID :rtype: str """ assert is_callable(update_callback), 'update callback must be a function type' callback_id = str(uuid4()) self._update_callbacks[callback_id] = update_callback return callback_id
[docs] def remove_update_callback(self, callback_id): """ Removes update callback from ID. :param callback_id: Callback ID :type callback_id: str :return: None """ assert isinstance(callback_id, str) if callback_id not in self._update_callbacks.keys(): raise IndexError('callback ID "{0}" does not exist'.format(callback_id)) del self._update_callbacks[callback_id]
[docs] def apply_update_callbacks(self): """ Apply callbacks on widget update. :return: None """ if len(self._update_callbacks) == 0: return for callback in self._update_callbacks.values(): callback(self, self._menu)
def _add_event(self, event): """ Add a custom event to the widget for the next update(). :param event: Custom event :type event: :py:class:`pygame.event.Event` :return: None """ self._events.append(event) def _merge_events(self, events): """ Append widget events to events list. :param events: Event list :type events: list[:py:class:`pygame.event.Event`] :return: Augmented event list :rtype: list[:py:class:`pygame.event.Event`] """ if len(self._events) == 0: return events copy_events = [] for e in events: copy_events.append(e) for e in self._events: copy_events.append(e) self._events = [] return copy_events
[docs] def show(self): """ Set widget visible. :return: None """ self.visible = True self._render() if self._menu is not None: # noinspection PyProtectedMember self._menu._update_selection_if_hidden()
[docs] def hide(self): """ Hides widget. :return: None """ self.visible = False self._render() if self._menu is not None: # noinspection PyProtectedMember self._menu._update_selection_if_hidden()
class _NullSelection(Selection): """ Null selection. It redefines NoneSelection because that class cannot be imported. """ def __init__(self): super(_NullSelection, self).__init__( margin_left=0, margin_right=0, margin_top=0, margin_bottom=0 ) # noinspection PyMissingOrEmptyDocstring def draw(self, surface, widget): return