Cherry-pick EvMenu list_node decorator from olc branch
This commit is contained in:
parent
0350b6c3c6
commit
bee7fa174d
1 changed files with 211 additions and 30 deletions
|
|
@ -43,13 +43,18 @@ command definition too) with function definitions:
|
||||||
def node_with_other_name(caller, input_string):
|
def node_with_other_name(caller, input_string):
|
||||||
# code
|
# code
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
def another_node(caller, input_string, **kwargs):
|
||||||
|
# code
|
||||||
|
return text, options
|
||||||
```
|
```
|
||||||
|
|
||||||
Where caller is the object using the menu and input_string is the
|
Where caller is the object using the menu and input_string is the
|
||||||
command entered by the user on the *previous* node (the command
|
command entered by the user on the *previous* node (the command
|
||||||
entered to get to this node). The node function code will only be
|
entered to get to this node). The node function code will only be
|
||||||
executed once per node-visit and the system will accept nodes with
|
executed once per node-visit and the system will accept nodes with
|
||||||
both one or two arguments interchangeably.
|
both one or two arguments interchangeably. It also accepts nodes
|
||||||
|
that takes **kwargs.
|
||||||
|
|
||||||
The menu tree itself is available on the caller as
|
The menu tree itself is available on the caller as
|
||||||
`caller.ndb._menutree`. This makes it a convenient place to store
|
`caller.ndb._menutree`. This makes it a convenient place to store
|
||||||
|
|
@ -82,12 +87,14 @@ menu is immediately exited and the default "look" command is called.
|
||||||
the callable. Those kwargs will also be passed into the next node if possible.
|
the callable. Those kwargs will also be passed into the next node if possible.
|
||||||
Such a callable should return either a str or a (str, dict), where the
|
Such a callable should return either a str or a (str, dict), where the
|
||||||
string is the name of the next node to go to and the dict is the new,
|
string is the name of the next node to go to and the dict is the new,
|
||||||
(possibly modified) kwarg to pass into the next node.
|
(possibly modified) kwarg to pass into the next node. If the callable returns
|
||||||
|
None or the empty string, the current node will be revisited.
|
||||||
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
|
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
|
||||||
and runs before it. If given a node name, the node will be executed but will not
|
and runs before it. If given a node name, the node will be executed but will not
|
||||||
be considered the next node. If node/callback returns str or (str, dict), these will
|
be considered the next node. If node/callback returns str or (str, dict), these will
|
||||||
replace the `goto` step (`goto` callbacks will not fire), with the string being the
|
replace the `goto` step (`goto` callbacks will not fire), with the string being the
|
||||||
next node name and the optional dict acting as the kwargs-input for the next node.
|
next node name and the optional dict acting as the kwargs-input for the next node.
|
||||||
|
If an exec callable returns the empty string (only), the current node is re-run.
|
||||||
|
|
||||||
If key is not given, the option will automatically be identified by
|
If key is not given, the option will automatically be identified by
|
||||||
its number 1..N.
|
its number 1..N.
|
||||||
|
|
@ -167,7 +174,7 @@ from evennia import Command, CmdSet
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils.evtable import EvTable
|
from evennia.utils.evtable import EvTable
|
||||||
from evennia.utils.ansi import strip_ansi
|
from evennia.utils.ansi import strip_ansi
|
||||||
from evennia.utils.utils import mod_import, make_iter, pad, m_len
|
from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
|
|
||||||
# read from protocol NAWS later?
|
# read from protocol NAWS later?
|
||||||
|
|
@ -182,7 +189,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
||||||
|
|
||||||
# i18n
|
# i18n
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.")
|
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is either not implemented or "
|
||||||
|
"caused an error. Make another choice.")
|
||||||
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
|
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
|
||||||
_ERR_NO_OPTION_DESC = _("No description.")
|
_ERR_NO_OPTION_DESC = _("No description.")
|
||||||
_HELP_FULL = _("Commands: <menu option>, help, quit")
|
_HELP_FULL = _("Commands: <menu option>, help, quit")
|
||||||
|
|
@ -573,6 +581,7 @@ class EvMenu(object):
|
||||||
except EvMenuError:
|
except EvMenuError:
|
||||||
errmsg = _ERR_GENERAL.format(nodename=callback)
|
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||||
self.caller.msg(errmsg, self._session)
|
self.caller.msg(errmsg, self._session)
|
||||||
|
logger.log_trace()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -606,9 +615,11 @@ class EvMenu(object):
|
||||||
nodetext, options = ret, None
|
nodetext, options = ret, None
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||||
|
logger.log_trace()
|
||||||
raise EvMenuError
|
raise EvMenuError
|
||||||
except Exception:
|
except Exception:
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
||||||
|
logger.log_trace()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# store options to make them easier to test
|
# store options to make them easier to test
|
||||||
|
|
@ -665,24 +676,27 @@ class EvMenu(object):
|
||||||
|
|
||||||
if isinstance(ret, basestring):
|
if isinstance(ret, basestring):
|
||||||
# only return a value if a string (a goto target), ignore all other returns
|
# only return a value if a string (a goto target), ignore all other returns
|
||||||
|
if not ret:
|
||||||
|
# an empty string - rerun the same node
|
||||||
|
return self.nodename
|
||||||
return ret, kwargs
|
return ret, kwargs
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def goto(self, nodename, raw_string, **kwargs):
|
def extract_goto_exec(self, nodename, option_dict):
|
||||||
"""
|
"""
|
||||||
Run a node by name, optionally dynamically generating that name first.
|
Helper: Get callables and their eventual kwargs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodename (str or callable): Name of node or a callable
|
nodename (str): The current node name (used for error reporting).
|
||||||
to be called as `function(caller, raw_string)` or `function(caller)`
|
option_dict (dict): The seleted option's dict.
|
||||||
to return the actual goto string.
|
|
||||||
raw_string (str): The raw default string entered on the
|
Returns:
|
||||||
previous node (only used if the node accepts it as an
|
goto (str, callable or None): The goto directive in the option.
|
||||||
argument)
|
goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.
|
||||||
|
execute (callable or None): Executable given by the `exec` directive.
|
||||||
|
exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def _extract_goto_exec(option_dict):
|
|
||||||
"Helper: Get callables and their eventual kwargs"
|
|
||||||
goto_kwargs, exec_kwargs = {}, {}
|
goto_kwargs, exec_kwargs = {}, {}
|
||||||
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||||
if goto and isinstance(goto, (tuple, list)):
|
if goto and isinstance(goto, (tuple, list)):
|
||||||
|
|
@ -705,6 +719,20 @@ class EvMenu(object):
|
||||||
execute = execute[0]
|
execute = execute[0]
|
||||||
return goto, goto_kwargs, execute, exec_kwargs
|
return goto, goto_kwargs, execute, exec_kwargs
|
||||||
|
|
||||||
|
def goto(self, nodename, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Run a node by name, optionally dynamically generating that name first.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodename (str or callable): Name of node or a callable
|
||||||
|
to be called as `function(caller, raw_string)` or `function(caller)`
|
||||||
|
to return the actual goto string.
|
||||||
|
raw_string (str): The raw default string entered on the
|
||||||
|
previous node (only used if the node accepts it as an
|
||||||
|
argument)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
if callable(nodename):
|
if callable(nodename):
|
||||||
# run the "goto" callable, if possible
|
# run the "goto" callable, if possible
|
||||||
inp_nodename = nodename
|
inp_nodename = nodename
|
||||||
|
|
@ -714,6 +742,9 @@ class EvMenu(object):
|
||||||
raise EvMenuError(
|
raise EvMenuError(
|
||||||
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
||||||
nodename, kwargs = nodename[:2]
|
nodename, kwargs = nodename[:2]
|
||||||
|
if not nodename:
|
||||||
|
# no nodename return. Re-run current node
|
||||||
|
nodename = self.nodename
|
||||||
try:
|
try:
|
||||||
# execute the found node, make use of the returns.
|
# execute the found node, make use of the returns.
|
||||||
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
||||||
|
|
@ -746,12 +777,12 @@ class EvMenu(object):
|
||||||
desc = dic.get("desc", dic.get("text", None))
|
desc = dic.get("desc", dic.get("text", None))
|
||||||
if "_default" in keys:
|
if "_default" in keys:
|
||||||
keys = [key for key in keys if key != "_default"]
|
keys = [key for key in keys if key != "_default"]
|
||||||
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
|
||||||
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
||||||
else:
|
else:
|
||||||
# use the key (only) if set, otherwise use the running number
|
# use the key (only) if set, otherwise use the running number
|
||||||
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
||||||
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
|
||||||
if keys:
|
if keys:
|
||||||
display_options.append((keys[0], desc))
|
display_options.append((keys[0], desc))
|
||||||
for key in keys:
|
for key in keys:
|
||||||
|
|
@ -945,14 +976,164 @@ class EvMenu(object):
|
||||||
node (str): The formatted node to display.
|
node (str): The formatted node to display.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
screen_width = self._session.protocol_flags.get("SCREENWIDTH", {0: 78})[0]
|
||||||
|
|
||||||
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
|
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
|
||||||
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
|
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
|
||||||
total_width = max(options_width_max, nodetext_width_max)
|
total_width = min(screen_width, max(options_width_max, nodetext_width_max))
|
||||||
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
|
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
|
||||||
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
|
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
|
||||||
return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext
|
return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
#
|
||||||
|
# List node (decorator turning a node into a list with
|
||||||
|
# look/edit/add functionality for the elements)
|
||||||
|
#
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
|
||||||
|
def list_node(option_generator, select=None, pagesize=10):
|
||||||
|
"""
|
||||||
|
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
|
||||||
|
prepending those options added in the node.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
option_generator (callable or list): A list of strings indicating the options, or a callable
|
||||||
|
that is called as option_generator(caller) to produce such a list.
|
||||||
|
select (callable, option): Will be called as select(caller, menuchoice)
|
||||||
|
where menuchoice is the chosen option as a string. Should return the target node to
|
||||||
|
goto after this selection (or None to repeat the list-node). Note that if this is not
|
||||||
|
given, the decorated node must itself provide a way to continue from the node!
|
||||||
|
pagesize (int): How many options to show per page.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@list_node(['foo', 'bar'], select)
|
||||||
|
def node_index(caller):
|
||||||
|
text = "describing the list"
|
||||||
|
return text, []
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept
|
||||||
|
**kwargs, get a new kwarg 'available_choices' injected. These are the ordered list of named
|
||||||
|
options (descs) visible on the current node page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
|
||||||
|
def _select_parser(caller, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Parse the select action
|
||||||
|
"""
|
||||||
|
available_choices = kwargs.get("available_choices", [])
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = int(raw_string.strip()) - 1
|
||||||
|
selection = available_choices[index]
|
||||||
|
except Exception:
|
||||||
|
caller.msg("|rInvalid choice.|n")
|
||||||
|
else:
|
||||||
|
if select:
|
||||||
|
try:
|
||||||
|
return select(caller, selection)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _list_node(caller, raw_string, **kwargs):
|
||||||
|
|
||||||
|
option_list = option_generator(caller) \
|
||||||
|
if callable(option_generator) else option_generator
|
||||||
|
|
||||||
|
npages = 0
|
||||||
|
page_index = 0
|
||||||
|
page = []
|
||||||
|
options = []
|
||||||
|
|
||||||
|
if option_list:
|
||||||
|
nall_options = len(option_list)
|
||||||
|
pages = [option_list[ind:ind + pagesize]
|
||||||
|
for ind in range(0, nall_options, pagesize)]
|
||||||
|
npages = len(pages)
|
||||||
|
|
||||||
|
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
|
||||||
|
page = pages[page_index]
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
extra_text = None
|
||||||
|
|
||||||
|
# dynamic, multi-page option list. Each selection leads to the `select`
|
||||||
|
# callback being called with a result from the available choices
|
||||||
|
options.extend([{"desc": opt,
|
||||||
|
"goto": (_select_parser,
|
||||||
|
{"available_choices": page})} for opt in page])
|
||||||
|
|
||||||
|
if npages > 1:
|
||||||
|
# if the goto callable returns None, the same node is rerun, and
|
||||||
|
# kwargs not used by the callable are passed on to the node. This
|
||||||
|
# allows us to call ourselves over and over, using different kwargs.
|
||||||
|
options.append({"key": ("|Wcurrent|n", "c"),
|
||||||
|
"desc": "|W({}/{})|n".format(page_index + 1, npages),
|
||||||
|
"goto": (lambda caller: None,
|
||||||
|
{"optionpage_index": page_index})})
|
||||||
|
if page_index > 0:
|
||||||
|
options.append({"key": ("|wp|Wrevious page|n", "p"),
|
||||||
|
"goto": (lambda caller: None,
|
||||||
|
{"optionpage_index": page_index - 1})})
|
||||||
|
if page_index < npages - 1:
|
||||||
|
options.append({"key": ("|wn|Wext page|n", "n"),
|
||||||
|
"goto": (lambda caller: None,
|
||||||
|
{"optionpage_index": page_index + 1})})
|
||||||
|
|
||||||
|
# add data from the decorated node
|
||||||
|
|
||||||
|
decorated_options = []
|
||||||
|
try:
|
||||||
|
text, decorated_options = func(caller, raw_string)
|
||||||
|
except TypeError:
|
||||||
|
try:
|
||||||
|
text, decorated_options = func(caller)
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
else:
|
||||||
|
if isinstance(decorated_options, {}):
|
||||||
|
decorated_options = [decorated_options]
|
||||||
|
else:
|
||||||
|
decorated_options = make_iter(decorated_options)
|
||||||
|
|
||||||
|
extra_options = []
|
||||||
|
for eopt in decorated_options:
|
||||||
|
cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None
|
||||||
|
if cback:
|
||||||
|
signature = eopt[cback]
|
||||||
|
if callable(signature):
|
||||||
|
# callable with no kwargs defined
|
||||||
|
eopt[cback] = (signature, {"available_choices": page})
|
||||||
|
elif is_iter(signature):
|
||||||
|
if len(signature) > 1 and isinstance(signature[1], dict):
|
||||||
|
signature[1]["available_choices"] = page
|
||||||
|
eopt[cback] = signature
|
||||||
|
elif signature:
|
||||||
|
# a callable alone in a tuple (i.e. no previous kwargs)
|
||||||
|
eopt[cback] = (signature[0], {"available_choices": page})
|
||||||
|
else:
|
||||||
|
# malformed input.
|
||||||
|
logger.log_err("EvMenu @list_node decorator found "
|
||||||
|
"malformed option to decorate: {}".format(eopt))
|
||||||
|
extra_options.append(eopt)
|
||||||
|
|
||||||
|
options.extend(extra_options)
|
||||||
|
text = text + "\n\n" + extra_text if extra_text else text
|
||||||
|
|
||||||
|
return text, options
|
||||||
|
|
||||||
|
return _list_node
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Simple input shortcuts
|
# Simple input shortcuts
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue