menu: redesigned name scroller & menu rendering (#3837)
Signed-off-by: Janar Sööt <janar.soot@gmail.com>
This commit is contained in:
parent
7e21350989
commit
5a7fbe671e
|
@ -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
|
||||||
|
|
|
@ -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:
|
|
||||||
self.__clear_scroll()
|
|
||||||
s = s[:self._width].ljust(self._width)
|
|
||||||
else:
|
else:
|
||||||
self.__clear_scroll()
|
self.__reset_scroller()
|
||||||
# add cursors
|
return name
|
||||||
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()))
|
||||||
|
|
Loading…
Reference in New Issue