diff --git a/config/example-menu.cfg b/config/example-menu.cfg index 453e1552db7b..2015bad69027 100644 --- a/config/example-menu.cfg +++ b/config/example-menu.cfg @@ -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. diff --git a/klippy/extras/display/display.py b/klippy/extras/display/display.py index 7f8800161001..8e05cd2d0238 100644 --- a/klippy/extras/display/display.py +++ b/klippy/extras/display/display.py @@ -49,6 +49,9 @@ def printer_state(self, state): 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 diff --git a/klippy/extras/display/menu.py b/klippy/extras/display/menu.py index 07f3f3b8db4e..c4db2baffb80 100644 --- a/klippy/extras/display/menu.py +++ b/klippy/extras/display/menu.py @@ -59,6 +59,13 @@ def is_scrollable(self): # 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 @@ def is_readonly(self): 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 @@ def get_longpress_gcode(self): 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 @@ def __init__(self, manager, config, namespace='', sep=','): 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 @@ def init(self): 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 @@ def _call_selected(self, method=None, *args): 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 @@ def render_content(self, eventtime): 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 @@ def __init__(self, config, lcd_chip): 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 @@ def __init__(self, config, lcd_chip): 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 @@ def _timeout_autorun_root(self): 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 @@ def get_status(self, eventtime): 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 @@ def update_parameters(self, eventtime): 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 @@ def update_parameters(self, eventtime): 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 @@ def render(self, eventtime): 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 @@ def run_action(self, action, *args): 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 @@ def lookup_menuitem(self, name): "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:])