Extended evmenu with optional callbacks for node formatting. Replaces and Resolves #826.

This commit is contained in:
Griatch 2015-10-18 11:06:19 +02:00
parent 609b1777c7
commit 36337f9853

View file

@ -234,7 +234,9 @@ class EvMenu(object):
""" """
def __init__(self, caller, menudata, startnode="start", def __init__(self, caller, menudata, startnode="start",
cmdset_mergetype="Replace", cmdset_priority=1, cmdset_mergetype="Replace", cmdset_priority=1,
allow_quit=True, cmd_on_quit="look"): allow_quit=True, cmd_on_quit="look",
nodetext_formatter=None, options_formatter=None,
node_formatter=None):
""" """
Initialize the menu tree and start the caller onto the first node. Initialize the menu tree and start the caller onto the first node.
@ -264,13 +266,35 @@ class EvMenu(object):
allow_quit (bool, optional): Allow user to use quit or allow_quit (bool, optional): Allow user to use quit or
exit to leave the menu at any point. Recommended during exit to leave the menu at any point. Recommended during
development! development!
cmd_on_quit (callback, str or None, optional): When exiting the menu cmd_on_quit (callable, str or None, optional): When exiting the menu
(either by reaching a node with no options or by using the (either by reaching a node with no options or by using the
in-built quit command (activated with `allow_quit`), this in-built quit command (activated with `allow_quit`), this
callback function or command string will be executed. callback function or command string will be executed.
The callback function takes two parameters, the caller then the The callback function takes two parameters, the caller then the
EvMenu object. This is called after cleanup is complete. EvMenu object. This is called after cleanup is complete.
Set to None to not call any command. Set to None to not call any command.
nodetext_formatter (callable, optional): This callable should be on
the form `function(nodetext, has_options)`, where `nodetext` is the
node text string and `has_options` a boolean specifying if there
are options associated with this node. It must return a formatted
string.
options_formatter (callable, optional): This callable should be on
the form `function(optionlist)`, where ` optionlist is a list
of option dictionaries, like
[{"key":..., "desc",..., "goto": ..., "exec",...}, ...]
Each dictionary describes each possible option. Note that this
will also be called if there are no options, and so should be
able to handle an empty list. This should
be formatted into an options list and returned as a string,
including the required separator to use between the node text
and the options. If not given the default EvMenu style will be used.
node_formatter (callable, optional): This callable should be on the
form `func(nodetext, optionstext)` where the arguments are strings
representing the node text and options respectively (possibly prepared
by `nodetext_formatter`/`options_formatter` or by the default styles).
It should return a string representing the final look of the node. This
can e.g. be used to create line separators that take into account the
dynamic width of the parts.
Raises: Raises:
EvMenuError: If the start/end node is not found in menu tree. EvMenuError: If the start/end node is not found in menu tree.
@ -280,6 +304,10 @@ class EvMenu(object):
self._startnode = startnode self._startnode = startnode
self._menutree = self._parse_menudata(menudata) self._menutree = self._parse_menudata(menudata)
self._nodetext_formatter = nodetext_formatter
self._options_formatter = nodetext_formatter
self._node_formatter = node_formatter
if startnode not in self._menutree: if startnode not in self._menutree:
raise EvMenuError("Start node '%s' not in menu tree!" % startnode) raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
@ -296,6 +324,7 @@ class EvMenu(object):
self.helptext = None self.helptext = None
self.options = None self.options = None
# store ourself on the object # store ourself on the object
self._caller.ndb._menutree = self self._caller.ndb._menutree = self
@ -354,74 +383,82 @@ class EvMenu(object):
# handle the node text # handle the node text
# #
nodetext = dedent(nodetext).strip() if self._nodetext_formatter:
# use custom formatter
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) nodetext = self._nodetext_formatter(nodetext, len(optionlist))
else:
if not optionlist: nodetext = dedent(nodetext).strip()
# return the node text "naked". nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
separator1 = "_" * nodetext_width_max + "\n\n" if nodetext_width_max else ""
separator2 = "\n" if nodetext_width_max else "" + "_" * nodetext_width_max
return separator1 + nodetext + separator2
# #
# handle the options # handle the options
# #
# column separation distance if self._options_formatter:
colsep = 4 # use custom formatter
optionstext = self._options_formatter(optionlist)
else:
# column separation distance
colsep = 4
nlist = len(optionlist) nlist = len(optionlist)
# get the widest option line in the table. # get the widest option line in the table.
table_width_max = -1 table_width_max = -1
table = [] table = []
for key, desc in optionlist: for key, desc in optionlist:
table_width_max = max(table_width_max, table_width_max = max(table_width_max,
max(m_len(p) for p in key.split("\n")) + max(m_len(p) for p in key.split("\n")) +
max(m_len(p) for p in desc.split("\n")) + colsep) max(m_len(p) for p in desc.split("\n")) + colsep)
raw_key = strip_ansi(key) raw_key = strip_ansi(key)
if raw_key != key: if raw_key != key:
# already decorations in key definition # already decorations in key definition
table.append(ANSIString(" {lc%s{lt%s{le: %s" % (raw_key, key, desc))) table.append(ANSIString(" {lc%s{lt%s{le: %s" % (raw_key, key, desc)))
else: else:
# add a default white color to key # add a default white color to key
table.append(ANSIString(" {lc%s{lt{w%s{n{le: %s" % (raw_key, raw_key, desc))) table.append(ANSIString(" {lc%s{lt{w%s{n{le: %s" % (raw_key, raw_key, desc)))
ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols
nlastcol = nlist % ncols # number of elements left in last row nlastcol = nlist % ncols # number of elements left in last row
# get the amount of rows needed (start with 4 rows) # get the amount of rows needed (start with 4 rows)
nrows = 4 nrows = 4
while nrows * ncols < nlist: while nrows * ncols < nlist:
nrows += 1 nrows += 1
ncols = nlist // nrows # number of full columns ncols = nlist // nrows # number of full columns
nlastcol = nlist % nrows # number of elements in last column nlastcol = nlist % nrows # number of elements in last column
# get the final column count # get the final column count
ncols = ncols + 1 if nlastcol > 0 else ncols ncols = ncols + 1 if nlastcol > 0 else ncols
if ncols > 1: if ncols > 1:
# only extend if longer than one column # only extend if longer than one column
table.extend([" " for i in xrange(nrows-nlastcol)]) table.extend([" " for i in xrange(nrows-nlastcol)])
# build the actual table grid # build the actual table grid
table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)] table = [table[icol*nrows:(icol*nrows) + nrows] for icol in xrange(0, ncols)]
# adjust the width of each column # adjust the width of each column
total_width = 0 options_total_width = 0
for icol in xrange(len(table)): for icol in xrange(len(table)):
col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep col_width = max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]] table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
total_width += col_width options_total_width += col_width
# format the table into columns # format the table into columns
table = EvTable(table=table, border="none") optionstext = unicode(EvTable(table=table, border="none"))
# build the page #
total_width = max(total_width, nodetext_width_max) # format the entire node
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" #
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" if self._node_formatter:
return separator1 + nodetext + separator2 + unicode(table) # use custom formatter
return self._node_formatter(nodetext, optionstext)
else:
# build the page
total_width = max(options_total_width, nodetext_width_max)
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
return separator1 + nodetext + separator2 + optionstext
def _execute_node(self, nodename, raw_string): def _execute_node(self, nodename, raw_string):
""" """