Source code for pygame_menu.widgets.widget.colorinput

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

COLOR INPUT
Color input class, Widget created in top of TextInput that provides a textbox
for entering and previewing colors in RGB and HEX format.

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

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

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

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

__all__ = [

    # Main class
    'ColorInput',

    # Constants
    'COLORINPUT_TYPE_HEX',
    'COLORINPUT_TYPE_RGB',
    'COLORINPUT_HEX_FORMAT_LOWER',
    'COLORINPUT_HEX_FORMAT_NONE',
    'COLORINPUT_HEX_FORMAT_UPPER',

    # Type
    'ColorInputColorType',
    'ColorInputHexFormatType'

]

import math

import pygame

from pygame_menu.locals import INPUT_TEXT
from pygame_menu.utils import check_key_pressed_valid, make_surface
from pygame_menu.widgets.widget.textinput import TextInput

from pygame_menu._types import Union, List, NumberType, Any, Optional, CallbackType, \
    Literal, Tuple3IntType, NumberInstance, EventVectorType

# Input modes
COLORINPUT_TYPE_HEX = 'hex'
COLORINPUT_TYPE_RGB = 'rgb'

# Apply format to hex color string
COLORINPUT_HEX_FORMAT_LOWER = 'lower'
COLORINPUT_HEX_FORMAT_NONE = 'none'
COLORINPUT_HEX_FORMAT_UPPER = 'upper'

# Custom typing
ColorInputColorType = Literal[COLORINPUT_TYPE_RGB, COLORINPUT_TYPE_HEX]
ColorInputHexFormatType = Literal[COLORINPUT_HEX_FORMAT_LOWER,
                                  COLORINPUT_HEX_FORMAT_UPPER,
                                  COLORINPUT_HEX_FORMAT_NONE]


# noinspection PyMissingOrEmptyDocstring
[docs]class ColorInput(TextInput): # lgtm [py/missing-call-to-init] """ Color input widget. The callbacks receive the current value and all unknown keyword arguments, where ``current_color=widget.get_value()``: .. code-block:: python onchange(current_color, **kwargs) onreturn(current_color, **kwargs) .. note:: This widget cannot select text as :py:class:`pygame_menu.widgets.TextInput` does. Also, copy and paste is disabled. .. note:: ColorInput accepts the same transformations as :py:class:`pygame_menu.widgets.TextInput`. :param title: Color input title :param colorinput_id: ID of the text input :param color_type: Type of color input :param cursor_switch_ms: Interval of cursor switch between off and on status. First status is ``off`` :param dynamic_width: If ``True`` the widget width changes if the previsualization color box is active or not :param hex_format: Hex format string mode :param input_separator: Divisor between RGB channels :param input_underline: Character drawn under each number input :param input_underline_vmargin: Vertical margin of underline in px :param cursor_color: Color of cursor :param onchange: Function when changing the values of the color text :param onreturn: Function when pressing return on the color text input :param onselect: Function when selecting the widget :param prev_margin: Horizontal margin between the previsualization and the input text in px :param prev_width_factor: Width of the previsualization box in terms of the height of the widget :param repeat_keys_initial_ms: Time in ms before keys are repeated when held :param repeat_keys_interval_ms: Interval between key press repetition when held :param repeat_mouse_interval_ms: Interval between mouse events when held :param kwargs: Optional keyword arguments """ _auto_separator_pos: List[int] _color_type: str _dynamic_width: bool _hex_format: str _last_g: int _last_r: int _prev_margin: int _previsualization_surface: Optional['pygame.Surface'] _separator: str def __init__( self, title: Any, colorinput_id: str = '', color_type: ColorInputColorType = COLORINPUT_TYPE_RGB, cursor_color: Tuple3IntType = (0, 0, 0), cursor_switch_ms: NumberType = 500, dynamic_width: bool = True, hex_format: ColorInputHexFormatType = COLORINPUT_HEX_FORMAT_NONE, input_separator: str = ',', input_underline: str = '_', input_underline_vmargin: int = 0, onchange: CallbackType = None, onreturn: CallbackType = None, onselect: CallbackType = None, prev_margin: int = 10, prev_width_factor: NumberType = 3, repeat_keys_initial_ms: NumberType = 450, repeat_keys_interval_ms: NumberType = 80, repeat_mouse_interval_ms: NumberType = 100, *args, **kwargs ) -> None: assert isinstance(color_type, str) assert isinstance(colorinput_id, str) assert isinstance(dynamic_width, bool) assert isinstance(hex_format, str) assert isinstance(input_separator, str) assert isinstance(input_underline, str) assert isinstance(prev_margin, int) assert isinstance(prev_width_factor, NumberInstance) assert len(input_separator) == 1, 'input_separator must be a single char' assert len(input_separator) != 0, 'input_separator cannot be empty' assert prev_width_factor > 0, \ 'previsualization width factor must be greater than zero' assert input_separator not in ('0', '1', '2', '3', '4', '5', '6', '7', '8', '9'), 'input_separator cannot be a number' assert color_type in (COLORINPUT_TYPE_HEX, COLORINPUT_TYPE_RGB), \ 'color type must be "{0}" or "{1}"' \ ''.format(COLORINPUT_TYPE_HEX, COLORINPUT_TYPE_RGB) assert hex_format in (COLORINPUT_HEX_FORMAT_NONE, COLORINPUT_HEX_FORMAT_LOWER, COLORINPUT_HEX_FORMAT_UPPER), \ 'invalid hex format mode, it must be "none", "lower" or "upper"' _maxchar = 0 self._color_type = color_type.lower() if self._color_type == COLORINPUT_TYPE_RGB: _maxchar = 11 # RRR,GGG,BBB self._valid_chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', input_separator] elif self._color_type == COLORINPUT_TYPE_HEX: _maxchar = 7 # #XXYYZZ self._valid_chars = ['a', 'A', 'b', 'B', 'c', 'C', 'd', 'D', 'e', 'E', 'f', 'F', '#', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] # noinspection PyArgumentEqualDefault super(ColorInput, self).__init__( copy_paste_enable=False, cursor_color=cursor_color, cursor_switch_ms=cursor_switch_ms, cursor_selection_enable=False, history=0, input_type=INPUT_TEXT, input_underline=input_underline, input_underline_vmargin=input_underline_vmargin, maxchar=_maxchar, maxwidth=0, onchange=onchange, onreturn=onreturn, onselect=onselect, password=False, repeat_keys_initial_ms=repeat_keys_initial_ms, repeat_keys_interval_ms=repeat_keys_interval_ms, repeat_mouse_interval_ms=repeat_mouse_interval_ms, text_ellipsis='', textinput_id=colorinput_id, title=title, valid_chars=self._valid_chars, *args, **kwargs ) # Store inner variables self._auto_separator_pos = [] # This stores indexes of auto separator added self._dynamic_width = dynamic_width self._hex_format = hex_format self._separator = input_separator # Previsualization surface, if -1 does not show self._last_b = -1 self._last_g = -1 self._last_r = -1 self._prev_margin = prev_margin self._prev_width_factor = prev_width_factor self._previsualization_surface = None # Disable parent callbacks self._apply_widget_update_callback = False # Disable alt+x self._alt_x_enabled = False def _apply_font(self) -> None: super(ColorInput, self)._apply_font() # Compute the size of the underline if self._input_underline != '': max_width = 0 # Max expected width if self._color_type == COLORINPUT_TYPE_RGB: max_width = self._font_render_string( '255{0}255{0}255'.format(self._separator)).get_width() else: for i in ('a', 'b', 'c', 'd', 'e', 'f'): max_width = max( max_width, self._font_render_string('#{0}'.format(i * 6)).get_width() ) char = math.ceil(max_width / self._input_underline_size) for i in range(10): # Find the best guess for fw = self._font_render_string(self._input_underline * int(char)).get_width() char += 1 if fw >= max_width: break self._input_underline_len = char
[docs] def clear(self) -> None: super(ColorInput, self).clear() self._previsualization_surface = None self._auto_separator_pos = [] if self._color_type == COLORINPUT_TYPE_HEX: super(ColorInput, self).set_value('#') self.change()
[docs] def set_value(self, color: Optional[Union[str, Tuple3IntType]]) -> None: if color is None: color = '' format_color = '' if self._color_type == COLORINPUT_TYPE_RGB: if color == '': super(ColorInput, self).set_value('') return assert isinstance(color, tuple), \ 'color in rgb format must be a tuple in (r,g,b) format' assert len(color) == 3, 'tuple must contain only 3 colors, R,G,B' r, g, b = color assert isinstance(r, int), 'red color must be an integer' assert isinstance(g, int), 'blue color must be an integer' assert isinstance(b, int), 'green color must be an integer' assert 0 <= r <= 255, 'red color must be between 0 and 255' assert 0 <= g <= 255, 'blue color must be between 0 and 255' assert 0 <= b <= 255, 'green color must be between 0 and 255' format_color = '{0}{3}{1}{3}{2}'.format(r, g, b, self._separator) self._auto_separator_pos = [0, 1] elif self._color_type == COLORINPUT_TYPE_HEX: text = str(color).strip() if text == '': format_color = '#' else: # Remove all invalid chars valid_text = '' for ch in text: if ch in self._valid_chars: valid_text += ch text = valid_text # Check if the color is valid count_hash = 0 for ch in text: if ch == '#': count_hash += 1 if count_hash == 1: assert text[0] == '#', 'color format must be "#RRGGBB"' if count_hash == 0: text = '#' + text assert len(text) == 7, \ 'invalid color, only formats "#RRGGBB" and "RRGGBB" are allowed' format_color = text super(ColorInput, self).set_value(format_color) self._format_hex()
[docs] def get_value(self, as_string: bool = False) -> Union[str, Tuple3IntType]: """ Return the color value as a tuple or red blue and green channels. .. note:: If the data is invalid the widget returns ``(-1, -1, -1)``. :param as_string: If ``True`` returns the widget value as plain text :return: Color tuple as (R, G, B) or color string """ assert isinstance(as_string, bool) if as_string: return self._input_string if self._color_type == COLORINPUT_TYPE_RGB: color = self._input_string.split(self._separator) if len(color) == 3 and color[0] != '' and color[1] != '' and color[2] != '': r, g, b = int(color[0]), int(color[1]), int(color[2]) if 0 <= r <= 255 and 0 <= g <= 255 and 0 <= g <= 255: return r, g, b elif self._color_type == COLORINPUT_TYPE_HEX: if len(self._input_string) == 7: color = self._input_string[1:] color = tuple(int(color[i:i + 2], 16) for i in (0, 2, 4)) return color[0], color[1], color[2] return -1, -1, -1
[docs] def is_valid(self) -> bool: """ Return ``True`` if the current value of the input is a valid color or not. :return: ``True`` if valid """ r, g, b = self.get_value() if r == -1 or g == -1 or b == -1: return False return True
def _draw(self, surface: 'pygame.Surface') -> None: super(ColorInput, self)._draw(surface) # This calls _render() # Draw previsualization box if self._previsualization_surface is not None: posx = self._rect.x + self._rect.width \ - self._prev_width_factor * self._rect.height \ + self._rect.height / 10 posy = self._rect.y surface.blit(self._previsualization_surface, (int(posx), int(posy))) def _render(self) -> Optional[bool]: render_text = super(ColorInput, self)._render() # Maybe TextInput did not rendered, so this has to be changed self._rect.width, self._rect.height = self._surface.get_size() if not self._dynamic_width or \ (self._dynamic_width and self._previsualization_surface is not None): self._rect.width += self._prev_width_factor * self._rect.height \ + self._prev_margin # Render the previsualization box r, g, b = self.get_value() if not self.is_valid(): # Remove previsualization if invalid color self._previsualization_surface = None return render_text # If previsualization surface is None or the color changed if self._last_r != r or self._last_b != b or self._last_g != g or \ self._previsualization_surface is None: width = self._prev_width_factor * self._rect.height if width == 0 or self._rect.height == 0: self._previsualization_surface = None else: self._previsualization_surface = make_surface(width, self._rect.height) self._previsualization_surface.fill((r, g, b)) self._last_r = r self._last_g = g self._last_b = b if self._dynamic_width: self._rect.width += self._prev_width_factor * self._rect.height \ + self._prev_margin return render_text def _format_hex(self) -> None: """ Apply hex format. :return: None """ if self._color_type != COLORINPUT_TYPE_HEX or \ self._hex_format == COLORINPUT_HEX_FORMAT_NONE: return elif self._hex_format == COLORINPUT_HEX_FORMAT_LOWER: self._input_string = self._input_string.lower() elif self._hex_format == COLORINPUT_HEX_FORMAT_UPPER: self._input_string = self._input_string.upper()
[docs] def update(self, events: EventVectorType) -> bool: self.apply_update_callbacks(events) if self.readonly or not self.is_visible(): return False input_str = self._input_string cursor_pos = self._cursor_position disable_remove_separator = True key = '' # Pressed key if self._color_type == COLORINPUT_TYPE_RGB: for event in events: # User writes if event.type == pygame.KEYDOWN and self._keyboard_enabled: # Check if any key is pressed, if True the event is invalid if not check_key_pressed_valid(event): return True if disable_remove_separator and len(input_str) > 0 and \ len(input_str) > cursor_pos and ( '{0}{0}'.format(self._separator) not in input_str or input_str[cursor_pos] == self._separator and len(input_str) == cursor_pos + 1 ): # Backspace button, delete text from right if event.key == pygame.K_BACKSPACE: if len(input_str) >= 1 and \ input_str[cursor_pos - 1] == self._separator: return True # Delete button, delete text from left elif event.key == pygame.K_DELETE: if input_str[cursor_pos] == self._separator: return True # Verify only on user key input, the rest of events are checked # by TextInput on super call key = str(event.unicode) if key in self._valid_chars: new_string = ( self._input_string[:self._cursor_position] + key + self._input_string[self._cursor_position:] ) # Cannot be separator at first if len(input_str) == 0 and key == self._separator: return False if len(input_str) > 1: # Check separators if key == self._separator: # If more than 2 separators total_separator = 0 for ch in input_str: if ch == self._separator: total_separator += 1 if total_separator >= 2: return False # Check the number between the current separators, # this number must be between 0-255 if key != self._separator: pos_before = 0 pos_after = 0 for i in range(cursor_pos): if new_string[cursor_pos - i - 1] == self._separator: pos_before = cursor_pos - i break for i in range(len(new_string) - cursor_pos): if new_string[cursor_pos + i] == self._separator: pos_after = cursor_pos + i break if pos_after == 0: pos_after = len(new_string) num = new_string[pos_before:pos_after].replace(',', '') if num == '': num = '0' if int(num) > 255: # Number exceeds 25X return False # User adds 0 at left, example: 12 -> 012 if num != str(int(num)) and key == '0': return False if len(num) > 3: # Number like 0XXX return False elif self._color_type == COLORINPUT_TYPE_HEX: self._format_hex() for event in events: # User writes if event.type == pygame.KEYDOWN and self._keyboard_enabled: # Check if any key is pressed, if True the event is invalid if not check_key_pressed_valid(event): return True # Backspace button, delete text from right if event.key == pygame.K_BACKSPACE: if cursor_pos == 1: return True # Delete button, delete text from left elif event.key == pygame.K_DELETE: if cursor_pos == 0: return True # Verify only on user key input, the rest of events are checked # by TextInput on super call key = str(event.unicode) if key in self._valid_chars: if key == '#': return True if cursor_pos == 0: return True # Update updated = super(ColorInput, self).update(events) # After if self._color_type == COLORINPUT_TYPE_RGB: total_separator = 0 for ch in input_str: if ch == self._separator: total_separator += 1 # Adds auto separator if key == '0' and len(self._input_string) == self._cursor_position and \ total_separator < 2 and \ (len(self._input_string) == 1 or (len(self._input_string) > 2 and self._input_string[ self._cursor_position - 2] == self._separator)): self._push_key_input(self._separator, sounds=False) # This calls .onchange() # Check number is valid (fix) because sometimes the user can type # too fast and avoid analysis of the text colors = self._input_string.split(self._separator) for c in colors: if len(c) > 0 and (int(c) > 255 or int(c) < 0): self._input_string = input_str self._cursor_position = cursor_pos break if len(colors) == 3: self._auto_separator_pos = [0, 1] # Add an auto separator if the number can't continue growing and the cursor # is at the end of the line if total_separator < 2 and len(self._input_string) == self._cursor_position: auto_pos = len(colors) - 1 last_num = colors[auto_pos] if (len(last_num) == 2 and int(last_num) > 25 or len(last_num) == 3 and int(last_num) <= 255) and \ auto_pos not in self._auto_separator_pos: self._push_key_input(self._separator, sounds=False) # This calls .onchange() self._auto_separator_pos.append(auto_pos) # If the user cleared all the string, reset auto separator if total_separator == 0 and \ (len(self._input_string) < 2 or len(self._input_string) == 2 and int(colors[0]) <= 25): self._auto_separator_pos = [] return updated