Source code for pygame_menu.widgets.widget.toggleswitch

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

TOGGLE SWITCH
Switch between several states.

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

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

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

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

__all__ = ['ToggleSwitch']

import pygame
import pygame_menu
import pygame_menu.controls as ctrl

from pygame_menu.font import FontType, assert_font
from pygame_menu.locals import FINGERUP
from pygame_menu.utils import check_key_pressed_valid, assert_color, assert_vector, \
    make_surface, get_finger_pos
from pygame_menu.widgets.core import Widget

from pygame_menu._types import Any, CallbackType, Union, List, Tuple, Optional, \
    ColorType, NumberType, Tuple2NumberType, Tuple2IntType, NumberInstance, \
    ColorInputType, EventVectorType


# noinspection PyMissingOrEmptyDocstring
[docs]class ToggleSwitch(Widget): """ Toggle switch widget. If the state of the widget changes the ``onchange`` callback is called. The state can change by pressing LEFT/RIGHT or RETURN if the widget only has two states. This class can handle more than 2 states. .. code-block:: python onchange(state_value, **kwargs) .. note:: This widget only accepts translation transformation. :param title: Toggle switch title :param toggleswitch_id: ToggleSwitch ID :param default_state: Default state index of the switch :param infinite: The state can rotate :param onchange: Callback when changing the state of the switch :param onselect: Function when selecting the widget :param single_click: Changes the state of the switch with 1 click instead of finding the closest position (events). If ``True`` the parameter ``infinite`` will also be ``True`` :param single_click_dir: Direction of the change if only 1 click is pressed. ``True`` for left direction, ``False`` for right :param slider_color: Slider color :param slider_height_factor: Height of the slider (factor of the switch height) :param slider_thickness: Slider thickness in px :param slider_vmargin: Vertical margin of the slider (factor of the switch height) :param state_color: Background color of each state, it modifies the whole width of the switch :param state_text: Text of each state of the switch :param state_text_font: Font of the state text. If ``None`` uses the widget font :param state_text_font_color: Color of the font of each state text :param state_text_font_size: Font size of the state text. If ``None`` uses the widget font size :param state_text_position: Position of the state text respect to the switch rect :param state_values: Value of each state of the switch :param state_width: Width of each state. For example if there's 2 states, ``state_width`` only can have 1 value :param switch_border_color: Border color of the switch :param switch_border_width: Border width of the switch in px :param switch_height: Height factor respect to the title font size height :param switch_margin: Switch margin on x-axis and y-axis (x, y) respect to the title of the widget in px :param args: Optional arguments for callbacks :param kwargs: Optional keyword arguments """ _infinite: bool _single_click: bool _single_click_dir: bool _slider: Optional['pygame.Surface'] _slider_color: ColorType _slider_height: int _slider_height_factor: float _slider_pos: Tuple2IntType _slider_thickness: int _slider_vmargin: int _state: int _state_color: Tuple[ColorType, ...] _state_font: Optional['pygame.font.Font'] _state_text: Tuple[str, ...] _state_text_font: Optional[FontType] _state_text_font_color: Tuple[ColorType, ...] _state_text_font_size: Optional[int] _state_text_position: Tuple2NumberType _state_values: Tuple[Any, ...] _state_width: List[int] _switch: Optional['pygame.Surface'] _switch_border_color: ColorType _switch_border_width: int _switch_font_rendered: List['pygame.Surface'] _switch_height: int _switch_height_factor: float _switch_margin: Tuple2NumberType _switch_pos: Tuple2IntType _switch_width: int _total_states: int def __init__( self, title: Any, toggleswitch_id: str = '', default_state: int = 0, infinite: bool = False, onchange: CallbackType = None, onselect: CallbackType = None, single_click: bool = False, single_click_dir: bool = True, slider_color: ColorInputType = (255, 255, 255), slider_height_factor: NumberType = 1, slider_thickness: int = 25, slider_vmargin: NumberType = 0, state_color: Tuple[ColorInputType, ...] = ((178, 178, 178), (117, 185, 54)), state_text: Tuple[str, ...] = ('Off', 'On'), state_text_font: Optional[FontType] = None, state_text_font_color: Tuple[ColorInputType, ...] = ((255, 255, 255), (255, 255, 255)), state_text_font_size: Optional[int] = None, state_text_position: Tuple2NumberType = (0.5, 0.5), state_values: Tuple[Any, ...] = (False, True), state_width: Union[Tuple[int, ...], int] = 150, switch_border_color: ColorInputType = (40, 40, 40), switch_border_width: int = 1, switch_height: NumberType = 1.25, switch_margin: Tuple2NumberType = (25, 0), *args, **kwargs ) -> None: super(ToggleSwitch, self).__init__( args=args, kwargs=kwargs, onchange=onchange, onselect=onselect, title=title, widget_id=toggleswitch_id ) # Asserts assert isinstance(default_state, int) assert isinstance(infinite, bool) assert isinstance(single_click, bool) assert isinstance(single_click_dir, bool) assert isinstance(state_values, tuple) # Check the number of states self._total_states = len(state_values) assert 2 <= self._total_states, 'the minimum number of states is 2' assert 0 <= default_state < self._total_states, 'invalid default state value' # Check font sizes if state_text_font is not None: assert_font(state_text_font) assert isinstance(state_text_font_size, (int, type(None))) if state_text_font_size is not None: assert state_text_font_size > 0, \ 'state text font size must be equal or greater than zero' # Check colors assert_vector(state_text_position, 2) switch_border_color = assert_color(switch_border_color) assert isinstance(switch_border_width, int) and switch_border_width >= 0, \ 'border width must be equal or greater than zero' slider_color = assert_color(slider_color) # Check dimensions and sizes assert slider_height_factor > 0, 'slider height factor cannot be negative' assert slider_thickness >= 0, 'slider thickness cannot be negative' assert isinstance(slider_vmargin, NumberInstance) assert_vector(switch_margin, 2) assert isinstance(switch_height, NumberInstance) and switch_height > 0, \ 'switch height factor cannot be zero or negative' assert isinstance(state_color, tuple) and len(state_color) == self._total_states # Create state color new_state_color = [] for c in state_color: new_state_color.append(assert_color(c)) state_color = tuple(new_state_color) # Create state texts assert isinstance(state_text, tuple) and len(state_text) == self._total_states for c in state_text: assert isinstance(c, str), 'all states text must be string-type' assert isinstance(state_text_font_color, tuple) and \ len(state_text_font_color) == self._total_states new_state_text_font_color = [] for c in state_text_font_color: new_state_text_font_color.append(assert_color(c)) state_text_font_color = tuple(new_state_text_font_color) # Crete state widths self._switch_width = 0 if isinstance(state_width, NumberInstance): state_width = [state_width] assert_vector(state_width, self._total_states - 1, int) for i in range(len(state_width)): assert isinstance(state_width[i], int), 'each state width must be an integer' assert state_width[i] > 0, 'each state width must be greater than zero' self._switch_width += state_width[i] # Finals if single_click: infinite = True # Store properties self._switch_border_color = switch_border_color self._switch_border_width = switch_border_width self._infinite = infinite self._single_click = single_click self._single_click_dir = single_click_dir self._slider_color = slider_color self._slider_height_factor = slider_height_factor self._slider_thickness = slider_thickness self._slider_vmargin = slider_vmargin self._state = default_state self._state_color = state_color self._state_text = state_text self._state_text_font = state_text_font self._state_text_font_color = state_text_font_color self._state_text_font_size = state_text_font_size self._state_text_position = state_text_position self._state_values = state_values self._state_width = state_width self._switch_height_factor = float(switch_height) self._switch_margin = switch_margin # Compute state width accum self._state_width_accum = [0] accum = 0 for w in self._state_width: accum += w accum_width = accum - self._slider_thickness - 2 * self._switch_border_width self._state_width_accum.append(accum_width) # Inner properties self._slider_height = 0 self._slider_pos = (0, 0) # to add to (rect.x, rect.y) self._state_font = None self._switch_font_rendered = [] # Stores font render for each state self._switch_height = 0 self._switch_pos = (0, 0) # horizontal pos, and delta to title
[docs] def set_value(self, state: int) -> None: assert isinstance(state, int), 'state value can only be an integer' assert 0 <= state < self._total_states, 'state value exceeds the total states' self._state = state
def scale(self, *args, **kwargs) -> 'ToggleSwitch': return self def resize(self, *args, **kwargs) -> 'ToggleSwitch': return self def set_max_width(self, *args, **kwargs) -> 'ToggleSwitch': return self def set_max_height(self, *args, **kwargs) -> 'ToggleSwitch': return self def rotate(self, *args, **kwargs) -> 'ToggleSwitch': return self def flip(self, *args, **kwargs) -> 'ToggleSwitch': return self
[docs] def get_value(self) -> Any: return self._state_values[self._state]
def _apply_font(self) -> None: if self._state_text_font is None: self._state_text_font = self._font_name if self._state_text_font_size is None: self._state_text_font_size = self._font_size self._state_font = pygame_menu.font.get_font( self._state_text_font, self._state_text_font_size ) # Compute the height height = self._font_render_string('TEST').get_height() self._switch_height = int(height * self._switch_height_factor) self._slider_height = int(self._switch_height * self._slider_height_factor) \ - 2 * self._switch_border_width # Render the state texts for t in range(self._total_states): f_render = self._state_font.render( self._state_text[t], True, self._state_text_font_color[t] ) self._switch_font_rendered.append(f_render) def _draw(self, surface: 'pygame.Surface') -> None: # Draw title surface.blit(self._surface, (self._rect.x, self._rect.y + self._switch_pos[1] - 1)) # Draw switch switch_x = self._rect.x + self._switch_margin[0] + self._switch_pos[0] switch_y = self._rect.y + self._switch_margin[1] surface.blit(self._switch, (switch_x, switch_y)) # Draw switch border if self._switch_border_width > 0: switch_rect = self._switch.get_rect() switch_rect.x += switch_x switch_rect.y += switch_y pygame.draw.rect( surface, self._switch_border_color, switch_rect, self._switch_border_width ) # Draw switch font render if self._state_text[self._state] != '': text = self._switch_font_rendered[self._state] surface.blit(text, ( switch_x + (self._switch_width - text.get_width()) * self._state_text_position[0], switch_y + (self._switch_height - text.get_height()) * self._state_text_position[1] )) # Draw slider slider_x = switch_x + self._slider_pos[0] + self._switch_border_width slider_y = switch_y + self._slider_pos[1] + self._switch_border_width surface.blit(self._slider, (slider_x, slider_y)) def _render(self) -> Optional[bool]: if not self._render_hash_changed( self._selected, self._title, self._visible, self.readonly, self._state): return True # Create basic title self._surface = self._render_string(self._title, self.get_font_color_status()) self._rect.width, self._rect.height = self._surface.get_size() # Create slider self._slider = make_surface(self._slider_thickness, self._slider_height, fill_color=self._slider_color) self._slider_pos = (self._state_width_accum[self._state], self._slider_vmargin * self._switch_height) # Create the switch surface self._switch = make_surface(self._switch_width, self._switch_height, fill_color=self._state_color[self._state]) self._switch_pos = (self._rect.width, int((self._switch_height - self._rect.height) / 2)) # Update maximum rect height self._rect.height = max(self._rect.height, self._switch_height, self._slider_height) self._rect.width += self._switch_margin[0] + self._switch_width # Finals self.force_menu_surface_update() def _left(self) -> None: """ State left. :return: None """ if self.readonly: return previous = self._state if self._infinite: self._state = (self._state - 1) % self._total_states else: self._state = max(0, self._state - 1) if previous != self._state: self.change() self._sound.play_key_add() def _right(self) -> None: """ State right. :return: None """ if self.readonly: return previous = self._state if self._infinite: self._state = (self._state + 1) % self._total_states else: self._state = min(self._state + 1, self._total_states - 1) if previous != self._state: self.change() self._sound.play_key_add()
[docs] def update(self, events: EventVectorType) -> bool: self.apply_update_callbacks(events) if self.readonly or not self.is_visible(): return False updated = False for event in events: if event.type == pygame.KEYDOWN: # Check key is valid if not check_key_pressed_valid(event): continue # Check mouse over self._check_mouseover(event) # Events keydown = self._keyboard_enabled and event.type == pygame.KEYDOWN joy_hatmotion = self._joystick_enabled and event.type == pygame.JOYHATMOTION joy_axismotion = self._joystick_enabled and event.type == pygame.JOYAXISMOTION # Left button if keydown and event.key == ctrl.KEY_LEFT or \ joy_hatmotion and event.value == ctrl.JOY_LEFT or \ joy_axismotion and event.axis == ctrl.JOY_AXIS_X and event.value < ctrl.JOY_DEADZONE: self._left() updated = True # Right button elif keydown and event.key == ctrl.KEY_RIGHT or \ joy_hatmotion and event.value == ctrl.JOY_RIGHT or \ joy_axismotion and event.axis == ctrl.JOY_AXIS_X and event.value > -ctrl.JOY_DEADZONE: self._right() updated = True # Press enter elif keydown and event.key == ctrl.KEY_APPLY and self._total_states == 2 or \ event.type == pygame.JOYBUTTONDOWN and self._joystick_enabled and \ event.button == ctrl.JOY_BUTTON_SELECT and self._total_states == 2: self._sound.play_key_add() self._state = int(not self._state) self.change() updated = True self.active = not self.active # Click on switch; don't consider the mouse wheel (button 4 & 5) elif event.type == pygame.MOUSEBUTTONUP and self._mouse_enabled and \ event.button in (1, 2, 3) or \ event.type == FINGERUP and self._touchscreen_enabled and \ self._menu is not None: event_pos = get_finger_pos(self._menu, event) # If collides rect = self.get_rect(to_real_position=True, apply_padding=False) if rect.collidepoint(*event_pos): # Check if mouse collides left or right as percentage, use # only X coordinate mouse_x, _ = event_pos topleft, _ = rect.topleft topright, _ = rect.topright # Distance from title dist = mouse_x - (topleft + self._switch_margin[0] + self._switch_pos[0]) if dist > 0: # User clicked the options, not title # Toggle with only 1 click if self._single_click: if self._single_click_dir: self._left() else: self._right() updated = True else: target_index = 0 best = 1e6 # Find the closest position for i in range(self._total_states): dx = abs(self._state_width_accum[i] - dist) if dx < best: target_index = i best = dx if target_index != self._state: self._sound.play_key_add() self._state = target_index self.change() updated = True return updated