Source code for pygame_menu.widgets.widget.textinput

# coding=utf-8
"""
pygame-menu
https://github.com/ppizarror/pygame-menu

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

License:
-------------------------------------------------------------------------------
The MIT License (MIT)
Copyright 2017-2020 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.
-------------------------------------------------------------------------------
"""

import math

import pygame
import pygame_menu.controls as _controls
import pygame_menu.locals as _locals
from pygame_menu.utils import check_key_pressed_valid, make_surface
from pygame_menu.widgets.core import Widget

try:
    # noinspection PyProtectedMember
    from pyperclip import copy, paste, PyperclipException
except ImportError:

    # noinspection PyUnusedLocal
    def copy(text):
        """
        Copy method.

        :return: None
        """
        pass

    def paste():
        """
        Paste method.

        :return: Empty string
        :rtype: str
        """
        return ''

    class PyperclipException(RuntimeError):
        """
        Pyperclip exception thrown by pyperclip.
        """
        pass


[docs]class TextInput(Widget): """ Text input widget. :param label: Input label text :type label: str :param textinput_id: ID of the text input :type textinput_id: str :param input_type: Type of data :type input_type: str :param input_underline: Character drawn under the input :type input_underline: str :param cursor_color: Color of cursor :type cursor_color: tuple :param cursor_selection_color: Selection box color :type cursor_selection_color: tuple :param cursor_selection_enable: Enables selection of text :type cursor_selection_enable: bool :param copy_paste_enable: Enables copy, paste and cut :type copy_paste_enable: bool :param history: Maximum number of editions stored :type history: int :param maxchar: Maximum length of input :type maxchar: int :param maxwidth: Maximum size of the text to be displayed (overflow), if 0 this feature is disabled :type maxwidth: int :param maxwidth_dynamically_update: Dynamically update maxwidth depending on char size :type maxwidth_dynamically_update: bool :param onchange: Callback when changing the selector :type onchange: callable, None :param onreturn: Callback when pressing return button :type onreturn: callable, None :param password: Input string is displayed as a password :type password: bool :param password_char: Character used by password type :type password_char: str :param repeat_keys_initial_ms: Time in ms before keys are repeated when held :type repeat_keys_initial_ms: int, float :param repeat_keys_interval_ms: Interval between key press repetition when held :type repeat_keys_interval_ms: int, float :param repeat_mouse_interval_ms: Interval between mouse events when held :type repeat_mouse_interval_ms: int, float :param tab_size: Tab whitespace characters :type tab_size: int :param text_ellipsis: Ellipsis text when overflow occurs (input length exceeds maxwidth) :type text_ellipsis: str :param valid_chars: List of chars that are valid, None if all chars are valid :type valid_chars: list :param kwargs: Optional keyword-arguments for callbacks :type kwargs: any """ def __init__(self, label='', textinput_id='', input_type=_locals.INPUT_TEXT, input_underline='', copy_paste_enable=True, cursor_color=(0, 0, 0), cursor_selection_color=(30, 30, 30), cursor_selection_enable=True, history=50, maxchar=0, maxwidth=0, maxwidth_dynamically_update=True, onchange=None, onreturn=None, password=False, password_char='*', repeat_keys_initial_ms=450, repeat_keys_interval_ms=80, repeat_mouse_interval_ms=100, tab_size=4, text_ellipsis='...', valid_chars=None, *args, **kwargs): assert isinstance(label, str) assert isinstance(textinput_id, str) assert isinstance(input_type, str) assert isinstance(input_underline, str) assert isinstance(cursor_color, tuple) assert isinstance(copy_paste_enable, bool) assert isinstance(cursor_selection_enable, bool) assert isinstance(history, int) assert isinstance(valid_chars, (type(None), list)) assert isinstance(maxchar, int) assert isinstance(maxwidth, int) assert isinstance(password, bool) assert isinstance(password_char, str) assert isinstance(repeat_keys_initial_ms, (int, float)) assert isinstance(repeat_keys_interval_ms, (int, float)) assert isinstance(repeat_mouse_interval_ms, (int, float)) assert isinstance(tab_size, int) assert isinstance(text_ellipsis, str) 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 tab_size >= 0, 'tab size must be equal or greater than zero' assert len(password_char) == 1, 'password char must be a character' super(TextInput, self).__init__(widget_id=textinput_id, onchange=onchange, onreturn=onreturn, args=args, kwargs=kwargs) self._input_string = '' # Inputted text self._ignore_keys = ( # Ignore keys on input-gathering events _controls.KEY_MOVE_DOWN, _controls.KEY_MOVE_UP, pygame.K_CAPSLOCK, pygame.K_LCTRL, pygame.K_LSHIFT, pygame.K_NUMLOCK, pygame.K_RCTRL, pygame.K_RETURN, pygame.K_RSHIFT, pygame.K_TAB ) # Vars to make keydowns repeat after user pressed a key for some time: self._block_copy_paste = False # Blocks event self._key_is_pressed = False 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 # type: int self._absolute_origin = (0, 0) # To calculate mouse collide point # Mouse handling self._keyrepeat_mouse_ms = 0.0 # type: float self._keyrepeat_mouse_interval_ms = repeat_mouse_interval_ms self._mouse_is_pressed = False # type: bool # Render box (overflow) self._ellipsis = text_ellipsis self._ellipsis_size = 0.0 # type: float self._renderbox = [0, 0, 0] # Left/Right/Inner, int # Things cursor: self._clock = pygame.time.Clock() # type: pygame.time.Clock self._cursor_color = cursor_color self._cursor_ms_counter = 0.0 # type: float self._cursor_offset = -1.0 # type: float self._cursor_position = 0 # Inside text self._cursor_render = True # If true cursor must be rendered self._cursor_surface = None # type: (pygame.Surface,None) self._cursor_surface_pos = [0.0, 0.0] # Position (x,y) of surface self._cursor_switch_ms = 500.0 # type: float self._cursor_visible = False # Switches every self._cursor_switch_ms ms # History of editions self._history = [] # type: list self._history_cursor = [] # type: list self._history_renderbox = [] # type: list 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_mouse_first_position = -1 # type: int self._selection_position = [0.0, 0.0] # x,y (float) self._selection_render = False self._selection_surface = None # type: (pygame.Surface,None) self._selection_color = cursor_selection_color self._selection_enabled = cursor_selection_enable # 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), 'Element "{0}" of valid_chars must be a string'.format(_char) assert len(_char) == 1, 'Element "{0}" of valid_chars must be character'.format(_char) assert len(valid_chars) > 0, 'valid_chars list must contain at least 1 element' self._valid_chars = valid_chars # Other self._copy_paste_enabled = copy_paste_enable self._input_type = input_type self._input_underline = input_underline self._input_underline_size = 0.0 # type: float self._keychar_size = {'': 0} # type: dict self._label = label self._label_size = 0.0 # type: float self._last_char = '' # type: str self._last_rendered_string = '__pygame_menu__last_render_string__' # type: str self._last_rendered_surface = None # type: (pygame.Surface,None) self._last_rendered_surface_underline_width = 0 # type: int 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.0 # Updated in _apply_font() self._password = password self._password_char = password_char self._tab_size = tab_size def _apply_font(self): self._ellipsis_size = self._font.size(self._ellipsis)[0] self._label_size = self._font.size(self._label)[0] # Generate the underline surface self._input_underline_size = self._font.size(self._input_underline)[0] # Size of maxwidth if not zero self._maxwidthsize = self.font_render_string('O' * self._maxwidth_base).get_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): """ Clear the current text. :return: None """ self._input_string = '' self._cursor_position = 0 self._renderbox = [0, 0, 0] self._delete() self.change()
[docs] def get_value(self): """ Returns the value of the text. :return: Text inside the widget :rtype: str """ value = '' if self._input_type == _locals.INPUT_TEXT: value = self._input_string # Without filters elif self._input_type == _locals.INPUT_FLOAT: try: value = float(self._input_string) except ValueError: value = 0 elif self._input_type == _locals.INPUT_INT: try: value = int(float(self._input_string)) except ValueError: value = 0 return value
# noinspection PyMissingOrEmptyDocstring
[docs] def draw(self, surface): self._render() self._clock.tick() # Draw background color self._fill_background_color(surface) # Draw selection first if self._selection_surface is not None: surface.blit(self._selection_surface, (self._selection_position[0], self._selection_position[1])) # Draw string surface.blit(self._surface, (self._rect.x, self._rect.y)) # Draw cursor if self.selected and self._cursor_surface and \ (self._cursor_visible or (self._mouse_is_pressed or self._key_is_pressed)): surface.blit(self._cursor_surface, (self._rect.x + self._cursor_surface_pos[0], self._rect.y + self._cursor_surface_pos[1]))
def _render(self): string = self._label + self._get_input_string() # Render string if not self._render_hash_changed(self._menu.get_id(), string, self.selected, self._cursor_render, self._selection_enabled, self.active): return if self.selected: color = self._font_selected_color else: color = self._font_color updated_surface = self._render_string_surface(string, color) # Apply underline if exists self._surface = self._render_underline(string, color, updated_surface) # 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() # Check if the size changed self._check_render_size_changed() def _render_selection_box(self, force=False): """ Render selected text. :param force: Force update :type force: bool :return: None """ if not self._selection_enabled: return if self._selection_active and \ (self._last_selection_render[0] != self._selection_box[0] or self._last_selection_render[1] != self._selection_box[1]) or force: # 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._label + string_init)[0] x2 = self._cursor_offset + self._font.size(self._label + 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._label)[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 # Create surface and fill self._selection_surface = make_surface(x, y) self._selection_surface.fill(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 _render_string_surface(self, string, color): """ Renders string surface. :param string: String to render :type string: str :param color: Color of the string to render :type color: tuple :return: True if surface is updated :rtype: bool """ new_surface = self._render_string(string, color) updated_surface = self._last_rendered_surface != new_surface if updated_surface: self._surface = new_surface self._last_rendered_surface = self._surface return updated_surface def _render_underline(self, string, color, updated): """ Render underline surface. :param string: String to render :type string: str :param color: Color of the string to render :type color: tuple :param updated: Render string has been updated or not :type updated: bool :return: New rendered surface :rtype: :py:class:`pygame.Surface` """ # If underline is not enabled if self._input_underline_size == 0: return self._surface # Render input underline if string != self._last_rendered_string or updated: menu = self.get_menu() # Calculate total available space current_rect = self._surface.get_rect() # type: pygame.Rect menu_rect = menu.get_rect() posx2 = menu_rect.x + menu_rect.width space_between_label = posx2 - self._label_size - self._rect.x char = math.ceil(space_between_label * 1.0 / self._input_underline_size) # floor does not work # If char limit max_width_current = 0 if self._maxchar != 0 or self._maxwidth != 0: max_chars = max(self._maxchar, self._maxwidth_base) basechar = 'O' if self._password: basechar = self._password_char max_size = self.font_render_string(basechar * max_chars) max_size = max_size.get_size()[0] maxchar_char = math.ceil((max_size + 4 * self._ellipsis_size) / self._input_underline_size) char = min(char, maxchar_char) max_width_current = current_rect.width underline_string = self._input_underline * int(char) # Render char underline = self.font_render_string(underline_string, color) # Create a new surface new_width = max(self._label_size + underline.get_size()[0], max_width_current, self._last_rendered_surface_underline_width) new_surface = make_surface(new_width + 1, current_rect.height + 3, alpha=True) # Blit current surface new_surface.blit(self._surface, (0, 0)) new_surface.blit(underline, (self._label_size - 1, 6)) # Position (x, y) self._last_rendered_surface_with_underline = new_surface self._last_rendered_surface_underline_width = new_width else: new_surface = self._last_rendered_surface_with_underline # Reuse the rendered surface return new_surface def _render_cursor(self): """ Cursor is rendered and stored. :return: None """ # Cursor should not be rendered if not self._cursor_render: return # Cursor surface does not exist if self._cursor_surface is None: if self._rect.height == 0: # If menu has not been initialized this error can occur return 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._label + 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._label + 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._label 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 += 3 # Store position self._cursor_surface_pos[0] = cursor_x_pos self._cursor_surface_pos[1] = cursor_y_pos self._cursor_render = False def _ellipsis_left(self): """ Return true if left ellipsis is active. :return: Boolean :rtype: bool """ return self._renderbox[0] != 0 and self._maxwidth != 0 def _ellipsis_right(self): """ Return true if right ellipsis is active. :return: Boolean :rtype: bool """ return self._renderbox[1] != len(self._input_string) and self._maxwidth != 0 def _ellipsis_left_and_right(self): """ Return true if left and right ellipsis are active. :return: Boolean :rtype: bool """ return self._ellipsis_left() and self._ellipsis_right() def _get_input_string_filtered(self): """ Returns input string where all filters have been applied. :return: Filtered string :rtype: str """ string = self._input_string # Apply password if self._password: string = self._password_char * len(string) return string def _get_input_string(self, add_ellipsis=True): """ Return input string, apply overflow if enabled. :param add_ellipsis: Adds ellipsis text :type add_ellipsis: bool :return: String :rtype: str """ string = 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 else: return string def _update_renderbox(self, left=0, right=0, addition=False, end=False, start=False, update_maxwidth=True): """ Update renderbox position. :param left: Left update :type left: int :param right: Right update :type right: int :param addition: Update if text addition/deletion :type addition: bool :param end: Move cursor to end :type end: bool :param start: Move cursor to start :type start: bool :param update_maxwidth: Update maxwidth limit depending on the chars written :type update_maxwidth: bool :return: None """ self._cursor_render = True if self._maxwidth == 0: return len_string = len(self._input_string) # 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 if start: self._renderbox[0] = 0 self._renderbox[1] = min(len_string, self._maxwidth) self._renderbox[2] = 0 return # Check limits if left < 0 and len_string == 0: return # If no overflow if len_string <= self._maxwidth: if right < 0 and self._renderbox[2] == len_string: # If del at the end of string return if 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 self._renderbox[2] += left self._renderbox[2] += right else: if addition: # If text is added if right < 0 and self._renderbox[2] == self._maxwidth: # If press del at the end of string return if left < 0 and self._renderbox[2] == 0: # If backspace at beginning of string return # If user deletes something and it is in the end if 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 if right > 0: if self._renderbox[2] == self._maxwidth: # If cursor is at the end push box self._renderbox[0] += right self._renderbox[1] += right self._renderbox[2] += right if left < 0: if self._renderbox[0] == 0: # If cursor is at the beginning 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 _update_maxlimit_renderbox(self): """ Update renderbox based on how many characters have been written on input. :return: None """ 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, mousex): """ Updates cursor position after mouse click in text. :param mousex: Mouse distance relative to surface :type mousex: int :return: None """ string = self._get_input_string() if string == '': # If string is empty cursor is not updated return # Calculate size of each character string_size = [] string_total_size = 0 for i in range(len(string)): cs = self._font.size(string[i])[0] # Char size string_size.append(cs) string_total_size += cs # Find the accumulated char size that gives the position of cursor size_sum = 0 cursor_pos = len(string) for i in range(len(string)): size_sum += string_size[i] / 2 if self._label_size + size_sum >= mousex: cursor_pos = i break size_sum += string_size[i] / 2 # If text have ellipsis if self._maxwidth != 0 and len(self._input_string) > self._maxwidth: if self._ellipsis_left(): cursor_pos -= 3 # 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): """ Check mouse collision, if true update cursor. :param pos: Position :type pos: tuple :return: None """ if self._rect.collidepoint(*pos): # Check if mouse collides left or right as percentage, use only X coordinate mousex, _ = pos topleft, _ = self._rect.topleft self._update_cursor_mouse(mousex - topleft) return True # Prevents double click
[docs] def set_value(self, text): """ Set the value of the text. :param text: New text of the widget :type text: str, int, float :return: None """ if self._password and text != '': raise ValueError('value cannot be set in password type') assert isinstance(text, (str, int, float)) if self._check_input_type(text): _default = str(text) # Filter valid chars if self._valid_chars is not None: _default_valid = '' for ch in _default: if ch in self._valid_chars: _default_valid += ch _default = _default_valid # Apply maxchar _ls = len(_default) if 0 < self._maxchar < _ls: _default = _default[_ls - self._maxchar:_ls] self._input_string = _default for i in range(len(_default) + 1): self._move_cursor_right() self._update_renderbox(right=1, addition=True) self._update_input_string(_default) else: raise ValueError('value "{0}" type is not correct according to input_type'.format(text)) self._update_renderbox() # Updates cursor self._render() # Renders the selection box
def _check_input_size(self): """ Check input size. :return: True if the input must be limited :rtype: bool """ if self._maxchar == 0: return False return self._maxchar <= len(self._input_string) def _check_input_type(self, string): """ Check if input type is valid. :param string: String to validate :type string: str :return: True if the input type is valid :rtype: bool """ if string == '': # Empty is valid return True if self._input_type == _locals.INPUT_TEXT: return True conv = None if self._input_type == _locals.INPUT_FLOAT: conv = float elif self._input_type == _locals.INPUT_INT: conv = int if string == '-': return True if conv is None: return False try: conv(string) return True except ValueError: return False def _move_cursor_left(self): """ Move cursor to left position. :return: None """ # 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): """ Move cursor to right position. :return: None """ # 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): # self._key_is_pressed = False self._mouse_is_pressed = False self._keyrepeat_mouse_ms = 0 self._cursor_visible = False self._unselect_text() # self._history_index = len(self._history) - 1 def _focus(self): self._cursor_ms_counter = 0 self._cursor_visible = True self._cursor_render = True def _unselect_text(self): """ Unselect text. :return: True if the selected text was removed in the method call :rtype: bool """ 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 return removed def _get_selected_text(self): """ Return text selected. :return: Text :rtype: str """ return self._input_string[self._selection_box[0]:self._selection_box[1]] def _update_input_string(self, new_string): """ Update input string with a new string, store changes into history. :param new_string: New string of text input :type new_string: str :return: None """ l_history = len(self._history) # If last edition is different than the new one -> updates the history if ((l_history > 0 and self._history[l_history - 1] != new_string) or l_history == 0) and self._max_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): """ Copy text from clipboard. :return: None """ if self._block_copy_paste: # Prevents multiple executions of event return False if self._password: # Password cannot be copied return False try: if self._selection_surface: # If text is selected copy(self._get_selected_text()) else: # Copy all text copy(self._input_string) except PyperclipException: pass self._block_copy_paste = True return True def _cut(self): """ Cut operation. :return: """ self._copy() # This is a safe operation, all checks have been passed # If text is selected if self._selection_surface: self._remove_selection() else: self._cursor_position = 0 self._renderbox[0] = 0 self._renderbox[1] = 0 self._renderbox[2] = 0 self._update_input_string('') self._cursor_render = True # Due to manually updating renderbox def _get_char_size(self, char): """ Return char size in pixels. :param char: Char """ 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 _paste(self): """ Paste text from clipboard. :return: None """ if self._block_copy_paste: # Prevents multiple executions of event return False # If text is selected if self._selection_surface: self._remove_selection() # Paste text in cursor try: text = paste() except PyperclipException: return False text = text.strip() for i in ['\n', '\r']: text = text.replace(i, '') # Delete escape chars escapes = ''.join([chr(char) for char in range(1, 32)]) text = text.translate(escapes) if text == '': return False # Remove invalid chars if 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 exists) text_end = 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 not more space, returns self.sound.play_event_error() return False new_string = 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: self._get_char_size(char) # This updates the self._keychar_size variable 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): """ Update all from history. :return: None """ 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): """ Undo operation. :return: None """ if self._history_index == 0: # There's no back history return False if 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): """ Redo operation. :return: None """ 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): """ Remove text from selection. :return: None """ removed = self._selection_box[1] - self._selection_box[0] left = False if self._selection_box[0] == self._cursor_position: left = True for i in range(removed): if left: self._delete() else: self._backspace() # Destroy selection self._unselect_text() def _backspace(self): """ Backspace event. :return: None """ new_string = ( self._input_string[:max(self._cursor_position - 1, 0)] + self._input_string[self._cursor_position:] ) self._update_input_string(new_string) 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): """ Delete event. :return: None """ new_string = ( self._input_string[:self._cursor_position] + self._input_string[self._cursor_position + 1:] ) self._update_input_string(new_string) self._update_renderbox(right=-1, addition=True) def _select_all(self): """ Select all text. :return: None """ 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, sounds=True): """ Insert a key in the cursor position. :param keychar: Char to be inserted :type keychar: str :param sounds: Use widget sounds :type sounds: bool :return: If False the event loop breaks :rtype: bool """ # 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 if 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 if self._check_input_type(new_string): lkey = len(keychar) if lkey > 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 # noinspection PyMissingOrEmptyDocstring
[docs] def update(self, events): updated = False for event in events: # type: pygame.event.Event if event.type == pygame.KEYDOWN: # Check if any key is pressed, if True the event is invalid if 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 pygame.key.get_mods() & pygame.KMOD_CTRL: # 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() return self.active = True copy_status = self._copy() if not copy_status: self.sound.play_event_error() return copy_status # Ctrl+V paste elif event.key == pygame.K_v: if not self._copy_paste_enabled: self.sound.play_event_error() return self.active = True return self._paste() # Ctrl+Z undo elif event.key == pygame.K_z: if self._max_history == 0: self.sound.play_event_error() return self.active = True self.sound.play_key_del() return self._undo() # Ctrl+Y redo elif event.key == pygame.K_y: if self._max_history == 0: self.sound.play_event_error() return self.active = True self.sound.play_key_add() return self._redo() # Ctrl+X cut elif event.key == pygame.K_x: if not self._copy_paste_enabled: self.sound.play_event_error() return self.active = True self.sound.play_key_del() return self._cut() # Ctrl+A select all elif event.key == pygame.K_a: if not self._selection_enabled: self.sound.play_event_error() return self.active = True self._select_all() return False # Command not found, returns else: return False # Backspace button, delete text from right if event.key == pygame.K_BACKSPACE: # 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() return True self._backspace() self.change() self.active = True updated = True # Delete button, delete text from left elif event.key == pygame.K_DELETE: # Play sound if self._cursor_position == len(self._input_string): self.sound.play_event_error() else: self.sound.play_key_del() # If text is selected if self._selection_surface: self._remove_selection() return True self._delete() self.change() self.active = True updated = True # Right arrow elif event.key == pygame.K_RIGHT: # 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(): return # Move cursor self._move_cursor_right() self.active = True updated = True # Left arrow elif event.key == pygame.K_LEFT: # 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(): return # Move cursor self._move_cursor_left() self.active = True updated = True # Up arrow elif event.key == _controls.KEY_MOVE_UP: self.active = False # Down arrow elif event.key == _controls.KEY_MOVE_DOWN: self.active = False # End elif event.key == pygame.K_END: 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 event.key == pygame.K_HOME: self.sound.play_key_add() self._cursor_position = 0 self._update_renderbox(start=True) self._unselect_text() self.active = True updated = True # Tab elif event.key == pygame.K_TAB: for _ in range(self._tab_size): self._push_key_input(' ') updated = True self.active = True # Enter elif event.key == _controls.KEY_APPLY: self.sound.play_open_menu() self.apply() self._unselect_text() updated = True self.active = not self.active # Escape elif event.key == pygame.K_ESCAPE: 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 return False # Any other key, add as input elif event.key not in self._ignore_keys: if not self._push_key_input(event.unicode): # Error in char, not valid or string limit exceeds break self.active = True updated = True elif event.type == pygame.KEYUP: # 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 elif self.mouse_enabled and event.type == pygame.MOUSEBUTTONUP: self._absolute_origin = getattr(event, 'origin', self._absolute_origin) self._selection_active = False self._check_mouse_collide_input(event.pos) self._cursor_ms_counter = 0 elif self.mouse_enabled and event.type == pygame.MOUSEBUTTONDOWN: self._absolute_origin = getattr(event, 'origin', self._absolute_origin) if self._selection_active: self._unselect_text() self._cursor_ms_counter = 0 self._selection_active = True self._selection_mouse_first_position = -1 self.active = True # Get time clock time_clock = self._clock.get_time() self._keyrepeat_mouse_ms += time_clock # 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 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] - self._absolute_origin[0], pos[1] - self._absolute_origin[1])) # Update key counters: 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] try: # noinspection PyArgumentList pygame.event.post(pygame.event.Event(pygame.KEYDOWN, key=event_key, unicode=event_unicode) ) except pygame.error: # If the keys are too fast pygame can raise a Sound Exception pass # Update self._cursor_visible 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 return updated