Source code for pygame_menu.widgets.widget.textinput

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

TEXT INPUT
Text input class, this widget lets user write text.
"""

from __future__ import annotations

__all__ = ["TextInput"]

import math
import os
import platform
from typing import Any

import pygame

import pygame_menu
import pygame_menu.controls as ctrl
from pygame_menu._types import (
    CallbackType,
    ColorInputType,
    ColorType,
    EventVectorType,
    NumberInstance,
    NumberType,
    Tuple2IntType,
    Tuple2NumberType,
    Tuple3IntType,
)
from pygame_menu.locals import FINGERDOWN, FINGERUP, INPUT_FLOAT, INPUT_INT, INPUT_TEXT
from pygame_menu.utils import (
    assert_color,
    assert_vector,
    check_key_pressed_valid,
    get_finger_pos,
    make_surface,
    warn,
)
from pygame_menu.widgets.core.widget import (
    Widget,
    WidgetTransformationNotImplemented,
)

try:
    # noinspection PyProtectedMember
    from pyperclip import PyperclipException, copy, paste  # type: ignore

except (ModuleNotFoundError, ImportError):
    copy, paste = lambda text: None, lambda: ""

    class PyperclipException(RuntimeError):  # type: ignore
        """
        Pyperclip exception thrown by pyperclip.
        """


def clipboard_copy(text: str) -> None:
    """
    Copy text into clipboard.

    :param text: Text to copy
    """
    copy(text)


def clipboard_paste() -> str:
    """
    Return copied text from clipboard.

    :return: Copied text
    """
    return paste()


def clipboard_warn(e: PyperclipException, op: str) -> None:
    """
    Generates a warning message if clipboard failed.

    :param e: Exception
    :param op: Operation that failed
    """
    paste_warn: str = f"{op} from clipboard failed ({e}). "
    if os.getenv("XDG_SESSION_TYPE") == "wayland":
        paste_warn += 'Please install package "wl-clipboard"'
    elif platform.system() == "Linux":
        paste_warn += 'On Linux, install "xclip" or "xsel"'
    else:
        paste_warn += (
            f"An unrecognized error happened. "
            f"Please create a new issue on {pygame_menu.__url_bug_tracker__}"
        )
    warn(paste_warn)


CTRL_KMOD = (
    pygame.KMOD_CTRL,
    pygame.KMOD_CTRL | pygame.KMOD_CAPS,
    pygame.KMOD_LCTRL,
    pygame.KMOD_LCTRL | pygame.KMOD_CAPS,
    pygame.KMOD_RCTRL,
    pygame.KMOD_RCTRL | pygame.KMOD_CAPS,
)


[docs] class TextInput(Widget): """ Text input widget. The callbacks receive the current value and all unknown keyword arguments, where ``current_text=widget.get_value``: .. code-block:: python onchange(current_text, **kwargs) onreturn(current_text, **kwargs) .. note:: TextInput text input is sensitive to the widget font, some fonts do not support some characters or languages (for example Chinese). Be careful about which font use. .. note:: TextInput only accepts vertical flip and translation transformations. :param title: Text input title :param textinput_id: ID of the text input :param copy_paste_enable: Enables copy, paste, and cut :param cursor_color: Color of cursor :param cursor_selection_color: Color of the text selection if the cursor is enabled on certain widgets :param cursor_selection_enable: Enables selection of text :param cursor_size: Set surface x,y of the cursor which determines cursor size :param cursor_switch_ms: Interval of cursor switch between off and on status. First status is ``off`` :param history: Maximum number of editions stored :param input_type: Type of the input data. See :py:mod:`pygame_menu.locals` :param input_underline: Character string drawn under the input :param input_underline_len: Total of characters to be drawn under the input. If ``0`` this number is computed automatically to fit the font :param input_underline_vmargin: Vertical margin of underline in px :param maxchar: Maximum length of input :param maxwidth: Maximum size of the text to be displayed (overflow). If ``0`` this feature is disabled :param maxwidth_dynamically_update: Dynamically update maxwidth depending on char size :param onchange: Callback when changing the text input :param onreturn: Callback when pressing return (apply) on the text input :param onselect: Function when selecting the widget :param password: Input string is displayed as a password :param password_char: Character used by password type :param repeat_keys: Enable key repeat :param repeat_keys_initial_ms: Time in milliseconds before keys are repeated when held in milliseconds :param repeat_keys_interval_ms: Interval between key press repetition when held in milliseconds :param repeat_mouse_interval_ms: Interval between mouse events when held in milliseconds :param text_ellipsis: Ellipsis text when overflow occurs (input length exceeds maxwidth) :param valid_chars: List of chars that are valid, ``None`` if all chars are valid :param kwargs: Optional keyword arguments """ _absolute_origin: Tuple2IntType _block_copy_paste: bool _clock: pygame.time.Clock _copy_paste_enabled: bool _current_underline_string: str # Testing _cursor_color: ColorType _cursor_ms_counter: NumberType # Stores the ms between cursor switch _cursor_offset: NumberType _cursor_position: int _cursor_render: bool _cursor_size: Tuple2IntType | None # Size defined by user _cursor_surface: pygame.Surface | None _cursor_surface_pos: list[int] _cursor_switch_ms: NumberType _cursor_visible: bool _ellipsis: str _ellipsis_size: NumberType _history: list[str] _history_cursor: list[int] _history_index: int _history_renderbox: list[list[int]] _ignore_keys: tuple[int, ...] _input_string: str _input_type: str _input_underline: str _input_underline_len: int _input_underline_size: NumberType _input_underline_vmargin: int _key_is_pressed: bool _keychar_size: dict[str, NumberType] _keyrepeat: bool _keyrepeat_counters: dict[int, list[int]] _keyrepeat_initial_interval_ms: NumberType _keyrepeat_interval_ms: NumberType _keyrepeat_mouse_interval_ms: NumberType _keyrepeat_mouse_ms: NumberType _last_char: str _last_container_width: int _last_key: int _last_selection_render: list[int] _maxchar: int _maxwidth: int _maxwidth_base: int _maxwidth_update: bool _maxwidthsize: NumberType _mouse_is_pressed: bool _password: bool _password_char: str _renderbox: list[int] _selection_active: bool _selection_box: list[int] _selection_color: ColorType _selection_enabled: bool _selection_mouse_first_position: int _selection_position: list[int] _selection_surface: pygame.Surface | None _title_size: NumberType _valid_chars: list[str] | None def __init__( self, title: Any, textinput_id: str = "", apply_widget_update_callback: bool = True, alt_x_enabled: bool = True, copy_paste_enable: bool = True, cursor_color: ColorInputType = (0, 0, 0), cursor_selection_color: ColorInputType = (30, 30, 30, 100), cursor_selection_enable: bool = True, cursor_size: Tuple2IntType | None = None, cursor_switch_ms: NumberType = 500, history: int = 50, input_type: str = INPUT_TEXT, input_underline: str = "", input_underline_len: int = 0, input_underline_vmargin: int = 0, maxchar: int = 0, maxwidth: int = 0, maxwidth_dynamically_update: bool = True, onchange: CallbackType = None, onreturn: CallbackType = None, onselect: CallbackType = None, password: bool = False, password_char: str = "*", repeat_keys: bool = True, repeat_keys_initial_ms: NumberType = 400, repeat_keys_interval_ms: NumberType = 50, repeat_mouse_interval_ms: NumberType = 400, text_ellipsis: str = "...", valid_chars: list[str] | None = None, *args, **kwargs, ) -> None: assert isinstance(copy_paste_enable, bool) assert isinstance(cursor_selection_enable, bool) assert isinstance(cursor_size, (type(None), tuple)) assert isinstance(cursor_switch_ms, NumberInstance) assert isinstance(history, int) assert isinstance(input_type, str) assert isinstance(input_underline, str) assert isinstance(input_underline_len, int) assert isinstance(input_underline_vmargin, int) assert isinstance(maxchar, int) assert isinstance(maxwidth, int) assert isinstance(password, bool) assert isinstance(password_char, str) assert isinstance(repeat_keys, bool) assert isinstance(repeat_keys_initial_ms, NumberInstance) assert isinstance(repeat_keys_interval_ms, NumberInstance) assert isinstance(repeat_mouse_interval_ms, NumberInstance) assert isinstance(text_ellipsis, str) assert isinstance(textinput_id, str) assert isinstance(valid_chars, (type(None), list)) assert history >= 0, "history must be equal or greater than zero" assert maxchar >= 0, "maxchar must be equal or greater than zero" assert maxwidth >= 0, "maxwidth must be equal or greater than zero" assert len(password_char) == 1, "password char must be a character" assert input_underline_len >= 0, ( "input underline length must be equal or greater than zero" ) assert cursor_switch_ms > 0, ( "cursor switch in milliseconds must be greater than zero" ) assert repeat_keys_initial_ms > 0, ( "repeat keys initial ms cannot be lower or equal than zero" ) assert repeat_keys_interval_ms > 0, ( "repeat keys interval ms cannot be lower or equal than zero" ) assert repeat_mouse_interval_ms > 0, ( "repeat mouse interval ms cannot be lower or equal than zero" ) cursor_color = assert_color(cursor_color) cursor_selection_color = assert_color(cursor_selection_color) if pygame.vernum[0] >= 2: # pygame 1.9.3 don't have vernum.major assert len(cursor_selection_color) == 4, ( "cursor selection color alpha must be defined" ) assert cursor_selection_color[3] != 255, ( "cursor selection color alpha cannot be opaque" ) if cursor_size is not None: assert_vector(cursor_size, 2, int) assert cursor_size[0] > 0, "cursor size width must be greater than zero" assert cursor_size[1] > 0, "cursor size height must be greater than zero" super().__init__( args=args, kwargs=kwargs, onchange=onchange, onreturn=onreturn, onselect=onselect, title=title, widget_id=textinput_id, accept_events=True, ) self._input_string = "" self._ignore_keys = ( # Ignore keys on keyrepeat event ctrl.KEY_MOVE_DOWN, ctrl.KEY_MOVE_UP, ctrl.KEY_TAB, pygame.K_CAPSLOCK, pygame.K_END, pygame.K_ESCAPE, pygame.K_HOME, pygame.K_LCTRL, pygame.K_LSHIFT, pygame.K_NUMLOCK, pygame.K_RCTRL, pygame.K_RETURN, pygame.K_RSHIFT, ) # Vars to make keydown repeat after user pressed a key for some time: self._block_copy_paste = False # Blocks event self._key_is_pressed = False self._keyrepeat = repeat_keys self._keyrepeat_counters = {} # {event.key: (counter_int, event.unicode)} (look for "***") self._keyrepeat_initial_interval_ms = repeat_keys_initial_ms self._keyrepeat_interval_ms = repeat_keys_interval_ms self._last_key = 0 # Mouse handling self._keyrepeat_mouse_ms = 0 self._keyrepeat_mouse_interval_ms = repeat_mouse_interval_ms self._mouse_is_pressed = False # Render box (overflow) self._ellipsis = text_ellipsis self._ellipsis_size = 0 self._renderbox = [0, 0, 0] # Left/Right/Inner, int # Cursor self._clock = pygame.time.Clock() self._cursor_color = cursor_color self._cursor_ms_counter = 0 self._cursor_offset = -1.0 self._cursor_position = 0 # Inside text self._cursor_render = True # If True cursor must be rendered self._cursor_surface = None self._cursor_surface_pos = [0, 0] # Position (x,y) of surface self._cursor_size = cursor_size self._cursor_switch_ms = cursor_switch_ms self._cursor_visible = False # Switches every self._cursor_switch_ms ms # History of editions self._history = [] self._history_cursor = [] self._history_renderbox = [] self._history_index = 0 # Index at which the new editions are added self._max_history = history # Text selection self._last_selection_render = [0, 0] # Position, int self._selection_active = False self._selection_box = [0, 0] # [from, to], int self._selection_color = cursor_selection_color self._selection_enabled = cursor_selection_enable self._selection_mouse_first_position = -1 # Touch emulates a mouse self._selection_position = [0, 0] # x,y (float) self._selection_surface = None # List of valid chars if valid_chars is not None: for ch in range(len(valid_chars)): _char = str(valid_chars[ch]) valid_chars[ch] = _char assert isinstance(_char, str), ( f'element "{_char}" of valid_chars must be a string' ) assert len(_char) == 1, ( f'element "{_char}" of valid_chars must be character' ) assert len(valid_chars) > 0, ( "valid_chars list must contain at least 1 element" ) self._valid_chars = valid_chars # Callbacks self._apply_widget_update_callback = apply_widget_update_callback # Other self._alt_x_enabled = alt_x_enabled self._copy_paste_enabled = copy_paste_enable self._current_underline_string = "" self._input_type = input_type self._input_underline = input_underline self._input_underline_len = input_underline_len self._input_underline_size = 0 self._input_underline_vmargin = input_underline_vmargin self._keychar_size = {"": 0} self._last_char = "" self._last_container_width = 0 self._maxchar = maxchar self._maxwidth = maxwidth # This value will be changed depending on how many chars are printed self._maxwidth_base = maxwidth self._maxwidth_update = maxwidth_dynamically_update self._maxwidthsize = 0 # Updated in _apply_font() self._password = password self._password_char = password_char self._title_size = 0 def _apply_font(self) -> None: self._ellipsis_size = self._font.size(self._ellipsis)[0] self._title_size = self._font.size(self._title)[0] # Generate the underline surface self._input_underline_size = self._font.size(self._input_underline * 3)[0] / 3 # Size of maxwidth if not zero max_char = "O" if self._password: max_char = self._password_char max_char_size = self._font_render_string( max_char * self._maxwidth_base ).get_size() self._maxwidthsize = max_char_size[0] # Update password char size if self._password: password_size = self._font_render_string(self._password_char).get_size()[0] if password_size == 0: raise ValueError( "password character is not valid, the size of the font is zero, " "use another character or change the font" ) self._keychar_size[self._password_char] = password_size
[docs] def clear(self) -> None: """ Clear the current text. """ self._input_string = "" self._cursor_position = 0 self._renderbox = [0, 0, 0] self._delete() self.change()
[docs] def get_value(self, as_string: bool = False) -> str | float | int | Tuple3IntType: """ Return the value of the text. :param as_string: If True, return the raw input string :return: Text inside the widget, or converted number """ if as_string: return self._input_string if self._input_type == INPUT_TEXT: return self._input_string if self._input_type == INPUT_FLOAT: try: return float(self._input_string) except ValueError: return 0 if self._input_type == INPUT_INT: try: return int(float(self._input_string)) except ValueError: return 0 return self._input_string
def scale(self, *args: Any, **kwargs: Any) -> TextInput: raise WidgetTransformationNotImplemented() def resize(self, *args: Any, **kwargs: Any) -> TextInput: raise WidgetTransformationNotImplemented() def set_max_width(self, *args: Any, **kwargs: Any) -> TextInput: raise WidgetTransformationNotImplemented() def set_max_height(self, *args: Any, **kwargs: Any) -> TextInput: raise WidgetTransformationNotImplemented() def rotate(self, *args: Any, **kwargs: Any) -> TextInput: raise WidgetTransformationNotImplemented()
[docs] def flip( self, x: bool, y: bool, render: bool = True ) -> TextInput: # Actually flip on x-axis is disabled super().flip(False, y, render) return self
def _draw(self, surface: pygame.Surface) -> None: # Draw selection surface if pygame.vernum[0] >= 2: # pygame 1.9.3 don't have vernum.major surface.blit(self._surface, (self._rect.x, self._rect.y)) # Draw string if self._selection_surface is not None: # Draw selection surface.blit( self._selection_surface, (self._selection_position[0], self._selection_position[1]), ) else: if self._selection_surface is not None: # Draw selection surface.blit( self._selection_surface, (self._selection_position[0], self._selection_position[1]), ) surface.blit(self._surface, (self._rect.x, self._rect.y)) # Draw string # Draw cursor if ( self._selected and self._cursor_surface and (self._cursor_visible or self._key_is_pressed) and not self.readonly ): x = self._rect.x + self._cursor_surface_pos[0] if self._flip[0]: # Flip on x-axis (bug) x = self._surface.get_width() - x y = self._rect.y + self._cursor_surface_pos[1] surface.blit(self._cursor_surface, (x, y)) def _render(self) -> bool | None: string = self._title + self._get_input_string() # Render string max_cont_width = self._get_max_container_width() if max_cont_width != 0: self._last_container_width = max_cont_width if not self._render_hash_changed( string, self._selected, self._cursor_render, self._cursor_position, self._selection_enabled, self.active, self._visible, self.readonly, self._last_container_width, self._selection_box[0], self._menu, self._selection_box[1], self._last_selection_render[0], self._padding, self._last_selection_render[1], self._renderbox[0], self._renderbox[1], self._renderbox[2], self._cursor_visible, self._title_size, self._selection_effect.get_width(), ): return True # Apply underline if exists self._surface = self._render_string_underline( string, self.get_font_color_status() ) self._apply_transforms() # Render the cursor self._render_cursor() # Render the selection box if text is selected self._render_selection_box() # Update last rendered self._last_rendered_string = string # Update the size of the render self._rect.width, self._rect.height = self._surface.get_size() # Force Menu update self.force_menu_surface_update() return None def _render_selection_box(self, force: bool = False) -> None: """ Render selected text. :param force: Force update """ if not self._selection_enabled: return elif ( force or self._selection_active and ( self._last_selection_render[0] != self._selection_box[0] or self._last_selection_render[1] != self._selection_box[1] ) ): # If there's no limit pos = [0, 0] if self._maxwidth == 0: pos[0] = self._selection_box[0] pos[1] = self._selection_box[1] else: pos[0] = max(self._selection_box[0], self._renderbox[0]) pos[1] = min(self._selection_box[1], self._renderbox[1]) # Find coordinates of each position string = self._get_input_string_filtered() string_init = string[self._renderbox[0] : pos[0]] string_final = string[self._renderbox[0] : pos[1]] x1 = self._cursor_offset + self._font.size(self._title + string_init)[0] x2 = ( self._cursor_offset + self._font.size(self._title + string_final)[0] + 1 ) self._last_selection_render[0] = self._selection_box[0] self._last_selection_render[1] = self._selection_box[1] x = x2 - x1 if x <= 1: self._selection_surface = None return y = self._font.size(self._title)[1] # Add ellipsis delta = self._ellipsis_size if self._ellipsis_left_and_right(): # If Left+Right ellipsis delta *= 1 elif self._ellipsis_right(): # Right ellipsis delta *= 0 elif self._ellipsis_left(): # Left ellipsis delta *= 1 else: delta *= 0 x1 += delta x2 += delta # Apply scale factor (experimental) x *= self._scale_factor[0] y *= self._scale_factor[1] x1 *= self._scale_factor[0] # Create surface and fill self._selection_surface = make_surface( x, y, fill_color=self._selection_color ) self._selection_position[0] = x1 + self._rect.x self._selection_position[1] = self._rect.y # Fill cursor if self._cursor_surface: self._cursor_surface.fill(self._cursor_color) def _get_max_container_width(self) -> int: """ Return the maximum textarea container width. It can be the column width, menu width or frame width if horizontal. :return: Container width """ menu = self._menu frame = self.get_frame() if menu is None: return 0 try: # noinspection PyProtectedMember max_width = menu._column_widths[self.get_col_row_index()[0]] except IndexError: max_width = menu.get_width(inner=True) # Textarea within frame if frame is not None: if frame.horizontal: raise RuntimeError( "horizontal frame cannot contain variable width sizing textinput " "(requested by input underline). Set input_underline_len variable " "to avoid this Exception" ) max_width = frame.get_width() return max_width - self._padding[1] - self._padding[3] def _render_string_underline( self, string: str, color: ColorInputType ) -> pygame.Surface: """ Render underline string surface. :param string: String to render :param color: Color of the string to render :return: New surface """ color = assert_color(color) # Create surface with no underline (just text) surface = self._render_string(string, color) # If underline is not enabled if self._input_underline_size == 0: return surface current_rect = surface.get_rect() # Compute initial char guess if ( self._input_underline_len != 0 ): # User defined the amount of underline chars to use char = self._input_underline_len else: # Compute available width and propose the maximum chars needed (fill all width) # Calculate total available space # |---------------------------------------------------| # |MENU | # | <---- space-between-title ---> | # | .===TITLE====|=THE INPUT OF USER===========. | # | self._rect.x title posx2 | # | | # |---------------------------------------------------| posx2 = max( self._get_max_container_width() - self._input_underline_size * 1.75, current_rect.width, ) delta_ch = posx2 - self._title_size - self._selection_effect.get_width() char = math.ceil(delta_ch / self._input_underline_size) for i in range(10): # Find the best guess for fw = self._font_render_string( self._input_underline * int(char), color ).get_width() char += 1 if fw >= delta_ch: break # If char limit if self._maxchar != 0 or self._maxwidth_base != 0: max_chars = max(self._maxchar, self._maxwidth_base) base_char = "O" if self._password: base_char = self._password_char max_size = self._font_render_string(base_char * max_chars) max_size = max_size.get_size()[0] maxchar_char = math.ceil( (max_size + self._ellipsis_size) / self._input_underline_size ) char = min(char, maxchar_char) underline_string = self._input_underline * max(int(char), 0) # Render char self._current_underline_string = underline_string underline = self._font_render_string( underline_string, color, use_background_color=False ) # Create a new surface new_width = max(self._title_size + underline.get_size()[0], current_rect.width) new_surface = make_surface( new_width, current_rect.height + 3 + self._input_underline_vmargin, alpha=True, ) # Compute underline vmargin by its height uvm = 5 # underline vertical margin if underline.get_height() <= 15: uvm = 4 if underline.get_height() <= 7: uvm = 3 # Blit current surface new_surface.blit(surface, (0, 0)) new_surface.blit( underline, (self._title_size, uvm + self._input_underline_vmargin) ) # Return new surface return new_surface def _render_cursor(self) -> None: """ Cursor is rendered and stored. """ # Cursor should not be rendered if not self._cursor_render: return # Cursor surface does not exist elif self._cursor_surface is None: if ( self._rect.height == 0 ): # If Menu has not been initialized this error can occur return elif self._cursor_size is not None: self._cursor_surface = make_surface(*self._cursor_size) else: self._cursor_surface = make_surface( self._font_size / 20 + 1, self._rect.height - 2 ) self._cursor_surface.fill(self._cursor_color) # Get string string = self._get_input_string_filtered() # Calculate x position if self._maxwidth == 0: # If no limit is provided cursor_x_pos = ( self._cursor_offset + self._font.size(self._title + string[: self._cursor_position])[0] ) else: # Calculate position depending on renderbox string = string[ self._renderbox[0] : (self._renderbox[0] + self._renderbox[2]) ] cursor_x_pos = ( self._cursor_offset + self._font.size(self._title + string)[0] ) # Add ellipsis delta = self._ellipsis_size if self._ellipsis_left_and_right(): # If Left+Right ellipsis delta *= 1 elif self._ellipsis_right(): # Right ellipsis delta *= 0 elif self._ellipsis_left(): # Left ellipsis delta *= 1 else: delta *= 0 cursor_x_pos += delta if self._cursor_position > 0 or (self._title and self._cursor_position == 0): # Without this, the cursor is invisible when self._cursor_position > 0: cursor_x_pos -= self._cursor_surface.get_width() # Calculate y position cursor_y_pos = 0 # Move x position cursor_x_pos += 2 # Store position, apply scale factor (experimental) self._cursor_surface_pos[0] = int(cursor_x_pos) * self._scale_factor[0] self._cursor_surface_pos[1] = int(cursor_y_pos) * self._scale_factor[1] self._cursor_render = False def _ellipsis_left(self) -> bool: """ Return ``True`` if left ellipsis is active. :return: Boolean """ return self._renderbox[0] != 0 and self._maxwidth != 0 def _ellipsis_right(self) -> bool: """ Return ``True`` if right ellipsis is active. :return: Boolean """ return self._renderbox[1] != len(self._input_string) and self._maxwidth != 0 def _ellipsis_left_and_right(self) -> bool: """ Return ``True`` if left and right ellipsis are active. :return: Boolean """ return self._ellipsis_left() and self._ellipsis_right() def _get_input_string_filtered(self) -> str: """ Return the input string where all filters have been applied. :return: Filtered string """ string = self._input_string # Apply password if self._password: string = self._password_char * len(string) return string def _get_input_string(self, add_ellipsis: bool = True) -> str: """ Return the input string, apply overflow if enabled. :param add_ellipsis: Adds ellipsis text :return: String """ string: str = self._get_input_string_filtered() if self._maxwidth != 0 and len(string) > self._maxwidth: text = string[self._renderbox[0] : self._renderbox[1]] if add_ellipsis: if self._ellipsis_right(): text += self._ellipsis if self._ellipsis_left(): text = self._ellipsis + text return text return string def _update_renderbox( self, left: int = 0, right: int = 0, addition: bool = False, end: bool = False, start: bool = False, update_maxwidth: bool = True, ) -> None: """ Update renderbox position. :param left: Left update :param right: Right update :param addition: Update if text addition/deletion :param end: Move cursor to end :param start: Move cursor to start :param update_maxwidth: Update maxwidth limit depending on the chars written """ self._cursor_render = True if self._maxwidth == 0: return len_string = len(self._input_string) prev_renderbox = self._renderbox.copy() # Move cursor to end if end: self._renderbox[0] = max(0, len_string - self._maxwidth) self._renderbox[1] = len_string self._renderbox[2] = min(len_string, self._maxwidth) return # Move cursor to start elif start: self._renderbox[0] = 0 self._renderbox[1] = min(len_string, self._maxwidth) self._renderbox[2] = 0 return # Check limits elif left < 0 and len_string == 0: return # If no overflow elif len_string <= self._maxwidth: if ( right < 0 and self._renderbox[2] == len_string ): # If del at the end of string return elif left < 0 and self._renderbox[2] == 0: # If cursor is at beginning return self._renderbox[0] = 0 # To catch unexpected errors if addition: # left/right are ignored if left < 0: self._renderbox[1] += left self._renderbox[1] += right if right < 0: self._renderbox[2] -= right # If text is typed increase inner position if self._renderbox[0] == prev_renderbox[0]: self._renderbox[2] += left self._renderbox[2] += right else: if addition: # If text is added # If press del at the end of string if right < 0 and self._renderbox[2] == self._maxwidth: return # If backspace at beginning of string elif left < 0 and self._renderbox[2] == 0: return # If user deletes something and it is in the end elif right < 0: # del if self._ellipsis_left(): if (self._renderbox[1] - 1) == len_string: # At the end self._renderbox[2] -= right # If the user writes, move renderbox elif right > 0: # If cursor is at the end push box if self._renderbox[2] == self._maxwidth: self._renderbox[0] += right self._renderbox[1] += right self._renderbox[2] += right if left < 0: # If cursor is at the beginning if self._renderbox[0] == 0: self._renderbox[2] += left self._renderbox[0] += left self._renderbox[1] += left if not addition: # Move inner (left/right) self._renderbox[2] += right self._renderbox[2] += left # If user pushes after limit the renderbox moves if self._renderbox[2] < 0: self._renderbox[0] += left self._renderbox[1] += left elif self._renderbox[2] > self._maxwidth: self._renderbox[0] += right self._renderbox[1] += right else: update_maxwidth = False # If cursor is at limit if self._renderbox[1] > len_string or self._renderbox[0] < 0: if self._renderbox[2] != self._maxwidth - 1: update_maxwidth = False # Apply string limits self._renderbox[1] = max( self._maxwidth, min(self._renderbox[1], len_string) ) self._renderbox[0] = self._renderbox[1] - self._maxwidth # Apply limits self._renderbox[0] = max(0, self._renderbox[0]) self._renderbox[1] = min(max(0, self._renderbox[1]), len_string) self._renderbox[2] = max( 0, min(self._renderbox[2], min(self._maxwidth, len_string)) ) if update_maxwidth: self._update_maxlimit_renderbox() def _get_char_size(self, char) -> int: """ Return the widget char size in pixels. :param char: Char :return: Char size in px """ if char in self._keychar_size.keys(): return self._keychar_size[char] self._keychar_size[char] = self._font_render_string(char).get_size()[0] return self._keychar_size[char] def _update_maxlimit_renderbox(self) -> None: """ Update renderbox based on how many characters have been written on input. """ if not self._maxwidth_update: return sign = 0 # Sign of search while True: curr_string = self._get_input_string(False) # Already filtered lcs = len(curr_string) if lcs > 0: accum_size = 0 if self._ellipsis_left(): accum_size += self._ellipsis_size + 5 biggest = 0 for char in curr_string: char_size = self._get_char_size(char) accum_size += char_size biggest = max(biggest, char_size) if self._ellipsis_right(): accum_size += self._ellipsis_size if accum_size < self._maxwidthsize - biggest: # Increase if sign < 0: break sign = 1 if self._renderbox[0] != 0: self._renderbox[0] -= 1 else: break self._maxwidth += 1 self._renderbox[2] += 1 elif accum_size > self._maxwidthsize: if sign > 0: break sign = -1 if self._renderbox[2] == 0: self._renderbox[1] -= 1 else: self._renderbox[0] += 1 # self._renderbox[1] += 1 self._renderbox[2] -= 1 self._maxwidth -= 1 else: break else: self._maxwidth = self._maxwidth_base # Return to normal break def _update_cursor_mouse(self, mouse_x: int) -> None: """ Updates cursor position after mouse click or touch action in text. :param mouse_x: Mouse distance relative to surface """ string = self._get_input_string() if string == "": # If string is empty cursor is not updated return self.force_menu_surface_cache_update() # Find the accumulated char size that gives the position of cursor cursor_pos = 0 for i in range(len(string)): curr_char = string[i] if i < len(string) - 1 else 0 curr_char_size = 0 if curr_char == 0 else self._font.size(curr_char)[0] if ( self._font.size(self._title + string[0:i])[0] + curr_char_size / 2 < mouse_x ): cursor_pos += 1 else: break # If text have ellipsis if self._maxwidth != 0 and len(self._input_string) > self._maxwidth: if self._ellipsis_left(): cursor_pos -= len(self._ellipsis) # Check if user clicked on ellipsis if cursor_pos < 0 or cursor_pos > self._maxwidth: if cursor_pos < 0: self._renderbox[2] = 0 self._move_cursor_left() if cursor_pos > self._maxwidth: self._renderbox[2] = self._maxwidth self._move_cursor_right() return # User clicked on text, update cursor cursor_pos = max(0, min(self._maxwidth, cursor_pos)) self._cursor_position = self._renderbox[0] + cursor_pos self._renderbox[2] = cursor_pos self._update_maxlimit_renderbox() # Text does not have ellipsis, inferred position is correct else: self._cursor_position = cursor_pos if self._maxwidth != 0: # Update renderbox self._cursor_position += self._renderbox[0] self._renderbox[2] = cursor_pos self._update_maxlimit_renderbox() if self._selection_mouse_first_position == -1: if self._selection_active: # Unselect and select again self._unselect_text() self._selection_mouse_first_position = self._cursor_position else: a = self._selection_mouse_first_position b = self._cursor_position self._selection_box[0] = min(a, b) self._selection_box[1] = max(a, b) self._render_selection_box(True) self._cursor_render = True def _check_mouse_collide_input(self, pos: Tuple2NumberType) -> bool: """ Check mouse collision. .. note:: If this method returns ``True`` the cursor must be updated. :param pos: Position :return: Cursor update status """ rect = self.get_rect(to_real_position=True, apply_padding=False) if rect.collidepoint(*pos): # Check if mouse collides left or right as percentage, use only X coordinate mouse_x, _ = pos topleft, _ = rect.topleft self._update_cursor_mouse(mouse_x - topleft) return True # Prevents double click return False def _check_touch_collide_input(self, pos: Tuple2NumberType) -> bool: """ Check touchscreen collision. .. note:: If this method returns ``True`` the cursor must be updated. :param pos: Position :return: Cursor update status """ rect = self.get_rect(to_real_position=True, apply_padding=False) if rect.collidepoint(*pos): # Check if touchscreen collides left or right as percentage, use only X coordinate touch_x, _ = pos topleft, _ = rect.topleft self._update_cursor_mouse(touch_x - topleft) return True # Prevents double click return False
[docs] def set_value(self, text: Any) -> None: """ Set the value of the text. :param text: New text of the widget """ if self._password and text != "": raise ValueError("value cannot be set in password type") elif self._check_input_type(text): default_text = str(text) # Filter valid chars if self._valid_chars is not None: default_valid = "" for ch in default_text: if ch in self._valid_chars: default_valid += ch default_text = default_valid # Apply maxchar len_text = len(default_text) if 0 < self._maxchar < len_text: default_text = default_text[len_text - self._maxchar : len_text] self._input_string = default_text for i in range(len(default_text) + 1): self._move_cursor_right() self._update_renderbox(right=1, addition=True) self._update_input_string(default_text) else: raise ValueError( f'value "{text}" type is not correct according to input_type' ) self._update_renderbox() # Updates cursor self._render() # Renders the selection box
def _check_input_size(self) -> bool: """ Check input size. :return: ``True`` if the input must be limited """ if self._maxchar == 0: return False return self._maxchar <= len(self._input_string) def _check_input_type(self, string: Any) -> bool: """ Check if input type is valid. :param string: String to validate :return: ``True`` if the input type is valid """ if string == "": # Empty is valid return True elif self._input_type == INPUT_TEXT: return True conv = None if self._input_type == INPUT_FLOAT: conv = float elif self._input_type == INPUT_INT: conv = int if string == "-": return True elif conv is None: return False try: conv(string) return True except ValueError: pass return False def _move_cursor_left(self) -> None: """ Move cursor to left position. """ # Subtract one from cursor_pos, but do not go below zero: self._cursor_position = max(self._cursor_position - 1, 0) self._update_renderbox(left=-1) def _move_cursor_right(self) -> None: """ Move cursor to right position. """ # Add one to cursor_pos, but do not exceed len(input_string) self._cursor_position = min(self._cursor_position + 1, len(self._input_string)) self._update_renderbox(right=1) def _blur(self) -> None: # self._key_is_pressed = False self._mouse_is_pressed = False self._keyrepeat_mouse_ms = 0 self._cursor_ms_counter = 0 self._cursor_visible = False self._unselect_text() # self._history_index = len(self._history) - 1 def _focus(self) -> None: self._cursor_ms_counter = 0 self._cursor_visible = True self._cursor_render = True def _unselect_text(self) -> bool: """ Unselect text. :return: ``True`` if the selected text was removed in the method call """ removed = self._selection_surface is not None if not removed: return False self._selection_box[0] = 0 self._selection_box[1] = 0 self._selection_surface = None if self._cursor_surface is not None: self._cursor_surface.fill(self._cursor_color) self._selection_active = False self.force_menu_surface_cache_update() return True def _get_selected_text(self) -> str: """ Return the selected text. :return: Text """ return self._input_string[self._selection_box[0] : self._selection_box[1]] def _update_input_string( self, new_string: str, update_history: bool = True ) -> None: """ Update input string with a new string, store changes into history. :param new_string: New string of text input :param update_history: Updates history """ assert isinstance(new_string, str) assert isinstance(update_history, bool) l_history = len(self._history) # If last edition is different from the new one -> updates the history if ( update_history and self._max_history > 0 and ( (l_history > 0 and self._history[l_history - 1] != new_string) or l_history == 0 ) ): # If index is not at last add the current status as new if self._history_index != l_history: last_string = self._history[self._history_index] self._history_index = len(self._history) self._update_input_string(last_string) # Add new status to history self._history.insert(self._history_index, new_string) self._history_cursor.insert(self._history_index, self._cursor_position) self._history_renderbox.insert( self._history_index, [self._renderbox[0], self._renderbox[1], self._renderbox[2]], ) if len(self._history) > self._max_history: self._history.pop(0) self._history_cursor.pop(0) self._history_renderbox.pop(0) self._history_index = len( self._history ) # This can be changed with undo/redo # Updates string self._input_string = new_string def _copy(self) -> bool: """ Copy text from clipboard. :return: ``True`` if copied """ if not self._copy_paste_enabled: # Not enabled return False elif self._block_copy_paste: # Prevents multiple executions of event return False elif self._password: # Password cannot be copied return False try: if self._selection_surface: # If text is selected clipboard_copy(self._get_selected_text()) else: clipboard_copy(self._input_string) # Copy all text except PyperclipException as e: if self._verbose: clipboard_warn(e, "Copying") return False self._block_copy_paste = True return True def _cut(self) -> bool: """ Cut operation. :return: ``True`` if cut """ if not self._copy_paste_enabled: # Not enabled return False self._copy() # This is a safe operation, all checks have been passed # If text is selected if self._selection_surface: self._remove_selection() return True return False def _paste(self) -> bool: """ Paste text from clipboard. :return: ``True`` if pasted """ if not self._copy_paste_enabled: # Not enabled return False elif self._block_copy_paste: # Prevents multiple executions of event return False elif self._selection_surface: # If text is selected self._remove_selection() # Paste text in cursor try: text: str = clipboard_paste() except PyperclipException as e: if self._verbose: clipboard_warn(e, "Pasting") return False text = text.strip() for i in ["\n", "\r"]: text = text.replace(i, "") # Delete escape chars escapes: str = "".join([chr(char) for char in range(1, 32)]) text = text.translate(escapes) if text == "": return False # Remove invalid chars elif self._valid_chars is not None: valid_text = "" for ch in text: if ch in self._valid_chars: valid_text += ch text = valid_text if text == "": return False # Cut string (if limit does exist) text_end: int = len(text) if self._maxchar != 0: char_limit = self._maxchar - len(self._input_string) text_end = min(char_limit, text_end) if text_end <= 0: # If there's no more space, returns self._sound.play_event_error() return False new_string: str = ( self._input_string[0 : self._cursor_position] + text[0:text_end] + self._input_string[self._cursor_position : len(self._input_string)] ) # If string is valid if self._check_input_type(new_string): # Update char size for char in new_string: if char not in self._keychar_size: # This updates the self._keychar_size variable self._get_char_size(char) self._sound.play_key_add() self._input_string = new_string # For a purpose of computing render_box for i in range(len(text)): # Move cursor self._move_cursor_right() self._update_input_string(new_string) self.change() self._update_maxlimit_renderbox() self._block_copy_paste = True else: self._sound.play_event_error() return False return True def _update_from_history(self) -> None: """ Update all from history. """ self._input_string = self._history[self._history_index] self._renderbox[0] = self._history_renderbox[self._history_index][0] self._renderbox[1] = self._history_renderbox[self._history_index][1] self._renderbox[2] = self._history_renderbox[self._history_index][2] self._cursor_position = self._history_cursor[self._history_index] self._cursor_render = True def _undo(self) -> bool: """ Undo operation. :return: ``True`` if undo """ if self._history_index == 0: # There's no back history return False elif self._history_index == len(self._history): # If the actual is the last self._history_index -= 1 self._history_index = max(0, self._history_index - 1) self._update_from_history() return True def _redo(self) -> bool: """ Redo operation. :return: ``True`` if redo """ if self._history_index == len(self._history) - 1: # There's no forward history return False self._history_index = min(len(self._history) - 1, self._history_index + 1) self._update_from_history() return True def _remove_selection(self) -> None: """ Remove text from selection. """ removed = self._selection_box[1] - self._selection_box[0] left: bool = False if self._selection_box[0] == self._cursor_position: left = True for i in range(removed): if left: self._delete(update_history=i == removed - 1) else: self._backspace(update_history=i == removed - 1) # Destroy selection self._unselect_text() def _backspace(self, update_history=True) -> None: """ Backspace event. :param update_history: Updates history on deletion """ new_string = ( self._input_string[: max(self._cursor_position - 1, 0)] + self._input_string[self._cursor_position :] ) self._update_input_string(new_string, update_history=update_history) self._update_renderbox(left=-1, addition=True) # Subtract one from cursor_pos, but do not go below zero: self._cursor_position = max(self._cursor_position - 1, 0) def _delete(self, update_history: bool = True) -> None: """ Delete event. :param update_history: Updates history on deletion """ new_string = ( self._input_string[: self._cursor_position] + self._input_string[self._cursor_position + 1 :] ) self._update_input_string(new_string, update_history=update_history) self._update_renderbox(right=-1, addition=True) def _select_all(self) -> None: """ Select all text. """ if not self._selection_enabled: return self._selection_box[0] = 0 self._selection_box[1] = len(self._input_string) self._cursor_position = self._selection_box[1] for i in range(len(self._input_string)): self._move_cursor_right() self._render_selection_box(True) self._selection_active = False def _push_key_input(self, keychar: str, sounds: bool = True) -> bool: """ Insert a key in the cursor position. :param keychar: Char to be inserted :param sounds: Use widget sounds :return: If ``False`` the event loop breaks """ # If selected text if self._selection_surface: self._remove_selection() # Check input exceeded the limit returns if self._check_input_size(): if sounds: self._sound.play_event_error() return False # If no special key is pressed, add Unicode of key to input_string new_string = ( self._input_string[: self._cursor_position] + keychar + self._input_string[self._cursor_position :] ) # If unwanted escape sequences event_escaped = repr(keychar) if "\\r" in event_escaped: return False # Check if char is valid elif self._valid_chars is not None and keychar not in self._valid_chars: if sounds: self._sound.play_event_error() return False # If data is valid elif self._check_input_type(new_string): l_key = len(keychar) if l_key > 0: # Update char size if keychar not in self._keychar_size.keys(): self._get_char_size(keychar) # This updates keychar size data self._last_char = keychar # Update string if sounds: self._sound.play_key_add() self._cursor_position += 1 # Some are empty, e.g. K_UP self._input_string = ( new_string # Only here this is changed (due to renderbox update) ) self._update_input_string( new_string ) # Update the string and the history self._update_renderbox(right=1, addition=True) self.change() return True else: if sounds: self._sound.play_event_error() return False
[docs] def update(self, events: EventVectorType) -> bool: if self._apply_widget_update_callback: self.apply_update_callbacks(events) self._clock.tick(60) # Check mouse pressed mouse_left, mouse_middle, mouse_right = pygame.mouse.get_pressed() self._mouse_is_pressed = ( mouse_left or mouse_right or mouse_middle ) and self._mouse_enabled rect = self.get_rect(to_real_position=True) if self.readonly or not self.is_visible(): self._readonly_check_mouseover(events, rect) return False # Get time clock time_clock = self._clock.get_time() # Update cursor switch self._cursor_ms_counter += time_clock if self._cursor_ms_counter >= self._cursor_switch_ms: self._cursor_ms_counter %= self._cursor_switch_ms self._cursor_visible = not self._cursor_visible self.force_menu_surface_cache_update() updated = False events = self._merge_events(events) # Extend events with custom events for event in events: # Check mouse over self._check_mouseover(event, rect) # User press a key if event.type == pygame.KEYDOWN and self._keyboard_enabled: # Check if any key is pressed, if True the event is invalid if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid( event ): continue self._cursor_visible = True # So the user sees where he writes self._key_is_pressed = True self._last_key = event.key # If None exist, create counter for that key: if ( event.key not in self._keyrepeat_counters and event.key not in self._ignore_keys and "unicode" in event.dict ): self._keyrepeat_counters[event.key] = [0, event.unicode] # User press ctrl+something if any(pygame.key.get_mods() & mod == mod for mod in CTRL_KMOD): # If test, disable CTRL if "test" in event.dict and event.dict["test"]: pygame.key.set_mods(pygame.KMOD_NONE) # Ctrl+C copy if event.key == pygame.K_c: if not self._copy_paste_enabled: self._sound.play_event_error() break self.active = True copy_status = self._copy() if not copy_status: self._sound.play_event_error() updated = updated or copy_status break # Ctrl+V paste elif event.key == pygame.K_v: if not self._copy_paste_enabled: self._sound.play_event_error() break self.active = True updated = updated or self._paste() break # Ctrl+Z undo elif event.key == pygame.K_z: if self._max_history == 0: self._sound.play_event_error() break self.active = True self._sound.play_key_del() updated = updated or self._undo() break # Ctrl+Y redo elif event.key == pygame.K_y: if self._max_history == 0: self._sound.play_event_error() break self.active = True self._sound.play_key_add() updated = updated or self._redo() break # Ctrl+X cut elif event.key == pygame.K_x: if not self._copy_paste_enabled: self._sound.play_event_error() break self.active = True self._sound.play_key_del() updated = updated or self._cut() break # Ctrl+A select all elif event.key == pygame.K_a: if not self._selection_enabled: self._sound.play_event_error() break self.active = True self._select_all() updated = True break # Command not found, returns break # User press alt+x get the Unicode char from string if ( pygame.key.get_mods() in (pygame.KMOD_ALT, pygame.KMOD_LALT) and event.key == pygame.K_x and self._alt_x_enabled ): # Get the last hex value last_space = self._input_string.rfind(" ") if last_space == -1: # space not found, try 0x last_space = self._input_string.rfind("0x") if last_space == -1: # 0x not found, try 0X last_space = self._input_string.rfind("0x") if ( last_space == -1 ): # Finally, find the subsequence of valid hex chars last_space = 0 for j in range(len(self._input_string)): if self._input_string[j].lower() not in ( "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", ): last_space = j + 1 if last_space >= len(self._input_string): last_space = -1 if last_space >= 0: try: unicode_hex = self._input_string[last_space:] if unicode_hex.lower() == "0x": continue unicode_int = int(unicode_hex, 16) # Remove the code for _ in range(len(unicode_hex)): self._backspace() if not self._push_key_input(chr(unicode_int)): break self.active = True updated = True continue except (ValueError, OverflowError): pass # Backspace button, delete text from right if self._ctrl.back(event, self): # Play sound if self._cursor_position == 0: self._sound.play_event_error() else: self._sound.play_key_del() # If text is selected if self._selection_surface: self._remove_selection() updated = True break self._backspace() self.change() self.active = True updated = True # Delete button, delete text from left elif self._ctrl.delete(event, self): # Play sound if self._cursor_position == len(self._input_string): self._sound.play_event_error() else: self._sound.play_key_del() updated = True # If text is selected if self._selection_surface: self._remove_selection() break self._delete() self.change() self.active = True # Right arrow elif self._ctrl.right(event, self): # Play sound if self._cursor_position == len(self._input_string): self._sound.play_event_error() else: self._sound.play_key_add() # Update selection box if self._selection_active: if self._cursor_position == self._selection_box[1]: if self._selection_box[0] == self._selection_box[1]: self._selection_box[1] = self._selection_box[0] + 1 else: self._selection_box[1] = min( len(self._input_string), self._selection_box[1] + 1 ) else: self._selection_box[0] = min( self._selection_box[1], self._selection_box[0] + 1 ) else: if self._unselect_text(): break # Move cursor self._move_cursor_right() self.active = True updated = True # Left arrow elif self._ctrl.left(event, self): # Play sound if self._cursor_position == 0: self._sound.play_event_error() else: self._sound.play_key_add() # Update selection box if self._selection_active: if self._cursor_position == self._selection_box[0]: self._selection_box[0] = max(0, self._selection_box[0] - 1) else: if self._selection_box[1] - self._selection_box[0] == 1: self._selection_box[1] = self._selection_box[0] else: self._selection_box[1] = max( self._selection_box[0], self._selection_box[1] - 1 ) else: if self._unselect_text(): break # Move cursor self._move_cursor_left() self.active = True updated = True # Up arrow elif self._ctrl.move_up(event, self): self.active = False # Down arrow elif self._ctrl.move_down(event, self): self.active = False # End elif self._ctrl.end(event, self): self._sound.play_key_add() self._cursor_position = len(self._input_string) self._update_renderbox(end=True) self._unselect_text() self.active = True updated = True # Home elif self._ctrl.home(event, self): self._sound.play_key_add() self._cursor_position = 0 self._update_renderbox(start=True) self._unselect_text() self.active = True updated = True # Tab elif self._ctrl.tab(event, self): for _ in range(self._tab_size): self._push_key_input(" ") updated = True self.active = True # Enter elif self._ctrl.apply(event, self): self._sound.play_open_menu() self.apply() self._unselect_text() updated = True self.active = not self.active # Escape elif self._ctrl.escape(event, self): if self._get_selected_text(): self._unselect_text() updated = True elif self.active: # Disable active status on the widget self.active = False updated = True # Press lshift, rshift -> selection elif event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT: if not self._selection_active: self._selection_active = True self._selection_box[0] = self._cursor_position self._selection_box[1] = self._cursor_position self.active = True break # Any other key, add as input elif event.key not in self._ignore_keys and hasattr(event, "unicode"): if event.unicode == " " and event.key != 32: if self._verbose: warn( f'{self.get_class_id()} received "{event.unicode}" ' f"unicode but key is different than 32 ({event.key}), " f"check if event has defined the proper unicode char" ) break # Error in char, not valid or string limit exceeds if not self._push_key_input(event.unicode): break self.active = True updated = True # User releases a key elif event.type == pygame.KEYUP and self._keyboard_enabled: # Because KEYUP doesn't include event.Unicode, this dict is stored # in such a weird way if event.key in self._keyrepeat_counters: del self._keyrepeat_counters[event.key] # If selection keys are released, stop selection elif event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT: self._selection_active = False # Release inputs self._block_copy_paste = False self._key_is_pressed = False # User releases the mouse button or finger; 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 ( rect.collidepoint(*event_pos) and self.get_selected_time() > 1.5 * self._keyrepeat_mouse_interval_ms ): self._selection_active = False self._check_mouse_collide_input(event_pos) self._cursor_ms_counter = 0 self._cursor_visible = True # User press the mouse button or finger; don't consider the mouse # wheel (button 4 & 5) elif ( event.type == pygame.MOUSEBUTTONDOWN and self._mouse_enabled and event.button in (1, 2, 3) or event.type == FINGERDOWN and self._touchscreen_enabled ): if ( self.get_selected_time() > self._keyrepeat_mouse_interval_ms or hasattr(event, "test") ): if self._selection_active: self._unselect_text() self._cursor_ms_counter = 0 self._cursor_visible = True self._selection_active = True if event.type == pygame.MOUSEBUTTONDOWN: self._selection_mouse_first_position = -1 self.active = True # Update mouse self._keyrepeat_mouse_ms += time_clock if self._keyrepeat_mouse_ms > self._keyrepeat_mouse_interval_ms: # self._keyrepeat_mouse_ms = 0 if mouse_left: pos = pygame.mouse.get_pos() self._check_mouse_collide_input((pos[0], pos[1])) # Update key counters if self._keyrepeat: for key in self._keyrepeat_counters: self._keyrepeat_counters[key][0] += time_clock # Update clock # Generate new key events if enough time has passed: if ( self._keyrepeat_counters[key][0] >= self._keyrepeat_initial_interval_ms ): self._keyrepeat_counters[key][0] = ( self._keyrepeat_initial_interval_ms - self._keyrepeat_interval_ms ) event_key, event_unicode = key, self._keyrepeat_counters[key][1] self._add_event( pygame.event.Event( pygame.KEYDOWN, key=event_key, unicode=event_unicode ) ) return updated