Source code for pygame_menu.widgets.widget.button

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

BUTTON
Button widget. Basically, a label with callback function and enhanced events.
"""

from __future__ import annotations

__all__ = ["Button", "ButtonManager"]

import re
import webbrowser
from abc import ABC
from typing import TYPE_CHECKING, Any

import pygame

import pygame_menu
import pygame_menu.events as _events
from pygame_menu.locals import CURSOR_HAND, FINGERUP
from pygame_menu.utils import get_finger_pos, warn
from pygame_menu.widgets.core.widget import AbstractWidgetManager, Widget
from pygame_menu.widgets.widget.label import Label

if TYPE_CHECKING:
    from collections.abc import Callable

    from pygame_menu._types import CallbackType, EventVectorType


[docs] class Button(Label): """ Button widget. The arguments and unknown keyword arguments are passed to the ``onreturn`` function: .. code-block:: python onreturn(*args, **kwargs) .. note:: Button accepts all transformations. :param title: Button title :param button_id: Button ID :param onreturn: Callback when pressing the button :param args: Optional arguments for callbacks :param kwargs: Optional keyword arguments """ to_menu: bool def __init__( self, title: Any, button_id: str = "", onreturn: CallbackType = None, *args, **kwargs, ) -> None: super().__init__(title=title, label_id=button_id, accept_events=True) self._args = list(args) self._kwargs = kwargs self.set_onreturn(onreturn) self.to_menu = False # True if button opens a new Menu
[docs] def set_selection_callback( self, callback: Callable[[bool, Widget, pygame_menu.Menu], Any] | None ) -> None: """ Update the button selection callback, once button is selected, the callback function is executed as follows: .. code-block:: python callback(selected, widget, menu) :param callback: Callback when selecting the widget, executed in :py:meth:`pygame_menu.widgets.core.widget.Widget.set_selected` """ if callback is not None: assert callable(callback), ( "callback must be callable (function-type) or None" ) self._onselect = callback
[docs] def update_callback(self, callback: Callable, *args) -> None: """ Update function triggered by the button; ``callback`` cannot point to a Menu, that behavior is only valid using :py:meth:`pygame_menu.menu.Menu.add.button` method. .. note:: If button points to a submenu, and the callback is changed to a function, the submenu will be removed from the parent Menu. Thus preserving the structure. :param callback: Function :param args: Arguments used by the function once triggered """ assert callable(callback), "only callable (function-type) are allowed" # If return is a Menu object, remove it from submenus list if self._menu is not None and self._onreturn is not None and self.to_menu: assert len(self._args) == 1 submenu = self._args[0] # Menu assert self._menu.in_submenu(submenu), ( "pointed menu is not in submenu list of parent container" ) # noinspection PyProtectedMember assert self._menu._remove_submenu(submenu, self), ( "submenu could not be removed" ) self.to_menu = False self._args = args or [] self._onreturn = callback
def _draw(self, surface: pygame.Surface) -> None: surface.blit(self._surface, self._rect.topleft)
[docs] def update(self, events: EventVectorType) -> bool: self.apply_update_callbacks(events) rect: pygame.Rect = self.get_rect(to_real_position=True) if self.readonly or not self.is_visible(): self._readonly_check_mouseover(events, rect) return False for event in events: # Check mouse over self._check_mouseover(event, rect) # User applies with key if ( event.type == pygame.KEYDOWN and self._keyboard_enabled and self._ctrl.apply(event, self) or event.type == pygame.JOYBUTTONDOWN and self._joystick_enabled and self._ctrl.joy_select(event, self) ): if self.to_menu: self._sound.play_open_menu() else: self._sound.play_key_add() self.apply() return True # User clicks the button; don't consider the mouse wheel (button 4 & 5) elif ( event.type == pygame.MOUSEBUTTONUP and self._mouse_enabled and event.button in (1, 2, 3) or event.type == FINGERUP and self._touchscreen_enabled and self._menu is not None ): if event.type == pygame.MOUSEBUTTONUP: self._sound.play_click_mouse() else: self._sound.play_click_touch() event_pos = get_finger_pos(self._menu, event) if rect.collidepoint(*event_pos): self.apply() return True return False
class ButtonManager(AbstractWidgetManager, ABC): """ Button manager. """ # noinspection PyProtectedMember def banner( self, image: pygame_menu.BaseImage | pygame.Surface, action: pygame_menu.Menu | _events.MenuAction | Callable | int | None = None, *args, **kwargs, ) -> pygame_menu.widgets.Button: """ Adds a clickeable image to the Menu with same behavior as a Button. The arguments and unknown keyword arguments are passed to the action, if it's a callable object: .. code-block:: python action(*args) If ``accept_kwargs=True`` then the ``**kwargs`` are also unpacked on action call: .. code-block:: python action(*args, **kwargs) If ``onselect`` is defined, the callback is executed as follows, where ``selected`` is a boolean representing the selected status: .. code-block:: python onselect(selected, widget, menu) kwargs (Optional) - ``accept_kwargs`` (bool) – Button action accepts ``**kwargs`` if it's a callable object (function-type), ``False`` by default - ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_ - ``back_count`` (int) – Number of menus to go back if action is :py:data:`pygame_menu.events.BACK` event, default is ``1`` - ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color - ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px - ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals` - ``border_width`` (int) – Border width in px. If ``0`` disables the border - ``button_id`` (str) – Widget ID - ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over - ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows - ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating - ``margin`` (tuple, list) – Widget (left, bottom) margin in px - ``onselect`` (callable, None) – Callback executed when selecting the widget - ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left) - ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color - ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect - ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow - ``shadow_radius`` (int) - Border radius of the shadow - ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'`` - ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled .. note:: All theme-related optional kwargs use the default Menu theme if not defined. .. note:: Using ``action=None`` is the same as using ``action=pygame_menu.events.NONE``. .. note:: This is applied only to the base Menu (not the currently displayed, stored in ``_current`` pointer); for such behavior apply to :py:meth:`pygame_menu.menu.Menu.get_current` object. .. warning:: Be careful with kwargs collision. Consider that all optional documented kwargs keys are removed from the object. :param image: Image or surface to display as the clickable banner. :param action: Action of the button, can be a Menu, an event, or a function :param args: Additional arguments used by a function :param kwargs: Optional keyword arguments :return: Widget object :rtype: :py:class:`pygame_menu.widgets.Button` """ if isinstance(image, pygame.Surface): image = pygame_menu.BaseImage(image) # We use setdefault so we don't overwrite the user # but WE provide the values for the filter to "pop" kwargs.setdefault("padding", 0) # This prevents the theme selection effect from overwriting the banner if "selection_effect" not in kwargs: kwargs["selection_effect"] = pygame_menu.widgets.NoneSelection() # Ensure the selection color doesn't show up on the ' ' character kwargs.setdefault("selection_color", (0, 0, 0, 0)) # Set image as background kwargs["background_color"] = image # Call button - this will now "pop" our custom padding and selection_effect btn = self.button(" ", action, *args, **kwargs) return btn.resize(*image.get_size()) # noinspection PyProtectedMember def button( self, title: Any, action: pygame_menu.Menu | _events.MenuAction | Callable | int | None = None, *args, **kwargs, ) -> pygame_menu.widgets.Button: """ Adds a button to the Menu. The arguments and unknown keyword arguments are passed to the action, if it's a callable object: .. code-block:: python action(*args) If ``accept_kwargs=True`` then the ``**kwargs`` are also unpacked on action call: .. code-block:: python action(*args, **kwargs) If ``onselect`` is defined, the callback is executed as follows, where ``selected`` is a boolean representing the selected status: .. code-block:: python onselect(selected, widget, menu) kwargs (Optional) - ``accept_kwargs`` (bool) – Button action accepts ``**kwargs`` if it's a callable object (function-type), ``False`` by default - ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_ - ``back_count`` (int) – Number of menus to go back if action is :py:data:`pygame_menu.events.BACK` event, default is ``1`` - ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color - ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px - ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color - ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px - ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals` - ``border_width`` (int) – Border width in px. If ``0`` disables the border - ``button_id`` (str) – Widget ID - ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over - ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows - ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating - ``font_background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Widget font background color - ``font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget font color - ``font_name`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Widget font path - ``font_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Font shadow color - ``font_shadow_offset`` (int) – Font shadow offset in px - ``font_shadow_position`` (str) – Font shadow position, see locals for position - ``font_shadow`` (bool) – Font shadow is enabled or disabled - ``font_size`` (int) – Font size of the widget - ``leading`` (int) - Font leading for ``wordwrap``. If ``None`` retrieves from widget font - ``margin`` (tuple, list) – Widget (left, bottom) margin in px - ``max_nlines`` (int) - Number of maximum lines for ``wordwrap``. If ``None`` the number is dynamically computed. If exceeded, ``get_overflow_lines()`` will return the non-displayed lines - ``onselect`` (callable, None) – Callback executed when selecting the widget - ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left) - ``readonly_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode - ``readonly_selected_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode and is selected - ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color - ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect - ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow - ``shadow_radius`` (int) - Border radius of the shadow - ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'`` - ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled - ``tab_size`` (int) – Width of a tab character - ``underline_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Color of the underline. If ``None`` use the same color of the text - ``underline_offset`` (int) – Vertical offset in px. ``2`` by default - ``underline_width`` (int) – Underline width in px. ``2`` by default - ``underline`` (bool) – Enables text underline, using a properly placed decoration. ``False`` by default - ``wordwrap`` (bool) – Wraps button text if newline (\n) is found on text, or it exceeds the maximum width from its container. If ``False`` the manager splits the string and creates a list of widgets, else, the widget itself splits and updates the height .. note:: All theme-related optional kwargs use the default Menu theme if not defined. .. note:: Using ``action=None`` is the same as using ``action=pygame_menu.events.NONE``. .. note:: This is applied only to the base Menu (not the currently displayed, stored in ``_current`` pointer); for such behavior apply to :py:meth:`pygame_menu.menu.Menu.get_current` object. .. warning:: Be careful with kwargs collision. Consider that all optional documented kwargs keys are removed from the object. :param title: Title of the button :param action: Action of the button, can be a Menu, an event, or a function :param args: Additional arguments used by a function :param kwargs: Optional keyword arguments :return: Widget object :rtype: :py:class:`pygame_menu.widgets.Button` """ total_back = kwargs.pop("back_count", 1) assert isinstance(total_back, int) and 1 <= total_back # Get ID button_id = kwargs.pop("button_id", "") assert isinstance(button_id, str), "id must be a string" # Accept kwargs accept_kwargs = kwargs.pop("accept_kwargs", False) assert isinstance(accept_kwargs, bool) # Onselect callback onselect = kwargs.pop("onselect", None) # Filter widget attributes to avoid passing them to the callbacks attributes = self._filter_widget_attributes(kwargs) # Button underline underline = kwargs.pop("underline", False) underline_color = kwargs.pop("underline_color", attributes["font_color"]) underline_offset = kwargs.pop("underline_offset", 1) underline_width = kwargs.pop("underline_width", 1) # Wordwrap wordwrap = kwargs.pop("wordwrap", False) assert isinstance(wordwrap, bool) leading = kwargs.pop("leading", None) assert isinstance(leading, (type(None), int)) max_nlines = kwargs.pop("max_nlines", None) assert isinstance(max_nlines, (type(None), int)) # Change action if certain events if action == _events.PYGAME_QUIT or action == _events.PYGAME_WINDOWCLOSE: action = _events.EXIT elif action is None: action = _events.NONE # If element is a Menu if isinstance(action, (pygame_menu.Menu, type(self._menu))): # Check for recursive if action == self._menu or action.in_submenu(self._menu, recursive=True): raise ValueError( f'{action.get_class_id()} title "{action.get_title()}" is ' f"already on submenu structure, recursive menus lead to " f"unexpected behaviours. For returning to previous menu" f"use pygame_menu.events.BACK event defining an optional " f"back_count number of menus to return from, default is 1" ) widget = Button(title, button_id, self._menu._open, action) widget.to_menu = True # If element is a MenuAction elif action == _events.BACK: # Back to Menu widget = Button(title, button_id, self._menu.reset, total_back) elif action == _events.CLOSE: # Close Menu widget = Button(title, button_id, self._menu._close) elif action == _events.EXIT: # Exit program widget = Button(title, button_id, self._menu._exit) elif action == _events.NONE: # None action widget = Button(title, button_id) elif action == _events.RESET: # Back to Top Menu widget = Button(title, button_id, self._menu.full_reset) # If element is a function or callable elif callable(action): if not accept_kwargs: widget = Button(title, button_id, action, *args) else: widget = Button(title, button_id, action, *args, **kwargs) else: raise ValueError( "action must be a Menu, a MenuAction (event), a " "function (callable), or None" ) # Configure and add the button if not accept_kwargs: try: self._check_kwargs(kwargs) except ValueError: if self._menu._verbose: warn( "button cannot accept kwargs. If you want to use kwargs " "options set accept_kwargs=True" ) raise self._configure_widget(widget=widget, **attributes) if underline: widget.add_underline(underline_color, underline_offset, underline_width) widget.set_selection_callback(onselect) widget._leading = leading # Configure wordwrap widget._max_nlines = max_nlines widget._wordwrap = wordwrap self._append_widget(widget) # Add to submenu if widget.to_menu: self._add_submenu(action, widget) # type: ignore return widget def url(self, href: str, title: str = "", **kwargs) -> pygame_menu.widgets.Button: """ Adds a Button url to the Menu. Clicking the widget will open the link. If ``title`` is defined, the link will not be written. For example: ``href='google.com', title=''`` will write the link, but ``href='google.com', title='Google'`` will write 'Google' and opens 'google.com' if clicked. If ``onselect`` is defined, the callback is executed as follows, where ``selected`` is a boolean representing the selected status: .. code-block:: python onselect(selected, widget, menu) kwargs (Optional) - ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_ - ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color - ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px - ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color - ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px - ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals` - ``border_width`` (int) – Border width in px. If ``0`` disables the border - ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over. By default, is ``HAND`` - ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows - ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating - ``font_background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Widget font background color - ``font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget font color. If not defined, uses ``theme.widget_url_color`` - ``font_name`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Widget font path - ``font_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Font shadow color - ``font_shadow_offset`` (int) – Font shadow offset in px - ``font_shadow_position`` (str) – Font shadow position, see locals for position - ``font_shadow`` (bool) – Font shadow is enabled or disabled - ``font_size`` (int) – Font size of the widget - ``margin`` (tuple, list) – Widget (left, bottom) margin in px - ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left) - ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color - ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect - ``tab_size`` (int) – Width of a tab character - ``underline_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Color of the underline. If ``None`` use the same color of the text - ``underline_offset`` (int) – Vertical offset in px. ``2`` by default - ``underline_width`` (int) – Underline width in px. ``2`` by default - ``underline`` (bool) – Enables text underline, using a properly placed decoration. ``True`` by default .. note:: All theme-related optional kwargs use the default Menu theme if not defined. .. note:: This is applied only to the base Menu (not the currently displayed, stored in ``_current`` pointer); for such behavior apply to :py:meth:`pygame_menu.menu.Menu.get_current` object. :param href: Link to open :param title: Alternative title of the link :param kwargs: Optional keyword arguments :return: Widget object, or List of widgets if the text overflows :rtype: :py:class:`pygame_menu.widgets.Button` """ # Validate link assert isinstance(href, str) and len(href) > 0 regex = re.compile( r"^(?:http|ftp)s?://" # http:// or https:// r"(?:(?:[A-Z\d](?:[A-Z\d-]{0,61}[A-Z\d])?\.)+(?:[A-Z]{2,6}\.?|[A-Z\d-]{2,}\.?)|" # domain... r"localhost|" # localhost... r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip r"(?::\d+)?" # optional port r"(?:/?|[/?]\S+)$", re.IGNORECASE, ) assert re.match(regex, href) is not None, "invalid link format" # Configure kwargs if "cursor" not in kwargs.keys(): kwargs["cursor"] = CURSOR_HAND if "font_color" not in kwargs.keys(): kwargs["font_color"] = self._theme.widget_url_color if "selection_color" not in kwargs.keys(): kwargs["selection_color"] = self._theme.widget_url_color if "selection_effect" not in kwargs.keys(): kwargs["selection_effect"] = pygame_menu.widgets.NoneSelection() if "underline" not in kwargs.keys(): kwargs["underline"] = True # Return new button return self.button( title if title != "" else href, lambda: webbrowser.open(href), **kwargs )