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