menu: redesigned name scroller & menu rendering (#3837)

Signed-off-by: Janar Sööt <janar.soot@gmail.com>
This commit is contained in:
Janar Sööt 2021-02-20 18:31:03 +02:00 committed by GitHub
parent 7e21350989
commit 5a7fbe671e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 95 additions and 93 deletions

View File

@ -226,6 +226,7 @@ class PrinterLCD:
else: else:
# write glyph # write glyph
pos += self.lcd_chip.write_glyph(pos, row, text) pos += self.lcd_chip.write_glyph(pos, row, text)
return pos
def draw_progress_bar(self, row, col, width, value): def draw_progress_bar(self, row, col, width, value):
pixels = -1 << int(width * 8 * (1. - value) + .5) pixels = -1 << int(width * 8 * (1. - value) + .5)
pixels |= (1 << (width * 8 - 1)) | 1 pixels |= (1 << (width * 8 - 1)) | 1

View File

@ -4,7 +4,7 @@
# Copyright (C) 2020 Janar Sööt <janar.soot@gmail.com> # Copyright (C) 2020 Janar Sööt <janar.soot@gmail.com>
# #
# This file may be distributed under the terms of the GNU GPLv3 license. # This file may be distributed under the terms of the GNU GPLv3 license.
import os, logging, ast import os, logging, ast, re
from string import Template from string import Template
from . import menu_keys from . import menu_keys
@ -24,8 +24,7 @@ class MenuElement(object):
raise error( raise error(
'Abstract MenuElement cannot be instantiated directly') 'Abstract MenuElement cannot be instantiated directly')
self._manager = manager self._manager = manager
self.cursor = '>' self._cursor = '>'
self._scroll = True
# set class defaults and attributes from arguments # set class defaults and attributes from arguments
self._index = kwargs.get('index', None) self._index = kwargs.get('index', None)
self._enable = kwargs.get('enable', True) self._enable = kwargs.get('enable', True)
@ -50,12 +49,9 @@ class MenuElement(object):
self._ns = Template( self._ns = Template(
'menu ' + kwargs.get('ns', __id)).safe_substitute(__id=__id) 'menu ' + kwargs.get('ns', __id)).safe_substitute(__id=__id)
self._last_heartbeat = None self._last_heartbeat = None
self.__scroll_offs = 0 self.__scroll_pos = None
self.__scroll_diff = 0 self.__scroll_request_pending = False
self.__scroll_dir = None self.__scroll_next = 0
self.__last_state = True
# display width is used and adjusted by cursor size
self._width = self.manager.cols - len(self._cursor)
# menu scripts # menu scripts
self._scripts = {} self._scripts = {}
# init # init
@ -110,7 +106,6 @@ class MenuElement(object):
# get default menu context # get default menu context
context = self.manager.get_context(cxt) context = self.manager.get_context(cxt)
context['menu'].update({ context['menu'].update({
'width': self._width,
'ns': self.get_ns() 'ns': self.get_ns()
}) })
return context return context
@ -122,63 +117,56 @@ class MenuElement(object):
# Called when a item is selected # Called when a item is selected
def select(self): def select(self):
self.__clear_scroll() self.__reset_scroller()
def heartbeat(self, eventtime): def heartbeat(self, eventtime):
self._last_heartbeat = eventtime self._last_heartbeat = eventtime
state = bool(int(eventtime) & 1) if eventtime >= self.__scroll_next:
if self.__last_state ^ state: self.__scroll_next = eventtime + 0.5
self.__last_state = state
if not self.is_editing(): if not self.is_editing():
self.__update_scroll(eventtime) self.__update_scroller()
def __clear_scroll(self): def __update_scroller(self):
self.__scroll_dir = None if self.__scroll_pos is None and self.__scroll_request_pending is True:
self.__scroll_diff = 0 self.__scroll_pos = 0
self.__scroll_offs = 0 elif self.__scroll_request_pending is True:
self.__scroll_pos += 1
self.__scroll_request_pending = False
elif self.__scroll_request_pending is False:
pass # hold scroll position
elif self.__scroll_request_pending is None:
self.__reset_scroller()
def __update_scroll(self, eventtime): def __reset_scroller(self):
if self.__scroll_dir == 0 and self.__scroll_diff > 0: self.__scroll_pos = None
self.__scroll_dir = 1 self.__scroll_request_pending = False
self.__scroll_offs = 0
elif self.__scroll_dir and self.__scroll_diff > 0:
self.__scroll_offs += self.__scroll_dir
if self.__scroll_offs >= self.__scroll_diff:
self.__scroll_dir = -1
elif self.__scroll_offs <= 0:
self.__scroll_dir = 1
else:
self.__clear_scroll()
def __name_scroll(self, s): def need_scroller(self, value):
if self.__scroll_dir is None: """
self.__scroll_dir = 0 Allows to control the scroller
self.__scroll_offs = 0 Parameters:
return s[ value (bool, None): True - inc. scroll pos. on next update
self.__scroll_offs:self._width + self.__scroll_offs False - hold scroll pos.
].ljust(self._width) None - reset the scroller
"""
self.__scroll_request_pending = value
def __slice_name(self, name, index):
chunks = []
for i, text in enumerate(re.split(r'(\~.*?\~)', name)):
if i & 1 == 0: # text
chunks += text
else: # glyph placeholder
chunks.append(text)
return "".join(chunks[index:])
def render_name(self, selected=False): def render_name(self, selected=False):
s = str(self._render_name()) name = str(self._render_name())
# scroller if selected and self.__scroll_pos is not None:
if self._width > 0: name = self.__slice_name(name, self.__scroll_pos)
self.__scroll_diff = len(s) - self._width
if (selected and self._scroll is True and self.is_scrollable()
and self.__scroll_diff > 0):
s = self.__name_scroll(s)
else: else:
self.__clear_scroll() self.__reset_scroller()
s = s[:self._width].ljust(self._width) return name
else:
self.__clear_scroll()
# add cursors
if selected and not self.is_editing():
s = self.cursor + s
elif selected and self.is_editing():
s = '*' + s
else:
s = ' ' + s
return s
def get_ns(self, name='.'): def get_ns(self, name='.'):
name = str(name).strip() name = str(name).strip()
@ -235,10 +223,6 @@ class MenuElement(object):
def cursor(self): def cursor(self):
return str(self._cursor)[:1] return str(self._cursor)[:1]
@cursor.setter
def cursor(self, value):
self._cursor = str(value)[:1]
@property @property
def manager(self): def manager(self):
return self._manager return self._manager
@ -256,7 +240,7 @@ class MenuContainer(MenuElement):
'Abstract MenuContainer cannot be instantiated directly') 'Abstract MenuContainer cannot be instantiated directly')
super(MenuContainer, self).__init__(manager, config, **kwargs) super(MenuContainer, self).__init__(manager, config, **kwargs)
self._populate_cb = kwargs.get('populate', None) self._populate_cb = kwargs.get('populate', None)
self.cursor = '>' self._cursor = '>'
self.__selected = None self.__selected = None
self._allitems = [] self._allitems = []
self._names = [] self._names = []
@ -413,8 +397,8 @@ class MenuContainer(MenuElement):
return self.select_at(index) return self.select_at(index)
# override # override
def render_container(self, nrows, eventtime): def draw_container(self, nrows, eventtime):
return [] pass
def __iter__(self): def __iter__(self):
return iter(self._items) return iter(self._items)
@ -614,9 +598,8 @@ class MenuList(MenuContainer):
# add back as first item # add back as first item
self.insert_item(self._itemBack, 0) self.insert_item(self._itemBack, 0)
def render_container(self, nrows, eventtime): def draw_container(self, nrows, eventtime):
manager = self.manager display = self.manager.display
lines = []
selected_row = self.selected selected_row = self.selected
# adjust viewport # adjust viewport
if selected_row is not None: if selected_row is not None:
@ -629,24 +612,51 @@ class MenuList(MenuContainer):
# clamps viewport # clamps viewport
self._viewport_top = max(0, min(self._viewport_top, len(self) - nrows)) self._viewport_top = max(0, min(self._viewport_top, len(self) - nrows))
try: try:
y = 0
for row in range(self._viewport_top, self._viewport_top + nrows): for row in range(self._viewport_top, self._viewport_top + nrows):
s = "" text = ""
prefix = ""
suffix = ""
if row < len(self): if row < len(self):
current = self[row] current = self[row]
selected = (row == selected_row) selected = (row == selected_row)
if selected: if selected:
current.heartbeat(eventtime) current.heartbeat(eventtime)
name = manager.stripliterals( text = current.render_name(selected)
manager.aslatin(current.render_name(selected))) # add prefix (selection indicator)
if isinstance(current, MenuList): if selected and not current.is_editing():
s += name[:manager.cols-1].ljust(manager.cols-1) prefix = current.cursor
s += '>' elif selected and current.is_editing():
prefix = '*'
else: else:
s += name prefix = ' '
lines.append(s[:manager.cols].ljust(manager.cols)) # add suffix (folder indicator)
if isinstance(current, MenuList):
suffix += '>'
# draw to display
plen = len(prefix)
slen = len(suffix)
width = self.manager.cols - plen - slen
# draw item prefix (cursor)
ppos = display.draw_text(y, 0, prefix, eventtime)
# draw item name
tpos = display.draw_text(y, ppos, text.ljust(width), eventtime)
# check scroller
if (selected and tpos > self.manager.cols
and current.is_scrollable()):
# scroll next
current.need_scroller(True)
else:
# reset scroller
current.need_scroller(None)
# draw item suffix
if suffix:
display.draw_text(
y, self.manager.cols - slen, suffix, eventtime)
# next display row
y += 1
except Exception: except Exception:
logging.exception('List rendering error') logging.exception('List drawing error')
return lines
class MenuVSDList(MenuList): class MenuVSDList(MenuList):
@ -829,21 +839,16 @@ class MenuManager:
container = self.menustack[self.stack_size() - lvl - 1] container = self.menustack[self.stack_size() - lvl - 1]
return container return container
def render(self, eventtime):
lines = []
self.update_context(eventtime)
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
container.heartbeat(eventtime)
lines = container.render_container(self.rows, eventtime)
return lines
def screen_update_event(self, eventtime): def screen_update_event(self, eventtime):
# screen update # screen update
if not self.is_running(): if not self.is_running():
return False return False
for y, line in enumerate(self.render(eventtime)): # draw menu
self.display.draw_text(y, 0, line, eventtime) self.update_context(eventtime)
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
container.heartbeat(eventtime)
container.draw_container(self.rows, eventtime)
return True return True
def up(self, fast_rate=False): def up(self, fast_rate=False):
@ -1057,10 +1062,6 @@ class MenuManager:
else: else:
return str(s) return str(s)
@classmethod
def asflatline(cls, s):
return ''.join(cls.aslatin(s).splitlines())
@classmethod @classmethod
def asflat(cls, s): def asflat(cls, s):
return cls.stripliterals(cls.asflatline(s)) return cls.stripliterals(''.join(cls.aslatin(s).splitlines()))