from time import sleep
from zpui_lib.ui.base_ui import BaseUIElement
from zpui_lib.ui.canvas import Canvas, swap_colors, expand_coords
from zpui_lib.ui.funcs import format_for_screen as ffs
from zpui_lib.helpers import setup_logger
logger = setup_logger(__name__, "info")
[docs]class DialogBox(BaseUIElement):
"""Implements a dialog box with given values (or some default ones if chosen)."""
view = None
value_selected = False
selected_option = 0
default_options = {"y":["Yes", True], 'n':["No", False], 'c':["Cancel", None]}
start_option = 0
[docs] def __init__(self, values, i, o, message="Are you sure?", name="DialogBox", **kwargs):
"""Initialises the DialogBox object.
Args:
* ``values``: values to be used. Should be a list of ``[label, returned_value]`` pairs.
* You can also pass a string "yn" to get "Yes(True), No(False)" options, or "ync" to get "Yes(True), No(False), Cancel(None)" options.
* Values put together with spaces between them shouldn't be longer than the screen's width.
* ``i``, ``o``: input&output device objects
Kwargs:
* ``message``: Message to be shown on the first line of the screen when UI element is activated
* ``name``: UI element name which can be used internally and for debugging.
"""
BaseUIElement.__init__(self, i, o, name, **kwargs)
if isinstance(values, basestring):
self.values = []
for char in values:
self.values.append(self.default_options[char])
#value_str = " ".join([value[0] for value in values])
#assert(len(value_str) <= o.cols, "Resulting string too long for the display!")
else:
if not type(values) in (list, tuple):
raise ValueError("Unsupported 'values' argument - needs a list, supplied {}".format(repr(values)))
if not values:
raise ValueError("Empty/invalid 'values' argument!")
for i, value in enumerate(values):
if isinstance(value, basestring) and value in self.default_options:
#"y", "n" or "c" supplied as a shorthand for one of three default arguments
values[i] = self.default_options[value]
self.values = values
self.message = message
self.set_view()
# Keymap will depend on view
self.set_default_keymap()
def set_view(self):
if "b&w" in self.o.type:
if self.o.width < 240 or self.o.height < 240:
view_class = GraphicalView
else: # screen large enough!
view_class = FancyGraphicalView
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)
[docs] def set_start_option(self, option_number):
"""
Allows you to set position of the option that'll be selected upon DialogBox activation.
"""
self.start_option = option_number
[docs] def before_activate(self):
self.value_selected = False
self.selected_option = self.start_option
@property
def is_active(self):
return self.in_foreground
[docs] def get_return_value(self):
if self.value_selected:
return self.values[self.selected_option][1]
else:
return None
[docs] def idle_loop(self):
sleep(0.1)
[docs] def generate_keymap(self):
km = {"KEY_ENTER": 'accept_value'}
scroll_is_vertical = getattr(self.view, 'scroll_is_vertical', False)
if scroll_is_vertical:
km.update({
"KEY_DOWN": 'move_right',
"KEY_UP": 'move_left',
"KEY_LEFT": 'key_deactivate',
})
else:
km.update({
"KEY_RIGHT": 'move_right',
"KEY_LEFT": 'move_left',
})
return km
def move_left(self):
scroll_is_vertical = getattr(self.view, 'scroll_is_vertical', False)
if self.selected_option == 0:
if not scroll_is_vertical:
self.key_deactivate()
return
self.selected_option -= 1
self.refresh()
def move_right(self):
if self.selected_option == len(self.values)-1:
return
self.selected_option += 1
self.refresh()
def accept_value(self):
self.value_selected = True
self.key_deactivate()
[docs] def refresh(self):
self.view.refresh()
[docs] def deactivate(self):
# if the previous image is present, write it back, to improve UX and avoid consequent dialog boxes writing over each other
image = getattr(self.view, "previous_image", None)
if image != None:
self.o.display_image(image)
BaseUIElement.deactivate(self)
class TextView(object):
scroll_is_vertical = False
def __init__(self, o, el):
self.o = o
self.el = el
self.process_values()
def process_values(self):
labels = [label for label, value in self.el.values]
label_string = " ".join(labels)
if len(label_string) > self.o.cols:
raise ValueError("DialogBox {}: all values combined are longer than screen's width".format(self.el.name))
self.right_offset = (self.o.cols - len(label_string))//2
self.displayed_label = " "*self.right_offset+label_string
#Need to go through the string to mark the first places because we need to remember where to put the cursors
labels = [label for label, value in self.el.values]
current_position = self.right_offset
self.positions = []
for label in labels:
self.positions.append(current_position)
current_position += len(label) + 1
def refresh(self):
self.o.noCursor()
self.o.setCursor(1, self.positions[self.el.selected_option])
self.o.display_data(self.el.message, self.displayed_label)
self.o.cursor()
class GraphicalView(TextView):
scroll_is_vertical = True
char_height = 8
char_width = 6
font=None
def process_values(self):
self.positions = []
labels = [label for label, value in self.el.values]
for label in labels:
label_width = len(label)*self.char_width
label_start = (self.o.width - label_width)//2
if label_start < 0: label_start = 0
self.positions.append(label_start)
def get_image(self):
c = Canvas(self.o)
#Drawing text
chunk_y = 0
cols = self.o.cols
formatted_message = ffs(self.el.message, self.o.cols)
if len(formatted_message)*(self.char_height+2) > self.o.height - self.char_height - 2:
raise ValueError("DialogBox {}: message is too long to fit on the screen: {}".format(self.el.name, formatted_message))
for line in formatted_message:
c.text(line, (0, chunk_y), font=self.font)
chunk_y += self.char_height + 2
first_label_y = chunk_y
for i, value in enumerate(self.el.values):
label = value[0]
label_start = self.positions[i]
c.text(label, (label_start, chunk_y), font=self.font)
chunk_y += self.char_height + 2
#Calculating the cursor dimensions
first_char_x = self.positions[self.el.selected_option]
option_length = len( self.el.values[self.el.selected_option][0] ) * self.char_width
c_x1 = first_char_x - 2
c_x2 = c_x1 + option_length + 2
c_y1 = first_label_y + self.el.selected_option*(2 + self.char_height)
c_y2 = c_y1 + self.char_height
#Some readability adjustments
cursor_dims = ( c_x1, c_y1, c_x2 + 2, c_y2 + 2 )
#Drawing the cursor
cursor_image = c.get_image(coords=cursor_dims)
# inverting colors - background to foreground and vice-versa
cursor_image = swap_colors(cursor_image, c.default_color, c.background_color, c.background_color, c.default_color)
c.paste(cursor_image, coords=cursor_dims[:2])
return c.get_image()
def refresh(self):
self.o.display_image(self.get_image())
class pt16GraphicalView(GraphicalView):
scroll_is_vertical = True
char_height = 16
char_width = 8
font = ("Fixedsys62.ttf", char_height)
class FancyGraphicalView(GraphicalView):
scroll_is_vertical = False
char_height = 16
char_width = 8
font = ("Fixedsys62.ttf", char_height)
previous_image = None
first_run = True
box_height_mul = 1.4
box_width_mul = 1.2
clear_mul = 0.025 # 5% of canvas height/width, since it's applied to both sides
def get_image(self):
c = Canvas(self.o)
# only save image on first run
if self.o.current_image or self.previous_image:
# maybe TODO next time get a previous-context image? todo allow that if the image is from the "main" context?
if self.first_run:
#print("Saving previous image")
self.previous_image = self.o.current_image
self.first_run = False
#if self.previous_image != None:
c.paste(self.previous_image, (0, 0))
else:
if getattr(self.el, "context", None):
context = self.el.context
self.previous_image = context.get_previous_context_image()
if self.previous_image:
c.paste(self.previous_image, (0, 0))
# shamelessly reusing code from the simpler graphicalview
text_height = 0
cols = (self.o.width)//self.char_width
formatted_message = ffs(self.el.message, cols)
max_len = max([len(m) for m in formatted_message])
text_width = max_len*self.char_width
# this checks if the text overfills the textbox vertically, but I don't wanna deal with it for this view just yet
#if len(formatted_message)*(char_height+2) > self.o.height - char_height - 2:
# raise ValueError("DialogBox {}: message is too long to fit on the screen: {}".format(self.el.name, formatted_message))
for line in formatted_message:
text_height += self.char_height + 2
labels_height = int(self.char_height * 1.5)
labels_width = 0
# pre-calculating width of all labels
for i, value in enumerate(self.el.values):
label = value[0]
label_start = self.positions[i]
labels_width += len(label) * self.char_width
# now, calculate the outer box dimensions
box_width_og = max(text_width, labels_width)
box_width = int(box_width_og * self.box_width_mul)
box_height_og = text_height + labels_height
box_height = int(box_height_og * self.box_height_mul)
box_coords = c.center_box(box_width, box_height, return_four=True)
e_coords = int(c.width*self.clear_mul), int(c.height*self.clear_mul), int(c.width*self.clear_mul), int(c.height*self.clear_mul)
clear_coords = expand_coords(box_coords, e_coords)
c.clear(clear_coords)
c.rectangle(box_coords)
# actually drawing text
top_x, top_y = box_coords[:2]
padding_y = int((box_height-box_height_og)//3)
total_padding_x = int(box_width-box_width_og)
text_y = top_y + padding_y
for line in formatted_message:
text_width = len(line)*self.char_width
x = top_x + box_width - text_width
c.text(line, (x, text_y), font=self.font)
text_y += self.char_height
# actually drawing labels
label_y = top_y + box_height - (labels_height+padding_y)
x = top_x + box_width - text_width
label_padding_x = int(total_padding_x // (len(self.el.values)+1))
label_start = top_x + box_width - labels_width - ((len(self.el.values)+1)*label_padding_x)
for i, value in enumerate(self.el.values):
label = value[0]
label_width = len(label)*self.char_width
if i == self.el.selected_option:
c.text(label, (label_start, label_y), font=self.font)
cursor_dims = (label_start, label_y, label_start+label_width, label_y + self.char_height)
#cursor_dims = (label_start-1, label_y-1, label_start+label_width+2, label_y + self.char_height+2)
# Drawing the cursor
cursor_image = c.get_image(coords=cursor_dims)
# inverting colors - background to foreground and vice-versa
cursor_image = swap_colors(cursor_image, c.default_color, c.background_color, c.background_color, c.default_color)
c.paste(cursor_image, coords=cursor_dims[:2])
else:
c.rectangle_wh((label_start-1, label_y-1, label_width+2, self.char_height+2),)
c.text(label, (label_start, label_y), font=self.font)
label_start += label_width + label_padding_x
return c.get_image()