From 298b2c23c680601d8c352b65e894136aeb2bad09 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 11 Aug 2018 11:49:10 +0200 Subject: [PATCH] Cleanup/refactoring of olc menus --- CHANGELOG.md | 15 ++ evennia/prototypes/menus.py | 243 +++++++++++++++++++++---------- evennia/prototypes/prototypes.py | 18 +-- evennia/utils/evmenu.py | 7 +- evennia/utils/evmore.py | 10 +- evennia/utils/utils.py | 15 +- 6 files changed, 216 insertions(+), 92 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ae990b85b..37ff5bfef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 8e88cad13c..c44cf0d5e2 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -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|W. Other actions:|n ") for action in args: actions.append("|w{}|n|W{} |w|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|W. Other actions: " "|we|Wxamine |w|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|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|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|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|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): diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 4c53ed7d1c..0cc016300f 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -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)) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 638f4eef6e..0297170da2 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -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): """ diff --git a/evennia/utils/evmore.py b/evennia/utils/evmore.py index e0ec091005..94173b9eca 100644 --- a/evennia/utils/evmore.py +++ b/evennia/utils/evmore.py @@ -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, diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 60d5c160d6..abe7d3c1e3 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -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):