diff --git a/src/utils/mudform.py b/src/utils/mudform.py new file mode 100644 index 000000000..cbce9e451 --- /dev/null +++ b/src/utils/mudform.py @@ -0,0 +1,303 @@ +# coding=utf-8 +""" +Evform - a way to create advanced ascii forms + + +This is intended for creating advanced ascii game forms, such as a +large pretty character sheet or info document. + +The system works on the basis of a readin template that is given in a +separate python file imported into the handler. This file contains +some optional settings and a string mapping out the form. The template +has markers in it to denounce fields to fill. The markers map the +absolute size of the field and will be filled with an evtable.Cell +object when displaying the form. + +Example of input file testform.py: + + +CELLCHAR = "x" +TABLECHAR = "c" +FORM = ''' + .-------------------------------------. +/ \ +| Name: xxx1xxxx Player: xxxxx2xxxxx | +| | +>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~< +| Desc: xxxxxxxxxxx Str:x4x Dex:x5x | +| xxxxx3xxxxx Int:x6x Sta:x7x | +| xxxxxxxxxxx Luc:x8x Mag:x9x | +| | +>~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~< +| | +| Skills: | +| ccccccccccccccccccccccccccccccccccccc | +| ccccccccccccccccccccccccccccccccccccc | +| ccccccccccccccccccccccccccccccccccccc | +| | +`--------------------------------------ยด +''' + +The first line of the FORM string is ignored. + +Use as follows: + + MudForm("path/to/testform.py") + + +By marking out rectangles, this area gets reserved for the Cell. +Embedded inside each area must be a one-character identifier to tag +the area (so the smallest form size is 3 characters including the +marker). This marker is any character except the designated formchar +("x" in this case). Rectangles can have any size, but must be +separated from each other by at least one other character's width. + +Parsing this file will result in a CharMap object. This is +primed with a dictionary of {:function} where the function +is responsible for producing a string for each form location. The +Cell in each location will enforce the size given by the template +and will crop too-long text. + +""" + +import re +import copy +from src.utils.mudtable import Cell, MudTable +from src.utils.utils import all_from_module + +class MudForm(object): + """ + This object is instantiated with a text file and parses + it for rectangular form fields. It can then be fed a + mapping so as to populate the fields with fixed-width + Cell objects for displaying + """ + def __init__(self, filename, cells=None, tables=None, **kwargs): + """ + Read the template file and parse it for formfields + + kwargs: + - text for fill into form + """ + self.filename = filename + + self.cells_mapping = dict((str(key), value) for key, value in cells.items()) if cells else {} + self.tables_mapping = dict((str(key), value) for key, value in tables.items()) if tables else {} + + self.cellchar = "x" + self.tablechar = "c" + + self.raw_form = [] + self.form = [] + + # clean kwargs (these cannot be overridden) + kwargs.pop("enforce_size", None) + kwargs.pop("width", None) + kwargs.pop("height", None) + # table/cell options + self.options = kwargs + + self.reload() + + def _parse_rectangles(self, cellchar, tablechar, form, **kwargs): + """ + Parse a form for rectangular formfields identified by + formchar enclosing an identifier. + """ + + # update options given at creation with new input - this + # allows e.g. self.map() to add custom settings for individual + # cells/tables + custom_options = copy.copy(self.options) + custom_options.update(kwargs) + + nform = len(form) + + mapping = {} + cell_coords = {} + table_coords = {} + + # Locate the identifier tags and the horizontal end coords for all forms + re_cellchar = re.compile(r"%s+([^%s])%s+" % (cellchar, cellchar, cellchar)) + re_tablechar = re.compile(r"%s+([^%s])%s+" % (tablechar, tablechar, tablechar)) + for iy, line in enumerate(form): + # find cells + ix0 = 0 + while True: + match = re_cellchar.search(line, ix0) + if match: + # get the width of the rectangle directly from the match + cell_coords[match.group(1)] = [iy, match.start(), match.end()] + ix0 = match.end() + else: + break + # find tables + ix0 = 0 + while True: + match = re_tablechar.search(line, ix0) + if match: + # get the width of the rectangle directly from the match + table_coords[match.group(1)] = [iy, match.start(), match.end()] + ix0 = match.end() + else: + break + print "table_coords:", table_coords + + # get rectangles and assign Cells + for key, (iy, leftix, rightix) in cell_coords.items(): + + # scan up to find top of rectangle + dy_up = 0 + if iy > 0: + for i in range(1,iy): + #print "dy_up:", [form[iy-i][ix] for ix in range(leftix, rightix)] + if all(form[iy-i][ix] == cellchar for ix in range(leftix, rightix)): + dy_up += 1 + else: + break + # find bottom edge of rectangle + dy_down = 0 + if iy < nform-1: + for i in range(1,nform-iy-1): + #print "dy_down:", [form[iy+i][ix]for ix in range(leftix, rightix)] + if all(form[iy+i][ix] == cellchar for ix in range(leftix, rightix)): + dy_down += 1 + else: + break + + # we have our rectangle. Calculate size of Cell. + iyup = iy - dy_up + iydown = iy + dy_down + width = rightix - leftix + height = abs(iyup - iydown) + 1 + + # we have all the coordinates we need. Create Cell. + data = self.cells_mapping.get(key, "") + #if key == "1": + #print "creating cell '%s' (%s):" % (key, data) + #print "iy=%s, iyup=%s, iydown=%s, leftix=%s, rightix=%s, width=%s, height=%s" % (iy, iyup, iydown, leftix, rightix, width, height) + + options = { "pad_left":0, "pad_right":0, "pad_top":0, "pad_bottom":0, "align":"l", "valign":"t", "enforce_size":True} + options.update(custom_options) + #if key=="4": + #print "options:", options + + mapping[key] = (iyup, leftix, width, height, Cell(data, width=width, height=height,**options)) + + # get rectangles and assign Tables + for key, (iy, leftix, rightix) in table_coords.items(): + + # scan up to find top of rectangle + dy_up = 0 + if iy > 0: + for i in range(1,iy): + #print "dy_up:", [form[iy-i][ix] for ix in range(leftix, rightix)] + if all(form[iy-i][ix] == tablechar for ix in range(leftix, rightix)): + dy_up += 1 + else: + break + # find bottom edge of rectangle + dy_down = 0 + if iy < nform-1: + for i in range(1,nform-iy-1): + #print "dy_down:", [form[iy+i][ix]for ix in range(leftix, rightix)] + if all(form[iy+i][ix] == tablechar for ix in range(leftix, rightix)): + dy_down += 1 + else: + break + + # we have our rectangle. Calculate size of Table. + iyup = iy - dy_up + iydown = iy + dy_down + width = rightix - leftix + height = abs(iyup - iydown) + 1 + + # we have all the coordinates we need. Create Table. + table = self.tables_mapping.get(key, None) + #if key == "1": + print "creating table '%s' (%s):" % (key, data) + print "iy=%s, iyup=%s, iydown=%s, leftix=%s, rightix=%s, width=%s, height=%s" % (iy, iyup, iydown, leftix, rightix, width, height) + + options = { "pad_left":0, "pad_right":0, "pad_top":0, "pad_bottom":0, + "align":"l", "valign":"t", "enforce_size":True} + options.update(custom_options) + #if key=="4": + print "options:", options + + if table: + table.reformat(width=width, height=height, **options) + else: + table = MudTable(width=width, height=height, **options) + mapping[key] = (iyup, leftix, width, height, table) + + return mapping + + def _populate_form(self, raw_form, mapping): + """ + Insert cell contents into form at given locations + """ + form = copy.copy(raw_form) + for key, (iy0, ix0, width, height, cell_or_table) in mapping.items(): + # rect is a list of lines, each wide + rect = cell_or_table.get() + for il, rectline in enumerate(rect): + formline = form[iy0+il] + # insert new content, replacing old + form[iy0+il] = formline = formline[:ix0] + rectline + formline[ix0+width:] + return form + + def map(self, cells=None, tables=None, **kwargs): + """ + Add mapping for form. + + keywords: + - text + """ + # clean kwargs (these cannot be overridden) + kwargs.pop("enforce_size", None) + kwargs.pop("width", None) + kwargs.pop("height", None) + + new_cells = dict((str(key), value) for key, value in cells.items()) if cells else {} + new_tables = dict((str(key), value) for key, value in tables.items()) if tables else {} + + self.cells_mapping.update(new_cells) + self.tables_mapping.update(new_tables) + self.reload() + + def reload(self, filename=None, **kwargs): + """ + Creates the form from a stored file name + """ + # clean kwargs (these cannot be overridden) + kwargs.pop("enforce_size", None) + kwargs.pop("width", None) + kwargs.pop("height", None) + + if filename: + self.filename = filename + filename = self.filename + + datadict = all_from_module(filename) + + cellchar = datadict.get("CELLCHAR", "x") + self.cellchar = cellchar[0] if len(cellchar) > 1 else cellchar + tablechar = datadict.get("TABLECHAR", "c") + self.tablechar = tablechar[0] if len(tablechar) > 1 else tablechar + + # split into a list of list of lines. Form can be indexed with form[iy][ix] + self.raw_form = datadict.get("FORM", "").split("\n") + # strip first line + self.raw_form = self.raw_form[1:] if self.raw_form else self.raw_form + + self.options.update(kwargs) + + # parse and replace + self.mapping = self._parse_rectangles(self.cellchar, self.tablechar, self.raw_form, **kwargs) + self.form = self._populate_form(self.raw_form, self.mapping) + + def __str__(self): + "Prints the form" + return "\n".join(self.form) + + diff --git a/src/utils/mudtable.py b/src/utils/mudtable.py index cbafc93d1..486987bb8 100644 --- a/src/utils/mudtable.py +++ b/src/utils/mudtable.py @@ -529,8 +529,10 @@ class MudTable(object): hchar = kwargs.pop("header_line_char", "~") self.header_line_char = hchar[0] if hchar else "~" - border = kwargs.pop("border", None) - if not border in (None, "none", "table", "tablecols", + border = kwargs.pop("border", "none") + if border is None: + border = "none" + if not border in ("none", "table", "tablecols", "header", "incols", "cols", "rows", "cells"): raise Exception("Unsupported border type: '%s'" % border) self.border = border