mirror of https://github.com/Desuuuu/klipper.git
menu: modifications (#3262)
- remove lot of helper methods - differentiate class instantiate from config or directly - don't use 'enable' template rendering when static value is used. - new element 'disabled' - other internal adjustments Signed-off-by: Janar Sööt <janar.soot@gmail.com>
This commit is contained in:
parent
422386e94c
commit
91de1560a7
|
@ -3212,6 +3212,11 @@ List of actions for menu element:
|
|||
|
||||
```
|
||||
# Common parameters available for all menu config sections.
|
||||
#[menu __some_list __some_name]
|
||||
#type: disabled
|
||||
# Permanently disabled menu element, only required attribute is 'type'.
|
||||
# Allows you to easily disable/hide existing menu items.
|
||||
|
||||
#[menu some_name]
|
||||
#type:
|
||||
# One of command, input, list, text:
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
# Copyright (C) 2020 Janar Sööt <janar.soot@gmail.com>
|
||||
#
|
||||
# This file may be distributed under the terms of the GNU GPLv3 license.
|
||||
import os, logging
|
||||
import os, logging, ast
|
||||
from string import Template
|
||||
from . import menu_keys
|
||||
|
||||
|
@ -17,37 +17,38 @@ class error(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class MenuConfig(dict):
|
||||
"""Wrapper for dict to emulate configfile get_name for namespace.
|
||||
__ns - item namespace key, used in item relative paths
|
||||
$__id - generated id text variable
|
||||
"""
|
||||
def get_name(self):
|
||||
__id = '__menu_' + hex(id(self)).lstrip("0x").rstrip("L")
|
||||
return Template('menu ' + self.get(
|
||||
'__ns', __id)).safe_substitute(__id=__id)
|
||||
|
||||
def get_prefix_options(self, prefix):
|
||||
return [o for o in self.keys() if o.startswith(prefix)]
|
||||
|
||||
|
||||
# Scriptable menu element abstract baseclass
|
||||
class MenuElement(object):
|
||||
def __init__(self, manager, config):
|
||||
def __init__(self, manager, config, **kwargs):
|
||||
if type(self) is MenuElement:
|
||||
raise error(
|
||||
'Abstract MenuElement cannot be instantiated directly')
|
||||
self._manager = manager
|
||||
self.cursor = '>'
|
||||
# scroll is always on
|
||||
self._scroll = True
|
||||
self._index = manager.asint(config.get('index', ''), None)
|
||||
self._enable_tpl = manager.gcode_macro.load_template(
|
||||
config, 'enable', 'True')
|
||||
# set class defaults and attributes from arguments
|
||||
self._index = kwargs.get('index', None)
|
||||
self._enable = kwargs.get('enable', True)
|
||||
self._name = kwargs.get('name', None)
|
||||
self._enable_tpl = self._name_tpl = None
|
||||
if config is not None:
|
||||
# overwrite class attributes from config
|
||||
self._index = config.getint('index', self._index)
|
||||
self._name_tpl = manager.gcode_macro.load_template(
|
||||
config, 'name')
|
||||
config, 'name', self._name)
|
||||
try:
|
||||
self._enable = config.getboolean('enable', self._enable)
|
||||
except config.error:
|
||||
self._enable_tpl = manager.gcode_macro.load_template(
|
||||
config, 'enable')
|
||||
# item namespace - used in relative paths
|
||||
self._ns = str(" ".join(config.get_name().split(' ')[1:])).strip()
|
||||
else:
|
||||
# ns - item namespace key, used in item relative paths
|
||||
# $__id - generated id text variable
|
||||
__id = '__menu_' + hex(id(self)).lstrip("0x").rstrip("L")
|
||||
self._ns = Template(
|
||||
'menu ' + kwargs.get('ns', __id)).safe_substitute(__id=__id)
|
||||
self._last_heartbeat = None
|
||||
self.__scroll_offs = 0
|
||||
self.__scroll_diff = 0
|
||||
|
@ -56,7 +57,7 @@ class MenuElement(object):
|
|||
# display width is used and adjusted by cursor size
|
||||
self._width = self.manager.cols - len(self._cursor)
|
||||
# menu scripts
|
||||
self._script_tpls = {}
|
||||
self._scripts = {}
|
||||
# init
|
||||
self.init()
|
||||
|
||||
|
@ -64,26 +65,24 @@ class MenuElement(object):
|
|||
def init(self):
|
||||
pass
|
||||
|
||||
def _name(self):
|
||||
def _render_name(self):
|
||||
if self._name_tpl is not None:
|
||||
context = self.get_context()
|
||||
return self.manager.asflat(self._name_tpl.render(context))
|
||||
return self.manager.asflat(self._name)
|
||||
|
||||
def _load_scripts(self, config, *args, **kwargs):
|
||||
"""Load script(s) from config"""
|
||||
|
||||
prefix = kwargs.get('prefix', '')
|
||||
for arg in args:
|
||||
name = arg[len(prefix):]
|
||||
if name in self._script_tpls:
|
||||
def _load_script(self, config, name, option=None):
|
||||
"""Load script template from config or callback from dict"""
|
||||
if name in self._scripts:
|
||||
logging.info(
|
||||
"Declaration of '%s' hides "
|
||||
"previous script declaration" % (name,))
|
||||
self._script_tpls[name] = self.manager.gcode_macro.load_template(
|
||||
config, arg, '')
|
||||
|
||||
# override
|
||||
def _second_tick(self, eventtime):
|
||||
pass
|
||||
option = option or name
|
||||
if isinstance(config, dict):
|
||||
self._scripts[name] = config.get(option, None)
|
||||
else:
|
||||
self._scripts[name] = self.manager.gcode_macro.load_template(
|
||||
config, option, '')
|
||||
|
||||
# override
|
||||
def is_editing(self):
|
||||
|
@ -95,7 +94,8 @@ class MenuElement(object):
|
|||
|
||||
# override
|
||||
def is_enabled(self):
|
||||
return self.eval_enable()
|
||||
context = self.get_context()
|
||||
return self.eval_enable(context)
|
||||
|
||||
# override
|
||||
def start_editing(self):
|
||||
|
@ -115,9 +115,10 @@ class MenuElement(object):
|
|||
})
|
||||
return context
|
||||
|
||||
def eval_enable(self):
|
||||
context = self.get_context()
|
||||
return self.manager.asbool(self._enable_tpl.render(context))
|
||||
def eval_enable(self, context):
|
||||
if self._enable_tpl is not None:
|
||||
return bool(ast.literal_eval(self._enable_tpl.render(context)))
|
||||
return bool(self._enable)
|
||||
|
||||
# Called when a item is selected
|
||||
def select(self):
|
||||
|
@ -129,7 +130,6 @@ class MenuElement(object):
|
|||
if self.__last_state ^ state:
|
||||
self.__last_state = state
|
||||
if not self.is_editing():
|
||||
self._second_tick(eventtime)
|
||||
self.__update_scroll(eventtime)
|
||||
|
||||
def __clear_scroll(self):
|
||||
|
@ -159,7 +159,7 @@ class MenuElement(object):
|
|||
].ljust(self._width)
|
||||
|
||||
def render_name(self, selected=False):
|
||||
s = str(self._name())
|
||||
s = str(self._render_name())
|
||||
# scroller
|
||||
if self._width > 0:
|
||||
self.__scroll_diff = len(s) - self._width
|
||||
|
@ -194,22 +194,34 @@ class MenuElement(object):
|
|||
"%s:%s" % (self.get_ns(), str(event)), *args)
|
||||
|
||||
def get_script(self, name):
|
||||
if name in self._script_tpls:
|
||||
return self._script_tpls[name]
|
||||
if name in self._scripts:
|
||||
return self._scripts[name]
|
||||
return None
|
||||
|
||||
def _run_script(self, name, context):
|
||||
_render = getattr(self._scripts[name], 'render', None)
|
||||
# check template
|
||||
if _render is not None and callable(_render):
|
||||
return _render(context)
|
||||
# check callback
|
||||
elif callable(self._scripts[name]):
|
||||
return self._scripts[name](self, context)
|
||||
# check static string
|
||||
elif isinstance(self._scripts[name], str):
|
||||
return self._scripts[name]
|
||||
|
||||
def run_script(self, name, **kwargs):
|
||||
event = kwargs.get('event', None)
|
||||
context = kwargs.get('context', None)
|
||||
render_only = kwargs.get('render_only', False)
|
||||
result = ""
|
||||
# init context
|
||||
if name in self._scripts:
|
||||
context = self.get_context(context)
|
||||
if name in self._script_tpls:
|
||||
context['menu'].update({
|
||||
'event': event or name
|
||||
})
|
||||
result = self._script_tpls[name].render(context)
|
||||
result = self._run_script(name, context)
|
||||
if not render_only:
|
||||
# run result as gcode
|
||||
self.manager.queue_gcode(result)
|
||||
|
@ -238,11 +250,12 @@ class MenuElement(object):
|
|||
|
||||
class MenuContainer(MenuElement):
|
||||
"""Menu container abstract class"""
|
||||
def __init__(self, manager, config):
|
||||
def __init__(self, manager, config, **kwargs):
|
||||
if type(self) is MenuContainer:
|
||||
raise error(
|
||||
'Abstract MenuContainer cannot be instantiated directly')
|
||||
super(MenuContainer, self).__init__(manager, config)
|
||||
super(MenuContainer, self).__init__(manager, config, **kwargs)
|
||||
self._populate_cb = kwargs.get('populate', None)
|
||||
self.cursor = '>'
|
||||
self.__selected = None
|
||||
self._allitems = []
|
||||
|
@ -342,6 +355,9 @@ class MenuContainer(MenuElement):
|
|||
self._insert_item(name)
|
||||
# populate successor items
|
||||
self._populate()
|
||||
# run populate callback
|
||||
if self._populate_cb is not None and callable(self._populate_cb):
|
||||
self._populate_cb(self)
|
||||
# send populate event
|
||||
self.send_event('populate', self)
|
||||
|
||||
|
@ -414,22 +430,41 @@ class MenuContainer(MenuElement):
|
|||
return self.__selected
|
||||
|
||||
|
||||
class MenuDisabled(MenuElement):
|
||||
def __init__(self, manager, config, **kwargs):
|
||||
super(MenuDisabled, self).__init__(manager, config, name='')
|
||||
|
||||
def is_enabled(self):
|
||||
return False
|
||||
|
||||
|
||||
class MenuCommand(MenuElement):
|
||||
def __init__(self, manager, config):
|
||||
super(MenuCommand, self).__init__(manager, config)
|
||||
self._load_scripts(config, 'gcode')
|
||||
def __init__(self, manager, config, **kwargs):
|
||||
super(MenuCommand, self).__init__(manager, config, **kwargs)
|
||||
self._load_script(config or kwargs, 'gcode')
|
||||
|
||||
|
||||
class MenuInput(MenuCommand):
|
||||
def __init__(self, manager, config,):
|
||||
super(MenuInput, self).__init__(manager, config)
|
||||
self._realtime = manager.asbool(config.get('realtime', 'false'))
|
||||
self._input_tpl = manager.gcode_macro.load_template(config, 'input')
|
||||
def __init__(self, manager, config, **kwargs):
|
||||
super(MenuInput, self).__init__(manager, config, **kwargs)
|
||||
# set class defaults and attributes from arguments
|
||||
self._input = kwargs.get('input', None)
|
||||
self._input_min = kwargs.get('input_min', -999999.0)
|
||||
self._input_max = kwargs.get('input_max', 999999.0)
|
||||
self._input_step = kwargs.get('input_step', 1.0)
|
||||
self._realtime = kwargs.get('realtime', False)
|
||||
self._input_tpl = self._input_min_tpl = self._input_max_tpl = None
|
||||
if config is not None:
|
||||
# overwrite class attributes from config
|
||||
self._realtime = config.getboolean('realtime', self._realtime)
|
||||
self._input_tpl = manager.gcode_macro.load_template(
|
||||
config, 'input')
|
||||
self._input_min_tpl = manager.gcode_macro.load_template(
|
||||
config, 'input_min', '-999999.0')
|
||||
config, 'input_min', str(self._input_min))
|
||||
self._input_max_tpl = manager.gcode_macro.load_template(
|
||||
config, 'input_max', '999999.0')
|
||||
self._input_step = config.getfloat('input_step', above=0.)
|
||||
config, 'input_max', str(self._input_max))
|
||||
self._input_step = config.getfloat(
|
||||
'input_step', self._input_step, above=0.)
|
||||
|
||||
def init(self):
|
||||
super(MenuInput, self).init()
|
||||
|
@ -466,44 +501,56 @@ class MenuInput(MenuCommand):
|
|||
|
||||
def get_context(self, cxt=None):
|
||||
context = super(MenuInput, self).get_context(cxt)
|
||||
context['menu'].update({
|
||||
'input': self.manager.asfloat(
|
||||
self._eval_value() if self._input_value is None
|
||||
value = (self._eval_value(context) if self._input_value is None
|
||||
else self._input_value)
|
||||
context['menu'].update({
|
||||
'input': value
|
||||
})
|
||||
return context
|
||||
|
||||
def eval_enable(self):
|
||||
def is_enabled(self):
|
||||
context = super(MenuInput, self).get_context()
|
||||
return self.manager.asbool(self._enable_tpl.render(context))
|
||||
return self.eval_enable(context)
|
||||
|
||||
def _eval_min(self):
|
||||
context = super(MenuInput, self).get_context()
|
||||
return self._input_min_tpl.render(context)
|
||||
def _eval_min(self, context):
|
||||
try:
|
||||
if self._input_min_tpl is not None:
|
||||
return float(ast.literal_eval(
|
||||
self._input_min_tpl.render(context)))
|
||||
return float(self._input_min)
|
||||
except ValueError:
|
||||
logging.exception("Input min value evaluation error")
|
||||
|
||||
def _eval_max(self):
|
||||
context = super(MenuInput, self).get_context()
|
||||
return self._input_max_tpl.render(context)
|
||||
def _eval_max(self, context):
|
||||
try:
|
||||
if self._input_max_tpl is not None:
|
||||
return float(ast.literal_eval(
|
||||
self._input_max_tpl.render(context)))
|
||||
return float(self._input_max)
|
||||
except ValueError:
|
||||
logging.exception("Input max value evaluation error")
|
||||
|
||||
def _eval_value(self):
|
||||
context = super(MenuInput, self).get_context()
|
||||
return self._input_tpl.render(context)
|
||||
def _eval_value(self, context):
|
||||
try:
|
||||
if self._input_tpl is not None:
|
||||
return float(ast.literal_eval(
|
||||
self._input_tpl.render(context)))
|
||||
return float(self._input)
|
||||
except ValueError:
|
||||
logging.exception("Input value evaluation error")
|
||||
|
||||
def _value_changed(self):
|
||||
self.__last_change = self._last_heartbeat
|
||||
self._is_dirty = True
|
||||
|
||||
def _init_value(self):
|
||||
context = super(MenuInput, self).get_context()
|
||||
self._input_value = None
|
||||
self._input_min = self.manager.asfloat(self._eval_min())
|
||||
self._input_max = self.manager.asfloat(self._eval_max())
|
||||
value = self._eval_value()
|
||||
if self.manager.isfloat(value):
|
||||
self._input_min = self._eval_min(context)
|
||||
self._input_max = self._eval_max(context)
|
||||
self._input_value = min(self._input_max, max(
|
||||
self._input_min, self.manager.asfloat(value)))
|
||||
self._input_min, self._eval_value(context)))
|
||||
self._value_changed()
|
||||
else:
|
||||
logging.error("Cannot init input value")
|
||||
|
||||
def _reset_value(self):
|
||||
self._input_value = None
|
||||
|
@ -548,15 +595,15 @@ class MenuInput(MenuCommand):
|
|||
|
||||
|
||||
class MenuList(MenuContainer):
|
||||
def __init__(self, manager, config):
|
||||
super(MenuList, self).__init__(manager, config)
|
||||
def __init__(self, manager, config, **kwargs):
|
||||
super(MenuList, self).__init__(manager, config, **kwargs)
|
||||
self._viewport_top = 0
|
||||
|
||||
def _cb(el, context):
|
||||
el.manager.back()
|
||||
# create back item
|
||||
self._itemBack = self.manager.menuitem_from({
|
||||
'type': 'command',
|
||||
'name': '..',
|
||||
'gcode': '{menu.back()}'
|
||||
})
|
||||
self._itemBack = self.manager.menuitem_from(
|
||||
'command', name='..', gcode=_cb)
|
||||
|
||||
def _names_aslist(self):
|
||||
return self.manager.lookup_children(self.get_ns())
|
||||
|
@ -603,8 +650,8 @@ class MenuList(MenuContainer):
|
|||
|
||||
|
||||
class MenuVSDList(MenuList):
|
||||
def __init__(self, manager, config):
|
||||
super(MenuVSDList, self).__init__(manager, config)
|
||||
def __init__(self, manager, config, **kwargs):
|
||||
super(MenuVSDList, self).__init__(manager, config, **kwargs)
|
||||
|
||||
def _populate(self):
|
||||
super(MenuVSDList, self)._populate()
|
||||
|
@ -612,17 +659,12 @@ class MenuVSDList(MenuList):
|
|||
if sdcard is not None:
|
||||
files = sdcard.get_file_list()
|
||||
for fname, fsize in files:
|
||||
gcode = [
|
||||
'M23 /%s' % str(fname)
|
||||
]
|
||||
self.insert_item(self.manager.menuitem_from({
|
||||
'type': 'command',
|
||||
'name': self.manager.asliteral(fname),
|
||||
'gcode': "\n".join(gcode)
|
||||
}))
|
||||
self.insert_item(self.manager.menuitem_from(
|
||||
'command', name=repr(fname), gcode='M23 /%s' % str(fname)))
|
||||
|
||||
|
||||
menu_items = {
|
||||
'disabled': MenuDisabled,
|
||||
'command': MenuCommand,
|
||||
'input': MenuInput,
|
||||
'list': MenuList,
|
||||
|
@ -911,11 +953,11 @@ class MenuManager:
|
|||
logging.exception("Script running error")
|
||||
self.gcode_queue.pop(0)
|
||||
|
||||
def menuitem_from(self, config):
|
||||
if isinstance(config, dict):
|
||||
config = MenuConfig(dict(config))
|
||||
return self.aschoice(
|
||||
config, 'type', menu_items)(self, config)
|
||||
def menuitem_from(self, type, **kwargs):
|
||||
if type not in menu_items:
|
||||
raise error("Choice '%s' for option '%s'"
|
||||
" is not a valid choice" % (type, menu_items))
|
||||
return menu_items[type](self, None, **kwargs)
|
||||
|
||||
def add_menuitem(self, name, item):
|
||||
existing_item = False
|
||||
|
@ -964,7 +1006,11 @@ class MenuManager:
|
|||
|
||||
def load_menuitems(self, config):
|
||||
for cfg in config.get_prefix_sections('menu '):
|
||||
item = self.menuitem_from(cfg)
|
||||
type = cfg.get('type')
|
||||
if type not in menu_items:
|
||||
raise error("Choice '%s' for option '%s'"
|
||||
" is not a valid choice" % (type, menu_items))
|
||||
item = menu_items[type](self, cfg)
|
||||
self.add_menuitem(item.get_ns(), item)
|
||||
|
||||
def _click_callback(self, eventtime, event):
|
||||
|
@ -1002,11 +1048,6 @@ class MenuManager:
|
|||
s = s[1:-1]
|
||||
return s
|
||||
|
||||
@classmethod
|
||||
def asliteral(cls, s):
|
||||
"""Enclose text by the single quotes"""
|
||||
return "'" + str(s) + "'"
|
||||
|
||||
@classmethod
|
||||
def aslatin(cls, s):
|
||||
if isinstance(s, str):
|
||||
|
@ -1023,92 +1064,3 @@ class MenuManager:
|
|||
@classmethod
|
||||
def asflat(cls, s):
|
||||
return cls.stripliterals(cls.asflatline(s))
|
||||
|
||||
@classmethod
|
||||
def asbool(cls, s):
|
||||
if isinstance(s, (bool, int, float)):
|
||||
return bool(s)
|
||||
elif cls.isfloat(s):
|
||||
return bool(cls.asfloat(s))
|
||||
s = str(s).strip()
|
||||
return s.lower() in ('y', 'yes', 't', 'true', 'on', '1')
|
||||
|
||||
@classmethod
|
||||
def asint(cls, s, default=sentinel):
|
||||
if isinstance(s, (int, float)):
|
||||
return int(s)
|
||||
s = str(s).strip()
|
||||
prefix = s[0:2]
|
||||
try:
|
||||
if prefix == '0x':
|
||||
return int(s, 16)
|
||||
elif prefix == '0b':
|
||||
return int(s, 2)
|
||||
else:
|
||||
return int(float(s))
|
||||
except ValueError as e:
|
||||
if default is not sentinel:
|
||||
return default
|
||||
raise e
|
||||
|
||||
@classmethod
|
||||
def asfloat(cls, s, default=sentinel):
|
||||
if isinstance(s, (int, float)):
|
||||
return float(s)
|
||||
s = str(s).strip()
|
||||
try:
|
||||
return float(s)
|
||||
except ValueError as e:
|
||||
if default is not sentinel:
|
||||
return default
|
||||
raise e
|
||||
|
||||
@classmethod
|
||||
def isfloat(cls, value):
|
||||
try:
|
||||
float(value)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def lines_aslist(cls, value, default=[]):
|
||||
if isinstance(value, str):
|
||||
value = filter(None, [x.strip() for x in value.splitlines()])
|
||||
try:
|
||||
return list(value)
|
||||
except Exception:
|
||||
logging.exception("Lines as list parsing error")
|
||||
return list(default)
|
||||
|
||||
@classmethod
|
||||
def words_aslist(cls, value, sep=',', default=[]):
|
||||
if isinstance(value, str):
|
||||
value = filter(None, [x.strip() for x in value.split(sep)])
|
||||
try:
|
||||
return list(value)
|
||||
except Exception:
|
||||
logging.exception("Words as list parsing error")
|
||||
return list(default)
|
||||
|
||||
@classmethod
|
||||
def aslist(cls, value, flatten=True, default=[]):
|
||||
values = cls.lines_aslist(value)
|
||||
if not flatten:
|
||||
return values
|
||||
result = []
|
||||
for value in values:
|
||||
subvalues = cls.words_aslist(value, sep=',')
|
||||
result.extend(subvalues)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def aschoice(cls, config, option, choices, default=sentinel):
|
||||
if default is not sentinel:
|
||||
c = config.get(option, default)
|
||||
else:
|
||||
c = config.get(option)
|
||||
if c not in choices:
|
||||
raise error("Choice '%s' for option '%s'"
|
||||
" is not a valid choice" % (c, option))
|
||||
return choices[c]
|
||||
|
|
Loading…
Reference in New Issue