191 lines
8.0 KiB
Python
191 lines
8.0 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
"""This widget implements a text based input field"""
|
||
|
from __future__ import division
|
||
|
from __future__ import absolute_import
|
||
|
from __future__ import print_function
|
||
|
from __future__ import unicode_literals
|
||
|
from builtins import chr
|
||
|
from re import match
|
||
|
from asciimatics.event import KeyboardEvent, MouseEvent
|
||
|
from asciimatics.screen import Screen
|
||
|
from asciimatics.widgets.utilities import _find_min_start, _enforce_width, _get_offset
|
||
|
from asciimatics.widgets.widget import Widget
|
||
|
|
||
|
|
||
|
class Text(Widget):
|
||
|
"""
|
||
|
A Text widget is a single line input field.
|
||
|
|
||
|
It consists of an optional label and an entry box.
|
||
|
"""
|
||
|
|
||
|
__slots__ = ["_label", "_column", "_start_column", "_on_change", "_validator", "_hide_char",
|
||
|
"_max_length", "_readonly"]
|
||
|
|
||
|
def __init__(self, label=None, name=None, on_change=None, validator=None, hide_char=None,
|
||
|
max_length=None, readonly=False, **kwargs):
|
||
|
"""
|
||
|
:param label: An optional label for the widget.
|
||
|
:param name: The name for the widget.
|
||
|
:param on_change: Optional function to call when text changes.
|
||
|
:param validator: Optional definition of valid data for this widget.
|
||
|
This can be a function (which takes the current value and returns True for valid
|
||
|
content) or a regex string (which must match the entire allowed value).
|
||
|
:param hide_char: Character to use instead of what the user types - e.g. to hide passwords.
|
||
|
:param max_length: Optional maximum length of the field. If set, the widget will limit
|
||
|
data entry to this length.
|
||
|
:param readonly: Whether the widget prevents user input to change values. Default is False.
|
||
|
|
||
|
Also see the common keyword arguments in :py:obj:`.Widget`.
|
||
|
"""
|
||
|
super(Text, self).__init__(name, **kwargs)
|
||
|
self._label = label
|
||
|
self._column = 0
|
||
|
self._start_column = 0
|
||
|
self._on_change = on_change
|
||
|
self._validator = validator
|
||
|
self._hide_char = hide_char
|
||
|
self._max_length = max_length
|
||
|
self._readonly = readonly
|
||
|
|
||
|
def set_layout(self, x, y, offset, w, h):
|
||
|
# Do the usual layout work. then apply max length to resulting dimensions.
|
||
|
super(Text, self).set_layout(x, y, offset, w, h)
|
||
|
if self._max_length:
|
||
|
# Allow extra char for cursor, so contents don't scroll at required length
|
||
|
self._w = min(self._w, self._max_length + self._offset + 1)
|
||
|
|
||
|
def update(self, frame_no):
|
||
|
self._draw_label()
|
||
|
|
||
|
# Calculate new visible limits if needed.
|
||
|
self._start_column = min(self._start_column, self._column)
|
||
|
self._start_column += _find_min_start(self._value[self._start_column:self._column + 1],
|
||
|
self.width, self._frame.canvas.unicode_aware,
|
||
|
self._column >= self.string_len(self._value))
|
||
|
|
||
|
# Render visible portion of the text.
|
||
|
(colour, attr, background) = self._pick_colours("readonly" if self._readonly else "edit_text")
|
||
|
text = self._value[self._start_column:]
|
||
|
text = _enforce_width(text, self.width, self._frame.canvas.unicode_aware)
|
||
|
if self._hide_char:
|
||
|
text = self._hide_char[0] * len(text)
|
||
|
text += " " * (self.width - self.string_len(text))
|
||
|
self._frame.canvas.print_at(
|
||
|
text,
|
||
|
self._x + self._offset,
|
||
|
self._y,
|
||
|
colour, attr, background)
|
||
|
|
||
|
# Since we switch off the standard cursor, we need to emulate our own
|
||
|
# if we have the input focus.
|
||
|
if self._has_focus:
|
||
|
text_width = self.string_len(text[:self._column - self._start_column])
|
||
|
self._draw_cursor(
|
||
|
" " if self._column >= len(self._value) else self._hide_char[0] if self._hide_char
|
||
|
else self._value[self._column],
|
||
|
frame_no,
|
||
|
self._x + self._offset + text_width,
|
||
|
self._y)
|
||
|
|
||
|
def reset(self):
|
||
|
# Reset to original data and move to end of the text.
|
||
|
self._start_column = 0
|
||
|
self._column = len(self._value)
|
||
|
|
||
|
def process_event(self, event):
|
||
|
if isinstance(event, KeyboardEvent):
|
||
|
if event.key_code == Screen.KEY_BACK and not self._readonly:
|
||
|
if self._column > 0:
|
||
|
# Delete character in front of cursor.
|
||
|
self._set_and_check_value("".join([self._value[:self._column - 1],
|
||
|
self._value[self._column:]]))
|
||
|
self._column -= 1
|
||
|
elif event.key_code == Screen.KEY_DELETE and not self._readonly:
|
||
|
if self._column < len(self._value):
|
||
|
self._set_and_check_value("".join([self._value[:self._column],
|
||
|
self._value[self._column + 1:]]))
|
||
|
elif event.key_code == Screen.KEY_LEFT:
|
||
|
self._column -= 1
|
||
|
self._column = max(self._column, 0)
|
||
|
elif event.key_code == Screen.KEY_RIGHT:
|
||
|
self._column += 1
|
||
|
self._column = min(len(self._value), self._column)
|
||
|
elif event.key_code == Screen.KEY_HOME:
|
||
|
self._column = 0
|
||
|
elif event.key_code == Screen.KEY_END:
|
||
|
self._column = len(self._value)
|
||
|
elif event.key_code >= 32 and not self._readonly:
|
||
|
# Enforce required max length - swallow event if not allowed
|
||
|
if self._max_length is None or len(self._value) < self._max_length:
|
||
|
# Insert any visible text at the current cursor position.
|
||
|
self._set_and_check_value(chr(event.key_code).join([self._value[:self._column],
|
||
|
self._value[self._column:]]))
|
||
|
self._column += 1
|
||
|
else:
|
||
|
# Ignore any other key press.
|
||
|
return event
|
||
|
elif isinstance(event, MouseEvent):
|
||
|
# Mouse event - rebase coordinates to Frame context.
|
||
|
if event.buttons != 0:
|
||
|
if self.is_mouse_over(event, include_label=False):
|
||
|
self._column = (self._start_column +
|
||
|
_get_offset(self._value[self._start_column:],
|
||
|
event.x - self._x - self._offset,
|
||
|
self._frame.canvas.unicode_aware))
|
||
|
self._column = min(len(self._value), self._column)
|
||
|
self._column = max(0, self._column)
|
||
|
return None
|
||
|
# Ignore other mouse events.
|
||
|
return event
|
||
|
else:
|
||
|
# Ignore other events
|
||
|
return event
|
||
|
|
||
|
# If we got here, we processed the event - swallow it.
|
||
|
return None
|
||
|
|
||
|
def required_height(self, offset, width):
|
||
|
return 1
|
||
|
|
||
|
@property
|
||
|
def frame_update_count(self):
|
||
|
# Force refresh for cursor if needed.
|
||
|
return 5 if self._has_focus and not self._frame.reduce_cpu else 0
|
||
|
|
||
|
@property
|
||
|
def readonly(self):
|
||
|
"""
|
||
|
Whether this widget is readonly or not.
|
||
|
"""
|
||
|
return self._readonly
|
||
|
|
||
|
@readonly.setter
|
||
|
def readonly(self, new_value):
|
||
|
self._readonly = new_value
|
||
|
|
||
|
@property
|
||
|
def value(self):
|
||
|
"""
|
||
|
The current value for this Text.
|
||
|
"""
|
||
|
return self._value
|
||
|
|
||
|
@value.setter
|
||
|
def value(self, new_value):
|
||
|
self._set_and_check_value(new_value, reset=True)
|
||
|
|
||
|
def _set_and_check_value(self, new_value, reset=False):
|
||
|
# Only trigger the notification after we've changed the value.
|
||
|
old_value = self._value
|
||
|
self._value = new_value if new_value else ""
|
||
|
if reset:
|
||
|
self.reset()
|
||
|
if old_value != self._value and self._on_change:
|
||
|
self._on_change()
|
||
|
if self._validator:
|
||
|
if callable(self._validator):
|
||
|
self._is_valid = self._validator(self._value)
|
||
|
else:
|
||
|
self._is_valid = match(self._validator, self._value) is not None
|