"""
pygame-menu
https://github.com/ppizarror/pygame-menu
TEXT INPUT
Text input class, this widget lets user to write text.
"""
__all__ = ['TextInput']
import math
import pygame
import pygame_menu.controls as ctrl
from pygame_menu.locals import FINGERDOWN, FINGERUP, INPUT_INT, INPUT_FLOAT, INPUT_TEXT
from pygame_menu.utils import check_key_pressed_valid, make_surface, assert_color, \
get_finger_pos, warn, assert_vector
from pygame_menu.widgets.core.widget import Widget, WidgetTransformationNotImplemented
from pygame_menu._types import Optional, Any, CallbackType, Tuple, List, ColorType, \
NumberType, Tuple2IntType, Dict, Tuple2NumberType, NumberInstance, ColorInputType, \
EventVectorType
try:
# noinspection PyProtectedMember
from pyperclip import copy, paste, PyperclipException
except (ModuleNotFoundError, ImportError):
copy, paste = lambda text: None, lambda: ''
class PyperclipException(RuntimeError):
"""
Pyperclip exception thrown by pyperclip.
"""
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
)
# noinspection PyMissingOrEmptyDocstring
[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 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_initial_ms: Time in ms before keys are repeated when held in ms
:param repeat_keys_interval_ms: Interval between key press repetition when held in ms
:param repeat_mouse_interval_ms: Interval between mouse events when held in ms
: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
_alt_x_enabled: bool
_apply_widget_update_callback: bool # Used in ColorInput
_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: Optional[Tuple2IntType] # Size defined by user
_cursor_surface: Optional['pygame.Surface']
_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_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: Optional['pygame.Surface']
_title_size: NumberType
_valid_chars: Optional[List[str]]
def __init__(
self,
title: Any,
textinput_id: str = '',
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: Optional[Tuple2IntType] = 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_initial_ms: NumberType = 400,
repeat_keys_interval_ms: NumberType = 50,
repeat_mouse_interval_ms: NumberType = 400,
text_ellipsis: str = '...',
valid_chars: Optional[List[str]] = 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_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, \
'ms cannot be lower or equal than zero'
assert repeat_keys_interval_ms > 0, \
'ms cannot be lower or equal than zero'
assert repeat_mouse_interval_ms > 0, \
'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(TextInput, self).__init__(
args=args,
kwargs=kwargs,
onchange=onchange,
onreturn=onreturn,
onselect=onselect,
title=title,
widget_id=textinput_id
)
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_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
# Things 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
# Touch emulates a mouse, so this is used by both touch and mouse
self._selection_mouse_first_position = -1
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 = True
# Other
self._accept_events = True
self._alt_x_enabled = True
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.
:return: None
"""
self._input_string = ''
self._cursor_position = 0
self._renderbox = [0, 0, 0]
self._delete()
self.change()
[docs] def get_value(self) -> str:
"""
Return the value of the text.
:return: Text inside the widget
"""
value = ''
if self._input_type == INPUT_TEXT:
value = self._input_string # Without filters
elif self._input_type == INPUT_FLOAT:
try:
value = float(self._input_string)
except ValueError:
value = 0
elif self._input_type == INPUT_INT:
try:
value = int(float(self._input_string))
except ValueError:
value = 0
return value
def scale(self, *args, **kwargs) -> 'TextInput':
raise WidgetTransformationNotImplemented()
def resize(self, *args, **kwargs) -> 'TextInput':
raise WidgetTransformationNotImplemented()
def set_max_width(self, *args, **kwargs) -> 'TextInput':
raise WidgetTransformationNotImplemented()
def set_max_height(self, *args, **kwargs) -> 'TextInput':
raise WidgetTransformationNotImplemented()
def rotate(self, *args, **kwargs) -> 'TextInput':
raise WidgetTransformationNotImplemented()
[docs] def flip(self, x: bool, y: bool) -> 'TextInput': # Actually flip on x axis is disabled
super(TextInput, self).flip(False, y)
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) -> Optional[bool]:
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._selection_box[1], self._last_selection_render[0],
self._last_selection_render[1], self._renderbox[0], self._renderbox[1],
self._renderbox[2], self._cursor_visible):
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()
def _render_selection_box(self, force: bool = False) -> None:
"""
Render selected text.
:param force: Force update
: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._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
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 -
self._padding[1] - self._padding[3],
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.
: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
if 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 = 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: 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
:return: None
"""
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
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
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
if left < 0 and self._renderbox[2] == 0:
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 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 _update_maxlimit_renderbox(self) -> None:
"""
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, mouse_x: int) -> None:
"""
Updates cursor position after mouse click or touch action in text.
:param mouse_x: Mouse distance relative to surface
:return: None
"""
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
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
[docs] def set_value(self, text: Any) -> None:
"""
Set the value of the text.
:param text: New text of the widget
:return: None
"""
if self._password and text != '':
raise ValueError('value cannot be set in password type')
if 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
if 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
if conv is None:
return False
try:
conv(string)
return True
except ValueError:
return False
def _move_cursor_left(self) -> None:
"""
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) -> None:
"""
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) -> 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
:return: None
"""
assert isinstance(new_string, str)
assert isinstance(update_history, bool)
l_history = len(self._history)
# If last edition is different than the new one -> updates the history
if update_history and \
((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) -> bool:
"""
Copy text from clipboard.
:return: ``True`` if copied
"""
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) -> bool:
"""
Cut operation.
:return: ``True`` if cut
"""
if not self._copy_paste_enabled: # Ignore cut
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 _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 _paste(self) -> bool:
"""
Paste text from clipboard.
:return: ``True`` if pasted
"""
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:
# 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.
: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) -> bool:
"""
Undo operation.
:return: ``True`` if undo
"""
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) -> 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.
: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(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
: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, 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
:return: None
"""
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.
: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: 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
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):
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
# noinspection PyArgumentList
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
if self.readonly or not self.is_visible():
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
rect = self.get_rect(to_real_position=True)
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 pygame.key.get_mods() in CTRL_KMOD:
# If test, disable CTRL
if 'test' in event.dict and event.dict['test']:
# noinspection PyArgumentList
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
else:
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 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()
updated = True
break
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()
updated = True
# If text is selected
if self._selection_surface:
self._remove_selection()
break
self._delete()
self.change()
self.active = 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():
break
# 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():
break
# Move cursor
self._move_cursor_left()
self.active = True
updated = True
# Up arrow
elif event.key == ctrl.KEY_MOVE_UP:
self.active = False
# Down arrow
elif event.key == ctrl.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 == ctrl.KEY_TAB:
for _ in range(self._tab_size):
self._push_key_input(' ')
updated = True
self.active = True
# Enter
elif event.key == ctrl.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
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:
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:
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