Cleanup/refactoring of olc menus

This commit is contained in:
Griatch 2018-08-11 11:49:10 +02:00
parent e49993fbb5
commit 298b2c23c6
6 changed files with 216 additions and 92 deletions

View file

@ -26,8 +26,23 @@
- A `goto` option callable returning None (rather than the name of the next node) will now rerun the
current node instead of failing.
- Better error handling of in-node syntax errors.
- Improve dedent of default text/helptext formatter. Right-strip whitespace.
### Utils
- Added new `columnize` function for easily splitting text into multiple columns. At this point it
is not working too well with ansi-colored text however.
- Extend the `dedent` function with a new `baseline_index` kwarg. This allows to force all lines to
the indentation given by the given line regardless of if other lines were already a 0 indentation.
This removes a problem with the original `textwrap.dedent` which will only dedent to the least
indented part of a text.
- Added `exit_cmd` to EvMore pager, to allow for calling a command (e.g. 'look') when leaving the pager.
### Genaral
- Start structuring the `CHANGELOG` to list features in more detail.
# Overviews

View file

@ -214,6 +214,10 @@ def _wizard_options(curr_node, prev_node, next_node, color="|W", search=False):
return options
def _set_actioninfo(caller, string):
caller.ndb._menutree.actioninfo = string
def _path_cropper(pythonpath):
"Crop path to only the last component"
return pythonpath.split('.')[-1]
@ -278,30 +282,65 @@ def _format_list_actions(*args, **kwargs):
prefix = kwargs.get('prefix', "|WSelect with |w<num>|W. Other actions:|n ")
for action in args:
actions.append("|w{}|n|W{} |w<num>|n".format(action[0], action[1:]))
return prefix + "|W,|n ".join(actions)
return prefix + " |W|||n ".join(actions)
def _get_current_value(caller, keyname, formatter=str, only_inherit=False):
"Return current value, marking if value comes from parent or set in this prototype"
prot = _get_menu_prototype(caller)
if keyname in prot:
# value in current prot
def _get_current_value(caller, keyname, comparer=None, formatter=str, only_inherit=False):
"""
Return current value, marking if value comes from parent or set in this prototype.
Args:
keyname (str): Name of prototoype key to get current value of.
comparer (callable, optional): This will be called as comparer(prototype_value,
flattened_value) and is expected to return the value to show as the current
or inherited one. If not given, a straight comparison is used and what is returned
depends on the only_inherit setting.
formatter (callable, optional)): This will be called with the result of comparer.
only_inherit (bool, optional): If a current value should only be shown if all
the values are inherited from the prototype parent (otherwise, show an empty string).
Returns:
current (str): The current value.
"""
def _default_comparer(protval, flatval):
if only_inherit:
return ''
return "Current {}: {}".format(keyname, formatter(prot[keyname]))
flat_prot = _get_flat_menu_prototype(caller)
if keyname in flat_prot:
# value in flattened prot
if keyname == 'prototype_key':
# we don't inherit prototype_keys
return "[No prototype_key set] (|rnot inherited|n)"
return "" if protval else flatval
else:
ret = "Current {} (|binherited|n): {}".format(keyname, formatter(flat_prot[keyname]))
if only_inherit:
return "{}\n\n".format(ret)
return ret
return protval if protval else flatval
return "[No {} set]".format(keyname)
if not callable(comparer):
comparer = _default_comparer
prot = _get_menu_prototype(caller)
flat_prot = _get_flat_menu_prototype(caller)
out = ""
if keyname in prot:
if keyname in flat_prot:
out = formatter(comparer(prot[keyname], flat_prot[keyname]))
if only_inherit:
if out:
return "|WCurrent|n {} |W(|binherited|W):|n {}".format(keyname, out)
return ""
else:
if out:
return "|WCurrent|n {}|W:|n {}".format(keyname, out)
return "|W[No {} set]|n".format(keyname)
elif only_inherit:
return ""
else:
out = formatter(prot[keyname])
return "|WCurrent|n {}|W:|n {}".format(keyname, out)
elif keyname in flat_prot:
out = formatter(flat_prot[keyname])
if out:
return "|WCurrent|n {} |W(|n|binherited|W):|n {}".format(keyname, out)
else:
return ""
elif only_inherit:
return ""
else:
return "|W[No {} set]|n".format(keyname)
def _default_parse(raw_inp, choices, *args):
@ -491,10 +530,9 @@ def node_search_object(caller, raw_inp, **kwargs):
text = """
Found {num} match{post}.
{actions}
(|RWarning: creating a prototype will |roverwrite|r |Rthe current prototype!)|n""".format(
num=nmatches, post="es" if nmatches > 1 else "",
actions=_format_list_actions(
num=nmatches, post="es" if nmatches > 1 else "")
_set_actioninfo(caller, _format_list_actions(
"examine", "create prototype from object", prefix="Actions: "))
else:
text = "Enter search criterion."
@ -758,8 +796,6 @@ def node_prototype_parent(caller):
parent is given, this prototype must define the typeclass (next menu node).
{current}
{actions}
"""
helptext = """
Prototypes can inherit from one another. Changes in the child replace any values set in a
@ -767,6 +803,8 @@ def node_prototype_parent(caller):
prototype to be valid.
"""
_set_actioninfo(caller, _format_list_actions("examine", "add", "remove"))
ptexts = []
if prot_parent_keys:
for pkey in utils.make_iter(prot_parent_keys):
@ -782,8 +820,7 @@ def node_prototype_parent(caller):
if not ptexts:
ptexts.append("[No prototype_parent set]")
text = text.format(current="\n\n".join(ptexts),
actions=_format_list_actions("examine", "add", "remove"))
text = text.format(current="\n\n".join(ptexts))
text = (text, helptext)
@ -854,8 +891,6 @@ def node_typeclass(caller):
one of the prototype's |cparents|n.
{current}
{actions}
""".format(current=_get_current_value(caller, "typeclass"),
actions="|WSelect with |w<num>|W. Other actions: "
"|we|Wxamine |w<num>|W, |wr|Wemove selection")
@ -962,11 +997,15 @@ def node_aliases(caller):
|cAliases|n are alternative ways to address an object, next to its |cKey|n. Aliases are not
case sensitive.
{actions}
{current}
""".format(actions=_format_list_actions("remove",
prefix="|w<text>|W to add new alias. Other action: "),
current)
""".format(current=_get_current_value(
caller, 'aliases',
comparer=lambda propval, flatval: [al for al in flatval if al not in propval],
formatter=lambda lst: "\n" + ", ".join(lst), only_inherit=True))
_set_actioninfo(caller,
_format_list_actions(
"remove",
prefix="|w<text>|W to add new alias. Other action: "))
helptext = """
Aliases are fixed alternative identifiers and are stored with the new object.
@ -1009,14 +1048,13 @@ def _display_attribute(attr_tuple):
attrkey, value, category, locks = attr_tuple
value = protlib.protfunc_parser(value)
typ = type(value)
out = ("|cAttribute key:|n '{attrkey}' "
"(|ccategory:|n {category}, "
"|clocks:|n {locks})\n"
"|cValue|n |W(parsed to {typ})|n:\n{value}").format(
attrkey=attrkey,
category=category if category else "|wNone|n",
locks=locks if locks else "|wNone|n",
typ=typ, value=value)
out = ("{attrkey} |c=|n {value} |W({typ}{category}{locks})|n".format(
attrkey=attrkey,
value=value,
typ=typ,
category=", category={}".format(category) if category else '',
locks=", locks={}".format(";".join(locks)) if any(locks) else ''))
return out
@ -1130,6 +1168,12 @@ def _attrs_actions(caller, raw_inp, **kwargs):
@list_node(_caller_attrs, _attr_select)
def node_attrs(caller):
def _currentcmp(propval, flatval):
"match by key + category"
cmp1 = [(tup[0].lower(), tup[2].lower() if tup[2] else None) for tup in propval]
return [tup for tup in flatval if (tup[0].lower(), tup[2].lower()
if tup[2] else None) not in cmp1]
text = """
|cAttributes|n are custom properties of the object. Enter attributes on one of these forms:
@ -1140,8 +1184,14 @@ def node_attrs(caller):
To give an attribute without a category but with a lockstring, leave that spot empty
(attrname;;lockstring=value). Attribute values can have embedded $protfuncs.
{actions}
""".format(actions=_format_list_actions("examine", "remove", prefix="Actions: "))
{current}
""".format(
current=_get_current_value(
caller, "attrs",
comparer=_currentcmp,
formatter=lambda lst: "\n" + "\n".join(_display_attribute(tup) for tup in lst),
only_inherit=True))
_set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Most commonly, Attributes don't need any categories or locks. If using locks, the lock-types
@ -1290,6 +1340,13 @@ def _tags_actions(caller, raw_inp, **kwargs):
@list_node(_caller_tags, _tag_select)
def node_tags(caller):
def _currentcmp(propval, flatval):
"match by key + category"
cmp1 = [(tup[0].lower(), tup[1].lower() if tup[2] else None) for tup in propval]
return [tup for tup in flatval if (tup[0].lower(), tup[1].lower()
if tup[1] else None) not in cmp1]
text = """
|cTags|n are used to group objects so they can quickly be found later. Enter tags on one of
the following forms:
@ -1297,8 +1354,14 @@ def node_tags(caller):
tagname;category
tagname;category;data
{actions}
""".format(actions=_format_list_actions("examine", "remove", prefix="Actions: "))
{current}
""".format(
current=_get_current_value(
caller, 'tags',
comparer=_currentcmp,
formatter=lambda lst: "\n" + "\n".join(_display_tag(tup) for tup in lst),
only_inherit=True))
_set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Tags are shared between all objects with that tag. So the 'data' field (which is not
@ -1325,18 +1388,7 @@ def _caller_locks(caller):
def _locks_display(caller, lock):
try:
locktype, lockdef = lock.split(":", 1)
except ValueError:
txt = "Malformed lock string - Missing ':'"
else:
txt = ("{lockstr}\n\n"
"|WLocktype: |w{locktype}|n\n"
"|WLock def: |w{lockdef}|n\n").format(
lockstr=lock,
locktype=locktype,
lockdef=lockdef)
return txt
return lock
def _lock_select(caller, lockstr):
@ -1395,6 +1447,11 @@ def _locks_actions(caller, raw_inp, **kwargs):
@list_node(_caller_locks, _lock_select)
def node_locks(caller):
def _currentcmp(propval, flatval):
"match by locktype"
cmp1 = [lck.split(":", 1)[0] for lck in propval.split(';')]
return ";".join(lstr for lstr in flatval.split(';') if lstr.split(':', 1)[0] not in cmp1)
text = """
The |cLock string|n defines limitations for accessing various properties of the object once
it's spawned. The string should be on one of the following forms:
@ -1402,8 +1459,15 @@ def node_locks(caller):
locktype:[NOT] lockfunc(args)
locktype: [NOT] lockfunc(args) [AND|OR|NOT] lockfunc(args) [AND|OR|NOT] ...
{action}
""".format(action=_format_list_actions("examine", "remove", prefix="Actions: "))
{current}{action}
""".format(
current=_get_current_value(
caller, 'locks',
comparer=_currentcmp,
formatter=lambda lockstr: "\n".join(_locks_display(caller, lstr)
for lstr in lockstr.split(';')),
only_inherit=True),
action=_format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Here is an example of two lock strings:
@ -1438,16 +1502,17 @@ def _caller_permissions(caller):
return perms
def _display_perm(caller, permission):
def _display_perm(caller, permission, only_hierarchy=False):
hierarchy = settings.PERMISSION_HIERARCHY
perm_low = permission.lower()
txt = ''
if perm_low in [prm.lower() for prm in hierarchy]:
txt = "Permission (in hieararchy): {}".format(
", ".join(
["|w[{}]|n".format(prm)
if prm.lower() == perm_low else "|W{}|n".format(prm)
for prm in hierarchy]))
else:
elif not only_hierarchy:
txt = "Permission: '{}'".format(permission)
return txt
@ -1500,12 +1565,23 @@ def _permissions_actions(caller, raw_inp, **kwargs):
@list_node(_caller_permissions, _permission_select)
def node_permissions(caller):
def _currentcmp(pval, fval):
cmp1 = [perm.lower() for perm in pval]
return [perm for perm in fval if perm.lower() not in cmp1]
text = """
|cPermissions|n are simple strings used to grant access to this object. A permission is used
when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions.
when a |clock|n is checked that contains the |wperm|n or |wpperm|n lock functions. Certain
permissions belong in the |cpermission hierarchy|n together with the |Wperm()|n lock
function.
{actions}
""".format(actions=_format_list_actions("examine", "remove"), prefix="Actions: ")
{current}
""".format(
current=_get_current_value(
caller, 'permissions',
comparer=_currentcmp,
formatter=lambda lst: "\n" + "\n".join(prm for prm in lst), only_inherit=True))
_set_actioninfo(caller, _format_list_actions("examine", "remove", prefix="Actions: "))
helptext = """
Any string can act as a permission as long as a lock is set to look for it. Depending on the
@ -1538,7 +1614,6 @@ def node_location(caller):
inventory of |c{caller}|n by default.
{current}
""".format(caller=caller.key, current=_get_current_value(caller, "location"))
helptext = """
@ -1734,9 +1809,13 @@ def node_prototype_tags(caller):
|cPrototype-Tags|n can be used to classify and find prototypes in listings Tag names are not
case-sensitive and can have not have a custom category.
{actions}
""".format(actions=_format_list_actions(
"remove", prefix="|w<text>|n|W to add Tag. Other Action:|n "))
{current}
""".format(
current=_get_current_value(
caller, 'prototype_tags',
formatter=lambda lst: ", ".join(tg for tg in lst), only_inherit=True))
_set_actioninfo(caller, _format_list_actions(
"remove", prefix="|w<text>|n|W to add Tag. Other Action:|n "))
helptext = """
Using prototype-tags is a good way to organize and group large numbers of prototypes by
genre, type etc. Under the hood, prototypes' tags will all be stored with the category
@ -1827,8 +1906,14 @@ def node_prototype_locks(caller):
If unsure, keep the open defaults.
{actions}
""".format(actions=_format_list_actions('examine', "remove", prefix="Actions: "))
{current}
""".format(
current=_get_current_value(
caller, 'prototype_locks',
formatter=lambda lstring: "\n".join(_locks_display(caller, lstr)
for lstr in lstring.split(';')),
only_inherit=True))
_set_actioninfo(caller, _format_list_actions('examine', "remove", prefix="Actions: "))
helptext = """
Prototype locks can be used to vary access for different tiers of builders. It also allows
@ -2204,9 +2289,8 @@ def node_prototype_load(caller, **kwargs):
text = """
Select a prototype to load. This will replace any prototype currently being edited!
{actions}
""".format(actions=_format_list_actions("examine", "delete"))
"""
_set_actioninfo(caller, _format_list_actions("examine", "delete"))
helptext = """
Loading a prototype will load it and return you to the main index. It can be a good idea
@ -2230,6 +2314,13 @@ class OLCMenu(EvMenu):
A custom EvMenu with a different formatting for the options.
"""
def nodetext_formatter(self, nodetext):
"""
Format the node text itself.
"""
return super(OLCMenu, self).nodetext_formatter(nodetext)
def options_formatter(self, optionlist):
"""
Split the options into two blocks - olc options and normal options
@ -2237,6 +2328,7 @@ class OLCMenu(EvMenu):
"""
olc_keys = ("index", "forward", "back", "previous", "next", "validate prototype",
"save prototype", "load prototype", "spawn prototype", "search objects")
actioninfo = self.actioninfo + "\n" if hasattr(self, 'actioninfo') else ''
olc_options = []
other_options = []
for key, desc in optionlist:
@ -2247,7 +2339,8 @@ class OLCMenu(EvMenu):
else:
other_options.append((key, desc))
olc_options = " | ".join(olc_options) + " | " + "|wQ|Wuit" if olc_options else ""
olc_options = actioninfo + \
" |W|||n ".join(olc_options) + " |W|||n " + "|wQ|Wuit" if olc_options else ""
other_options = super(OLCMenu, self).options_formatter(other_options)
sep = "\n\n" if olc_options and other_options else ""
@ -2257,10 +2350,10 @@ class OLCMenu(EvMenu):
"""
Show help text
"""
return "|c --- Help ---|n\n" + helptext
return "|c --- Help ---|n\n" + utils.dedent(helptext)
def display_helptext(self):
evmore.msg(self.caller, self.helptext, session=self._session)
evmore.msg(self.caller, self.helptext, session=self._session, exit_cmd='look')
def start_olc(caller, session=None, prototype=None):

View file

@ -192,16 +192,14 @@ def prototype_to_str(prototype):
category = "|ccategory:|n {}".format(category) if category else ''
cat_locks = ""
if category or locks:
cat_locks = "(|ccategory:|n {category}, ".format(
cat_locks = " (|ccategory:|n {category}, ".format(
category=category if category else "|wNone|n")
out.append(
"{attrkey} "
"{cat_locks}\n"
" |c=|n {value}".format(
attrkey=attrkey,
cat_locks=cat_locks,
locks=locks if locks else "|wNone|n",
value=value))
"{attrkey}{cat_locks} |c=|n {value}".format(
attrkey=attrkey,
cat_locks=cat_locks,
locks=locks if locks else "|wNone|n",
value=value))
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
tags = prototype.get('tags', '')
if tags:
@ -209,10 +207,10 @@ def prototype_to_str(prototype):
for (tagkey, category, data) in tags:
out.append("{tagkey} (category: {category}{dat})".format(
tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""))
tags = "|ctags:|n\n {tags}".format(tags="\n ".join(out))
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
locks = prototype.get('locks', '')
if locks:
locks = "|clocks:|n\n {locks}".format(locks="\n ".join(locks.split(";")))
locks = "|clocks:|n\n {locks}".format(locks=locks)
permissions = prototype.get("permissions", '')
if permissions:
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))

View file

@ -167,14 +167,13 @@ from __future__ import print_function
import random
from builtins import object, range
from textwrap import dedent
from inspect import isfunction, getargspec
from django.conf import settings
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, is_iter
from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter, dedent
from evennia.commands import cmdhandler
# read from protocol NAWS later?
@ -896,7 +895,7 @@ class EvMenu(object):
nodetext (str): The formatted node text.
"""
return dedent(nodetext).strip()
return dedent(nodetext.strip('\n'), baseline_index=0).rstrip()
def helptext_formatter(self, helptext):
"""
@ -909,7 +908,7 @@ class EvMenu(object):
helptext (str): The formatted help text.
"""
return dedent(helptext).strip()
return dedent(helptext.strip('\n'), baseline_index=0).rstrip()
def options_formatter(self, optionlist):
"""

View file

@ -122,7 +122,8 @@ class EvMore(object):
"""
def __init__(self, caller, text, always_page=False, session=None,
justify_kwargs=None, exit_on_lastpage=False, **kwargs):
justify_kwargs=None, exit_on_lastpage=False,
exit_cmd=None, **kwargs):
"""
Initialization of the text handler.
@ -141,6 +142,10 @@ class EvMore(object):
page being completely filled, exit pager immediately. If unset,
another move forward is required to exit. If set, the pager
exit message will not be shown.
exit_cmd (str, optional): If given, this command-string will be executed on
the caller when the more page exits. Note that this will be using whatever
cmdset the user had *before* the evmore pager was activated (so none of
the evmore commands will be available when this is run).
kwargs (any, optional): These will be passed on
to the `caller.msg` method.
@ -151,6 +156,7 @@ class EvMore(object):
self._npages = []
self._npos = []
self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager."
if not session:
# if not supplied, use the first session to
@ -269,6 +275,8 @@ class EvMore(object):
if not quiet:
self._caller.msg(text=self._exit_msg, **self._kwargs)
self._caller.cmdset.remove(CmdSetMore)
if self.exit_cmd:
self._caller.execute_cmd(self.exit_cmd, session=self._session)
def msg(caller, text="", always_page=False, session=None,

View file

@ -160,12 +160,16 @@ def crop(text, width=None, suffix="[...]"):
return to_str(utext)
def dedent(text):
def dedent(text, baseline_index=None):
"""
Safely clean all whitespace at the left of a paragraph.
Args:
text (str): The text to dedent.
baseline_index (int or None, optional): Which row to use as a 'base'
for the indentation. Lines will be dedented to this level but
no further. If None, indent so as to completely deindent the
least indented text.
Returns:
text (str): Dedented string.
@ -178,7 +182,14 @@ def dedent(text):
"""
if not text:
return ""
return textwrap.dedent(text)
if baseline_index is None:
return textwrap.dedent(text)
else:
lines = text.split('\n')
baseline = lines[baseline_index]
spaceremove = len(baseline) - len(baseline.lstrip(' '))
return "\n".join(line[min(spaceremove, len(line) - len(line.lstrip(' '))):]
for line in lines)
def justify(text, width=None, align="f", indent=0):