Source code for pygame_menu.widgets.core.widget
"""
pygame-menu
https://github.com/ppizarror/pygame-menu
WIDGET
Base class for widgets.
"""
__all__ = [
# Main class
'Widget',
# Utils
'check_widget_mouseleave',
# Types
'WidgetBorderPositionType',
# Global widget mouseover list
'WIDGET_MOUSEOVER',
# Others
'WIDGET_BORDER_POSITION_FULL',
'WIDGET_BORDER_POSITION_NONE',
'WIDGET_FULL_BORDER',
'WidgetTransformationNotImplemented'
]
import random
import time
import pygame
import pygame_menu
from pygame_menu._base import Base
from pygame_menu._decorator import Decorator
from pygame_menu.font import FontType
from pygame_menu.locals import POSITION_NORTHWEST, POSITION_SOUTHWEST, POSITION_WEST, \
POSITION_EAST, POSITION_NORTHEAST, POSITION_CENTER, POSITION_NORTH, POSITION_SOUTH, \
POSITION_SOUTHEAST, ALIGN_CENTER
from pygame_menu.sound import Sound
from pygame_menu.utils import make_surface, assert_alignment, assert_color, \
assert_position, assert_vector, is_callable, parse_padding, uuid4, \
mouse_motion_current_mouse_position, PYGAME_V2, set_pygame_cursor, warn, \
get_cursor
from pygame_menu.widgets.core.selection import Selection
from pygame_menu._types import Optional, ColorType, Tuple2IntType, NumberType, \
PaddingType, Union, List, Tuple, Any, CallbackType, Dict, Callable, Tuple4IntType, \
Tuple2BoolType, Tuple3IntType, NumberInstance, ColorInputType, EventType, \
EventVectorType, EventListType, CursorInputType, CursorType, VectorInstance, \
Tuple2NumberType, CallableNoArgsType
# This list stores the current widget which requested the mouseover status, and
# the previous widget list which requested the mouseover. Each time the widget
# changes the over status, if leaves all previous widgets that are not hovered
# trigger mouseleave. The format of each item of the list is
# [..., [widget, previous_cursor, [previous_widget, previous_cursor2, [....]
WIDGET_MOUSEOVER: List[Any] = [None, []]
# Stores the top cursor for validation
WIDGET_TOP_CURSOR: List[Any] = [None]
WIDGET_TOP_CURSOR_WARNING = False
WIDGET_BORDER_POSITION_NONE = 'border-none'
WIDGET_BORDER_POSITION_FULL = 'border-position-border-full'
WIDGET_FULL_BORDER = (POSITION_NORTH, POSITION_SOUTH, POSITION_EAST, POSITION_WEST)
def check_widget_mouseleave(event: Optional[EventType] = None, force: bool = False) -> None:
"""
Check if the active widget (WIDGET_MOUSEOVER[0]) is still over, else, execute
previous list (WIDGET_MOUSEOVER[1]).
:param event: Mouse motion event. If ``None`` this method creates the event
:param force: If ``True`` calls all mouse leave without checking if the mouse is still over
:return: None
"""
return _check_widget_mouseleave(event, force)
# noinspection PyProtectedMember
def _check_widget_mouseleave(
event: Optional[EventType] = None,
force: bool = False,
recursive: bool = False
) -> None:
"""
Check if the active widget (WIDGET_MOUSEOVER[0]) is still over, else, execute
previous list (WIDGET_MOUSEOVER[1]).
:param event: Mouse motion event. If ``None`` this method creates the event
:param force: If ``True`` calls all mouse leave without checking if the mouse is still over
:param recursive: If ``True`` the call is recursive
:return: None
"""
# If no widget is over, return
if WIDGET_MOUSEOVER[0] is None:
assert len(WIDGET_MOUSEOVER[1]) == 0, 'widget leave sublist must be empty'
assert WIDGET_TOP_CURSOR[0] is None, 'widget top cursor must be None'
return
if event is None:
event = mouse_motion_current_mouse_position()
# Check widget is still over
current: 'Widget' = WIDGET_MOUSEOVER[0]
current._check_mouseover(event, check_all_widget_mouseleave=False) # This may change WIDGET_MOUSEOVER
# If mouse is not visible, forces
if PYGAME_V2:
force = force or not pygame.mouse.get_visible()
# The active widget is not over
if (not current._mouseover or force) and WIDGET_MOUSEOVER[0] is not None:
assert len(WIDGET_MOUSEOVER[1]) == 3, 'invalid widget leave sublist length'
# Unpack list
prev_widget: 'Widget' = WIDGET_MOUSEOVER[1][0]
prev_cursor = WIDGET_MOUSEOVER[1][1]
prev_list: List[Any] = WIDGET_MOUSEOVER[1][2]
assert WIDGET_MOUSEOVER[0] == prev_widget, \
'inconsistent widget leave sublist'
# Set previous cursor
set_pygame_cursor(prev_cursor)
# Unpack list
if len(prev_list) == 0:
WIDGET_MOUSEOVER[0] = None
WIDGET_MOUSEOVER[1] = []
if prev_cursor != WIDGET_TOP_CURSOR[0]:
if WIDGET_TOP_CURSOR_WARNING:
warn(
f'expected {WIDGET_TOP_CURSOR[0]} to be the top cursor '
f'(WIDGET_TOP_CURSOR), but {prev_cursor} is the current '
f'previous cursor from WIDGET_MOUSEOVER recursive list. '
f'The top cursor {WIDGET_TOP_CURSOR[0]} will be established '
f'as the pygame default mouse cursor'
)
set_pygame_cursor(WIDGET_TOP_CURSOR[0])
WIDGET_TOP_CURSOR[0] = None
else:
assert len(prev_list) == 3, 'invalid widget leave sublist length'
WIDGET_MOUSEOVER[0] = prev_list[0]
WIDGET_MOUSEOVER[1] = prev_list
# Call leave
prev_widget.mouseleave(event, check_all_widget_mouseleave=False)
# Recursive call
_check_widget_mouseleave(event, force, recursive=True)
# Check sublist
if len(WIDGET_MOUSEOVER[1]) == 3 and len(WIDGET_MOUSEOVER[1][2]) > 0 and \
not recursive and not force:
prev: List[Any] = WIDGET_MOUSEOVER[1][2] # [widget, cursor, [widget, cursor, [...]]]
while True:
if len(prev) == 0:
break
widget: 'Widget' = prev[0]
cursor = prev[1]
# Check widget is still over
widget._check_mouseover(event, check_all_widget_mouseleave=False)
# If not active
if not widget._mouseover:
# Update the array
if len(prev[2]) == 3:
prev[0] = prev[2][0]
prev[1] = prev[2][1]
prev[2] = prev[2][2]
# Set previous cursor
set_pygame_cursor(cursor)
else:
for _ in range(len(prev)):
prev.pop()
break
else:
prev = prev[2] # Recursive call
# Types
CallbackMouseType = Optional[Union[Callable[['Widget', EventType], Any], CallableNoArgsType]]
CallbackSelectType = Optional[Union[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any], CallableNoArgsType]]
WidgetBorderPositionType = Union[str, List[str], Tuple[str, ...]]
WidgetBorderType = Tuple[ColorType, int, WidgetBorderPositionType, Tuple2IntType]
# noinspection PyProtectedMember
[docs]class Widget(Base):
"""
Widget abstract class.
.. note::
Widget cannot be copied or deep-copied.
:param title: Widget title
:param widget_id: Widget identifier
:param onchange: Callback when updating the status of the widget, executed in :py:meth:`pygame_menu.widgets.core.widget.Widget.change`
:param onreturn: Callback when applying on the widget (return), executed in :py:meth:`pygame_menu.widgets.core.widget.Widget.apply`
:param onselect: Callback when selecting the widget, executed in :py:meth:`pygame_menu.widgets.core.widget.Widget.set_selected`
:param args: Optional arguments for callbacks
:param kwargs: Optional keyword arguments
"""
_accept_events: bool
_alignment: str
_angle: NumberType
_args: List[Any]
_background_color: Optional[Union[ColorType, 'pygame_menu.BaseImage']]
_background_inflate: Tuple2IntType
_background_surface: Optional[List[Union['pygame.Rect', 'pygame.Surface']]]
_border_color: ColorType
_border_inflate: Tuple2IntType
_border_position: WidgetBorderPositionType
_border_width: int
_check_mouseleave_call_render: bool
_col_row_index: Tuple3IntType
_cursor: CursorType
_decorator: 'Decorator'
_default_value: Any
_draw_callbacks: Dict[str, Callable[['Widget', 'pygame_menu.Menu'], Any]]
_events: EventListType
_flip: Tuple2BoolType
_floating: bool
_floating_origin_position: bool
_font: Optional['pygame.font.Font']
_font_antialias: bool
_font_background_color: Optional[ColorType]
_font_color: ColorType
_font_name: FontType
_font_readonly_color: ColorType
_font_readonly_selected_color: ColorType
_font_selected_color: ColorType
_font_shadow: bool
_font_shadow_color: ColorType
_font_shadow_offset: NumberType
_font_shadow_position: str
_font_shadow_tuple: Tuple2IntType
_font_size: int
_frame: Optional['pygame_menu.widgets.Frame']
_joystick_enabled: bool
_keyboard_enabled: bool
_keyboard_ignore_nonphysical: bool
_kwargs: Dict[Any, Any]
_last_render_hash: int
_margin: Tuple2IntType
_max_height: List[Optional[bool]]
_max_width: List[Optional[bool]]
_menu: Optional['pygame_menu.Menu'] # Menu which contains the Widget
_menu_hook: Optional['pygame_menu.Menu'] # Menu the Widget points to (submenu)
_mouse_enabled: bool
_mouseover: bool # Check if mouse is over
_mouseover_called: Optional[bool] # Check if the mouseover/mouseleave callbacks were called
_mouseover_check_rect: Callable[[], 'pygame.Rect']
_onchange: CallbackType
_onmouseleave: CallbackMouseType
_onmouseover: CallbackMouseType
_onreturn: CallbackType
_onselect: CallbackSelectType
_padding: Tuple4IntType
_padding_transform: Tuple4IntType
_position: Tuple2IntType
_rect: 'pygame.Rect'
_rect_size_delta: Tuple2IntType
_scale: List[Union[bool, NumberType]]
_scrollarea: Optional['pygame_menu._scrollarea.ScrollArea'] # Parent scrollarea
_selected: bool
_selection_effect: 'Selection'
_selection_effect_draw_post: bool
_selection_time: NumberType
_sound: 'Sound'
_surface: Optional['pygame.Surface']
_tab_size: int
_title: str
_touchscreen_enabled: bool
_translate: Tuple2IntType # Translation made by user
_translate_virtual: Tuple2IntType # Virtual translation applied by api
_update_callbacks: Dict[str, Callable[[EventListType, 'Widget', 'pygame_menu.Menu'], Any]]
_visible: bool
active: bool
configured: bool
force_menu_draw_focus: bool
is_scrollable: bool
is_selectable: bool
last_surface: Optional['pygame.Surface']
lock_position: bool
readonly: bool
selection_expand_background: bool
def __init__(
self,
title: Any = '',
widget_id: str = '',
onchange: CallbackType = None,
onmouseleave: Optional[Callable[['Widget', EventType], Any]] = None,
onmouseover: Optional[Callable[['Widget', EventType], Any]] = None,
onreturn: CallbackType = None,
onselect: Optional[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any]] = None,
args=None,
kwargs=None
) -> None:
super(Widget, self).__init__(object_id=widget_id)
self._accept_events = False # Indicate the widget receives events (info)
self._alignment = ALIGN_CENTER # Widget alignment
self._background_color = None
self._background_inflate = (0, 0)
self._background_surface = None
self._check_mouseleave_call_render = False
self._col_row_index = (-1, -1, -1)
self._cursor = None
self._decorator = Decorator(self)
self._default_value = _WidgetNoValue()
self._events = []
self._frame = None
self._margin = (0, 0)
self._max_height = [None, False, True] # size, width_scale, smooth
self._max_width = [None, False, True] # size, height_scale, smooth
self._mouseover = False
self._mouseover_called = None
self._padding = (0, 0, 0, 0) # top, right, bottom, left
self._padding_transform = (0, 0, 0, 0)
self._position = (0, 0)
self._scrollarea = None # Widget scrollarea container
self._selected = False # Use select() to modify this status
self._selection_time = 0
self._sound = Sound()
self._tab_size = 0 # Tab spaces
self._title = str(title)
self._visible = True # Use show() or hide() to modify this status
# If True, the widget don't contribute width/height to the Menu widget
# positioning computation. Use .set_float() to modify this status
self._floating = False
self._floating_origin_position = False
# Which function is used to get the rect which checks if the widget is
# active or not
self._mouseover_check_rect = lambda: self.get_rect(to_real_position=True)
# Widget transforms
self._angle = 0 # Rotation angle (degrees)
self._flip = (False, False) # x, y
self._scale = [False, 1, 1, False] # do_scale, x, y, smooth
self._scale_factor = (1, 1) # Transformed/Original in x, y
self._translate = (0, 0)
self._translate_virtual = (0, 0) # Translation virtual used by scrollarea's
# Widget rect. This object does not contain padding. For getting the
# widget+padding use .get_rect() widget method instead. Widget subclass
# should ONLY modify width/height, in rendering and READ position (rect.x,
# rect.y) in drawing. Position during rendering is not the same as it will
# have in menu (menu rendering changes widget position). Some widgets like
# MenuBar are the exception, as its position never changes during menu
# execution (unless user triggers a change), then widgets like these may
# access without problems
self._rect = pygame.Rect(0, 0, 0, 0)
self._rect_size_delta = (0, 0) # Size added to rect width/height
# Callbacks
self._draw_callbacks = {}
self._update_callbacks = {}
self.set_onchange(onchange) # lgtm [py/init-calls-subclass]
self.set_onmouseleave(onmouseleave) # lgtm [py/init-calls-subclass]
self.set_onmouseover(onmouseover) # lgtm [py/init-calls-subclass]
self.set_onreturn(onreturn) # lgtm [py/init-calls-subclass]
self.set_onselect(onselect) # lgtm [py/init-calls-subclass]
self._args = args or []
self._kwargs = kwargs or {}
# Surface of the widget
self._surface = None
# Menu reference
self._menu = None # Menu which contains the widget
self._menu_hook = None # Menu the widget points to. Modified by WidgetManager._add_submenu
# Modified in set_font() method
self._font = None
self._font_antialias = True
self._font_background_color = None
self._font_color = (0, 0, 0)
self._font_name = ''
self._font_readonly_color = (0, 0, 0)
self._font_readonly_selected_color = (255, 255, 255)
self._font_selected_color = (255, 255, 255)
self._font_size = 0
# Font shadow
self._font_shadow = False
self._font_shadow_color = (0, 0, 0)
self._font_shadow_offset = 2.0
self._font_shadow_position = POSITION_NORTHWEST
self._font_shadow_tuple = (0, 0) # (x px offset, y px offset)
# Border
self._border_color = (0, 0, 0)
self._border_inflate = (0, 0)
self._border_position = 'none'
self._border_width = 0
# 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
# Selection effect, for avoiding exception while getting object rect,
# NullSelection was created. Initially it was None
self._selection_effect = pygame_menu.widgets.NoneSelection()
# If False, the selection effect is drawn previous the widget surface
self._selection_effect_draw_post = True
# Inputs
self._keyboard_enabled = True
self._keyboard_ignore_nonphysical = True # Ignores non physical keyboard buttons pressed
self._joystick_enabled = True
self._mouse_enabled = True # Accept mouse interaction
self._touchscreen_enabled = True
# Public statutes. These values can be changed without calling for
# methods (safe to update)
self.active = False # Widget requests focus if selected
self.configured = False # Widget has been configured
self.force_menu_draw_focus = False # If True Menu draw focus if widget is selected, don't considering the previous requisites
self.is_scrollable = False # Some widgets can be scrolled, such as the Frame
self.is_selectable = True # Some widgets cannot be selected like labels
self.last_surface = None # Stores the last surface the widget has been drawn
self.lock_position = False # If True, the widget don't updates the position if .set_position() is executed
self.readonly = False # If True, widget ignores all input
self.selection_expand_background = False # If True, the widget background will inflate to match selection margin if selected
def _ignores_keyboard_nonphysical(self) -> bool:
"""
Ignores the keyboard non-physical button events.
:return: True if ignored
"""
if self._menu is None:
return self._keyboard_ignore_nonphysical
return self._keyboard_ignore_nonphysical and self._menu._keyboard_ignore_nonphysical
[docs] def set_onchange(self, onchange: CallbackType) -> 'Widget':
"""
Set ``onchange`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.change` method. The
callback function receives the following arguments:
.. code-block:: python
onchange(value, *args, *widget._args, **widget._kwargs)
:param onchange: Callback executed if the Widget changes its value; it can be a function or None
:return: Self reference
"""
if onchange:
assert is_callable(onchange), \
'onchange must be callable (function-type) or None'
self._onchange = onchange
return self
[docs] def set_onreturn(self, onreturn: CallbackType) -> 'Widget':
"""
Set ``onreturn`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.apply` method. The
callback function receives the following arguments:
.. code-block:: python
onreturn(value, *args, *widget._args, **widget._kwargs)
:param onreturn: Callback executed if user applies on Widget; it can be a function or None
:return: Self reference
"""
if onreturn:
assert is_callable(onreturn), \
'onreturn must be callable (function-type) or None'
self._onreturn = onreturn
return self
[docs] def set_onselect(self, onselect: CallbackSelectType) -> 'Widget':
"""
Set ``onselect`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.select` method. The
callback function receives the following arguments:
.. code-block:: python
onselect(selected, widget, menu) <or> onselect()
:param onselect: Callback executed if user selects the Widget; it can be a function or None
:return: Self reference
"""
if onselect:
assert is_callable(onselect), \
'onselect must be callable (function-type) or None'
self._onselect = onselect
return self
[docs] def set_onmouseover(self, onmouseover: CallbackMouseType) -> 'Widget':
"""
Set ``onmouseover`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.mouseover` method. The
callback function receives the following arguments:
.. code-block:: python
onmouseover(widget, event) <or> onmouseover()
:param onmouseover: Callback executed if user enters the Widget with the mouse; it can be a function or None
:return: Self reference
"""
if onmouseover:
assert is_callable(onmouseover), \
'onmouseover must be callable (function-type) or None'
self._onmouseover = onmouseover
return self
[docs] def set_onmouseleave(self, onmouseleave: CallbackMouseType) -> 'Widget':
"""
Set ``onmouseleave`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.mouseleave` method. The
callback function receives the following arguments:
.. code-block:: python
onmouseleave(widget, event) <or> onmouseleave()
:param onmouseleave: Callback executed if user leaves the Widget with the mouse; it can be a function or None
:return: Self reference
"""
if onmouseleave:
assert is_callable(onmouseleave), \
'onmouseleave must be callable (function-type) or None'
self._onmouseleave = onmouseleave
return self
[docs] def mouseover(
self,
event: EventType,
check_all_widget_mouseleave: bool = True
) -> 'Widget':
"""
Run the ``onmouseover`` if the mouse is placed over the Widget. The
callback receive the Widget object reference and the mouse event:
.. code-block:: python
onmouseover(widget, event) <or> onmouseover()
.. warning::
This method does not evaluate if the mouse is placed over the Widget.
Only executes the callback and updates the cursor if enabled.
:param event: ``MOUSEMOVE`` pygame event
:param check_all_widget_mouseleave: Check widget leave statutes
:return: Self reference
"""
# Check if within frame, and the previous frame has not been called, call it
if check_all_widget_mouseleave:
if self._frame is not None and WIDGET_MOUSEOVER[0] != self._frame:
in_prev = False
# Check frame not in previous
prev = WIDGET_MOUSEOVER[1]
if len(prev) != 0:
while True:
if len(prev) == 0:
break
if prev[0] == self._frame:
in_prev = True
break
prev = prev[2]
if not in_prev:
self._frame.mouseover(event, check_all_widget_mouseleave)
if self._onmouseover is not None:
if self._mouseover_called is None or not self._mouseover_called:
try:
self._onmouseover(self, event)
except TypeError:
self._onmouseover()
self._mouseover_called = True
# Check previous state
if check_all_widget_mouseleave:
check_widget_mouseleave(event)
# Change cursor
previous_cursor = get_cursor() # Previous cursor
set_pygame_cursor(self._cursor)
# Update previous state
if check_all_widget_mouseleave:
if WIDGET_MOUSEOVER[0] is None:
WIDGET_TOP_CURSOR[0] = previous_cursor
WIDGET_MOUSEOVER[0] = self
WIDGET_MOUSEOVER[1] = [self, previous_cursor, WIDGET_MOUSEOVER[1]]
return self
[docs] def mouseleave(
self,
event: EventType,
check_all_widget_mouseleave: bool = True
) -> 'Widget':
"""
Run the ``onmouseleave`` callback if the mouse is placed outside the Widget.
The callback receive the Widget object reference and the mouse event:
.. code-block:: python
onmouseleave(widget, event) <or> onmouseleave()
.. warning::
This method does not evaluate if the mouse is placed over the Widget.
Only executes the callback and updates the cursor if enabled.
:param event: ``MOUSEMOVE`` pygame event
:param check_all_widget_mouseleave: Check widget leave statutes
:return: Self reference
"""
# Check for consistency
if WIDGET_MOUSEOVER[0] != self or not check_all_widget_mouseleave:
if self._onmouseleave is not None and self._mouseover_called:
try:
self._onmouseleave(self, event)
except TypeError:
self._onmouseleave()
self._mouseover_called = False
if check_all_widget_mouseleave:
check_widget_mouseleave(event)
return self
def _check_mouseover(
self,
event: EventType,
rect: Optional['pygame.Rect'] = None,
check_all_widget_mouseleave: bool = True
) -> bool:
"""
Check the mouse is over the widget. If so, execute the methods.
:param event: Mouse event (``MOUSEMOTION`` or ``ACTIVEEVENT``)
:param rect: Rect object. If ``None`` uses the widget rect in real position
:param check_all_widget_mouseleave: Check widget leave statutes
:return: ``True`` if the mouseover status changed
"""
if event.type not in (pygame.MOUSEMOTION, pygame.ACTIVEEVENT):
return False
# If mouse out from window
if event.type == pygame.ACTIVEEVENT:
if event.gain == 1:
return False
else: # Mouse out from window
check_widget_mouseleave(force=True)
return True
if rect is None:
rect = self._mouseover_check_rect()
updated = False
# Check if menu is active
menu_enabled = True if self._menu is None else self._menu.is_enabled()
# Check if mouse is over the widget, the widget must be visible
if self.is_visible() and self._mouse_enabled and \
rect.collidepoint(*event.pos) and menu_enabled:
if not self._mouseover:
self._mouseover = True
self.mouseover(event, check_all_widget_mouseleave)
updated = True
else:
if self._mouseover:
self._mouseover = False
self.mouseleave(event, check_all_widget_mouseleave)
updated = True
if updated and self._check_mouseleave_call_render:
self._render()
return updated
[docs] def set_cursor(self, cursor: CursorInputType) -> 'Widget':
"""
Set the Widget cursor if user places the mouse over the Widget.
:param cursor: Pygame cursor
:return: Self reference
"""
self._cursor = cursor
return self
[docs] def get_sound(self) -> 'Sound':
"""
Return the Widget sound engine.
:return: Sound API
"""
return self._sound
[docs] def is_selected(self) -> bool:
"""
Return ``True`` if the Widget is selected.
:return: Selected status
"""
return self._selected
[docs] def is_visible(self, check_frame: bool = True) -> bool:
"""
Return ``True`` if the Widget is visible.
:param check_frame: If ``True`` check frame and sub-frames if they're opened as well
:return: Visible status
"""
if not check_frame:
return self._visible
if not self._visible:
return False
frame = self._frame
if frame is not None:
while True:
if frame is None:
break
if not frame._visible:
return False
frame = frame._frame
return True
[docs] def is_floating(self) -> bool:
"""
Return ``True`` if the Widget is floating.
:return: Float status
"""
return self._floating
def __copy__(self) -> 'pygame_menu.Menu':
"""
Copy method.
:return: Raises copy exception
"""
raise _WidgetCopyException('Widget class cannot be copied')
def __deepcopy__(self, memodict: Dict) -> 'pygame_menu.Menu':
"""
Deep-copy method.
:param memodict: Memo dict
:return: Raises copy exception
"""
raise _WidgetCopyException('Widget class cannot be deep-copied')
def _force_render(self) -> Optional[bool]:
"""
Forces Widget render.
.. note::
If this method is used it's not necessary to call Widget methods
:py:meth:`pygame_menu.widgets.core.widget.Widget.force_menu_surface_update` and
:py:meth:`pygame_menu.widgets.core.widget.Widget.force_menu_surface_cache_update`.
As `render` should force Menu render, updating both surface and cache.
:return: Render return value
"""
self._last_render_hash = 0
return self._render()
[docs] def render(self) -> Optional[bool]:
"""
Public rendering method.
.. note::
Unlike private ``_render`` method, public method forces widget rendering
(calling :py:meth:`pygame_menu.widgets.core.widget.Widget._force_render`).
Use this method only if the widget has changed the state. Running this
function many times may affect the performance.
.. note::
Before rendering, check out if the widget font/title/values are
set. If not, it is probable that a zero-size surface is set.
:return: ``True`` if widget has rendered a new state, ``None`` if the widget has not changed, so render used a cache
"""
return self._force_render()
def _render(self) -> Optional[bool]:
"""
Render the Widget surface.
This method shall update the attribute ``_surface`` with a :py:class:`pygame.Surface`
object representing the outer borders of the widget.
.. note::
Before rendering, check out if the widget font/title/values are
set. If not, it is probable that a zero-size surface is set.
.. note::
Render methods should call
:py:meth:`pygame_menu.widgets.core.widget.Widget.force_menu_surface_update`
to force Menu to update the drawing surface.
:return: ``True`` if widget has rendered a new state, ``None`` if the widget has not changed, so render used a cache
"""
raise NotImplementedError('override is mandatory')
@staticmethod
def _hash_variables(*args) -> int:
"""
Compute hash from a series of variables.
:param args: Variables to compute hash
:return: Hash data
"""
h = hash(args)
if h == 0: # Menu considers 0 as un-rendered status
h = random.randrange(-100000, 100000)
return h
def _render_hash_changed(self, *args) -> bool:
"""
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
:return: ``True`` if render has changed the widget
"""
_hash = self._hash_variables(*args)
if _hash != self._last_render_hash or self._last_render_hash == 0:
self._last_render_hash = _hash
return True
return False
[docs] def set_title(self, title: str) -> 'Widget': # lgtm [py/inheritance/incorrect-overridden-signature]
"""
Update the Widget title.
.. note::
Not all widgets implements this method, for example, images don't
accept a title.
:param title: New title
:return: Self reference
"""
self._title = str(title)
self._apply_font()
self._force_render()
return self
[docs] def get_title(self) -> str:
"""
Return the Widget title.
.. note::
Not all widgets implements this method, for example, images don't
accept a title, and such widget would return an empty string if this
method is called.
:return: Widget title
"""
return self._title
[docs] def set_background_color(
self,
color: Optional[Union[ColorInputType, 'pygame_menu.BaseImage']],
inflate: Optional[Tuple2IntType] = (0, 0)
) -> 'Widget':
"""
Set the Widget background color.
:param color: Widget background color
:param inflate: Inflate background on x-axis and y-axis (x, y). If ``None``, the widget value is not updated
:return: Self reference
"""
if color is not None:
if isinstance(color, pygame_menu.BaseImage):
assert color.get_drawing_mode() == pygame_menu.baseimage.IMAGE_MODE_FILL, \
'currently widget only supports IMAGE_MODE_FILL drawing mode'
else:
color = assert_color(color)
if inflate is None:
inflate = self._background_inflate
assert_vector(inflate, 2, int)
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._background_surface = None
self._force_render()
return self
[docs] def background_inflate_to_selection_effect(self) -> 'Widget':
"""
Expand the Widget background inflate to match the selection effect
(the Widget don't require to be selected).
This is a permanent change; for dynamic purposes, 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: Self reference
"""
self._background_inflate = self._selection_effect.get_xy_margin()
self._background_surface = None
return self
def _draw_background_color(
self,
surface: 'pygame.Surface',
rect: Optional['pygame.Rect'] = None
) -> None:
"""
Fill a surface with the widget background color.
:param surface: Surface to fill
:param rect: If given, use that rect instead of widget rect
:return: None
"""
if self._background_color is None:
return
# Create rect
if not (self.selection_expand_background and self._selected):
inflate = self._background_inflate
else:
inflate = self._selection_effect.get_xy_margin()
if rect is None:
rect = self.get_rect(inflate=inflate)
# Create the background surface
if self._background_surface is None or self._background_surface[0] != rect:
background_surface = make_surface(rect.width, rect.height, alpha=True)
if isinstance(self._background_color, pygame_menu.BaseImage):
self._background_color.draw(
surface=background_surface,
area=background_surface.get_rect(),
position=(0, 0)
)
else:
background_surface.fill(self._background_color, background_surface.get_rect())
if self._background_surface is None:
self._background_surface = [rect, background_surface]
else:
self._background_surface[0] = rect
self._background_surface[1] = background_surface
# Draw the background surface
surface.blit(self._background_surface[1], rect)
[docs] def get_border(self) -> WidgetBorderType:
"""
Return the widget border properties.
:return: Color, width, position, and inflate
"""
return self._border_color, self._border_width, \
self._border_position, self._border_inflate
def _draw_border(self, surface: 'pygame.Surface') -> None:
"""
Draw Widget border in the surface.
:param surface: Surface to draw the border
:return: None
"""
if self._border_width == 0 or self._border_color is None:
return
rect = self.get_rect(
inflate=(self._border_inflate[0] + self._background_inflate[0],
self._border_inflate[1] + self._background_inflate[1]))
if self._border_position == WIDGET_BORDER_POSITION_NONE:
return
elif self._border_position == WIDGET_BORDER_POSITION_FULL:
pygame.draw.rect(
surface,
self._border_color,
rect,
self._border_width
)
else:
for pos in self._border_position:
if pos == POSITION_NORTH:
start, end = rect.topleft, rect.topright
elif pos == POSITION_SOUTH:
start, end = rect.bottomleft, rect.bottomright
elif pos == POSITION_EAST:
start, end = rect.topright, rect.bottomright
elif pos == POSITION_WEST:
start, end = rect.topleft, rect.bottomleft
else:
raise RuntimeError('invalid border position')
pygame.draw.line(
surface,
self._border_color,
start,
end,
self._border_width
)
[docs] def set_border(
self,
width: int,
color: Optional[ColorInputType],
inflate: Tuple2IntType = (0, 0),
position: WidgetBorderPositionType = WIDGET_FULL_BORDER
) -> 'Widget':
"""
Set the Widget border.
.. note::
Inflate is added to the background inflate in drawing time.
:param width: Border width in px
:param color: Border color
:param inflate: Inflate on x-axis and y-axis (x, y) in px
:param position: Border position. Valid only: north, south, east, and west. See :py:mod:`pygame_menu.locals`
:return: Self reference
"""
assert isinstance(width, int) and width >= 0
if color is not None:
color = assert_color(color)
assert_vector(inflate, 2, int)
assert inflate[0] >= 0 and inflate[1] >= 0
# Check position
assert isinstance(position, (str, VectorInstance))
if isinstance(position, str):
position = [position]
# Check positioning
if POSITION_WEST in position and POSITION_SOUTH in position and \
POSITION_NORTH in position and POSITION_EAST in position:
position = WIDGET_BORDER_POSITION_FULL
else:
for pos in position:
assert pos in (POSITION_NORTH, POSITION_SOUTH, POSITION_EAST,
POSITION_WEST), \
f'only north, south, east, and west positions are valid, ' \
f'but received "{pos}"'
if width == 0:
position = WIDGET_BORDER_POSITION_NONE
self._border_width = width
self._border_color = color
self._border_inflate = inflate
self._border_position = position
return self
[docs] def get_selection_effect(self) -> 'Selection':
"""
Return the selection effect.
.. note::
If no selection has been provided, ``_WidgetNullSelection`` class
will be returned.
.. note::
For drawing, use
:py:meth:`pygame_menu.widgets.core.widget.Widget.draw_selection_effect`.
.. warning::
Use with caution.
:return: Selection effect
"""
return self._selection_effect
[docs] def set_selection_effect(self, selection: Optional['Selection'] = None) -> 'Widget':
"""
Set the selection effect handler.
.. note::
If ``selection=None`` the selection effect will be established to
``_WidgetNullSelection`` class.
:param selection: Selection effect class
:return: Self reference
"""
assert isinstance(selection, (Selection, type(None)))
if selection is None:
selection = pygame_menu.widgets.NoneSelection()
self._selection_effect = selection
self._force_render()
return self
[docs] def apply(self, *args) -> Any:
"""
Run ``onreturn`` callback when return event. The callback function receives
the following arguments:
.. code-block:: python
onreturn(value, *args, *widget._args, **widget._kwargs)
Where
- ``value`` if something is returned by :py:meth:`pygame_menu.widgets.core.widget.Widget.get_value`
- ``args`` given to this method
- ``args`` of the widget
- ``kwargs`` of the widget
.. note::
Not all widgets have an ``onreturn`` method.
:param args: Extra arguments passed to the callback
:return: Callback return value
"""
self.scroll_to_widget(scroll_parent=False)
if self.readonly:
return
if self._onreturn:
args = list(args) + list(self._args)
try:
args.insert(0, self.get_value())
except ValueError:
pass
return self._onreturn(*args, **self._kwargs)
[docs] def change(self, *args) -> Any:
"""
Run ``onchange`` callback after change event is triggered. The callback
function receives the following arguments:
.. code-block:: python
onchange(value, *args, *widget._args, **widget._kwargs)
Where
- ``value`` if something is returned by :py:meth:`pygame_menu.widgets.core.widget.Widget.get_value`
- ``args`` given to this method
- ``args`` of the widget
- ``kwargs`` of the widget
.. note::
Not all widgets have an ``onchange`` method.
:param args: Extra arguments passed to the callback
:return: Callback return value
"""
val = None
self.scroll_to_widget(scroll_parent=False)
if self.readonly:
return val
if self._onchange:
args = list(args) + list(self._args)
try:
args.insert(0, self.get_value())
except ValueError:
pass
val = self._onchange(*args, **self._kwargs)
if self._menu is not None and self._menu._onwidgetchange is not None:
self._menu._onwidgetchange(self._menu, self)
return val
[docs] def value_changed(self) -> bool:
"""
Return ``True`` if the Widget's value changed from the default value.
:return: ``True`` if changed
"""
try:
return self.get_value() != self._default_value
except ValueError:
return False
[docs] def draw(self, surface: 'pygame.Surface') -> 'Widget':
"""
Draw the Widget on a given surface.
.. note:: Widget drawing order:
1. Background color
2. ``prev`` decorator
3. Widget selection effect (if prev)
4. Widget surface
5. Widget selection effect (if post)
6. Widget border
7. ``post`` decorator
:param surface: Surface to draw
:return: Self reference
"""
if not self.is_visible():
return self
# Check for consistency
if self.active and not self._selected:
self.active = False
# Force rendering
self._render()
if self.is_selected() and not self._selection_effect_draw_post:
self._selection_effect.draw(surface, self)
self._draw_background_color(surface)
self._decorator.draw_prev(surface)
self._draw(surface)
self._draw_border(surface)
self._decorator.draw_post(surface)
# Apply callbacks
self.apply_draw_callbacks()
# Store last surface
self.last_surface = surface
return self
[docs] def draw_after_if_selected(self, surface: Optional['pygame.Surface']) -> 'Widget':
"""
Draw Widget if selected after all widgets have been drawn. This method
should also update ``last_surface``; see
:py:class:`pygame_menu.widgets.DropSelect` widget example or
:py:class:`pygame_menu.widgets.RangeSlider`.
:param surface: Surface to draw. ``None`` if frame is requesting the draw, as some widgets are drawn outside the frame surface
:return: Self reference
"""
if self.is_selected() and self._selection_effect_draw_post:
self._selection_effect.draw(surface, self)
return self
def _draw(self, surface: 'pygame.Surface') -> None:
"""
Draw the Widget on a given surface. This method must be overridden by all
classes.
:param surface: Surface to draw
:return: None
"""
raise NotImplementedError('override is mandatory')
[docs] def get_margin(self) -> Tuple2IntType:
"""
Return the Widget margin.
:return: Widget margin (left, bottom)
"""
return self._margin
[docs] def set_margin(self, x: NumberType, y: NumberType) -> 'Widget':
"""
Set the Widget margin (left, bottom).
:param x: Margin on x-axis (left)
:param y: Margin on y-axis (bottom)
:return: Self reference
"""
assert isinstance(x, NumberInstance)
assert isinstance(y, NumberInstance)
self._margin = (int(x), int(y))
self._force_render()
return self
[docs] def get_padding(self, transformed: bool = True) -> Tuple:
"""
Return the Widget padding.
:param transformed: If ``True``, returns the scaled padding if widget is transformed (flip, scale)
:return: Widget padding (top, right, bottom, left)
"""
if transformed:
return self._padding_transform
return self._padding
[docs] def set_padding(self, padding: PaddingType) -> 'Widget':
"""
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
:return: Self reference
"""
self._padding = parse_padding(padding)
self._padding_transform = self._padding
self._force_render()
return self
[docs] def set_scrollarea(self, scrollarea: Optional['pygame_menu._scrollarea.ScrollArea']) -> None:
"""
Set scrollarea reference. Mostly used for events.
:param scrollarea: Scrollarea object
:return: None
"""
assert isinstance(scrollarea, (type(None), pygame_menu._scrollarea.ScrollArea))
self._scrollarea = scrollarea
[docs] def get_scrollarea(self) -> 'pygame_menu._scrollarea.ScrollArea':
"""
Return the scrollarea object.
:return: ScrollArea object
"""
return self._scrollarea
[docs] def scroll_to_widget(self, margin: Tuple2NumberType = (0, 0), scroll_parent: bool = True) -> 'Widget':
"""
The container ScrollArea scrolls to the Widget.
:param margin: Extra margin around the rect in px on x-axis and y-axis
:param scroll_parent: If ``True`` parent scroll also scrolls to widget
:return: Self reference
"""
if self.has_attribute('ignore_scroll_to_widget'):
return self
if self._frame is not None and self._frame.is_scrollable and \
self._frame.get_scrollarea() is not None:
self._frame.get_scrollarea().scroll_to_rect(self.get_frame().get_rect(),
margin, scroll_parent)
if self._scrollarea is not None:
rect = self.get_rect()
# rect.y += self._border_width
self._scrollarea.scroll_to_rect(rect, margin, scroll_parent)
return self
[docs] def get_focus_rect(self) -> 'pygame.Rect':
"""
Return rect to be used in Widget focus.
:return: Focus rect
"""
return self.get_rect(to_real_position=True)
[docs] def get_rect(
self,
inflate: Optional[Tuple2IntType] = None,
apply_padding: bool = True,
use_transformed_padding: bool = True,
to_real_position: bool = False,
to_absolute_position: bool = False,
render: bool = False,
real_position_visible: bool = True
) -> 'pygame.Rect':
"""
Return the :py:class:`pygame.Rect` object of the Widget. This method
forces rendering.
:param inflate: Inflate rect on x-axis and y-axis (x, y) in px
:param apply_padding: Apply widget padding
:param use_transformed_padding: Use scaled padding if the widget is scaled
:param to_real_position: Transform the widget rect to real coordinates (if the Widget change the position if scrollbars move offsets). Used by events
:param to_absolute_position: Transform the widget rect to absolute coordinates (if the Widget does not change the position if scrollbars move offsets). Used by events
:param render: Force widget rendering
:param real_position_visible: Return only the visible width/height if ``to_real_position=True``
:return: Widget rect object
"""
if render:
self._render()
# Padding + inflate
if inflate is None:
inflate = (0, 0)
padding = self.get_padding(transformed=use_transformed_padding) # top,right,bottom,left
pad_top = padding[0] * apply_padding + inflate[1] / 2
pad_right = padding[1] * apply_padding + inflate[0] / 2
pad_bottom = padding[2] * apply_padding + inflate[1] / 2
pad_left = padding[3] * apply_padding + inflate[0] / 2
rect = pygame.Rect(int(self._rect.x - pad_left),
int(self._rect.y - pad_top),
int(self._rect.width + pad_left + pad_right + self._rect_size_delta[0]),
int(self._rect.height + pad_bottom + pad_top + self._rect_size_delta[1]))
if self._scrollarea is not None:
assert not (to_real_position and to_absolute_position), \
'real and absolute positions cannot be True at the same time'
if to_real_position:
rect = self._scrollarea.to_real_position(rect, visible=real_position_visible)
elif to_absolute_position:
rect = self._scrollarea.to_absolute_position(rect)
return rect
[docs] def get_value(self) -> Any:
"""
Return the Widget value. If exception ``ValueError`` is raised, no value
will be passed to the callbacks.
.. warning::
Not all widgets return a value.
:return: Widget data value
"""
raise ValueError(f'{self.get_class_id()} does not accept value')
[docs] def add_self_to_kwargs(self, key: str = 'widget') -> 'Widget':
"""
Adds the Widget object to kwargs, it helps to get the Widget reference for
callbacks. It raises ``KeyError`` if key is duplicated.
:param key: Name of the parameter
:return: Self reference
"""
assert isinstance(key, str)
if key in self._kwargs.keys():
raise KeyError('duplicated key')
self._kwargs[key] = self
return self
def _apply_transforms(self) -> None:
"""
Apply surface transforms: angle, flip and scaling. Translation is applied
on Widget positioning.
: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])
self._padding_transform = self._padding # Reset pad scaling
# Get width/height
width, height = self._surface.get_size()
width_pad, height_pad = width + self._padding[1] + self._padding[3], \
height + self._padding[0] + self._padding[2]
new_size, smooth = None, None
# Compute scale factor
if self._max_width[0] is None and self._max_height[0] is None:
if self._scale[0] and (self._scale[1] != 1 or self._scale[2] != 1):
w = self._scale[1]
h = self._scale[2]
new_size = int(w * width), int(h * height)
smooth = self._scale[3]
elif self._max_width[0] is not None:
if width_pad > self._max_width[0]:
w = width * self._max_width[0] / width_pad
if self._max_width[1]: # If scale height
height *= self._max_width[0] / width_pad
new_size = int(w), int(height)
smooth = self._max_width[2]
elif self._max_height[0] is not None:
if height_pad > self._max_height[0]:
h = height * self._max_height[0] / height_pad
if self._max_height[1]: # If scale width
width *= self._max_height[0] / height_pad
new_size = int(width), int(h)
smooth = self._max_height[2]
else:
raise RuntimeError('max_width and max_height cannot be non-None at '
'the same time')
# Apply scaling
self._scale_factor = (1, 1)
if new_size is not None and smooth is not None and width > 0 and height > 0:
# Apply surface transformation
if smooth and self._surface.get_bitsize() >= 24:
self._surface = pygame.transform.smoothscale(self._surface, new_size)
else:
self._surface = pygame.transform.scale(self._surface, new_size)
self._scale_factor = new_size[0] / width, new_size[1] / height
# Scale pad
w, h = new_size
pad_width = w / width
pad_height = h / height
# (top,right,bottom,left)
self._padding_transform = (int(self._padding[0] * pad_height),
int(self._padding[1] * pad_width),
int(self._padding[2] * pad_height),
int(self._padding[3] * pad_width))
def _font_render_string(
self,
text: str,
color: ColorInputType = (0, 0, 0),
use_background_color: bool = True
) -> 'pygame.Surface':
"""
Render text. If the font is not defined returns a zero-width surface.
:param text: Text to render
:param color: Text color
:param use_background_color: Use default background color
:return: Text surface
"""
assert isinstance(text, str)
assert isinstance(use_background_color, bool), \
'use_background_color must be boolean'
color = assert_color(color)
bgcolor = self._font_background_color
# Disable
if not use_background_color:
bgcolor = None
if self._font is None:
return make_surface(0, 0)
# Replace tabs
text = text.replace('\t', ' ' * self._tab_size)
surface = self._font.render(text, self._font_antialias, color, bgcolor)
return surface
def _render_string(self, string: str, color: ColorInputType) -> 'pygame.Surface':
"""
Render text and turn it into a surface.
:param string: Text to render
:param color: Text color
:return: Text 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._font_shadow:
text_bg = self._font_render_string(string, self._font_shadow_color)
surface.blit(text_bg, self._font_shadow_tuple)
surface.blit(text, (0, 0))
return surface
[docs] def get_font_color_status(self, check_selection: bool = True) -> ColorType:
"""
Return the Widget font color based on the widget status.
:param check_selection: If ``True`` font is also checked if selected
:return: Color by widget status
"""
if self.readonly:
if self._selected:
return self._font_readonly_selected_color
return self._font_readonly_color
if self._selected and check_selection and self._selection_effect.widget_apply_font_color:
return self._font_selected_color
return self._font_color
[docs] def set_font(
self,
font: FontType,
font_size: int,
color: ColorInputType,
selected_color: ColorInputType,
readonly_color: ColorInputType,
readonly_selected_color: ColorInputType,
background_color: Optional[ColorInputType],
antialias: bool = True
) -> 'Widget':
"""
Set the Widget font.
:param font: Font name (see :py:meth:`pygame.font.match_font` for precise format)
:param font_size: Size of font in pixels
:param color: Normal font color
:param selected_color: Font color if widget is selected
:param readonly_color: Font color if widget is in readonly mode
:param readonly_selected_color: Font color if widget is selected and in readonly mode
:param background_color: Font background color. If ``None`` no background color is used
:param antialias: Determines if antialias is applied to font (uses more processing power)
:return: Self reference
"""
assert isinstance(font_size, int) and font_size > 0
assert isinstance(antialias, bool)
color = assert_color(color)
selected_color = assert_color(selected_color)
readonly_color = assert_color(readonly_color)
readonly_selected_color = assert_color(readonly_selected_color)
if background_color is not None:
background_color = 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
warn('font background color must be opaque, alpha channel must be 255')
font_size = int(font_size)
self._font = pygame_menu.font.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_readonly_color = readonly_color
self._font_readonly_selected_color = readonly_selected_color
self._font_selected_color = selected_color
self._font_size = font_size
self._apply_font()
self._force_render()
return self
[docs] def set_font_shadow(
self,
enabled: bool = True,
color: Optional[ColorInputType] = None,
position: Optional[str] = None,
offset: int = 2
) -> 'Widget':
"""
Set the Widget font shadow.
.. note::
See :py:mod:`pygame_menu.locals` for valid ``position`` values.
:param enabled: Shadow is enabled or not
:param color: Shadow color
:param position: Shadow position
:param offset: Shadow offset
:return: Self reference
"""
self._font_shadow = enabled
if color is not None:
color = assert_color(color)
self._font_shadow_color = color
if position is not None:
assert_position(position)
self._font_shadow_position = position
assert isinstance(offset, int)
assert offset > 0, 'shadow offset must be greater than zero if enabled'
self._font_shadow_offset = offset
# Set position
x = 0
y = 0
if self._font_shadow_position == POSITION_NORTHWEST:
x = -1
y = -1
elif self._font_shadow_position == POSITION_NORTH:
y = -1
elif self._font_shadow_position == POSITION_NORTHEAST:
x = 1
y = -1
elif self._font_shadow_position == POSITION_EAST:
x = 1
elif self._font_shadow_position == POSITION_SOUTHEAST:
x = 1
y = 1
elif self._font_shadow_position == POSITION_SOUTH:
y = 1
elif self._font_shadow_position == POSITION_SOUTHWEST:
x = -1
y = 1
elif self._font_shadow_position == POSITION_WEST:
x = -1
elif self._font_shadow_position == POSITION_CENTER:
pass # (0, 0)
self._font_shadow_tuple = (x * self._font_shadow_offset,
y * self._font_shadow_offset)
self._force_render()
return self
[docs] def update_font(self, style: Dict[str, Any]) -> 'Widget':
"""
Updates the Widget font. This method receives a style dict (non empty).
Optional style keys
- ``antialias`` (bool) – Font antialias
- ``background_color`` (tuple) – Background color
- ``color`` (tuple) – Font color
- ``name`` (str) – Name of the font
- ``readonly_color`` (tuple) – Readonly color
- ``readonly_selected_color`` (tuple) – Readonly selected color
- ``selected_color`` (tuple) – Selected color
- ``size`` (int) – Size of the font
.. note::
If a key is not defined it will be rewritten using current font style
from :py:meth:`pygame_menu.widgets.core.widget.Widget.get_font_info`
method.
:param style: Font style dict
:return: Self reference
"""
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]
return self.set_font(
antialias=style['antialias'],
background_color=style['background_color'],
color=style['color'],
font=style['name'],
font_size=style['size'],
readonly_color=style['readonly_color'],
readonly_selected_color=style['readonly_selected_color'],
selected_color=style['selected_color']
)
[docs] def get_font_info(self) -> Dict[str, Any]:
"""
Return a dict with the information of the Widget font.
:return: Font information dict
"""
return {
'antialias': self._font_antialias,
'background_color': self._font_background_color,
'color': self._font_color,
'name': self._font_name,
'readonly_color': self._font_readonly_color,
'readonly_selected_color': self._font_readonly_selected_color,
'selected_color': self._font_selected_color,
'size': self._font_size
}
def _get_menu_widgets(self) -> List['Widget']:
"""
Return the menu API widgets list.
.. warning::
Use with caution.
:return: Widget list if the menu reference is not ``None``, else, return an empty list
"""
if self._menu is not None:
return self._menu._widgets
return []
def _get_menu_update_widgets(self) -> List['Widget']:
"""
Return the menu update widgets.
.. warning::
Use with caution.
:return: Widget update list if the menu reference is not ``None``, else, return an empty list
"""
if self._menu is not None:
return self._menu._update_widgets
return []
def _menu_render(self) -> None:
"""
Call menu _render if reference is not ``None``.
:return: None
"""
if self._menu is not None:
self._menu._render()
def _apply_font(self) -> None:
"""
Function triggered after a font is applied to the widget.
:return: None
"""
raise NotImplementedError('override is mandatory')
def _set_position_relative_to_frame(self, index: int = -1) -> 'Widget':
"""
Set the Widget position relative to its frame.
:param index: Widget index
:return: Self reference
"""
if self._frame is not None:
fx, fy = self._frame.get_position()
self.set_position(fx + self._padding[3], fy + self._padding[0])
c, r, _ = self._frame.get_col_row_index()
self.set_col_row_index(c, r, index)
self._frame.update_indices()
else:
# raise ValueError(f'{self.get_class_id()} is not within a frame')
pass
return self
[docs] def set_position(self, x: NumberType, y: NumberType) -> 'Widget':
"""
Set the Widget position relative to the Menu/Frame.
This method is executed by the Menu when updating the widget positioning.
For moving the widget use ``translate`` method instead, as this position
will be rewritten on next menu rendering phase.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force Widget rendering after calling this method.
:param x: X position in px
:param y: Y position in px
:return: Self reference
"""
assert isinstance(x, NumberInstance)
assert isinstance(y, NumberInstance)
if self.lock_position:
return self
self._position = (int(x), int(y))
self._rect.x = self._position[0] + self._translate[0] + self._translate_virtual[0]
self._rect.y = self._position[1] + self._translate[1] + self._translate_virtual[1]
return self
[docs] def get_position(
self,
apply_padding: bool = False,
use_transformed_padding: bool = True,
to_real_position: bool = False,
to_absolute_position: bool = False,
real_position_visible: bool = True
) -> Tuple2IntType:
"""
Return the widget position tuple on x-axis and y-axis (x, y) in px.
:param apply_padding: Apply widget padding to position
:param use_transformed_padding: Use scaled padding if the widget is scaled
:param to_real_position: Get the real position within window (not the surface container)
:param to_absolute_position: Get the absolute position within surface container, considering also the parent scrollarea positioning
:param real_position_visible: Return only the visible width/height if ``to_real_position=True``
:return: Widget position
"""
if not (apply_padding or to_real_position or to_absolute_position):
return self._rect.x, self._rect.y
rect = self.get_rect(
apply_padding=apply_padding,
use_transformed_padding=use_transformed_padding,
to_real_position=to_real_position,
to_absolute_position=to_absolute_position,
real_position_visible=real_position_visible
)
return rect.x, rect.y
[docs] def flip(self, x: bool, y: bool) -> 'Widget':
"""
Transformation: This method can flip the Widget either vertically,
horizontally, or both. Flipping a Widget is non-destructive and does not
change the dimensions.
.. note::
Flip is only applied after Widget rendering. Thus, the changes are
not immediate.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method to force
Widget rendering after calling this method.
:param x: Flip on x-axis
:param y: Flip on y-axis
:return: Self reference
"""
assert isinstance(x, bool)
assert isinstance(y, bool)
self._flip = (x, y)
self._force_render()
return self
def _disable_scale(self) -> None:
"""
Disables Widget scale.
:return: None
"""
self._scale[0] = False
self._scale[1] = 1
self._scale[2] = 1
self._max_width[0] = None
self._max_height[0] = None
self.render()
def _scale_warn(
self,
scale: bool = True,
maxwidth: bool = True,
maxheight: bool = True
) -> None:
"""
Warns user about overriding properties of scale/maxwidth/maxheight.
:param scale: Warn about scale
:param maxwidth: Warn bout maxwidth
:param maxheight: Warn about maxheight
:return: None
"""
if self._scale[0] and scale:
warn('widget already has a scaling factor applied. Scaling has '
'been disabled')
if self._max_width[0] is not None and maxwidth:
warn(
'widget max width is not None. Set widget.set_max_width(None) '
'for disabling such feature. This scaling will be ignored'
)
if self._max_height[0] is not None and maxheight:
warn(
'widget max height is not None. Set widget.set_max_height(None) '
'for disabling such feature. This scaling will be ignored'
)
[docs] def set_max_width(
self,
width: Optional[NumberType],
scale_height: NumberType = False,
smooth: bool = True
) -> 'Widget':
"""
Transformation: Set the Widget max width, it applies a scaling factor if
the widget width is greater than the limit.
.. note::
If ``width=0`` the widget will use the max column width of the Menu
(using the column the widget belongs to).
.. note::
Max width considers padding.
.. note::
Max width is only applied after widget rendering. Thus, the changes
are not immediate.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force widget rendering after calling this method.
.. warning::
Final Widget size may not be exactly the same as the desired (width,
height) tuple due to rounding errors, expect ±2 px average.
.. warning::
Widget will scale only if :py:meth:`pygame_menu.widgets.core.widget.Widget.scale`
and :py:meth:`pygame_menu.widgets.core.widget.Widget.set_max_height`
are set to ``None``. Thus, calling this method disables both scale and max height.
:param width: Width in px, ``None`` if max width is disabled
:param scale_height: If ``True`` the height is also scaled if the width exceeds the limit
:param smooth: Smooth scaling
:return: Self reference
"""
assert isinstance(scale_height, bool)
assert isinstance(smooth, bool)
self._scale_warn(maxwidth=False)
self._disable_scale()
if width is None:
self._max_width[0] = None
else:
assert isinstance(width, NumberInstance), 'width must be numeric'
assert width >= 0, 'width must be equal or greater than zero'
self._max_width = [width, scale_height, smooth]
self._force_render()
return self
[docs] def set_max_height(
self,
height: Optional[NumberType],
scale_width: NumberType = False,
smooth: bool = True
) -> 'Widget':
"""
Transformation: Set the Widget max height, it applies a scaling factor
if the widget height is greater than the limit.
.. note::
Max height considers padding.
.. note::
Max height is only applied after Widget rendering. Thus, the changes
are not immediate.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force widget rendering after calling this method.
.. warning::
Final Widget size may not be exactly the same as the desired (width,
height) tuple due to rounding errors, expect ±2 px average.
.. warning::
Widget will scale only if :py:meth:`pygame_menu.widgets.core.widget.Widget.scale`
and :py:meth:`pygame_menu.widgets.core.widget.Widget.set_max_width`
are set to ``None``. Thus, calling this method disables both scale and max width.
:param height: Height in px, ``None`` if max height is disabled
:param scale_width: If ``True`` the width is also scaled if the height exceeds the limit
:param smooth: Smooth scaling
:return: Self reference
"""
assert isinstance(scale_width, bool)
assert isinstance(smooth, bool)
self._scale_warn(maxheight=False)
self._disable_scale()
if height is None:
self._max_height[0] = None
else:
assert isinstance(height, NumberInstance), 'height must be numeric'
assert height > 0, 'height must be greater than zero'
self._max_height = [height, scale_width, smooth]
self._force_render()
return self
[docs] def scale(
self,
width: NumberType,
height: NumberType,
smooth: bool = True
) -> 'Widget':
"""
Transformation: Scale the Widget to a desired width and height factor.
.. note::
Not all widgets are affected by scale.
.. note::
Scale considers widget padding.
.. note::
Scale is only applied after widget rendering. Thus, the changes are
not immediate.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force widget rendering after calling this method.
.. warning::
Widget will scale only if :py:meth:`pygame_menu.widgets.core.widget.Widget.set_max_width`
and :py:meth:`pygame_menu.widgets.core.widget.Widget.set_max_height`
are set to ``None``. Thus, calling this method disables both max width and height.
:param width: Scale factor of the width
:param height: Scale factor of the height
:param smooth: Smooth scaling
:return: Self reference
"""
assert isinstance(width, NumberInstance)
assert isinstance(height, NumberInstance)
assert isinstance(smooth, bool)
assert width > 0 and height > 0, \
'width and height must be greater than zero'
self._scale_warn(scale=False)
self._disable_scale()
self._scale = [True, width, height, smooth]
if width == 1 and height == 1: # Disables scaling
self._scale[0] = False
self._force_render()
return self
[docs] def resize(
self,
width: NumberType,
height: NumberType,
smooth: bool = True
) -> 'Widget':
"""
Transformation: Set the Widget size to another size.
.. note::
This method calls :py:meth:`pygame_menu.widgets.core.widget.Widget.scale`
method; thus, some widgets may not support this transformation.
.. note::
The resize method uses the base Widget size, without any transformation,
if a scaling factor is applied it unscales and then scales back to get
the desired width/height.
.. note::
Resize is only applied after Widget rendering. Thus, the changes are
not immediate.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force widget rendering after calling this method.
.. warning::
Final Widget size may not be exactly the same as the desired (width,
height) tuple due to rounding errors, expect ±2 px average.
:param width: New width of the widget in px
:param height: New height of the widget in px
:param smooth: Smooth scaling
:return: Self reference
"""
self._disable_scale()
if width == 1 and height == 1:
warn('did you mean widget.scale(1,1) instead of widget.resize(1,1)?')
self.scale(float(width) / self.get_width(),
float(height) / self.get_height(), smooth)
return self
[docs] def translate(self, x: NumberType, y: NumberType) -> 'Widget':
"""
Transformation: Translate to (+x, +y) according to the default position.
.. note::
Translate is only applied when updating the widget position (calling
:py:meth:`pygame_menu.widgets.core.widget.Widget.set_position`. This
is done by Menu when rendering the surface. Thus, the position change
is not immediate. To force translation update you may call Menu render
method.
.. note::
To revert changes, only set to ``(0, 0)``.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force widget rendering after calling this method.
:param x: +X in px
:param y: +Y in px
:return: None
"""
assert isinstance(x, NumberInstance)
assert isinstance(y, NumberInstance)
self._translate = (int(x), int(y))
self._force_render()
return self
[docs] def get_translate(self, virtual: bool = False) -> Tuple2IntType:
"""
Get Widget translation on x-axis and y-axis (x, y) in px.
:param virtual: If ``True`` get virtual translation, usually applied within frame scrollarea
:return: Translation on both axis
"""
if virtual:
return self._translate_virtual
return self._translate
[docs] def rotate(self, angle: NumberType) -> 'Widget':
"""
Transformation: 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.
.. note::
Rotation is only applied after widget rendering. Thus, the changes are
not immediate.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force widget rendering after calling this method.
:param angle: Rotation angle (degrees ``0-360``)
:return: Self reference
"""
assert isinstance(angle, NumberInstance)
self._angle = angle
self._force_render()
return self
[docs] def set_alignment(self, align: str) -> 'Widget':
"""
Set the alignment of the Widget.
.. note::
Alignment is only applied when updating the widget position, done by
Menu when rendering the surface. Thus, the alignment change is not
immediate.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force widget rendering after calling this method.
.. note::
See :py:mod:`pygame_menu.locals` for valid ``align`` values.
:param align: Widget align
:return: Self reference
"""
assert_alignment(align)
self._alignment = align
self._force_render()
return self
[docs] def get_alignment(self) -> str:
"""
Return the Widget alignment.
:return: Widget align
"""
return self._alignment
[docs] def select(self, status: bool = True, update_menu: bool = False) -> 'Widget':
"""
Mark the Widget as selected and execute the ``onselect`` callback function
as follows:
.. code-block:: python
onselect(selected, widget, menu) <or> onselect()
If Widget ``is_selectable`` is ``False`` this function is not executed.
.. note::
Use :py:meth:`pygame_menu.widgets.core.widget.Widget.render` method
to force Widget rendering after calling this method.
.. warning::
This method should only be used by the menu.
:param status: Selection status
:param update_menu: If ``True`` this status is also applied on the menu that contains this widget
:return: Self reference
"""
assert isinstance(status, bool)
if not self.is_selectable:
return self
# Update status
self._selected = status
self.active = False
# Toggle between focus and blur events
if self._selected:
self._focus()
self._selection_time = time.time()
else:
self._blur()
self._events = [] # Remove events
self._force_render()
# Call selection event
if self._onselect is not None:
try:
self._onselect(self._selected, self, self._menu)
except TypeError:
self._onselect()
# Update the menu object
if update_menu:
assert self._menu is not None
self._menu.select_widget(None) # Unselect previous one
self._menu.select_widget(self)
return self
[docs] def get_selected_time(self) -> NumberType:
"""
Return time the Widget has been selected in milliseconds. If the Widget
is not currently selected, return ``0``.
:return: Time in milliseconds
"""
if not self._selected:
return 0
return (time.time() - self._selection_time) * 1000
[docs] def get_surface(self) -> 'pygame.Surface':
"""
Return the Widget surface.
.. warning::
Use with caution.
:return: Widget surface object
"""
return self._surface
[docs] def get_width(
self,
apply_padding: bool = True,
apply_selection: bool = False
) -> int:
"""
Return the Widget width.
.. warning::
If the Widget is not rendered, this method will return ``0``.
:param apply_padding: Apply padding
:param apply_selection: Apply selection
:return: Widget width in px
"""
assert isinstance(apply_padding, bool)
assert isinstance(apply_selection, bool)
rect = self.get_rect(apply_padding=apply_padding, render=True)
width = rect.width
if apply_selection:
width += self._selection_effect.get_width()
return int(width)
[docs] def get_height(
self,
apply_padding: bool = True,
apply_selection: bool = False
) -> int:
"""
Return the Widget height.
.. warning::
If the widget is not rendered, this method will return ``0``.
:param apply_padding: Apply padding
:param apply_selection: Apply selection
:return: Widget height in px
"""
assert isinstance(apply_padding, bool)
assert isinstance(apply_selection, bool)
rect = self.get_rect(apply_padding=apply_padding, render=True)
height = rect.height
if apply_selection:
height += self._selection_effect.get_height()
return int(height)
[docs] def get_size(
self,
apply_padding: bool = True,
apply_selection: bool = False
) -> Tuple2IntType:
"""
Return the Widget size.
.. warning::
If the widget is not rendered this method might return ``(0, 0)``.
:param apply_padding: Apply padding
:param apply_selection: Apply selection
:return: Widget width and height in px
"""
return self.get_width(apply_padding=apply_padding, apply_selection=apply_selection), \
self.get_height(apply_padding=apply_padding, apply_selection=apply_selection)
def _focus(self) -> None:
"""
Function that is executed when the Widget receives the user focus (is
selected).
:return: None
"""
pass
def _blur(self) -> None:
"""
Function that is executed when the Widget loses the focus.
:return: None
"""
pass
def _configure(self) -> None:
"""
Function that is executed after the Widget is configured.
:return: None
"""
pass
def _append_to_menu(self) -> None:
"""
Function that is executed after the Widget is appended to the Menu.
:return: None
"""
pass
[docs] def set_sound(self, sound: 'Sound') -> 'Widget':
"""
Set sound engine to the Widget.
:param sound: Sound object
:return: Self reference
"""
self._sound = sound
return self
[docs] def set_controls(
self,
joystick: bool = True,
mouse: bool = True,
touchscreen: bool = True,
keyboard: bool = True
) -> 'Widget':
"""
Enable interfaces to control the Widget.
:param joystick: Use joystick events
:param mouse: Use mouse events
:param touchscreen: Use touchscreen events
:param keyboard: Use keyboard events
:return: Self reference
"""
assert isinstance(joystick, bool)
assert isinstance(mouse, bool)
assert isinstance(touchscreen, bool)
assert isinstance(keyboard, bool)
self._joystick_enabled = joystick
self._mouse_enabled = mouse
self._touchscreen_enabled = touchscreen
self._keyboard_enabled = keyboard
return self
[docs] def set_value(self, value: Any) -> None:
"""
Set the Widget value.
.. note::
Not all widgets accepts a value, for example the image widget.
.. 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
:return: None
"""
raise ValueError(f'{self.get_class_id()} does not accept value')
[docs] def set_default_value(self, value: Any) -> 'Widget':
"""
Set the Widget value, and then make it as default.
.. note::
This method is intended to be used along
:py:meth:`pygame_menu.widgets.core.widget.Widget.reset_value`
method that sets the Widget value back to the default set with this
method.
.. note::
Not all widgets accepts a value, for example the image widget.
:param value: Default widget value
:return: Self reference
"""
self.set_value(value)
self._default_value = value
return self
[docs] def reset_value(self) -> 'Widget':
"""
Reset the Widget value to the default one.
:return: Self reference
"""
if not isinstance(self._default_value, _WidgetNoValue):
self.set_value(self._default_value)
return self
[docs] def update(self, events: EventVectorType) -> bool:
"""
Update according to the given events list and fire the callbacks. This
method must return ``True`` if it updated (the internal variables
changed during user input).
.. note::
Update is not performed if the Widget is in ``readonly`` state or
it's hidden. However, ``apply_update_callbacks`` method is called
in most widgets, except :py:class:`pygame_menu.widgets.NoneWidget`.
:param events: List/Tuple of pygame events
:return: ``True`` if updated
"""
raise NotImplementedError('override is mandatory')
[docs] def add_draw_callback(
self,
draw_callback: Callable[['Widget', 'pygame_menu.Menu'], Any]
) -> str:
"""
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
:py:meth:`pygame_menu.widgets.core.widget.Widget.remove_draw_callback`.
.. note::
If Menu surface cache is enabled this method may run only once. To
force running the added method each time call
``widget.force_menu_surface_update()`` to force Menu update the cache
status if the drawing callback does not make the Widget to render.
Remember that rendering the Widget forces the Menu to update its
surface, thus updating the cache too.
:param draw_callback: Function
:return: Callback ID
"""
assert is_callable(draw_callback), \
'draw callback must be callable (function-type)'
callback_id = uuid4()
self._draw_callbacks[callback_id] = draw_callback
return callback_id
[docs] def remove_draw_callback(self, callback_id: str) -> 'Widget':
"""
Removes draw callback from ID.
:param callback_id: Callback ID
:return: Self reference
"""
assert isinstance(callback_id, str)
if callback_id not in self._draw_callbacks.keys():
raise IndexError(f'callback ID "{callback_id}" does not exist')
del self._draw_callbacks[callback_id]
return self
[docs] def apply_draw_callbacks(self) -> 'Widget':
"""
Apply callbacks on Widget draw.
:return: Self reference
"""
if len(self._draw_callbacks) == 0:
return self
for callback in self._draw_callbacks.values():
callback(self, self._menu)
return self
[docs] def add_update_callback(
self,
update_callback: Callable[[EventListType, 'Widget', 'pygame_menu.Menu'], Any]
) -> str:
"""
Adds a function to the Widget to be executed each time the Widget is
updated.
The function that this method receives receives three objects: the events
list, the Widget itself and the Menu reference. It is similar to
:py:meth:`pygame_menu.widgets.core.widget.Widget.add_draw_callback`.
After creating a new callback, this functions returns the ID of the call.
It can be removed anytime using
:py:meth:`pygame_menu.widgets.core.widget.Widget.remove_update_callback`.
.. note::
Not all widgets are updated, so the provided function may never be
executed in some widgets (for example, label or images).
:param update_callback: Function
:return: Callback ID
"""
assert is_callable(update_callback), \
'update callback must be callable (function-type)'
callback_id = uuid4()
self._update_callbacks[callback_id] = update_callback
return callback_id
[docs] def remove_update_callback(self, callback_id: str) -> 'Widget':
"""
Removes update callback from ID.
:param callback_id: Callback ID
:return: Self reference
"""
assert isinstance(callback_id, str)
if callback_id not in self._update_callbacks.keys():
raise IndexError(f'callback<"{callback_id}"> does not exist')
del self._update_callbacks[callback_id]
return self
[docs] def apply_update_callbacks(self, events: EventListType) -> 'Widget':
"""
Apply callbacks on Widget update.
.. note::
Readonly widgets or hidden widgets do not apply update callbacks.
:param events: Events list
:return: Self reference
"""
if len(self._update_callbacks) == 0 or self.readonly:
return self
for callback in self._update_callbacks.values():
callback(events, self, self._menu)
return self
def _add_event(self, event: EventType) -> None:
"""
Add a custom event to the Widget for the next update.
:param event: Custom event
:return: None
"""
self._events.append(event)
def _merge_events(self, events: EventListType) -> EventListType:
"""
Append the Widget events to events list.
:param events: Event list
:return: Augmented event list
"""
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 set_float(
self,
float_status: bool = True,
menu_render: bool = False,
origin_position: bool = False
) -> 'Widget':
"""
Set the floating status. If ``True`` the Widget don't contributes its
width/height to the Menu widget positioning computation (for example,
the surface area or the column/row layout), and don't add one unit to
the rows (use the same vertical place as the previous widget.
For example, before floating:
.. code-block:: python
----------------------------
| wid1 | wid3 |
| wid2 | wid4 |
----------------------------
After ``wid3.set_float(True)``:
.. code-block:: python
----------------------------
| wid1 | wid4 |
| wid2,wid3 | |
----------------------------
If the Widget is within a Frame, it does not contribute to the
width/height of the layout. Also, it is been set to the *(0, 0)* position,
thus, the only way to move the Widget to a desired position is by
translating it.
:param float_status: Float status
:param menu_render: If ``True`` forces the Menu to render instantly; else, rendering is controlled by menu
:param origin_position: If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating (updated by the Menu render phase)
:return: None
"""
assert isinstance(float_status, bool)
assert isinstance(menu_render, bool)
assert isinstance(origin_position, bool)
self._floating = float_status
self._floating_origin_position = origin_position
self.force_menu_surface_update()
if menu_render and self._menu is not None:
self._menu.render()
return self
[docs] def show(self) -> 'Widget':
"""
Set the Widget visible.
:return: Self reference
"""
self._visible = True
self._render()
if self._menu is not None:
self._menu._update_selection_if_hidden()
return self
[docs] def hide(self) -> 'Widget':
"""
Hides the Widget.
:return: Self reference
"""
if self._mouseover:
self._mouseover = False
self.mouseleave(mouse_motion_current_mouse_position())
self._visible = False
self.active = False
self._render()
if self._menu is not None:
self._menu._update_selection_if_hidden()
return self
[docs] def set_col_row_index(self, col: int, row: int, index: int) -> 'Widget':
"""
Set the (column, row, index) position. If the column or row is ``-1`` then
the widget is not assigned to a certain column/row (for example, if it's
hidden).
:param col: Column
:param row: Row
:param index: Index in Menu widget list
:return: Self reference
"""
assert isinstance(col, int) and col >= -1
assert isinstance(row, int) and row >= -1
assert isinstance(index, int) and index >= -1
self._col_row_index = (col, row, index)
return self
[docs] def get_col_row_index(self) -> Tuple3IntType:
"""
Get the Widget column/row position.
:return: (column, row, index) tuple
"""
return self._col_row_index
[docs] def get_decorator(self) -> 'Decorator':
"""
Return the Widget decorator API.
:return: Decorator API
"""
return self._decorator
[docs] def get_frame(self) -> Optional['pygame_menu.widgets.Frame']:
"""
Get container frame of Widget. If Widget is not within a Frame, the method
returns ``None``.
:return: Frame object
:rtype: :py:class:`pygame_menu.widgets.Frame`
"""
return self._frame
[docs] def set_frame(self, frame: 'pygame_menu.widgets.Frame') -> 'Widget':
"""
Set Widget frame.
:param frame: Frame object
:return: Self reference
"""
assert self._frame is None, 'widget is already in another frame'
assert isinstance(frame, pygame_menu.widgets.Frame)
self._frame = frame
return self
[docs] def get_frame_depth(self) -> int:
"""
Get frame depth (If frame is packed within another frame).
:return: Frame depth
"""
frame = self._frame
depth = 0
if frame is not None:
while True:
if frame is None:
break
depth += 1
frame = frame._frame
return depth
def _get_status(self) -> Tuple[Any, ...]:
"""
Get the status of the Widget as a tuple (position, indices, values, etc).
:return: Data
"""
# Assemble class name
cls_name = self.__class__.__name__
if self.get_title() != '':
cls_name += '-' + self.get_title()
# Assemble geometric data
rect = self.get_rect(render=True)
rect_real = self.get_rect(to_real_position=True)
rect_abs = self.get_rect(to_absolute_position=True)
cri = self.get_col_row_index()
geom = (
cri[0], cri[1], cri[2], # Column/row layout
rect.x, rect.y, rect.width, rect.height, # Drawing rect
rect_real.x, rect_real.y, # Real rect positioning
rect_abs.x, rect_abs.y # Absolute rect positioning
)
# Bool info
bool_status = (
int(self.is_selectable),
int(self.is_floating()),
int(self.is_selected()),
int(self.is_visible()),
int(self.is_scrollable),
int(self.get_frame() is not None),
self.get_frame_depth()
)
# Starting data
data = [cls_name, geom, bool_status]
# Append inner widgets if frame and not menu
if isinstance(self, pygame_menu.widgets.Frame):
data.append(self.get_indices())
for ww in self.get_widgets():
if ww.get_menu() != self.get_menu():
data.append(ww._get_status())
# Append inner widgets if drop select
if isinstance(self, pygame_menu.widgets.DropSelect) and self._drop_frame is not None:
data.append(self._drop_frame._get_status())
for btn in self._option_buttons:
data.append(btn._get_status())
try:
data.append(self.get_value())
except ValueError:
pass
return tuple(data)
[docs] def set_tab_size(self, tab_size: int) -> 'Widget':
"""
Set widget tab size.
:param tab_size: Width of a tab character
:return: Self reference
"""
assert isinstance(tab_size, int) and tab_size >= 0
self._tab_size = tab_size
return self
class _WidgetCopyException(Exception):
"""
If user tries to copy a Widget.
"""
pass
class _WidgetNoValue(object):
"""
No value class.
"""
pass
class WidgetTransformationNotImplemented(Exception):
"""
Exception raised if widget does not implement a transformation.
"""
pass