Source code for pygame_menu._decorator

"""
pygame-menu
https://github.com/ppizarror/pygame-menu

DECORATOR
Generic decorator, adds additional images, polygons or text to the object.
"""
# File constants no. 2000

__all__ = ['Decorator']

from math import pi
import math

import pygame
import pygame_menu
import pygame_menu.menu
import pygame.draw as pydraw
import pygame.gfxdraw as gfxdraw

from pygame_menu._base import Base
from pygame_menu.font import FontType
from pygame_menu.utils import assert_list_vector, assert_color, make_surface, \
    assert_vector, uuid4, warn

from pygame_menu._types import List, Tuple2NumberType, ColorInputType, Tuple, \
    Any, Dict, Union, NumberType, Tuple2IntType, Optional, Callable, NumberInstance, \
    CallableNoArgsType

# Decoration constants
DECORATION_ARC = 2000
DECORATION_BASEIMAGE = 2001
DECORATION_BEZIER = 2002
DECORATION_CALLABLE = 2003
DECORATION_CALLABLE_NO_ARGS = 2015
DECORATION_CIRCLE = 2004
DECORATION_ELLIPSE = 2005
DECORATION_FILL = 2020
DECORATION_LINE = 2006
DECORATION_NONE = 2007
DECORATION_PIE = 2008
DECORATION_PIXEL = 209
DECORATION_POLYGON = 2010
DECORATION_RECT = 2011
DECORATION_SURFACE = 2012
DECORATION_TEXT = 2013
DECORATION_TEXTURE_POLYGON = 2014

DECOR_TYPE_PREV = 'prev'
DECOR_TYPE_POST = 'post'


# noinspection PyProtectedMember
[docs]class Decorator(Base): """ Decorator class. :param obj: Object to decorate :param decorator_id: ID of the decorator """ _coord_cache: Dict[ str, Tuple[int, int, Union[Tuple[Tuple2NumberType, ...], Tuple2NumberType]]] # centerx, centery, coords _cache_last_status: Dict[str, Tuple[int, int, int, int, int, int]] _cache_needs_update: Dict[str, bool] _cache_surface: Dict[str, Optional['pygame.Surface']] _decor: Dict[str, List[Tuple[int, str, Any]]] # type, id, data _decor_enabled: Dict[str, bool] _decor_prev_id: List[str] _obj: Union['pygame_menu.widgets.Widget', 'pygame_menu._scrollarea.ScrollArea', 'pygame_menu.Menu'] _post_enabled: bool _prev_enabled: bool cache: bool def __init__( self, obj: Union['pygame_menu.widgets.Widget', 'pygame_menu._scrollarea.ScrollArea', 'pygame_menu.Menu'], decorator_id: str = '' ) -> None: super(Decorator, self).__init__(object_id=decorator_id) self._coord_cache = {} self._decor = {DECOR_TYPE_PREV: [], DECOR_TYPE_POST: []} self._decor_prev_id = [] # Stores all decoration prev ids self._obj = obj self._decor_enabled = {} self._prev_enabled = True self._post_enabled = True # If True, enables surface cache. This is intended to be used if there's # many decorations in the object (for example, 400). This is an expensive # method anyway because surface is called many times. See the following # rendering times to guess how much does a decoration takes time to # render 1000 times (object: button) # 100 decoration, no cache: 0.214 # 100 decoration, with cache: 0.646 # 300 decoration, no cache: 0.581 # 300 decoration, with cache: 0.606 # 1000 decoration, no cache: 2.228 # 1000 decoration, with cache: 0.615 # 10000 decoration, no cache: 20.430 # 10000 decoration, with cache: 0.599 self.cache = False # Previous (surf.width, surf.height, rect.x, rect.y, rect.centerx, rect.centery self._cache_last_status = {DECOR_TYPE_PREV: (0, 0, 0, 0, 0, 0), DECOR_TYPE_POST: (0, 0, 0, 0, 0, 0)} self._cache_needs_update = {DECOR_TYPE_PREV: False, DECOR_TYPE_POST: False} self._cache_surface = {DECOR_TYPE_PREV: None, DECOR_TYPE_POST: None} def __copy__(self) -> 'Decorator': """ Copy method. :return: Raises copy exception """ raise _DecoratorCopyException('Decorator class cannot be copied') def __deepcopy__(self, memodict: Dict) -> 'Decorator': """ Deep-copy method. :param memodict: Memo dict :return: Raises copy exception """ raise _DecoratorCopyException('Decorator class cannot be deep-copied') def _add_decor(self, decortype: int, prev: bool, data: Any) -> str: """ Adds a decoration. :param decortype: Decoration type :param prev: To prev or post :param data: Data of the decoration :return: ID of the decoration """ decor_id = uuid4() if prev: assert self._prev_enabled, 'prev decorators are not enabled' self._decor[DECOR_TYPE_PREV].append((decortype, decor_id, data)) self._decor_prev_id.append(decor_id) else: assert self._post_enabled, 'post decorators are not enabled' self._decor[DECOR_TYPE_POST].append((decortype, decor_id, data)) # Force surface cache update if hasattr(self._obj, 'force_menu_surface_cache_update'): self._obj.force_menu_surface_cache_update() # Forces cache update self._cache_needs_update[DECOR_TYPE_PREV if prev else DECOR_TYPE_POST] = True # Check sizes if self._total_decor() >= 300 and not self.cache: warn('cache is recommended if the total number of decorations exceeds 300') # Set automatically as enabled self._decor_enabled[decor_id] = True return decor_id def _add_none(self, prev: bool = True) -> str: """ Adds a none decorator. :param prev: If ``True`` draw previous the object, else draws post :return: ID of the decoration """ return self._add_decor(DECORATION_NONE, prev, None) def _total_decor(self) -> int: """ Return total number of decorations. """ return len(self._decor[DECOR_TYPE_PREV]) + len(self._decor[DECOR_TYPE_POST])
[docs] def force_cache_update(self, prev: Optional[bool] = None) -> 'Decorator': """ Forces cache update. :param prev: Update the previous or post surface cache. If ``None`` forces both caches to update :return: Self reference """ if prev is None: self.force_cache_update(True) self.force_cache_update(False) return self self._cache_needs_update[DECOR_TYPE_PREV if prev else DECOR_TYPE_POST] = True return self
[docs] def add_polygon( self, coords: Union[List[Tuple2NumberType], Tuple[Tuple2NumberType, ...]], color: ColorInputType, filled: bool, width: int = 0, prev: bool = True, gfx: bool = True, **kwargs ) -> str: """ Adds a polygon. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param coords: Coordinate list, being ``(0, 0)`` the center of the object :param color: Color of the polygon :param filled: If ``True`` fills the polygon with the given color :param width: Line border width. Only valid if ``filled=False`` :param prev: If ``True`` draw previous the object, else draws post :param gfx: If ``True`` uses pygame gfxdraw instead of draw :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert_list_vector(coords, 2) color = assert_color(color) assert len(coords) >= 3 assert isinstance(filled, bool) assert isinstance(width, int) and width >= 0 if filled: assert width == 0, 'width must be 0 if the polygon is filled' assert gfx, 'only gfxdraw support filled polygon, then gfx should be True' else: if width != 0 and gfx: gfx = False # gfx don't support width return self._add_decor( DECORATION_POLYGON, prev, (tuple(coords), color, filled, width, gfx, kwargs) )
[docs] def add_bezier( self, coords: Union[List[Tuple2NumberType], Tuple[Tuple2NumberType, ...]], color: ColorInputType, steps: int = 5, prev: bool = True, **kwargs ) -> str: """ Adds a Bézier curve. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param coords: Coordinate list, being ``(0, 0)`` the center of the object :param color: Color of the polygon :param steps: Interpolation steps :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert_list_vector(coords, 2) color = assert_color(color) assert len(coords) >= 3 assert isinstance(steps, int) and steps >= 1 return self._add_decor( DECORATION_BEZIER, prev, (tuple(coords), color, steps, kwargs) )
[docs] def add_circle( self, x: NumberType, y: NumberType, radius: NumberType, color: ColorInputType, filled: bool, width: int = 0, prev: bool = True, gfx: bool = True, **kwargs ) -> str: """ Adds a circle. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param radius: Circle radius in px :param color: Color of the polygon :param filled: If ``True`` fills the polygon with the given color :param width: Line border width. Only valid if ``filled=False`` :param prev: If ``True`` draw previous the object, else draws post :param gfx: If ``True`` uses pygame gfxdraw instead of draw :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) color = assert_color(color) assert isinstance(radius, NumberInstance) and radius > 0 assert isinstance(filled, bool) assert isinstance(width, int) and width >= 0 if filled: assert width == 0, 'width must be 0 if the circle is filled' else: if width != 0 and gfx: gfx = False # gfx don't support width return self._add_decor( DECORATION_CIRCLE, prev, (tuple(coords), int(radius), color, filled, width, gfx, kwargs) )
[docs] def add_arc( self, x: NumberType, y: NumberType, radius: NumberType, init_angle: NumberType, final_angle: NumberType, color: ColorInputType, width: int = 0, prev: bool = True, gfx: bool = True, **kwargs ) -> str: """ Adds an arc. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param radius: Circle radius in px :param init_angle: Initial angle in degrees, from ``0`` to ``360`` :param final_angle: Final angle in degrees, from ``0`` to ``360`` :param color: Color of the polygon :param width: Line border width. Only valid if ``filled=False`` :param prev: If ``True`` draw previous the object, else draws post :param gfx: If ``True`` uses pygame gfxdraw instead of draw :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) color = assert_color(color) assert isinstance(radius, NumberInstance) and radius > 0 assert isinstance(init_angle, NumberInstance) assert isinstance(final_angle, NumberInstance) assert isinstance(width, int) and width >= 0 assert init_angle != final_angle return self._add_decor( DECORATION_ARC, prev, (tuple(coords), int(radius), init_angle, final_angle, color, width, gfx, kwargs) )
[docs] def add_pie( self, x: NumberType, y: NumberType, radius: NumberType, init_angle: NumberType, final_angle: NumberType, color: ColorInputType, prev: bool = True, **kwargs ) -> str: """ Adds an unfilled pie. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param radius: Circle radius in px :param init_angle: Initial angle in degrees, from ``0`` to ``360`` :param final_angle: Final angle in degrees, from ``0`` to ``360`` :param color: Color of the polygon :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) color = assert_color(color) assert isinstance(radius, NumberInstance) and radius > 0 assert isinstance(init_angle, NumberInstance) assert isinstance(final_angle, NumberInstance) assert init_angle != final_angle return self._add_decor( DECORATION_PIE, prev, (tuple(coords), int(radius), init_angle, final_angle, color, kwargs) )
[docs] def add_surface( self, x: NumberType, y: NumberType, surface: 'pygame.Surface', prev: bool = True, centered: bool = False, **kwargs ) -> str: """ Adds a surface. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param surface: Surface :param prev: If ``True`` draw previous the object, else draws post :param centered: If ``True`` the surface is centered :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) assert isinstance(surface, pygame.Surface) return self._add_decor( DECORATION_SURFACE, prev, (tuple(coords), surface, centered, kwargs) )
[docs] def add_baseimage( self, x: NumberType, y: NumberType, image: 'pygame_menu.BaseImage', prev: bool = True, centered: bool = False, **kwargs ) -> str: """ Adds a :py:class:`pygame_menu.baseimage.BaseImage` object. .. note:: If your :py:class:`pygame_menu.baseimage.BaseImage` object changes over time set ``decorator.cache=False`` or force cache manually by calling :py:meth:`pygame_menu._decorator.Decorator.force_cache_update`. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param image: ``BaseImage`` object :param prev: If ``True`` draw previous the object, else draws post :param centered: If ``True`` the image is centered :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) assert isinstance(image, pygame_menu.BaseImage) return self._add_decor( DECORATION_BASEIMAGE, prev, (tuple(coords), image, centered, kwargs) )
[docs] def add_rect( self, x: NumberType, y: NumberType, rect: 'pygame.Rect', color: ColorInputType, width: int = 0, prev: bool = True, **kwargs ) -> str: """ Adds a BaseImage object. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param rect: Rect to draw :param color: Color of the rect :param width: Border width of the rect. If ``0`` draw a filled rectangle :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert isinstance(width, int) and width >= 0 coords = [(x, y)] assert_list_vector(coords, 2) color = assert_color(color) assert isinstance(rect, pygame.Rect) return self._add_decor( DECORATION_RECT, prev, (tuple(coords), rect, color, width, kwargs) )
[docs] def add_rectangle( self, x: NumberType, y: NumberType, width: NumberType, height: NumberType, color: ColorInputType, border: int = 0, prev: bool = True, **kwargs ) -> str: """ Adds a BaseImage object. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param width: Rectangle width :param height: Rectangle height :param color: Color of the rectangle :param border: Border width of the rectangle. If ``0`` draw a filled rectangle :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert isinstance(width, NumberInstance) and width > 0 assert isinstance(height, NumberInstance) and height > 0 rect = pygame.Rect(0, 0, width, height) return self.add_rect(x, y, rect, color, border, prev, **kwargs)
[docs] def add_text( self, x: NumberType, y: NumberType, text: str, font: FontType, size: int, color: ColorInputType, prev: bool = True, antialias=True, centered=False, **kwargs ) -> str: """ Adds a text. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param text: Text to draw :param font: Font path or pygame object :param size: Size of the font to render :param color: Font color :param prev: If ``True`` draw previous the object, else draws post :param antialias: Font antialias enabled :param centered: If ``True`` the text is centered :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) text = str(text) font_obj = pygame_menu.font.get_font(font, size) color = assert_color(color) surface_font = font_obj.render(text, antialias, color) surface = make_surface( width=surface_font.get_width(), height=surface_font.get_height(), alpha=True ) surface.blit(surface_font, (0, 0)) return self._add_decor( DECORATION_TEXT, prev, (tuple(coords), surface, centered, kwargs) )
[docs] def add_ellipse( self, x: NumberType, y: NumberType, rx: NumberType, ry: NumberType, color: ColorInputType, filled: bool, prev: bool = True, **kwargs ) -> str: """ Adds an ellipse. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param rx: Horizontal radius of the ellipse :param ry: Vertical radius of the ellipse :param color: Color of the polygon :param filled: If ``True`` fills the polygon with the given color :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) color = assert_color(color) assert isinstance(rx, NumberInstance) and rx > 0 assert isinstance(ry, NumberInstance) and ry > 0 assert isinstance(filled, bool) return self._add_decor( DECORATION_ELLIPSE, prev, (tuple(coords), rx, ry, color, filled, kwargs) )
[docs] def add_pixel( self, x: NumberType, y: NumberType, color: ColorInputType, prev: bool = True, **kwargs ) -> str: """ Adds a pixel. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: X position in px, being ``0`` the center of the object :param y: Y position in px, being ``0`` the center of the object :param color: Color of the pixel :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ coords = [(x, y)] assert_list_vector(coords, 2) color = assert_color(color) return self._add_decor( DECORATION_PIXEL, prev, (tuple(coords), color, kwargs) )
[docs] def add_callable( self, fun: Union[Callable[['pygame.Surface', Any], Any], CallableNoArgsType], prev: bool = True, pass_args: bool = True ) -> str: """ Adds a callable method. The function receives the surface and the object; for example, if adding to a widget: .. code-block:: python fun(surface, object) .. note:: If your callable function changes over time set ``decorator.cache=False`` or force cache manually by calling Decorator method :py:meth:`pygame_menu._decorator.Decorator.force_cache_update`. Also, the object should force the menu surface cache to update. :param fun: Function :param prev: If ``True`` draw previous the object, else draws post :param pass_args: If ``False`` function is called without (surface, object) as args :return: ID of the decoration """ assert callable(fun), 'fun must be a callable type' assert isinstance(pass_args, bool) if pass_args: return self._add_decor(DECORATION_CALLABLE, prev, fun) else: return self._add_decor(DECORATION_CALLABLE_NO_ARGS, prev, fun)
[docs] def add_textured_polygon( self, coords: Union[List[Tuple2NumberType], Tuple[Tuple2NumberType, ...]], texture: Union['pygame.Surface', 'pygame_menu.BaseImage'], tx: int = 0, ty: int = 0, prev: bool = True, **kwargs ) -> str: """ Adds a textured polygon. .. note:: If your :py:class:`pygame_menu.baseimage.BaseImage` object changes over time set ``decorator.cache=False`` or force cache manually by calling :py:class:`pygame_menu._decorator.Decorator.force_cache_update`. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param coords: Coordinate list, being ``(0, 0)`` the center of the object :param texture: Texture (Surface) or Baseimage object :param tx: X offset of the texture in px :param ty: Y offset of the texture in px :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert_list_vector(coords, 2) assert len(coords) >= 3 assert isinstance(texture, (pygame.Surface, pygame_menu.BaseImage)) assert isinstance(tx, int) and isinstance(ty, int) return self._add_decor( DECORATION_TEXTURE_POLYGON, prev, (tuple(coords), texture, tx, ty, kwargs) )
[docs] def add_line( self, pos1: Tuple2NumberType, pos2: Tuple2NumberType, color: ColorInputType, width: int = 1, prev: bool = True, **kwargs ) -> str: """ Adds a line. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param pos1: Position 1 (x1, y1) :param pos2: Position 2 (x2, y2) :param color: Line color :param width: Line width in px :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert_vector(pos1, 2) assert_vector(pos2, 2) color = assert_color(color) assert isinstance(width, int) and width >= 1 length = math.sqrt(math.pow(pos1[0] - pos2[0], 2) + math.pow(pos1[1] - pos2[1], 2)) assert length > 0, 'line cannot be zero-length' return self._add_decor( DECORATION_LINE, prev, ((tuple(pos1), tuple(pos2)), color, width, kwargs) )
[docs] def add_fill( self, color: ColorInputType, prev: bool = True ) -> str: """ Fills the decorator rect object. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param color: Fill color :param prev: If ``True`` draw previous the object, else draws post :return: ID of the decoration """ return self._add_decor(DECORATION_FILL, prev, assert_color(color))
[docs] def add_hline( self, x1: NumberType, x2: NumberType, y: NumberType, color: ColorInputType, width: int = 1, prev: bool = True, **kwargs ) -> str: """ Adds a horizontal line. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x1: Horizontal position 1 in px :param x2: Horizontal position 2 in px :param y: Vertical position in px :param color: Line color :param width: Line width in px :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert x1 != x2 return self.add_line((x1, y), (x2, y), color, width, prev, **kwargs)
[docs] def add_vline( self, x: NumberType, y1: NumberType, y2: NumberType, color: ColorInputType, width: int = 1, prev: bool = True, **kwargs ) -> str: """ Adds a vertical line. kwargs (Optional) - ``use_center_positioning`` (bool) – Uses object center position as *(0, 0)*. ``True`` by default :param x: Horizontal position in px :param y1: Vertical position 1 in px :param y2: Vertical position 2 in px :param color: Line color :param width: Line width in px :param prev: If ``True`` draw previous the object, else draws post :param kwargs: Optional keyword arguments :return: ID of the decoration """ assert y1 != y2 return self.add_line((x, y1), (x, y2), color, width, prev, **kwargs)
[docs] def disable(self, decorid: str) -> 'Decorator': """ Disable a certain decoration from ID. Raises ``IndexError`` if decoration was not found. :param decorid: Decoration ID :return: Self reference """ if decorid not in self._decor_enabled.keys(): raise IndexError(f'decoration<"{decorid}"> was not found') self._decor_enabled[decorid] = False self.force_cache_update(prev=decorid in self._decor_prev_id) return self
[docs] def enable(self, decorid: str) -> 'Decorator': """ Enable a certain decoration from ID. Raises ``IndexError`` if decoration was not found. :param decorid: Decoration ID :return: Self reference """ if decorid not in self._decor_enabled.keys(): raise IndexError(f'decoration<"{decorid}"> was not found') self._decor_enabled[decorid] = True self.force_cache_update(prev=decorid in self._decor_prev_id) return self
[docs] def is_enabled(self, decorid: str) -> bool: """ Checks if a certain decoration is enabled or not. Raises ``IndexError`` if decoration was not found. :param decorid: Decoration ID :return: ``True`` if enabled """ if decorid not in self._decor_enabled.keys(): raise IndexError(f'decoration<"{decorid}"> was not found') return self._decor_enabled[decorid]
[docs] def remove(self, decorid: str) -> 'Decorator': """ Remove a decoration from a given ID. Raises ``IndexError`` if decoration was not found. :param decorid: Decoration ID :return: Self reference """ assert isinstance(decorid, str) if decorid in self._coord_cache.keys(): del self._coord_cache[decorid] for p in (DECOR_TYPE_PREV, DECOR_TYPE_POST): for d in self._decor[p]: if d[1] == decorid: self._decor[p].remove(d) self._cache_needs_update[p] = True if decorid in self._decor_prev_id: self._decor_prev_id.remove(decorid) del self._decor_enabled[decorid] return self raise IndexError(f'decoration<"{decorid}"> was not found')
[docs] def remove_all(self, prev: Optional[bool] = None) -> 'Decorator': """ Remove all decorations. :param prev: Remove from ``prev`` or ``post``. If ``None`` both are removed :return: Self reference """ if prev is None: self.remove_all(True) self.remove_all(False) return self p = DECOR_TYPE_PREV if prev else DECOR_TYPE_POST self._cache_needs_update[p] = False del self._decor[p] self._decor[p] = [] return self
def _draw_assemble_cache( self, prev: str, deco: List[Tuple[int, str, Any]], surface: 'pygame.Surface' ) -> None: """ Draw cache, assemble if needed. :param prev: Mode :param deco: Decoration lists :param surface: Source surface to draw from """ if len(deco) == 0: return w, h = surface.get_size() rect = self._obj.get_rect() # If it needs update, or the surface size changed, or the rect position changed prev_surf_changed = self._cache_last_status[prev][0] != w or \ self._cache_last_status[prev][1] != h prev_rect_changed = self._cache_last_status[prev][2] != rect.x or \ self._cache_last_status[prev][3] != rect.y or \ self._cache_last_status[prev][4] != rect.width or \ self._cache_last_status[prev][5] != rect.height if self._cache_needs_update[prev] or prev_surf_changed or prev_rect_changed or \ self._cache_surface[prev] is None: self._cache_last_status[prev] = (w, h, rect.x, rect.y, rect.width, rect.height) del self._cache_surface[prev] self._cache_surface[prev] = make_surface(surface.get_width(), surface.get_height()) self._draw(deco, self._cache_surface[prev]) self._cache_needs_update[prev] = False surface.blit(self._cache_surface[prev], (0, 0))
[docs] def draw_prev(self, surface: 'pygame.Surface') -> 'Decorator': """ Draw prev. :param surface: Pygame surface :return: Self reference """ if not self.cache: self._draw(self._decor[DECOR_TYPE_PREV], surface) else: self._draw_assemble_cache(DECOR_TYPE_PREV, self._decor[DECOR_TYPE_PREV], surface) return self
[docs] def draw_post(self, surface: 'pygame.Surface') -> 'Decorator': """ Draw post. :param surface: Pygame surface :return: Self reference """ if not self.cache: self._draw(self._decor[DECOR_TYPE_POST], surface) else: self._draw_assemble_cache(DECOR_TYPE_POST, self._decor[DECOR_TYPE_POST], surface) return self
# noinspection PyArgumentList def _draw(self, deco: List[Tuple[int, str, Any]], surface: 'pygame.Surface') -> None: """ Draw. :param deco: Decoration list :param surface: Pygame surface """ if len(deco) == 0: return rect = self._obj.get_rect() for d in deco: dtype, decoid, data = d if not self._decor_enabled[decoid]: continue if dtype == DECORATION_POLYGON: points, color, filled, width, gfx, kwargs = data points = self._update_pos_list(rect, decoid, points, **kwargs) if gfx: if filled: gfxdraw.filled_polygon(surface, points, color) else: gfxdraw.polygon(surface, points, color) else: pydraw.polygon(surface, color, points, width) elif dtype == DECORATION_CIRCLE: points, r, color, filled, width, gfx, kwargs = data points = self._update_pos_list(rect, decoid, points, **kwargs) x, y = points[0] if filled: if gfx: gfxdraw.filled_circle(surface, x, y, r, color) else: pydraw.circle(surface, color, (x, y), r) else: pydraw.circle(surface, color, (x, y), r, width) elif dtype == DECORATION_SURFACE or dtype == DECORATION_BASEIMAGE or dtype == DECORATION_TEXT: pos, surf, centered, kwargs = data if isinstance(surf, pygame_menu.BaseImage): surf = surf.get_surface(new=False) pos = self._update_pos_list(rect, decoid, pos, **kwargs)[0] surf_rect = surf.get_rect() surf_rect.x += pos[0] surf_rect.y += pos[1] if centered: surf_rect.x -= int(surf_rect.width / 2) surf_rect.y -= int(surf_rect.height / 2) surface.blit(surf, surf_rect) elif dtype == DECORATION_ELLIPSE: pos, rx, ry, color, filled, kwargs = data pos = self._update_pos_list(rect, decoid, pos, **kwargs)[0] if filled: gfxdraw.filled_ellipse(surface, pos[0], pos[1], rx, ry, color) else: gfxdraw.ellipse(surface, pos[0], pos[1], rx, ry, color) elif dtype == DECORATION_CALLABLE: data(surface, self._obj) elif dtype == DECORATION_CALLABLE_NO_ARGS: data() elif dtype == DECORATION_TEXTURE_POLYGON: pos, texture, tx, ty, kwargs = data pos = self._update_pos_list(rect, decoid, pos, **kwargs) if isinstance(texture, pygame_menu.BaseImage): texture = texture.get_surface() gfxdraw.textured_polygon(surface, pos, texture, tx, ty) elif dtype == DECORATION_ARC: points, r, ia, fa, color, width, gfx, kwargs = data points = self._update_pos_list(rect, decoid, points, **kwargs) x, y = points[0] rect_arc = pygame.Rect(x - r, y - r, x + 2 * r, y + 2 * r) if gfx: gfxdraw.arc(surface, x, y, r, ia, fa, color) else: pydraw.arc(surface, color, rect_arc, ia / (2 * pi), fa / (2 * pi), width) elif dtype == DECORATION_PIE: points, r, ia, fa, color, kwargs = data points = self._update_pos_list(rect, decoid, points, **kwargs) x, y = points[0] gfxdraw.pie(surface, x, y, r, ia, fa, color) elif dtype == DECORATION_BEZIER: points, color, steps, kwargs = data points = self._update_pos_list(rect, decoid, points, **kwargs) gfxdraw.bezier(surface, points, steps, color) elif dtype == DECORATION_FILL: surface.fill(data, rect) elif dtype == DECORATION_RECT: d_rect: 'pygame.Rect' pos, d_rect, color, width, kwargs = data pos = self._update_pos_list(rect, decoid, pos, **kwargs)[0] d_rect = d_rect.copy() d_rect.x += pos[0] d_rect.y += pos[1] pygame.draw.rect(surface, color, d_rect, width) elif dtype == DECORATION_PIXEL: pos, color, kwargs = data pos = self._update_pos_list(rect, decoid, pos, **kwargs)[0] gfxdraw.pixel(surface, pos[0], pos[1], color) elif dtype == DECORATION_LINE: pos, color, width, kwargs = data pos = self._update_pos_list(rect, decoid, pos, **kwargs) pydraw.line(surface, color, pos[0], pos[1], width) else: raise ValueError('unknown decoration type') def _update_pos_list( self, rect: 'pygame.Rect', decoid: str, pos: Union[Tuple2NumberType, Tuple[Tuple2NumberType, ...]], # only (x, y) or ((x1,y1), ... use_center_positioning=True ) -> Union[Tuple[Tuple2IntType, ...], Tuple2IntType]: """ Updates position list based on rect center. If position of the rect changes, update the coords. :param rect: Object precomputed rect :param decoid: Decoration id :param pos: Original position tuple of the decoration :param use_center_positioning: If ``True`` use *(0, 0)* as the object center :return: Position list updated to """ if not use_center_positioning: return tuple(pos) cx, cy = rect.centerx, rect.centery # Center position # Position of the rect has not changed and exists decoid_exists = False try: decoid_exists = self._coord_cache[decoid] is not None except KeyError: pass if decoid_exists and self._coord_cache[decoid][0] == cx and self._coord_cache[decoid][1] == cy: return self._coord_cache[decoid][2] # Update the position new_pos = [] for p in pos: new_pos.append((int(p[0] + cx), int(p[1] + cy))) new_pos = tuple(new_pos) self._coord_cache[decoid] = (cx, cy, new_pos) return new_pos
class _DecoratorCopyException(Exception): """ If user tries to copy a Decorator. """ pass