menu: enhancements

- changes that make easier to use menu as module UI
- new helper method for delayed callbacks
- method for getting the menu instance from display
- new action for sending menu:action events
- allow_without_selection option for cards

Signed-off-by: Janar Sööt <janar.soot@gmail.com>
This commit is contained in:
Janar Sööt 2018-12-20 21:09:19 +02:00 committed by Kevin O'Connor
parent 005cbe157a
commit 0cbe851777
3 changed files with 120 additions and 26 deletions

View File

@ -85,6 +85,7 @@
# and [respond response_info] command. Respond command will send '// response_info' to host.
#[menu input1]
#type: input
#name:
#cursor:
#width:
@ -197,3 +198,8 @@
# This way only simple menu items can be grouped.
# Example: 5,prt_time, prt_progress - elements prt_time and prt_progress are switched after 5s
# Example: msg,xpos|ypos - elements xpos and ypos are grouped and showed together when msg is disabled.
#use_cursor:
# This attribute accepts static boolean value.
# When enabled the menu system uses a cursor instead of blinking to visualize item selection
# and edit mode for this card. Cursor and placeholder is always added as item name prefix.
# The default is False. This parameter is optional.

View File

@ -49,6 +49,9 @@ class PrinterLCD:
self.gcode.register_command('M117', self.cmd_M117)
# Start screen update timer
self.reactor.update_timer(self.screen_update_timer, self.reactor.NOW)
# Get menu instance
def get_menu(self):
return self.menu
# Graphics drawing
def animate_glyphs(self, eventtime, x, y, glyph_name, do_animate):
frame = do_animate and int(eventtime) & 1

View File

@ -59,6 +59,13 @@ class MenuElement(object):
# override
def is_enabled(self):
return self.eval_enable()
# override
def reset_editing(self):
pass
def eval_enable(self):
return self._parse_bool(self._enable)
def init(self):
@ -246,6 +253,11 @@ class MenuContainer(MenuElement):
def is_editing(self):
return any([item.is_editing() for item in self._items])
def reset_editing(self):
for item in self._items:
if item.is_editing():
item.reset_editing()
def _lookup_item(self, item):
if isinstance(item, str):
s = item.strip()
@ -514,6 +526,9 @@ class MenuInput(MenuCommand):
def is_editing(self):
return self._input_value is not None
def reset_editing(self):
self.reset_value()
def _onchange(self):
self._manager.queue_gcode(self.get_gcode())
@ -573,6 +588,7 @@ class MenuGroup(MenuContainer):
self._sep = sep
self._show_back = False
self.selected = None
self.use_cursor = self._asbool(config.get('use_cursor', 'false'))
self.items = config.get('items', '')
def is_accepted(self, item):
@ -599,9 +615,20 @@ class MenuGroup(MenuContainer):
def _render_item(self, item, selected=False, scroll=False):
name = "%s" % str(item.render(scroll))
if selected and not self.is_editing():
name = name if self._manager.blink_slow_state else ' '*len(name)
if self.use_cursor:
name = (item.cursor if isinstance(item, MenuElement)
else MenuCursor.SELECT) + name
else:
name = (name if self._manager.blink_slow_state
else ' '*len(name))
elif selected and self.is_editing():
name = name if self._manager.blink_fast_state else ' '*len(name)
if self.use_cursor:
name = MenuCursor.EDIT + name
else:
name = (name if self._manager.blink_fast_state
else ' '*len(name))
elif self.use_cursor:
name = MenuCursor.NONE + name
return name
def _render(self):
@ -626,6 +653,9 @@ class MenuGroup(MenuContainer):
logging.exception("Call selected error")
return res
def reset_editing(self):
return self._call_selected('reset_editing')
def is_editing(self):
return self._call_selected('is_editing')
@ -799,6 +829,8 @@ class MenuCard(MenuGroup):
def __init__(self, manager, config, namespace=''):
super(MenuCard, self).__init__(manager, config, namespace)
self.content = config.get('content')
self._allow_without_selection = self._asbool(
config.get('allow_without_selection', 'true'))
if not self.items:
self.content = self._parse_content_items(self.content)
@ -852,6 +884,8 @@ class MenuCard(MenuGroup):
if self.selected is not None:
self.selected = (
(self.selected % len(self)) if len(self) > 0 else None)
if self._allow_without_selection is False and self.selected is None:
self.selected = 0 if len(self) > 0 else None
items = []
for i, item in enumerate(self):
@ -947,6 +981,7 @@ class MenuManager:
self.timeout_idx = 0
self.lcd_chip = lcd_chip
self.printer = config.get_printer()
self.pconfig = self.printer.lookup_object('configfile')
self.gcode = self.printer.lookup_object('gcode')
self.gcode_queue = []
self.parameters = {}
@ -1002,21 +1037,12 @@ class MenuManager:
self.gcode.register_mux_command("MENU", "DO", 'dump', self.cmd_DO_DUMP,
desc=self.cmd_DO_help)
# Parse local config file in same directory as current module
pconfig = self.printer.lookup_object('configfile')
localname = os.path.join(os.path.dirname(__file__), 'menu.cfg')
localconfig = pconfig.read_config(localname)
# Load items from local config
self.load_menuitems(localconfig)
# Load local config file in same directory as current module
self.load_config(os.path.dirname(__file__), 'menu.cfg')
# Load items from main config
self.load_menuitems(config)
# Load menu root
if self._root is not None:
self.root = self.lookup_menuitem(self._root)
if isinstance(self.root, MenuDeck):
self._autorun = True
self.load_root()
def printer_state(self, state):
if state == 'ready':
@ -1071,6 +1097,45 @@ class MenuManager:
return (self._autorun is True and self.root is not None
and self.stack_peek() is self.root and self.selected == 0)
def restart_root(self, root=None, force_exit=True):
if self.is_running():
self.exit(force_exit)
self.load_root(root)
def load_root(self, root=None, autorun=False):
root = self._root if root is None else root
if root is not None:
self.root = self.lookup_menuitem(root)
if isinstance(self.root, MenuDeck):
self._autorun = True
else:
self._autorun = autorun
def register_object(self, obj, name=None, override=False):
"""Register an object with a "get_status" callback"""
if obj is not None:
if name is None:
name = obj.__class__.__name__
if override or name not in self.objs:
self.objs[name] = obj
def unregister_object(self, name):
"""Unregister an object from "get_status" callback list"""
if name is not None:
if not isinstance(name, str):
name = name.__class__.__name__
if name in self.objs:
self.objs.pop(name)
def after(self, timeout, callback, *args):
"""Helper method for reactor.register_callback.
The callback will be executed once after given timeout (sec)."""
def callit(eventtime):
callback(eventtime, *args)
reactor = self.printer.get_reactor()
starttime = reactor.monotonic() + max(0., float(timeout))
reactor.register_callback(callit, starttime)
def is_running(self):
return self.running
@ -1106,15 +1171,16 @@ class MenuManager:
def update_parameters(self, eventtime):
self.parameters = {}
objs = dict(self.objs)
# getting info this way is more like hack
# all modules should have special reporting method (maybe get_status)
# for available parameters
# Only 2 level dot notation
for name in self.objs.keys():
for name in objs.keys():
try:
if self.objs[name] is not None:
class_name = str(self.objs[name].__class__.__name__)
get_status = getattr(self.objs[name], "get_status", None)
if objs[name] is not None:
class_name = str(objs[name].__class__.__name__)
get_status = getattr(objs[name], "get_status", None)
if callable(get_status):
self.parameters[name] = get_status(eventtime)
else:
@ -1123,7 +1189,7 @@ class MenuManager:
self.parameters[name].update({'is_enabled': True})
# get additional info
if class_name == 'ToolHead':
pos = self.objs[name].get_position()
pos = objs[name].get_position()
self.parameters[name].update({
'xpos': pos[0],
'ypos': pos[1],
@ -1139,21 +1205,21 @@ class MenuManager:
self.parameters[name]['status'] == "Idle")
})
elif class_name == 'PrinterExtruder':
info = self.objs[name].get_heater().get_status(
info = objs[name].get_heater().get_status(
eventtime)
self.parameters[name].update(info)
elif class_name == 'PrinterLCD':
self.parameters[name].update({
'progress': self.objs[name].progress or 0,
'message': self.objs[name].message or '',
'progress': objs[name].progress or 0,
'message': objs[name].message or '',
'is_enabled': True
})
elif class_name == 'PrinterHeaterFan':
info = self.objs[name].fan.get_status(eventtime)
info = objs[name].fan.get_status(eventtime)
self.parameters[name].update(info)
elif class_name in ('PrinterOutputPin', 'PrinterServo'):
self.parameters[name].update({
'value': self.objs[name].last_value
'value': objs[name].last_value
})
else:
self.parameters[name] = {'is_enabled': False}
@ -1216,14 +1282,14 @@ class MenuManager:
container = self.stack_peek()
if self.running and isinstance(container, MenuContainer):
container.heartbeat(eventtime)
if(isinstance(container, MenuDeck) and not container.is_editing()):
container.update_items()
# clamps
self.top_row = max(0, min(
self.top_row, len(container) - self.rows))
self.selected = max(0, min(
self.selected, len(container) - 1))
if isinstance(container, MenuDeck):
if not container.is_editing():
container.update_items()
container[self.selected].heartbeat(eventtime)
lines = container[self.selected].render_content(eventtime)
else:
@ -1392,6 +1458,13 @@ class MenuManager:
self.exit()
elif action == 'respond':
self.gcode.respond_info("{}".format(' '.join(map(str, args))))
elif action == 'event' and len(args) > 0:
if len(str(args[0])) > 0:
self.printer.send_event(
"menu:action:" + str(args[0]), *args[1:])
else:
logging.error("Malformed event call: {} {}".format(
action, ' '.join(map(str, args))))
else:
logging.error("Unknown action %s" % (action))
except Exception:
@ -1429,6 +1502,18 @@ class MenuManager:
"Unknown menuitem '%s'" % (name,))
return self.menuitems[name]
def load_config(self, *args):
cfg = None
filename = os.path.join(*args)
try:
cfg = self.pconfig.read_config(filename)
except Exception:
raise self.printer.config_error(
"Cannot load config '%s'" % (filename,))
if cfg:
self.load_menuitems(cfg)
return cfg
def load_menuitems(self, config):
for cfg in config.get_prefix_sections('menu '):
name = " ".join(cfg.get_name().split()[1:])