mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 00:06:30 +01:00
Start add edit_node decorator (untested)
This commit is contained in:
parent
02b9654f1c
commit
ed3e57edd0
2 changed files with 331 additions and 106 deletions
|
|
@ -175,7 +175,7 @@ from evennia import Command, CmdSet
|
|||
from evennia.utils import logger
|
||||
from evennia.utils.evtable import EvTable
|
||||
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
|
||||
|
||||
# read from protocol NAWS later?
|
||||
|
|
@ -683,6 +683,43 @@ class EvMenu(object):
|
|||
return ret, kwargs
|
||||
return None
|
||||
|
||||
def extract_goto_exec(self, nodename, option_dict):
|
||||
"""
|
||||
Helper: Get callables and their eventual kwargs.
|
||||
|
||||
Args:
|
||||
nodename (str): The current node name (used for error reporting).
|
||||
option_dict (dict): The seleted option's dict.
|
||||
|
||||
Returns:
|
||||
goto (str, callable or None): The goto directive in the option.
|
||||
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.
|
||||
|
||||
"""
|
||||
goto_kwargs, exec_kwargs = {}, {}
|
||||
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||
if goto and isinstance(goto, (tuple, list)):
|
||||
if len(goto) > 1:
|
||||
goto, goto_kwargs = goto[:2] # ignore any extra arguments
|
||||
if not hasattr(goto_kwargs, "__getitem__"):
|
||||
# not a dict-like structure
|
||||
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
|
||||
nodename, goto_kwargs))
|
||||
else:
|
||||
goto = goto[0]
|
||||
if execute and isinstance(execute, (tuple, list)):
|
||||
if len(execute) > 1:
|
||||
execute, exec_kwargs = execute[:2] # ignore any extra arguments
|
||||
if not hasattr(exec_kwargs, "__getitem__"):
|
||||
# not a dict-like structure
|
||||
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
|
||||
nodename, goto_kwargs))
|
||||
else:
|
||||
execute = execute[0]
|
||||
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.
|
||||
|
|
@ -696,29 +733,6 @@ class EvMenu(object):
|
|||
argument)
|
||||
|
||||
"""
|
||||
def _extract_goto_exec(option_dict):
|
||||
"Helper: Get callables and their eventual kwargs"
|
||||
goto_kwargs, exec_kwargs = {}, {}
|
||||
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||
if goto and isinstance(goto, (tuple, list)):
|
||||
if len(goto) > 1:
|
||||
goto, goto_kwargs = goto[:2] # ignore any extra arguments
|
||||
if not hasattr(goto_kwargs, "__getitem__"):
|
||||
# not a dict-like structure
|
||||
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
|
||||
nodename, goto_kwargs))
|
||||
else:
|
||||
goto = goto[0]
|
||||
if execute and isinstance(execute, (tuple, list)):
|
||||
if len(execute) > 1:
|
||||
execute, exec_kwargs = execute[:2] # ignore any extra arguments
|
||||
if not hasattr(exec_kwargs, "__getitem__"):
|
||||
# not a dict-like structure
|
||||
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
|
||||
nodename, goto_kwargs))
|
||||
else:
|
||||
execute = execute[0]
|
||||
return goto, goto_kwargs, execute, exec_kwargs
|
||||
|
||||
if callable(nodename):
|
||||
# run the "goto" callable, if possible
|
||||
|
|
@ -764,12 +778,12 @@ class EvMenu(object):
|
|||
desc = dic.get("desc", dic.get("text", None))
|
||||
if "_default" in keys:
|
||||
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)
|
||||
else:
|
||||
# use the key (only) if set, otherwise use the running number
|
||||
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:
|
||||
display_options.append((keys[0], desc))
|
||||
for key in keys:
|
||||
|
|
@ -975,11 +989,143 @@ class EvMenu(object):
|
|||
|
||||
# -----------------------------------------------------------
|
||||
#
|
||||
# List node
|
||||
# Edit node (decorator turning a node into an editing
|
||||
# point for a given resource
|
||||
#
|
||||
# -----------------------------------------------------------
|
||||
|
||||
def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
|
||||
def edit_node(edit_text, add_text, edit_callback, add_callback, get_choices=None):
|
||||
"""
|
||||
Decorator for turning an EvMenu node into an editing
|
||||
page. Will add new options, prepending those options
|
||||
added in the node.
|
||||
|
||||
Args:
|
||||
edit_text (str or callable): Will be used as text for the edit node. If
|
||||
callable, it will be called as edittext(selection)
|
||||
and should return the node text for the edit-node, probably listing
|
||||
the current value of all editable propnames, if possible.
|
||||
add_text (str) or callable: Gives text for node in add-mode. If a callable,
|
||||
called as add_text() and should return the text for the node.
|
||||
edit_callback (callable): Will be called as edit_callback(editable, raw_string)
|
||||
and should return a boolean True/False if the setting of the property
|
||||
succeeded or not. The value will always be a string and should be
|
||||
converted as needed.
|
||||
add_callback (callable): Will be called as add_callback(raw_string) and
|
||||
should return a boolean True/False if the addition succeded.
|
||||
|
||||
get_choices (callable): Produce the available editable choices. If this
|
||||
is not given, the `goto` callable must have been provided with the
|
||||
kwarg `available_choices` by the decorated node.
|
||||
|
||||
"""
|
||||
|
||||
def decorator(func):
|
||||
|
||||
def _setter_goto(caller, raw_string, **kwargs):
|
||||
editable = kwargs.get("editable")
|
||||
mode = kwargs.get("edit_node_mode")
|
||||
try:
|
||||
if mode == 'edit':
|
||||
is_ok = edit_callback(editable, raw_string)
|
||||
else:
|
||||
is_ok = add_callback(raw_string)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
if not is_ok:
|
||||
caller.msg("|rValue could not be set.")
|
||||
return None
|
||||
|
||||
def _patch_goto(caller, raw_string, **kwargs):
|
||||
|
||||
# parse incoming string to figure out if there is a match to edit/add
|
||||
match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string)
|
||||
cmd, number = match.groups()
|
||||
edit_mode = None
|
||||
available_choices = None
|
||||
selection = None
|
||||
|
||||
if get_choices:
|
||||
available_choices = make_iter(get_choices(caller, raw_string, **kwargs))
|
||||
if not available_choices:
|
||||
available_choices = kwargs.get("available_choices", [])
|
||||
|
||||
if available_choices and cmd.startswith("e"):
|
||||
try:
|
||||
index = int(cmd) - 1
|
||||
selection = available_choices[index]
|
||||
edit_mode = 'edit'
|
||||
except (IndexError, TypeError):
|
||||
caller.msg("|rNot a valid 'edit' command.")
|
||||
|
||||
if cmd.startswith("a") and not number:
|
||||
# add mode
|
||||
edit_mode = "add"
|
||||
|
||||
if edit_mode:
|
||||
# replace with edit text/options
|
||||
text = edit_text(selection) if edit_mode == "edit" else add_text()
|
||||
options = ({"key": "_default",
|
||||
"goto": (_setter_goto,
|
||||
{"selection": selection,
|
||||
"edit_node_mode": edit_mode})})
|
||||
return text, options
|
||||
|
||||
# no matches - pass through to the original decorated goto instruction
|
||||
|
||||
decorated_opt = kwargs.get("decorated_opt")
|
||||
|
||||
if decorated_opt:
|
||||
# use EvMenu's parser to get the goto/goto-kwargs out of
|
||||
# the decorated option structure
|
||||
dec_goto, dec_goto_kwargs, _, _ = \
|
||||
caller.ndb._menutree.extract_goto_exec("edit-node", decorated_opt)
|
||||
|
||||
if callable(dec_goto):
|
||||
try:
|
||||
return dec_goto(caller, raw_string,
|
||||
**{dec_goto_kwargs if dec_goto_kwargs else {}})
|
||||
except Exception:
|
||||
caller.msg("|rThere was an error in the edit node.")
|
||||
logger.log_trace()
|
||||
return None
|
||||
|
||||
def _edit_node(caller, raw_string, **kwargs):
|
||||
|
||||
text, options = func(caller, raw_string, **kwargs)
|
||||
|
||||
if options:
|
||||
# find eventual _default in options and patch it with a handler for
|
||||
# catching editing
|
||||
|
||||
decorated_opt = None
|
||||
iopt = 0
|
||||
for iopt, optdict in enumerate(options):
|
||||
if optdict.get('key') == "_default":
|
||||
decorated_opt = optdict
|
||||
break
|
||||
|
||||
if decorated_opt:
|
||||
# inject our wrapper over the original goto instruction for the
|
||||
# _default action (save the original)
|
||||
options[iopt]["goto"] = (_patch_goto,
|
||||
{"decorated_opt": decorated_opt})
|
||||
|
||||
return text, options
|
||||
|
||||
return _edit_node
|
||||
return decorator
|
||||
|
||||
|
||||
|
||||
# -----------------------------------------------------------
|
||||
#
|
||||
# List node (decorator turning a node into a list with
|
||||
# look/edit/add functionality for the elements)
|
||||
#
|
||||
# -----------------------------------------------------------
|
||||
|
||||
def list_node(option_generator, select=None, examine=None, edit=None, add=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.
|
||||
|
|
@ -987,17 +1133,25 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
|
|||
Args:
|
||||
option_generator (callable or list): A list of strings indicating the options, or a callable
|
||||
that is called without any arguments to produce such a list.
|
||||
examine_processor (callable, optional): Will be called with the caller and the chosen option
|
||||
when examining said option. Should return a text string to display in the node.
|
||||
goto_processor (callable, optional): Will be called as goto_processor(caller, menuchoice)
|
||||
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.
|
||||
goto after this selection. Note that if this is not given, the decorated node must itself
|
||||
provide a way to continue from the node!
|
||||
examine (callable, optional): If given, allows for examining options in detail. Will
|
||||
be called with examine(caller, menuchoice) and should return a text string to
|
||||
display in-place in the node.
|
||||
edit (callable, optional): If given, this callable will be called as edit(caller, menuchoice).
|
||||
It should return the node-key to a node decorated with the `edit_node` decorator. The
|
||||
menuchoice will automatically be stored on the menutree as `list_node_edit`.
|
||||
add (tuple, optional): If given, this callable will be called as add(caller, menuchoice).
|
||||
It should return the node-key to a node decorated with the `edit_node` decorator. The
|
||||
menuchoice will automatically be stored on the menutree as `list_node_add`.
|
||||
|
||||
pagesize (int): How many options to show per page.
|
||||
|
||||
Example:
|
||||
|
||||
@list_node(['foo', 'bar'], examine_processor, goto_processor)
|
||||
@list_node(['foo', 'bar'], examine, select)
|
||||
def node_index(caller):
|
||||
text = "describing the list"
|
||||
return text, []
|
||||
|
|
@ -1006,27 +1160,63 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
|
|||
|
||||
def decorator(func):
|
||||
|
||||
def _input_parser(caller, raw_string, **kwargs):
|
||||
"Parse which input was given, select from option_list"
|
||||
|
||||
def _select_parser(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Parse the select action
|
||||
"""
|
||||
available_choices = kwargs.get("available_choices", [])
|
||||
processor = kwargs.get("selection_processor")
|
||||
|
||||
try:
|
||||
match_ind = int(re.search(r"[0-9]+$", raw_string).group()) - 1
|
||||
selection = available_choices[match_ind]
|
||||
except (AttributeError, KeyError, IndexError, ValueError):
|
||||
return None
|
||||
|
||||
if processor:
|
||||
index = int(raw_string.strip()) - 1
|
||||
selection = available_choices[index]
|
||||
except Exception:
|
||||
caller.msg("|rInvalid choice.|n")
|
||||
else:
|
||||
try:
|
||||
return processor(caller, selection)
|
||||
return select(caller, selection)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
return selection
|
||||
return None
|
||||
|
||||
def _input_parser(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Parse which input was given, select from option_list.
|
||||
|
||||
Understood input is [cmd]<num>, where [cmd] is either empty (`select`)
|
||||
or one of the supported actions `look`, `edit` or `add` depending on
|
||||
which processors are available.
|
||||
|
||||
"""
|
||||
|
||||
available_choices = kwargs.get("available_choices", [])
|
||||
match = re.search(r"(^[a-zA-Z]*)\s*([0-9]*)$", raw_string)
|
||||
cmd, number = match.groups()
|
||||
mode, selection = None, None
|
||||
|
||||
if number:
|
||||
number = int(number) - 1
|
||||
cmd = cmd.lower().strip()
|
||||
if cmd.startswith("e") or cmd.startswith("a") and edit:
|
||||
mode = "edit"
|
||||
elif examine:
|
||||
mode = "examine"
|
||||
|
||||
try:
|
||||
selection = available_choices[number]
|
||||
except IndexError:
|
||||
caller.msg("|rInvalid index")
|
||||
mode = None
|
||||
else:
|
||||
caller.msg("|rMust supply a number.")
|
||||
|
||||
return mode, selection
|
||||
|
||||
def _relay_to_edit_or_add(caller, raw_string, **kwargs):
|
||||
pass
|
||||
|
||||
def _list_node(caller, raw_string, **kwargs):
|
||||
|
||||
mode = kwargs.get("list_mode", None)
|
||||
option_list = option_generator() if callable(option_generator) else option_generator
|
||||
|
||||
nall_options = len(option_list)
|
||||
|
|
@ -1035,71 +1225,104 @@ def list_node(option_generator, examine_processor, goto_processor, pagesize=10):
|
|||
|
||||
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
|
||||
page = pages[page_index]
|
||||
entry = None
|
||||
extra_text = None
|
||||
|
||||
# dynamic, multi-page option list. We use _input_parser as a goto-callable,
|
||||
# with the `goto_processor` redirecting when we leave the node.
|
||||
options = [{"desc": opt,
|
||||
"goto": (_input_parser,
|
||||
{"available_choices": page,
|
||||
"selection_processor": goto_processor})} for opt in page]
|
||||
if mode == "arbitrary":
|
||||
# freeform input, we must parse it for the allowed commands (look/edit)
|
||||
mode, entry = _input_parser(caller, raw_string,
|
||||
**{"available_choices": 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})})
|
||||
|
||||
|
||||
# this catches arbitrary input, notably to examine entries ('look 4' or 'l4' etc)
|
||||
options.append({"key": "_default",
|
||||
"goto": (lambda caller: None,
|
||||
{"show_detail": True, "optionpage_index": page_index})})
|
||||
|
||||
# update text with detail, if set. Here we call _input_parser like a normal function
|
||||
text_detail = None
|
||||
if raw_string and 'show_detail' in kwargs:
|
||||
text_detail = _input_parser(
|
||||
caller, raw_string, **{"available_choices": page,
|
||||
"selection_processor": examine_processor})
|
||||
if text_detail is None:
|
||||
text_detail = "|rThat's not a valid command or option.|n"
|
||||
|
||||
# add data from the decorated node
|
||||
|
||||
text = ''
|
||||
extra_options = []
|
||||
try:
|
||||
text, extra_options = func(caller, raw_string)
|
||||
except TypeError:
|
||||
if examine and mode: # == "look":
|
||||
# look mode - we are examining a given entry
|
||||
try:
|
||||
text, extra_options = func(caller)
|
||||
text = examine(caller, entry)
|
||||
except Exception:
|
||||
raise
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
print("extra_options:", extra_options)
|
||||
logger.log_trace()
|
||||
text = "|rCould not view."
|
||||
options = [{"key": ("|wb|Wack|n", "b"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index})},
|
||||
{"key": "_default",
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index})}]
|
||||
return text, options
|
||||
|
||||
# if edit and mode == "edit":
|
||||
# pass
|
||||
# elif add and mode == "add":
|
||||
# # add mode - we are adding a new entry
|
||||
# pass
|
||||
|
||||
else:
|
||||
if isinstance(extra_options, {}):
|
||||
extra_options = [extra_options]
|
||||
# normal mode - list
|
||||
pass
|
||||
|
||||
if select:
|
||||
# We have a processor to handle selecting an entry
|
||||
|
||||
# dynamic, multi-page option list. Each selection leads to the `select`
|
||||
# callback being called with a result from the available choices
|
||||
options = [{"desc": opt,
|
||||
"goto": (_select_parser,
|
||||
{"available_choices": page})} for opt in page]
|
||||
|
||||
if add:
|
||||
# We have a processor to handle adding a new entry. Re-run this node
|
||||
# in the 'add' mode
|
||||
options.append({"key": ("|wadd|Wdd new|n", "a"),
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index,
|
||||
"list_mode": "add"})})
|
||||
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})})
|
||||
|
||||
# this catches arbitrary input and reruns this node with the 'arbitrary' mode
|
||||
# this could mean input on the form 'look <num>' or 'edit <num>'
|
||||
options.append({"key": "_default",
|
||||
"goto": (lambda caller: None,
|
||||
{"optionpage_index": page_index,
|
||||
"available_choices": page,
|
||||
"list_mode": "arbitrary"})})
|
||||
|
||||
# add data from the decorated node
|
||||
|
||||
extra_options = []
|
||||
try:
|
||||
text, extra_options = func(caller, raw_string)
|
||||
except TypeError:
|
||||
try:
|
||||
text, extra_options = func(caller)
|
||||
except Exception:
|
||||
raise
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
print("extra_options:", extra_options)
|
||||
else:
|
||||
extra_options = make_iter(extra_options)
|
||||
if isinstance(extra_options, {}):
|
||||
extra_options = [extra_options]
|
||||
else:
|
||||
extra_options = make_iter(extra_options)
|
||||
|
||||
options.extend(extra_options)
|
||||
text = text + "\n\n" + text_detail if text_detail else text
|
||||
text += "\n\n(Make a choice or enter 'look <num>' to examine an option closer)"
|
||||
options.extend(extra_options)
|
||||
text = text + "\n\n" + extra_text if extra_text else text
|
||||
text += "\n\n(Make a choice or enter 'look <num>' to examine an option closer)"
|
||||
|
||||
return text, options
|
||||
return text, options
|
||||
|
||||
return _list_node
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -690,9 +690,11 @@ def spawn(*prototypes, **kwargs):
|
|||
# Helper functions
|
||||
|
||||
def _get_menu_metaprot(caller):
|
||||
|
||||
metaproto = None
|
||||
if hasattr(caller.ndb._menutree, "olc_metaprot"):
|
||||
return caller.ndb._menutree.olc_metaprot
|
||||
else:
|
||||
metaproto = caller.ndb._menutree.olc_metaprot
|
||||
if not metaproto:
|
||||
metaproto = build_metaproto(None, '', [], [], None)
|
||||
caller.ndb._menutree.olc_metaprot = metaproto
|
||||
caller.ndb._menutree.olc_new = True
|
||||
|
|
@ -931,7 +933,7 @@ def _prototype_select(caller, prototype):
|
|||
return ret
|
||||
|
||||
|
||||
@list_node(_all_prototypes, _prototype_examine, _prototype_select)
|
||||
@list_node(_all_prototypes, _prototype_select, examine=_prototype_examine)
|
||||
def node_prototype(caller):
|
||||
metaprot = _get_menu_metaprot(caller)
|
||||
prot = metaprot.prototype
|
||||
|
|
@ -979,7 +981,7 @@ def _typeclass_select(caller, typeclass):
|
|||
return ret
|
||||
|
||||
|
||||
@list_node(_all_typeclasses, _typeclass_examine, _typeclass_select)
|
||||
@list_node(_all_typeclasses, _typeclass_select, examine=_typeclass_examine)
|
||||
def node_typeclass(caller):
|
||||
metaprot = _get_menu_metaprot(caller)
|
||||
prot = metaprot.prototype
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue