Source code for ui.char_input
from time import sleep
from helpers import setup_logger
logger = setup_logger(__name__, "warning")
import string
from utils import to_be_foreground
from canvas import Canvas
from base_ui import BaseUIElement
[docs]class CharArrowKeysInput(BaseUIElement):
"""
Implements a character input dialog which allows to input a character string using arrow keys to scroll through characters
"""
chars = string.ascii_lowercase
Chars = string.ascii_uppercase
numbers = '0123456789'
hexadecimals = '0123456789ABCDEF'
specials = "!\"#$%&'()[]<>*+,-./:;=?^_|"
space = ' '
backspace = chr(0x08)
mapping = {
'][c':chars,
'][C':Chars,
'][n':numbers,
'][S':space,
'][b':backspace,
'][h':hexadecimals,
'][s':specials}
in_foreground = False
value = []
position = 0
cancel_flag = False
charmap = ""
[docs] def __init__(self, i, o, message="Value:", value="", allowed_chars=['][S', '][c', '][C', '][s', '][n'], name="CharArrowKeysInput", initial_value=""):
"""Initialises the CharArrowKeysInput object.
Args:
* ``i``, ``o``: input&output device objects
Kwargs:
* ``value``: Value to be edited. If not set, will start with an empty string.
* ``allowed_chars``: Characters to be used during input. Is a list of strings designating ranges which can be the following:
* '][c' for lowercase ASCII characters
* '][C' for uppercase ASCII characters
* '][s' for special characters
* '][S' for space
* '][n' for numbers
* '][h' for hexadecimal characters (0-F)
If a string does not designate a range of characters, it'll be added to character map as-is.
* ``message``: Message to be shown in the first row of the display
* ``name``: UI element name which can be used internally and for debugging.
"""
BaseUIElement.__init__(self, i, o, name)
self.message = message
self.allowed_chars = allowed_chars
self.allowed_chars.append("][b") #Adding backspace by default
self.generate_charmap()
#Support for obsolete attribute
if not value and initial_value:
value = initial_value
if type(value) != str:
raise ValueError("CharArrowKeysInput needs a string!")
self.value = list(value)
self.char_indices = [] #Fixes a bug with char_indices remaining from previous input ( 0_0 )
for char in self.value:
self.char_indices.append(self.charmap.index(char))
self.set_view()
def set_view(self):
if "b&w" in self.o.type:
view_class = GraphicalView
elif "char" in self.o.type:
view_class = TextView
else:
raise ValueError("Unsupported display type: {}".format(repr(self.o.type)))
self.view = view_class(self.o, self)
def get_return_value(self):
if self.cancel_flag:
return None
else:
return ''.join(self.value) #Making string from the list we have
def idle_loop(self):
sleep(0.1)
@property
def is_active(self):
return self.in_foreground
def print_value(self):
""" A debug method. Useful for hooking up to an input event so that you can see current value. """
logger.info(self.value)
@to_be_foreground
def move_up(self):
"""Changes the current character to the next character in the charmap"""
while len(self.char_indices) <= self.position:
self.char_indices.append(0)
self.value.append(self.charmap[0])
char_index = self.char_indices[self.position]
if char_index >= (len(self.charmap)-1):
char_index = 0
else:
char_index += 1
self.char_indices[self.position] = char_index
self.value[self.position] = self.charmap[char_index]
self.refresh()
@to_be_foreground
def move_down(self):
"""Changes the current character to the previous character in the charmap"""
while len(self.char_indices) <= self.position:
self.char_indices.append(0)
self.value.append(self.charmap[0])
char_index = self.char_indices[self.position]
if char_index == 0:
char_index = len(self.charmap) - 1
else:
char_index -= 1
self.char_indices[self.position] = char_index
self.value[self.position] = self.charmap[char_index]
self.refresh()
@to_be_foreground
def move_right(self):
"""Moves cursor to the next element. """
self.check_for_backspace()
self.position += 1
if self.view.last_displayed_char < self.position: #Went too far to the part of the value that isn't currently displayed
self.view.last_displayed_char = self.position
self.view.first_displayed_char = self.position - self.o.cols
self.refresh()
@to_be_foreground
def move_left(self):
"""Moves cursor to the previous element. If first element is chosen, exits and makes the element return None."""
self.check_for_backspace()
if self.position == 0:
self.exit()
return
self.position -= 1
if self.view.first_displayed_char > self.position: #Went too far back to the part that's not currently displayed
self.view.first_displayed_char = self.position
self.view.last_displayed_char = self.position + self.o.cols
self.refresh()
@to_be_foreground
def accept_value(self):
"""Selects the currently active number value, making activate() return it."""
self.check_for_backspace()
logger.debug("Value accepted")
self.deactivate()
@to_be_foreground
def exit(self):
"""Exits discarding all the changes to the value."""
logger.debug("{} exited without changes".format(self.name))
self.cancel_flag = True
self.deactivate()
def generate_keymap(self):
return {
"KEY_RIGHT": 'move_right',
"KEY_UP": 'move_up',
"KEY_DOWN": 'move_down',
"KEY_LEFT": 'move_left',
"KEY_ENTER": 'accept_value'
}
def generate_charmap(self):
for value in self.allowed_chars:
if value in self.mapping.keys():
self.charmap += self.mapping[value]
else:
self.charmap += value
def check_for_backspace(self):
for i, char_value in enumerate(self.value):
if char_value == self.backspace:
self.value.pop(i)
self.char_indices.pop(i)
@to_be_foreground
def refresh(self):
self.view.refresh()
logger.debug("{}: refreshed data on display".format(self.name))
class TextView(object):
last_displayed_char = 0
first_displayed_char = 0
def __init__(self, o, el):
self.o = o
self.el = el
self.last_displayed_char = self.o.cols
def get_displayed_data(self):
"""
Formats the value and the message to show it on the screen,
then returns a list that can be directly used by o.display_data.
Uses HD44780-specific characters.
"""
if self.first_displayed_char >= len(self.el.value): #Value is off-screen
value = ""
else:
value = ''.join(self.el.value)[self.first_displayed_char:][:self.o.cols]
return [self.el.message, value]
def convert_chars_to_hd44780_charset(self, message, value):
value = value.replace(self.el.backspace, chr(0x7f))
value = value.replace(' ', chr(255)) #Displaying all spaces as black boxes
return message, value
def refresh(self):
self.o.noCursor()
#self.o.cursor()# Only needed for testing TextView on luma.oled
displayed_data = self.convert_chars_to_hd44780_charset( *self.get_displayed_data() )
self.o.display_data(*displayed_data)
self.o.cursor()
class GraphicalView(TextView):
def get_image(self):
c = Canvas(self.o)
#Getting displayed data, drawing it
lines = self.get_displayed_data()
for i, line in enumerate(lines):
y = (i*self.o.char_height - 1) if i != 0 else 0
c.text(line, (2, y))
#Calculating the cursor dimensions
c_x1 = (self.el.position-self.first_displayed_char) * self.o.char_width
c_x2 = c_x1 + self.o.char_width
c_y1 = self.o.char_height * 1 #second line
c_y2 = c_y1 + self.o.char_height
#Some readability adjustments
cursor_dims = ( c_x1, c_y1, c_x2 + 2, c_y2 + 1 )
#Drawing the cursor
c.invert_rect(cursor_dims)
return c.get_image()
def refresh(self):
self.o.display_image(self.get_image())