"""
The fancy text module allows to write formated text to a Calico shape object.
The fancy text module allows you to write nicely formated text to a Calico Shape
object. It includes the ability to do automatic text wrapping, justification of
text, indentation, and inline objects like buttons.
The core of the fancy text module is the writeText function. The writeText
functions takes two arguments, a target onto which all the text will be drawn
and a list of of strings, shapes, and special commands that indicated what
should be drawn. A simple example would be:
::
win = Window(1000, 1000)
rec = Rectangle((0,0), (500,1000), color=Color("white"))
writeText(rec, ['This is some text.\\n'])
win.draw(rec)
Which will write "This is some text." to a rectangle and draw it to the window.
Obviously, this is not the main strength of the fancy text module, drawing a
simple piece of text to the screen can be done by Calico as well.
To make use of the full power of the fancy text module, you will have to use
some of the special commands that come with the module. There are two types of
commands: global commands and local commands.
Global commands allow you to set properties for all text that will follow next.
For example, the following code will write the above text in a bigger font:
::
writeText(rec, [Size(30), "This is some text.\\n"])
Local commands allow you to set a property only for a specific piece of text.
For example, the following code will write one word in italics:
::
writeText(rec, ["This ", Italic("is"), " some text.\\n"])
To enable line-wrapping after 200 pixels, use the :class:`Wrap` command:
::
text = [Wrap(200), "This text is a little bit to long to fit in 200 pixels,"]
text += ["but luckily, it will be wrapped automatically."]
writeText(rec, text)
You can enable justification (filling the width of the wrapped space), by
supplying True as the second argument for :class:`Wrap`:
::
text = [Wrap(200, True), "This text is a little long to fit in 200 pixels,"]
text += ["but it will be wrapped and justified by the fancy text writer."]
writeText(rec, text)
To change the indentation of the text, simply add the :class:`Indent` global command:
::
text = [Indent(30), "This text is indented.\\n"]
text += [Indent(0), "This text is not indented.\\n"]
writeText(rec, text)
Lastly, to add an inline image or other shape to the text, simply add the shape
to the list:
::
rec = Rectangle((0,0), (20,20))
writeText(rec, "What follows is a rectangle:", rec)
For more options, see the list of global and local commands below.
"""
from Myro import *
from Graphics import *
from os.path import dirname, realpath
from constructionFunctions import *
from utilityFunctions import getWidth, getHeight
from copy import deepcopy
IMAGE_PATH=dirname(dirname(realpath(__file__)))+"/CommonImages/"
FONT_REGULAR = "Helvetica Neue Light"
FONT_ITALIC = "Helvetica Neue Light Italic"
#FONT_BOLD = "Helvetica Neue"
FONT_BOLD = "Helvetica Neue Bold"
FONT_MONO = "Courier New"
DEFAULT_FONT_FACE = FONT_REGULAR
DEFAULT_FONT_SIZE = 18
DEFAULT_FONT_COLOR = Color("black")
DEFAULT_LINE_HEIGHT = 18
DEFAULT_MARGIN = 10
DEFAULT_V_MARGIN = 4
Y_MARGIN = 4
#Can't use Line. Line is a class in Graphics.
class Entry():
"""Class representing one line in a fancy text."""
def __init__(self):
"""
Constructs an entry.
Args:
min_height: this line can never be lower than this.
"""
self.elements = []
self.width = 0
self.wrap = -1
self.vmargin = 0
self.spaces = 0
self.justify = False
self.hspace = None
self.indent = 0
self.stretch = 0
self.vMin = 0
self.vMax = 0
def __iter__(self):
"""Enables 'for each' over all elements."""
return self.elements.__iter__()
def append(self, context, element):
"""
Adds an element to this line.
Args:
element: the element to add.
width: the width of the element, if applicable.
space: whether or not to add a space at the end of this element.
justify: whether to justify this line (affects entire line)
"""
self.elements.append(element)
self.width += getWidth(element)
self.vMin = min(self.vMin, -context.top().baselineOffset)
self.vMax = max(self.vMax, getHeight(element) - context.top().baselineOffset)
if isinstance(element, Space): self.spaces += 1
def getSpaces(self):
"""Returns the number of spaces in this entry."""
return self.spaces
def popSpace(self):
"""Removes a trailing space from this entry."""
if len(self.elements) > 0:
if isinstance(self.elements[-1], Space):
self.width -= self.elements[-1].width
self.elements.pop()
self.spaces-=1
def setMinHeight(self, height):
"""
Sets the minimum height of this entry.
Args:
height: The new minimum height of this entry.
"""
if height > self.vMax - self.vMin:
self.vMax = height + self.vMin
def adjustSpace(self):
"""Adjusts the size of the spaces to justify the text."""
if self.wrap > 0 and self.spaces > 0 and self.justify:
remaining = self.wrap - (self.indent + self.width)
self.stretch = float(remaining)/float(self.spaces)
else:
self.stretch = 0
def finalize(self, context, justify=False):
"""
Finalizes the Entry once all content is added.
Args:
context: Context holding the current settings to be applied.
justify: If True, marks this entry as needing justification.
"""
if justify:
self.justify = context.top().justify
else:
self.justify = False
self.wrap = context.top().wrap
self.setMinHeight(context.top().fontSize)
self.indent = context.top().indent
self.vmargin = context.top().vmargin
self.popSpace()
def getHeigth(self):
"""Returns the height of this entry."""
return self.vMax - self.vMin + self.vmargin
#########################################################
######### Private global modifiers ######################
#########################################################
# JH:
# These modifiers are either superflous (a new line can be
# added with "\n", a space can be added with " ", etc.),
# or require an understanding of the underlying mechanisms.
# I believe the documentation can remain cleaner if we
# consider these commands `private'.
class GlobalCommand(object):
"""Base class for global commands, does nothing."""
def applyContext(self, context, lines):
pass
def applyTransformation(self, pos, line):
pass
class NewLine(GlobalCommand):
"""Adds a new line to the text."""
def applyContext(self, context, lines):
lines[-1].finalize(context)
lines.append(Entry())
class PopContext(GlobalCommand):
"""Pops the current context."""
def applyContext(self, context, lines):
context.pop()
class PushContext(GlobalCommand):
"""Pushes a new context onto the stack."""
def applyContext(self, context, lines):
context.push()
class Backup(GlobalCommand):
"""Resests the curser all the way to the start of the same line."""
def applyTransformation(self, pos, line):
pos[0] = line.indent
pos[1] -= line.getHeigth()
class Space(GlobalCommand):
"""Adds a stretchable space to the text"""
width = 0
def applyContext(self, context, lines):
self.width = context.top().fontSize / 3
def applyTransformation(self, pos, line):
pos[0] += self.width + line.stretch
#########################################################
######### Public global modifiers #######################
#########################################################
# Documentation note: these classes are so simple that we don't
# document their constructor. Instead, we describe the constructor
# arguments in the class description itself.
[docs]class Font(GlobalCommand):
"""
Changes the font of all future text.
The fancy text module defines 4 global variables for fonts:
* FONT_REGULAR
* FONT_BOLD
* FONT_ITALIC
* FONT_MONO
Args:
font: string indicating the font to switch to.
"""
def __init__(self, font):
self.font = font
def applyContext(self, context, lines):
context.top().fontFace = self.font
[docs]class Size(GlobalCommand):
"""
Class to change font size.
Args:
size: integer representing the new fontsize.
"""
def __init__(self, size):
self.size = size
def applyContext(self, context, lines):
context.top().fontSize = self.size
context.top().fontSize = self.size
[docs]class Wrap(GlobalCommand):
"""
Enables automatic line wrapping.
Args:
wrapWidth: after how many pixels the line should be wrapped.
justify: should the text be justified.
"""
def __init__(self, wrapWidth, justify=True):
self.wrapWidth = wrapWidth
self.justify = justify
def applyContext(self, context, lines):
context.top().wrap = self.wrapWidth
context.top().justify = self.justify
[docs]class Indent(GlobalCommand):
"""
Indents this and all subsequent text.
Args:
space: the distance this text should be indented.
"""
def __init__(self, space):
self.space = space
def applyContext(self, context, lines):
context.top().indent = self.space
class CopyHeight(GlobalCommand):
"""
Indents this and all subsequent text.
Args:
space: the distance this text should be indented.
"""
def __init__(self, target, source):
self.source = source
self.target = target
def applyContext(self, context, lines):
lines[self.target].vMax = lines[self.source].vMax
[docs]class SkipV(GlobalCommand):
"""
Advances the vertical cursor position.
Args:
space: the distance to skip.
"""
def __init__(self, space):
self.space = space
def applyTransformation(self, pos, line):
pos[1] += self.space
[docs]class SkipH(GlobalCommand):
"""
Advances the horizontal cursor position.
Args:
space: the distance to skip.
"""
def __init__(self, space):
self.width = space
def applyTransformation(self, pos, line):
pos[0] += self.width
class SetV(GlobalCommand):
"""
Sets the vertical cursor position.
Args:
y: the new y position of the cursor.
"""
def __init__(self, y):
self.y = y
def applyTransformation(self, pos, line):
pos[1] = self.y
class SetH(GlobalCommand):
"""
Sets the horizontal cursor position.
Args:
x: the new x position of the cursor.
"""
def __init__(self, x):
self.x = x
def applyTransformation(self, pos, line):
pos[0] = self.x
class SetSpacing(GlobalCommand):
"""
Sets the spacing between lines.
Args:
spacing: the new spacing between lines.
"""
def __init__(self, spacing):
self.spacing = spacing
def applyContext(self, context, lines):
context.top().vmargin = self.spacing
#########################################################
######### Local modifiers ##############################
#########################################################
class LocalCommand(object):
"""Base class for local commands, does nothing."""
elements = []
def applyContext(self, context, lines):
pass
[docs]class Bullet(LocalCommand):
"""
Creates a bullet point for in a bulleted list.
Args:
bullet: the text or object to be used as a bullet.
text: the text to display with this shorthand.
indent: the size of indent to go with the bullet.
"""
def __init__(self, bullet, text, indent=30, parskip=4):
self.elements = [bullet, Backup(), NewLine(), Indent(indent), text, NewLine(), SkipV(parskip)]
def applyContext(self, context, lines):
self.elements.append(CopyHeight(len(lines)-1, len(lines)))
[docs]class Offset(LocalCommand):
"""
Moves a shape or piece of text vertically.
Args:
text: the text (or shapes) to be displaced.
offset: the distance by which the text should be moved.
"""
def __init__(self, text, offset):
self.offset = offset
self.elements = [SkipV(offset), text, SkipV(-offset)]
def applyContext(self, context, lines):
context.top().baselineOffset = self.offset
[docs]class Mono(LocalCommand):
"""
Displays the provided text in a mono-spaced font.
Args:
text: the text to be displayed.
"""
def __init__(self, text):
self.elements = [Font(FONT_MONO), text]
[docs]class Code(LocalCommand):
"""
Displays the provided text as inline code.
Args:
text: the text to be displayed.
"""
def __init__(self, text):
self.elements = [Font(FONT_MONO), text, SkipH(4)]
[docs]class Bold(LocalCommand):
"""
Displays the provided text in a bold.
Args:
text: the text to be displayed.
"""
def __init__(self, text):
self.elements = [Font(FONT_BOLD), text]
[docs]class Italic(LocalCommand):
"""
Displays the provided text in italics.
Args:
text: the text to be displayed.
"""
def __init__(self, text):
self.elements = [Font(FONT_ITALIC), text]
INLINE_BUTTON_WIDTH_2 = 80
INLINE_BUTTON_HEIGHT_2 = 20
class Inline2(LocalCommand):
"""
Adds an inline button to the briefscreen.
Args:
text: the text to appear on the button.
tag: the tag used to determine what should happen if
this button is clicked.
"""
def __init__(self,text,tag=None,color=Color("white")):
if tag:
tag = tag
else:
tag = text
tmp = Rectangle((0,0),(100,100))
newButton, _ = createPanelButton(tmp, 0, 0, INLINE_BUTTON_WIDTH_2, INLINE_BUTTON_HEIGHT_2, text, tag, buttonColor=color)
self.elements = [Offset(newButton, 5)]
class MyShape(object):
def getP1(self):
return point(0,0)
def getP2(self):
return point(0,0)
def moveTo(self,x,y):
print("I'm being moved!")
def draw(self, target):
print("I'm being drawn!")
# JH: This doesn't work because, while shapes can be connected
# They can never be disconnected
## class MyButton(MyShape):
## def __init__(self, button,action=None):
## self.button = button
## self.action = action
##
## def getP1(self):
## return self.button.getP1()
##
## def getP2(self):
## return self.button.getP2()
##
## def moveTo(self,x,y):
## self.button.moveTo(x,y)
##
## def draw(self, target):
## self.button.draw(target)
## self.button.connect("click", self.onClick)
##
## def onClick(self,o,e):
## print("You clicked on:", self.button.tag)
## if self.action is not None: self.action()
class LRCButton(LocalCommand):
"""
Adds an inline button to the briefscreen.
Args:
text: the text to appear on the button.
tag: the tag used to determine what should happen if
this button is clicked.
"""
def __init__(self,text,action=None,color=Color("white")):
tmp = Rectangle((0,0),(100,100))
newButton, _ = createPanelButton(tmp, 0, 0, INLINE_BUTTON_WIDTH_2, INLINE_BUTTON_HEIGHT_2, text, "button", buttonColor=color)
self.elements = [Offset(newButton, 5)]
self.button = newButton
self.action = action
def hit(self, x, y):
return self.button.hit(x, y)
RELATED_PAGES_BUTTON_WIDTH_2 = 92
RELATED_PAGES_BUTTON_HEIGHT_2 = 35
class Related(LocalCommand):
"""
Adds an inline button to the briefscreen.
Args:
text: the text to appear on the button.
tag: the tag used to determine what should happen if
this button is clicked.
"""
def __init__(self,text,tag=None,color=Color("white")):
if tag:
tag = tag
else:
tag = text
tmp = Rectangle((0,0),(100,100))
newButton, _ = createPanelButton(tmp, 0, 0, RELATED_PAGES_BUTTON_WIDTH_2, RELATED_PAGES_BUTTON_HEIGHT_2, text, tag, buttonColor=color)
self.elements = [Offset(newButton, 5)]
#########################################################
######### Context ######################################
#########################################################
class Context(object):
"""Class to keep context information"""
fontFace = DEFAULT_FONT_FACE
fontSize = DEFAULT_FONT_SIZE
fontColor = DEFAULT_FONT_COLOR
justify = False
wrap = -1
indent = 0
vmargin = DEFAULT_V_MARGIN
baselineOffset = 0
class ContextStack(object):
"""Class to keep track of a context stack"""
stack = None
def __init__(self):
self.stack = [Context()]
self.win = getWindow()
def top(self):
return self.stack[-1]
def add(self, context):
context.applyContext(self)
def push(self):
self.stack.append(deepcopy(self.stack[-1]))
def pop(self):
del self.stack[-1]
#########################################################
######### Fancy text functions ##########################
#########################################################
def _processText(context, element):
"""
Creates a number of Text objects out of the provided element.
Args:
contex: The current context, holding font size, font color, etc.
element: The element to be turned in to text, usually a string.
Return:
A list of Text objects, Space objects, and NewLine objects, in
the form [Text, Space, Text, Space, Text, Newline].
"""
result = []
text = str(element)
if text == "": return result
split_string = text.split("\n")
for string in split_string:
for word in string.split(" "):
if word != "":
text_obj = Text((0,0), word)
#The only alignment that works is bottom
text_obj.yJustification = "bottom"
text_obj.xJustification = "left"
text_obj.fill = context.top().fontColor
text_obj.fontFace = context.top().fontFace
text_obj.fontSize = context.top().fontSize
text_obj.window = getWindow()
result.append(text_obj)
result.append(Space())
result.pop()
result.append(NewLine())
result.pop()
return result
def _processShape(context, lines, element):
"""
Adds a shape to the current lines.
Args:
context: The current context, holding font size, font color, etc.
lines: The current list of lines.
element: The shape to be added.
"""
new_width = getWidth(element)
total_width = context.top().indent + lines[-1].width + new_width
if context.top().wrap > 0 and total_width > context.top().wrap:
lines[-1].finalize(context, True)
lines.append(Entry())
lines[-1].append(context, element)
def _processElements(context, lines, elements):
"""
Processes the list of elements.
Args:
context: The current context, holding font size, font color, etc.
lines: The current list of lines.
elements: A list of elements to be added.
"""
for element in elements:
if isinstance(element, GlobalCommand):
element.applyContext(context, lines)
lines[-1].append(context, element)
elif isinstance(element, LocalCommand):
context.push()
element.applyContext(context, lines)
_processElements(context, lines, element.elements)
context.pop()
elif hasattr(element, '__iter__'):
_processElements(context, lines, element)
elif isinstance(element, Shape) or isinstance(element, MyShape):
_processShape(context, lines, element)
elif hasattr(element, '__str__'):
textShapes = _processText(context, element)
_processElements(context, lines, textShapes)
else:
print("WARNING: unreconized element", element, "ignored")
def _createEntries(elements):
"""
Creates a set of lines out of a list of elements.
Args:
elements: A list of elements to be added.
Return:
A list of lines.
"""
context = ContextStack()
lines = [ Entry() ]
_processElements(context, lines, elements)
lines[-1].finalize(context)
return lines
def _writeLines(target, lines, x_pos_start, y_pos_start):
"""
Writes a list of lines to a target object.
Args:
target: The object to draw to.
lines: The list of lines.
x_pos_start: The x coordinate from where to start writing.
y_pos_start: The y coordinate from where to start writing.
Return:
The final y coordinate.
"""
pos = [0, 0]
for i in xrange(len(lines)):
line = lines[i]
pos[0] = line.indent
line.adjustSpace()
for element in line:
if isinstance(element, GlobalCommand):
element.applyTransformation(pos, line)
elif hasattr(element, "moveTo") and hasattr(element, "draw"):
width = getWidth(element)
if isinstance(element, Text):
x = x_pos_start + pos[0]
y = y_pos_start + pos[1] + line.vMax
else:
height = getHeight(element)
x = x_pos_start + pos[0] + width/2
y = y_pos_start + pos[1] + line.vMax - height/2
element.moveTo(x, y)
element.draw(target)
pos[0] += width
pos[1] += line.getHeigth()
return pos[1]
[docs]def writeText(target, elements, x_pos_start=None, y_pos_start=None):
"""
Write the list of elements to the target.
Args:
target: The target object to draw to.
elemetns: A list of elemetns indicating what, and how to draw.
x_pos_start: The x coordinate from where to start writing.
y_pos_start: The y coordinate from where to start writing.
Return:
The final y coordinate.
"""
if not x_pos_start: x_pos_start = -getWidth(target)/2 + DEFAULT_MARGIN
if not y_pos_start: y_pos_start = -getHeight(target)/2 + DEFAULT_MARGIN
lines = _createEntries(elements)
return _writeLines(target, lines, x_pos_start, y_pos_start)
def testTextFunctions():
"""
Informal unit test for the text drawing functions.
"""
win = Window(1000, 1000)
rec = Rectangle((0,0), (500,1000), color=Color("white"))
# Test basic text
text = ["Hi! This is a test, to see if things go wonky at some point.|\n\n"]
# Test changing the font
text += [Font(FONT_ITALIC), "Font should be italic.\n", Font(FONT_REGULAR)]
text += ["This text should not be italic.\n\n"]
# Test shorthand
text += [Italic("This text should be in italic.\n")]
text += [Italic(["This italic text...", SkipH(50), "has a gap.\n\n"])]
# Test text wrapping and justification
text += [Wrap(200, True), "This automatically wrapped piece of text should"]
text += [" be justified,"]
text += [" and this space", SkipH(50), "should count for wrapping.\n\n"]
# Test indentation
text += [Indent(30), "This text is indented.\n"]
text += ["Just like this text.\n"]
text += [Indent(0), "But unlike this text.\n\n"]
# Test bullet points
text += [Bullet("-", "This is an indent, which should work on wrap.\n")]
text += ["This text is not indented.\n\n"]
# Test inline shapes
inlineRec1 = Rectangle((0,0), (20,20))
text += [Wrap(-1, False)]
text += ["This text has an inline rectangle: ", inlineRec1, "\n"]
inlineRec2 = Rectangle((0,0), (20,20))
text += ["This rectangle is shifted down: "]
text += [SkipV(5), inlineRec2, SkipV(-5), "\n", SkipV(5)]
text += ["Multiple spaces are not ignored.\n"]
inlineRec3 = Rectangle((0,0), (100,100))
text += ["This large rectangle is shifted down: "]
text += [Offset(inlineRec3, 50), "\n"]
# Test converting numbers to text
text += ["The following numbers are displayed as text: "]
text += [1, " ", 5, " ", 17, " ", 3.5, "\n\n"]
# Test spacing
text += [SetSpacing(0), "Line 1\n"]
text += ["Line 2\n"]
text += ["Line 3\n"]
text += ["Line 4\n"]
text += ["Line 5\n"]
# Test font size
text += [Size(30), "This text is larger than normal!\n"]
text += [Size(10), "This text is smaller than normal!\n", Size(18)]
# Test bullet points with inline images
inlineRec4 = Rectangle((0,0), (30,30))
text += [Wrap(300, False), Bullet("1.", ["This is an indent ", Offset(inlineRec4, 10), " with a rectangle in it.\n"])]
writeText(rec, text)
win.draw(rec)