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:
parent
005cbe157a
commit
0cbe851777
|
@ -85,6 +85,7 @@
|
||||||
# and [respond response_info] command. Respond command will send '// response_info' to host.
|
# and [respond response_info] command. Respond command will send '// response_info' to host.
|
||||||
|
|
||||||
#[menu input1]
|
#[menu input1]
|
||||||
|
#type: input
|
||||||
#name:
|
#name:
|
||||||
#cursor:
|
#cursor:
|
||||||
#width:
|
#width:
|
||||||
|
@ -197,3 +198,8 @@
|
||||||
# This way only simple menu items can be grouped.
|
# 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: 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.
|
# 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.
|
||||||
|
|
|
@ -49,6 +49,9 @@ class PrinterLCD:
|
||||||
self.gcode.register_command('M117', self.cmd_M117)
|
self.gcode.register_command('M117', self.cmd_M117)
|
||||||
# Start screen update timer
|
# Start screen update timer
|
||||||
self.reactor.update_timer(self.screen_update_timer, self.reactor.NOW)
|
self.reactor.update_timer(self.screen_update_timer, self.reactor.NOW)
|
||||||
|
# Get menu instance
|
||||||
|
def get_menu(self):
|
||||||
|
return self.menu
|
||||||
# Graphics drawing
|
# Graphics drawing
|
||||||
def animate_glyphs(self, eventtime, x, y, glyph_name, do_animate):
|
def animate_glyphs(self, eventtime, x, y, glyph_name, do_animate):
|
||||||
frame = do_animate and int(eventtime) & 1
|
frame = do_animate and int(eventtime) & 1
|
||||||
|
|
|
@ -59,6 +59,13 @@ class MenuElement(object):
|
||||||
|
|
||||||
# override
|
# override
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
|
return self.eval_enable()
|
||||||
|
|
||||||
|
# override
|
||||||
|
def reset_editing(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def eval_enable(self):
|
||||||
return self._parse_bool(self._enable)
|
return self._parse_bool(self._enable)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
|
@ -246,6 +253,11 @@ class MenuContainer(MenuElement):
|
||||||
def is_editing(self):
|
def is_editing(self):
|
||||||
return any([item.is_editing() for item in self._items])
|
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):
|
def _lookup_item(self, item):
|
||||||
if isinstance(item, str):
|
if isinstance(item, str):
|
||||||
s = item.strip()
|
s = item.strip()
|
||||||
|
@ -514,6 +526,9 @@ class MenuInput(MenuCommand):
|
||||||
def is_editing(self):
|
def is_editing(self):
|
||||||
return self._input_value is not None
|
return self._input_value is not None
|
||||||
|
|
||||||
|
def reset_editing(self):
|
||||||
|
self.reset_value()
|
||||||
|
|
||||||
def _onchange(self):
|
def _onchange(self):
|
||||||
self._manager.queue_gcode(self.get_gcode())
|
self._manager.queue_gcode(self.get_gcode())
|
||||||
|
|
||||||
|
@ -573,6 +588,7 @@ class MenuGroup(MenuContainer):
|
||||||
self._sep = sep
|
self._sep = sep
|
||||||
self._show_back = False
|
self._show_back = False
|
||||||
self.selected = None
|
self.selected = None
|
||||||
|
self.use_cursor = self._asbool(config.get('use_cursor', 'false'))
|
||||||
self.items = config.get('items', '')
|
self.items = config.get('items', '')
|
||||||
|
|
||||||
def is_accepted(self, item):
|
def is_accepted(self, item):
|
||||||
|
@ -599,9 +615,20 @@ class MenuGroup(MenuContainer):
|
||||||
def _render_item(self, item, selected=False, scroll=False):
|
def _render_item(self, item, selected=False, scroll=False):
|
||||||
name = "%s" % str(item.render(scroll))
|
name = "%s" % str(item.render(scroll))
|
||||||
if selected and not self.is_editing():
|
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():
|
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
|
return name
|
||||||
|
|
||||||
def _render(self):
|
def _render(self):
|
||||||
|
@ -626,6 +653,9 @@ class MenuGroup(MenuContainer):
|
||||||
logging.exception("Call selected error")
|
logging.exception("Call selected error")
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def reset_editing(self):
|
||||||
|
return self._call_selected('reset_editing')
|
||||||
|
|
||||||
def is_editing(self):
|
def is_editing(self):
|
||||||
return self._call_selected('is_editing')
|
return self._call_selected('is_editing')
|
||||||
|
|
||||||
|
@ -799,6 +829,8 @@ class MenuCard(MenuGroup):
|
||||||
def __init__(self, manager, config, namespace=''):
|
def __init__(self, manager, config, namespace=''):
|
||||||
super(MenuCard, self).__init__(manager, config, namespace)
|
super(MenuCard, self).__init__(manager, config, namespace)
|
||||||
self.content = config.get('content')
|
self.content = config.get('content')
|
||||||
|
self._allow_without_selection = self._asbool(
|
||||||
|
config.get('allow_without_selection', 'true'))
|
||||||
if not self.items:
|
if not self.items:
|
||||||
self.content = self._parse_content_items(self.content)
|
self.content = self._parse_content_items(self.content)
|
||||||
|
|
||||||
|
@ -852,6 +884,8 @@ class MenuCard(MenuGroup):
|
||||||
if self.selected is not None:
|
if self.selected is not None:
|
||||||
self.selected = (
|
self.selected = (
|
||||||
(self.selected % len(self)) if len(self) > 0 else None)
|
(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 = []
|
items = []
|
||||||
for i, item in enumerate(self):
|
for i, item in enumerate(self):
|
||||||
|
@ -947,6 +981,7 @@ class MenuManager:
|
||||||
self.timeout_idx = 0
|
self.timeout_idx = 0
|
||||||
self.lcd_chip = lcd_chip
|
self.lcd_chip = lcd_chip
|
||||||
self.printer = config.get_printer()
|
self.printer = config.get_printer()
|
||||||
|
self.pconfig = self.printer.lookup_object('configfile')
|
||||||
self.gcode = self.printer.lookup_object('gcode')
|
self.gcode = self.printer.lookup_object('gcode')
|
||||||
self.gcode_queue = []
|
self.gcode_queue = []
|
||||||
self.parameters = {}
|
self.parameters = {}
|
||||||
|
@ -1002,21 +1037,12 @@ class MenuManager:
|
||||||
self.gcode.register_mux_command("MENU", "DO", 'dump', self.cmd_DO_DUMP,
|
self.gcode.register_mux_command("MENU", "DO", 'dump', self.cmd_DO_DUMP,
|
||||||
desc=self.cmd_DO_help)
|
desc=self.cmd_DO_help)
|
||||||
|
|
||||||
# Parse local config file in same directory as current module
|
# Load local config file in same directory as current module
|
||||||
pconfig = self.printer.lookup_object('configfile')
|
self.load_config(os.path.dirname(__file__), 'menu.cfg')
|
||||||
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 items from main config
|
# Load items from main config
|
||||||
self.load_menuitems(config)
|
self.load_menuitems(config)
|
||||||
|
|
||||||
# Load menu root
|
# Load menu root
|
||||||
if self._root is not None:
|
self.load_root()
|
||||||
self.root = self.lookup_menuitem(self._root)
|
|
||||||
if isinstance(self.root, MenuDeck):
|
|
||||||
self._autorun = True
|
|
||||||
|
|
||||||
def printer_state(self, state):
|
def printer_state(self, state):
|
||||||
if state == 'ready':
|
if state == 'ready':
|
||||||
|
@ -1071,6 +1097,45 @@ class MenuManager:
|
||||||
return (self._autorun is True and self.root is not None
|
return (self._autorun is True and self.root is not None
|
||||||
and self.stack_peek() is self.root and self.selected == 0)
|
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):
|
def is_running(self):
|
||||||
return self.running
|
return self.running
|
||||||
|
|
||||||
|
@ -1106,15 +1171,16 @@ class MenuManager:
|
||||||
|
|
||||||
def update_parameters(self, eventtime):
|
def update_parameters(self, eventtime):
|
||||||
self.parameters = {}
|
self.parameters = {}
|
||||||
|
objs = dict(self.objs)
|
||||||
# getting info this way is more like hack
|
# getting info this way is more like hack
|
||||||
# all modules should have special reporting method (maybe get_status)
|
# all modules should have special reporting method (maybe get_status)
|
||||||
# for available parameters
|
# for available parameters
|
||||||
# Only 2 level dot notation
|
# Only 2 level dot notation
|
||||||
for name in self.objs.keys():
|
for name in objs.keys():
|
||||||
try:
|
try:
|
||||||
if self.objs[name] is not None:
|
if objs[name] is not None:
|
||||||
class_name = str(self.objs[name].__class__.__name__)
|
class_name = str(objs[name].__class__.__name__)
|
||||||
get_status = getattr(self.objs[name], "get_status", None)
|
get_status = getattr(objs[name], "get_status", None)
|
||||||
if callable(get_status):
|
if callable(get_status):
|
||||||
self.parameters[name] = get_status(eventtime)
|
self.parameters[name] = get_status(eventtime)
|
||||||
else:
|
else:
|
||||||
|
@ -1123,7 +1189,7 @@ class MenuManager:
|
||||||
self.parameters[name].update({'is_enabled': True})
|
self.parameters[name].update({'is_enabled': True})
|
||||||
# get additional info
|
# get additional info
|
||||||
if class_name == 'ToolHead':
|
if class_name == 'ToolHead':
|
||||||
pos = self.objs[name].get_position()
|
pos = objs[name].get_position()
|
||||||
self.parameters[name].update({
|
self.parameters[name].update({
|
||||||
'xpos': pos[0],
|
'xpos': pos[0],
|
||||||
'ypos': pos[1],
|
'ypos': pos[1],
|
||||||
|
@ -1139,21 +1205,21 @@ class MenuManager:
|
||||||
self.parameters[name]['status'] == "Idle")
|
self.parameters[name]['status'] == "Idle")
|
||||||
})
|
})
|
||||||
elif class_name == 'PrinterExtruder':
|
elif class_name == 'PrinterExtruder':
|
||||||
info = self.objs[name].get_heater().get_status(
|
info = objs[name].get_heater().get_status(
|
||||||
eventtime)
|
eventtime)
|
||||||
self.parameters[name].update(info)
|
self.parameters[name].update(info)
|
||||||
elif class_name == 'PrinterLCD':
|
elif class_name == 'PrinterLCD':
|
||||||
self.parameters[name].update({
|
self.parameters[name].update({
|
||||||
'progress': self.objs[name].progress or 0,
|
'progress': objs[name].progress or 0,
|
||||||
'message': self.objs[name].message or '',
|
'message': objs[name].message or '',
|
||||||
'is_enabled': True
|
'is_enabled': True
|
||||||
})
|
})
|
||||||
elif class_name == 'PrinterHeaterFan':
|
elif class_name == 'PrinterHeaterFan':
|
||||||
info = self.objs[name].fan.get_status(eventtime)
|
info = objs[name].fan.get_status(eventtime)
|
||||||
self.parameters[name].update(info)
|
self.parameters[name].update(info)
|
||||||
elif class_name in ('PrinterOutputPin', 'PrinterServo'):
|
elif class_name in ('PrinterOutputPin', 'PrinterServo'):
|
||||||
self.parameters[name].update({
|
self.parameters[name].update({
|
||||||
'value': self.objs[name].last_value
|
'value': objs[name].last_value
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
self.parameters[name] = {'is_enabled': False}
|
self.parameters[name] = {'is_enabled': False}
|
||||||
|
@ -1216,14 +1282,14 @@ class MenuManager:
|
||||||
container = self.stack_peek()
|
container = self.stack_peek()
|
||||||
if self.running and isinstance(container, MenuContainer):
|
if self.running and isinstance(container, MenuContainer):
|
||||||
container.heartbeat(eventtime)
|
container.heartbeat(eventtime)
|
||||||
|
if(isinstance(container, MenuDeck) and not container.is_editing()):
|
||||||
|
container.update_items()
|
||||||
# clamps
|
# clamps
|
||||||
self.top_row = max(0, min(
|
self.top_row = max(0, min(
|
||||||
self.top_row, len(container) - self.rows))
|
self.top_row, len(container) - self.rows))
|
||||||
self.selected = max(0, min(
|
self.selected = max(0, min(
|
||||||
self.selected, len(container) - 1))
|
self.selected, len(container) - 1))
|
||||||
if isinstance(container, MenuDeck):
|
if isinstance(container, MenuDeck):
|
||||||
if not container.is_editing():
|
|
||||||
container.update_items()
|
|
||||||
container[self.selected].heartbeat(eventtime)
|
container[self.selected].heartbeat(eventtime)
|
||||||
lines = container[self.selected].render_content(eventtime)
|
lines = container[self.selected].render_content(eventtime)
|
||||||
else:
|
else:
|
||||||
|
@ -1392,6 +1458,13 @@ class MenuManager:
|
||||||
self.exit()
|
self.exit()
|
||||||
elif action == 'respond':
|
elif action == 'respond':
|
||||||
self.gcode.respond_info("{}".format(' '.join(map(str, args))))
|
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:
|
else:
|
||||||
logging.error("Unknown action %s" % (action))
|
logging.error("Unknown action %s" % (action))
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -1429,6 +1502,18 @@ class MenuManager:
|
||||||
"Unknown menuitem '%s'" % (name,))
|
"Unknown menuitem '%s'" % (name,))
|
||||||
return self.menuitems[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):
|
def load_menuitems(self, config):
|
||||||
for cfg in config.get_prefix_sections('menu '):
|
for cfg in config.get_prefix_sections('menu '):
|
||||||
name = " ".join(cfg.get_name().split()[1:])
|
name = " ".join(cfg.get_name().split()[1:])
|
||||||
|
|
Loading…
Reference in New Issue