]> git.saurik.com Git - wxWidgets.git/commitdiff
New version from Timothy Hochberg
authorRobin Dunn <robin@alldunn.com>
Sat, 18 Oct 2003 04:33:35 +0000 (04:33 +0000)
committerRobin Dunn <robin@alldunn.com>
Sat, 18 Oct 2003 04:33:35 +0000 (04:33 +0000)
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@24221 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775

wxPython/wxPython/lib/fancytext.py

index 0767c986e76a7f1dabf1bc2ea2fb0350e2a2bc99..c91c49ec4fd49666cb88d517f828e214357b8b23 100644 (file)
-"""wxFancyText -- methods for rendering XML specified text
-
-This module has four main methods:
-
-def getExtent(str, dc=None, enclose=1):
-def renderToBitmap(str, background=None, enclose=1)
-def renderToDC(str, dc, x, y, enclose=1)
-
-In all cases, 'str' is an XML string. The tags in the string can
-currently specify the font, subscripts, superscripts, and the angle
-sign. The allowable properties of font are size, family, style, weght,
-encoding, and color. See the example on the bottom for a better idea
-of how this works.
-
-Note that start and end tags for the string are provided if enclose is
-True, so for instance, renderToBitmap("X<sub>1</sub>") will work.
-
-"""
-# Copyright 2001 Timothy Hochberg
-# Use as you see fit. No warantees, I cannot be held responsible, etc.
-
-
-
-# TODO:  Make a wxFancyTextCtrl class that derives from wxControl.
-#        Add support for line breaks
-#        etc.
-#        - Robin
-
-
-
-from wxPython.wx import *
-import xml.parsers.expat, copy
-
-_families = {"default" : wxDEFAULT, "decorative" : wxDECORATIVE, "roman" : wxROMAN,
-                "swiss" : wxSWISS, "modern" : wxMODERN}
-_styles = {"normal" : wxNORMAL, "slant" : wxSLANT, "italic" : wxITALIC}
-_weights = {"normal" : wxNORMAL, "light" : wxLIGHT, "bold" : wxBOLD}
+"""<font weight="bold" size="16">FancyText</font> -- <font style="italic" size="16">methods for rendering XML specified text</font>
+<font family="swiss" size="12">
+This module exports four main methods::
+<font family="fixed" style="slant">
+    def GetExtent(str, dc=None, enclose=True)
+    def GetFullExtent(str, dc=None, enclose=True)
+    def RenderToBitmap(str, background=None, enclose=True)
+    def RenderToDC(str, dc, x, y, enclose=True)
+</font>
+In all cases, 'str' is an XML string. Note that start and end tags
+are only required if *enclose* is set to False. In this case the 
+text should be wrapped in FancyText tags.
+
+In addition, the module exports one class::
+<font family="fixed" style="slant">
+    class StaticFancyText(self, window, id, text, background, ...)
+</font>
+This class works similar to StaticText except it interprets its text 
+as FancyText.
+
+The text can support<sup>superscripts</sup> and <sub>subscripts</sub>, text
+in different <font size="20">sizes</font>, <font color="blue">colors</font>, <font style="italic">styles</font>, <font weight="bold">weights</font> and
+<font family="script">families</font>. It also supports a limited set of symbols,
+currently <times/>, <infinity/>, <angle/> as well as greek letters in both
+upper case (<Alpha/><Beta/>...<Omega/>) and lower case (<alpha/><beta/>...<omega/>).
+
+We can use doctest/guitest to display this string in all its marked up glory.
+<font family="fixed">
+>>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition)
+>>> sft = StaticFancyText(frame, -1, __doc__, wx.Brush("light grey", wx.SOLID))
+>>> frame.SetClientSize(sft.GetSize())
+>>> didit = frame.Show()
+>>> from guitest import PauseTests; PauseTests()
+
+</font></font>
+The End"""
+# Copyright 2001-2003 Timothy Hochberg
+# Use as you see fit. No warantees, I cannot be help responsible, etc.
+import copy
+import math
+import sys
+import wx
+import xml.parsers.expat
+
+__all__ = "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText"
+
+if sys.platform == "win32":
+    _greekEncoding = str(wx.FONTENCODING_CP1253)
+else:
+    _greekEncoding = str(wx.FONTENCODING_ISO8859_7)
+
+_families = {"fixed" : wx.FIXED, "default" : wx.DEFAULT, "decorative" : wx.DECORATIVE, "roman" : wx.ROMAN,
+                "script" : wx.SCRIPT, "swiss" : wx.SWISS, "modern" : wx.MODERN}
+_styles = {"normal" : wx.NORMAL, "slant" : wx.SLANT, "italic" : wx.ITALIC}
+_weights = {"normal" : wx.NORMAL, "light" : wx.LIGHT, "bold" : wx.BOLD}
 
 # The next three classes: Renderer, SizeRenderer and DCRenderer are
 # what you will need to override to extend the XML language. All of
-# the font stuff as well as the subscript and superscript stuff are in
+# the font stuff as well as the subscript and superscript stuff are in 
 # Renderer.
 
+_greek_letters = ("alpha", "beta", "gamma", "delta", "epsilon",  "zeta",
+                 "eta", "theta", "iota", "kappa", "lambda", "mu", "nu",
+                 "xi", "omnikron", "pi", "rho", "altsigma", "sigma", "tau", "upsilon",
+                 "phi", "chi", "psi", "omega")
+
+def iround(number):
+    return int(round(number))
+    
+def iceil(number):
+    return int(math.ceil(number))
+
 class Renderer:
+    """Class for rendering XML marked up text.
+
+    See the module docstring for a description of the markup.
+
+    This class must be subclassed and the methods the methods that do
+    the drawing overridden for a particular output device.
 
-    defaultSize = wxNORMAL_FONT.GetPointSize()
-    defaultFamily = wxDEFAULT
-    defaultStyle = wxNORMAL
-    defaultWeight = wxNORMAL
-    defaultEncoding = wxFont_GetDefaultEncoding()
+    """
+    defaultSize = wx.NORMAL_FONT.GetPointSize()
+    defaultFamily = wx.DEFAULT
+    defaultStyle = wx.NORMAL
+    defaultWeight = wx.NORMAL
+    defaultEncoding = wx.Font_GetDefaultEncoding()
     defaultColor = "black"
 
-    def __init__(self, dc=None):
+    def __init__(self, dc=None, x=0, y=None):
         if dc == None:
-            dc = wxMemoryDC()
+            dc = wx.MemoryDC()
         self.dc = dc
         self.offsets = [0]
         self.fonts = [{}]
-
+        self.width = self.height = 0
+        self.x = x
+        self.minY = self.maxY = self._y = y
+        
+    def getY(self):
+        if self._y is None:
+            self.minY = self.maxY = self._y = self.dc.GetTextExtent("M")[1]
+        return self._y
+    def setY(self, value):
+        self._y = y
+    y = property(getY, setY)
+        
     def startElement(self, name, attrs):
         method = "start_" + name
         if not hasattr(self, method):
@@ -64,16 +111,37 @@ class Renderer:
         getattr(self, method)(attrs)
 
     def endElement(self, name):
-        method = "end_" + name
-        if not hasattr(self, method):
-            raise ValueError("XML tag '%s' not supported" % name)
-        getattr(self, method)()
-
-    def start_wxFancyString(self, attrs):
-        pass
+        methname = "end_" + name
+        if hasattr(self, methname):
+            getattr(self, methname)()
+        elif hasattr(self, "start_" + name):
+            pass
+        else:
+            raise ValueError("XML tag '%s' not supported" % methname)
+        
+    def characterData(self, data):
+        self.dc.SetFont(self.getCurrentFont())
+        for i, chunk in enumerate(data.split('\n')):
+            if i:
+                self.x = 0
+                self.y = self.mayY = self.maxY + self.dc.GetTextExtent("M")[1]
+            if chunk:
+                width, height, descent, extl = self.dc.GetFullTextExtent(chunk)
+                self.renderCharacterData(data, iround(self.x), iround(self.y + self.offsets[-1] - height + descent))
+            else:
+                width = height = descent = extl = 0
+            self.updateDims(width, height, descent, extl)
+
+    def updateDims(self, width, height, descent, externalLeading):
+        self.x += width
+        self.width = max(self.x, self.width)
+        self.minY = min(self.minY, self.y+self.offsets[-1]-height+descent)
+        self.maxY = max(self.maxY, self.y+self.offsets[-1]+descent)
+        self.height = self.maxY - self.minY
 
-    def end_wxFancyString(self):
+    def start_FancyText(self, attrs):
         pass
+    start_wxFancyText = start_FancyText # For backward compatibility
 
     def start_font(self, attrs):
         for key, value in attrs.items():
@@ -86,7 +154,7 @@ class Renderer:
             elif key == "weight":
                 value = _weights[value]
             elif key == "encoding":
-                pass
+                value = int(value)
             elif key == "color":
                 pass
             else:
@@ -119,11 +187,11 @@ class Renderer:
 
     def end_sup(self):
         self.fonts.pop()
-        self.offsets.pop()
+        self.offsets.pop()        
 
     def getCurrentFont(self):
         font = self.fonts[-1]
-        return wxFont(font.get("size", self.defaultSize),
+        return wx.TheFontList.FindOrCreateFont(font.get("size", self.defaultSize),
                              font.get("family", self.defaultFamily),
                              font.get("style", self.defaultStyle),
                              font.get("weight", self.defaultWeight),
@@ -131,119 +199,212 @@ class Renderer:
 
     def getCurrentColor(self):
         font = self.fonts[-1]
-        return wxNamedColour(font.get("color", self.defaultColor))
+        return wx.TheColourDatabase.FindColour(font.get("color", self.defaultColor))
+        
+    def getCurrentPen(self):
+        return wx.ThePenList.FindOrCreatePen(self.getCurrentColor(), 1, wx.SOLID)
+        
+    def renderCharacterData(self, data, x, y):
+        raise NotImplementedError()
+
+
+def _addGreek():
+    alpha = 0xE1
+    Alpha = 0xC1
+    def end(self):
+        pass
+    for i, name in enumerate(_greek_letters):
+        def start(self, attrs, code=chr(alpha+i)):
+            self.start_font({"encoding" : _greekEncoding})
+            self.characterData(code)
+            self.end_font()
+        setattr(Renderer, "start_%s" % name, start)
+        setattr(Renderer, "end_%s" % name, end)
+        if name == "altsigma":
+            continue # There is no capital for altsigma
+        def start(self, attrs, code=chr(Alpha+i)):
+            self.start_font({"encoding" : _greekEncoding})
+            self.characterData(code)
+            self.end_font()
+        setattr(Renderer, "start_%s" % name.capitalize(), start)
+        setattr(Renderer, "end_%s" % name.capitalize(), end)
+_addGreek()    
 
 
-class SizeRenderer(Renderer):
 
+class SizeRenderer(Renderer):
+    """Processes text as if rendering it, but just computes the size."""
+    
     def __init__(self, dc=None):
-        Renderer.__init__(self, dc)
-        self.width = self.height = 0
-        self.minY = self.maxY = 0
+        Renderer.__init__(self, dc, 0, 0)
+    
+    def renderCharacterData(self, data, x, y):
+        pass
+        
+    def start_angle(self, attrs):
+        self.characterData("M")
 
-    def characterData(self, data):
-        self.dc.SetFont(self.getCurrentFont())
-        width, height = self.dc.GetTextExtent(data)
-        self.width = self.width +  width
-        self.minY = min(self.minY, self.offsets[-1])
-        self.maxY = max(self.maxY, self.offsets[-1] + height)
-        self.height = self.maxY - self.minY
+    def start_infinity(self, attrs):
+        width, height = self.dc.GetTextExtent("M")
+        width = max(width, 10)
+        height = max(height, width / 2)
+        self.updateDims(width, height, 0, 0)
 
-    def start_angle(self, attrs):
+    def start_times(self, attrs):
         self.characterData("M")
 
-    def end_angle(self):
-        pass
+    def start_in(self, attrs):
+        self.characterData("M")
 
-class DCRenderer(Renderer):
+    def start_times(self, attrs):
+        self.characterData("M")        
 
-    def __init__(self, dc=None, x=0, y=0):
-        Renderer.__init__(self, dc)
-        self.x = x
-        self.y = y
+    
+class DCRenderer(Renderer):
+    """Renders text to a wxPython device context DC."""
 
-    def characterData(self, data):
-        self.dc.SetFont(self.getCurrentFont())
+    def renderCharacterData(self, data, x, y):
         self.dc.SetTextForeground(self.getCurrentColor())
-        width, height = self.dc.GetTextExtent(data)
-        self.dc.DrawText(data, self.x, self.y + self.offsets[-1])
-        self.x = self.x + width
+        self.dc.DrawText(data, x, y)
 
     def start_angle(self, attrs):
         self.dc.SetFont(self.getCurrentFont())
-        self.dc.SetPen(wxPen(self.getCurrentColor(), 1))
+        self.dc.SetPen(self.getCurrentPen())
         width, height, descent, leading = self.dc.GetFullTextExtent("M")
-        y = self.y + self.offsets[-1] + height - descent
-        self.dc.DrawLine(self.x, y, self.x+width, y)
-        self.dc.DrawLine(self.x, y, self.x+width, y-width)
-        self.x = self.x + width
+        y = self.y + self.offsets[-1]
+        self.dc.DrawLine(iround(self.x), iround(y),iround( self.x+width), iround(y)) 
+        self.dc.DrawLine(iround(self.x), iround(y), iround(self.x+width), iround(y-width))
+        self.updateDims(width, height, descent, leading)
+      
 
-    def end_angle(self):
-        pass
+    def start_infinity(self, attrs):
+        self.dc.SetFont(self.getCurrentFont())
+        self.dc.SetPen(self.getCurrentPen())
+        width, height, descent, leading = self.dc.GetFullTextExtent("M")
+        width = max(width, 10)
+        height = max(height, width / 2)
+        self.dc.SetPen(wx.Pen(self.getCurrentColor(), max(1, width/10)))
+        self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
+        y = self.y + self.offsets[-1]
+        r = iround( 0.95 * width / 4)
+        xc = (2*self.x + width) / 2
+        yc = iround(y-1.5*r)
+        self.dc.DrawCircle(xc - r, yc, r)
+        self.dc.DrawCircle(xc + r, yc, r)
+        self.updateDims(width, height, 0, 0)
+
+    def start_times(self, attrs):
+        self.dc.SetFont(self.getCurrentFont())
+        self.dc.SetPen(self.getCurrentPen())
+        width, height, descent, leading = self.dc.GetFullTextExtent("M")
+        y = self.y + self.offsets[-1]
+        width *= 0.8
+        width = iround(width+.5)
+        self.dc.SetPen(wx.Pen(self.getCurrentColor(), 1))
+        self.dc.DrawLine(iround(self.x), iround(y-width), iround(self.x+width-1), iround(y-1))
+        self.dc.DrawLine(iround(self.x), iround(y-2), iround(self.x+width-1), iround(y-width-1))
+        self.updateDims(width, height, 0, 0)
 
-# This is a rendering function that is primarily used internally,
-# although it could be used externally if one had overridden the
-# Renderer classes.
 
-def renderToRenderer(str, renderer, enclose=1):
-    if enclose:
-        str = '<?xml version="1.0"?><wxFancyString>%s</wxFancyString>' % str
-    p = xml.parsers.expat.ParserCreate()
-    p.returns_unicode = 0
-    p.StartElementHandler = renderer.startElement
-    p.EndElementHandler = renderer.endElement
-    p.CharacterDataHandler = renderer.characterData
-    p.Parse(str, 1)
+def RenderToRenderer(str, renderer, enclose=True):
+    try:
+        if enclose:
+            str = '<?xml version="1.0"?><FancyText>%s</FancyText>' % str
+        p = xml.parsers.expat.ParserCreate()
+        p.returns_unicode = 0
+        p.StartElementHandler = renderer.startElement
+        p.EndElementHandler = renderer.endElement
+        p.CharacterDataHandler = renderer.characterData
+        p.Parse(str, 1)
+    except xml.parsers.expat.error, err:
+        raise ValueError('error parsing text text "%s": %s' % (str, err)) 
 
 
-def getExtent(str, dc=None, enclose=1):
+# Public interface
+
+
+def GetExtent(str, dc=None, enclose=True):
     "Return the extent of str"
     renderer = SizeRenderer(dc)
-    renderToRenderer(str, renderer, enclose)
-    return wxSize(renderer.width, renderer.height)
+    RenderToRenderer(str, renderer, enclose)
+    return iceil(renderer.width), iceil(renderer.height) # XXX round up
 
-# This should probably only be used internally....
 
-def getFullExtent(str, dc=None, enclose=1):
+def GetFullExtent(str, dc=None, enclose=True):
     renderer = SizeRenderer(dc)
-    renderToRenderer(str, renderer, enclose)
-    return renderer.width, renderer.height, -renderer.minY
+    RenderToRenderer(str, renderer, enclose)
+    return iceil(renderer.width), iceil(renderer.height), -iceil(renderer.minY) # XXX round up
 
-def renderToBitmap(str, background=None, enclose=1):
+
+def RenderToBitmap(str, background=None, enclose=1):
     "Return str rendered on a minumum size bitmap"
-    dc = wxMemoryDC()
-    width, height, dy = getFullExtent(str, dc)
-    bmp = wxEmptyBitmap(width, height)
+    dc = wx.MemoryDC()
+    width, height, dy = GetFullExtent(str, dc, enclose)
+    bmp = wx.EmptyBitmap(width, height)
     dc.SelectObject(bmp)
-    if background is not None:
-        dc.SetBackground(background)
+    if background is None:
+        dc.SetBackground(wx.WHITE_BRUSH)
+    else:
+        dc.SetBackground(background) 
     dc.Clear()
     renderer = DCRenderer(dc, y=dy)
     dc.BeginDrawing()
-    renderToRenderer(str, renderer, enclose)
+    RenderToRenderer(str, renderer, enclose)
     dc.EndDrawing()
-    dc.SelectObject(wxNullBitmap)
+    dc.SelectObject(wx.NullBitmap)
+    if background is None:
+        img = wx.ImageFromBitmap(bmp)
+        bg = dc.GetBackground().GetColour()
+        img.SetMaskColour(bg.Red(), bg.Green(), bg.Blue())
+        bmp = img.ConvertToBitmap()
     return bmp
 
-def renderToDC(str, dc, x, y, enclose=1):
+
+def RenderToDC(str, dc, x, y, enclose=1):
     "Render str onto a wxDC at (x,y)"
-    width, height, dy = getFullExtent(str, dc)
+    width, height, dy = GetFullExtent(str, dc)
     renderer = DCRenderer(dc, x, y+dy)
-    renderToRenderer(str, renderer, enclose)
-
-
-if __name__ == "__main__":
-    str = ('<font style="italic" family="swiss" color="red" weight="bold" >some  |<sup>23</sup> <angle/>text<sub>with <angle/> subscript</sub> </font> some other text'
-            '<font family="swiss" color="green" size="40">big green text</font>')
-    ID_EXIT  = 102
-    class myApp(wxApp):
-        def OnInit(self):
-            return 1
-    app = myApp()
-    frame = wxFrame(NULL, -1, "wxFancyText demo", wxDefaultPosition)
-    frame.SetClientSize(getExtent(str))
-    bmp = renderToBitmap(str, wxCYAN_BRUSH)
-    sb = wxStaticBitmap(frame, -1, bmp)
-    EVT_MENU(frame, ID_EXIT, frame.Destroy)
-    frame.Show(1)
+    RenderToRenderer(str, renderer, enclose)
+    
+    
+class StaticFancyText(wx.StaticBitmap):
+    def __init__(self, window, id, text, *args, **kargs):
+        args = list(args)
+        kargs.setdefault('name', 'staticFancyText')
+        if 'background' in kargs:
+            background = kargs.pop('background')
+        elif args:
+            background = args.pop(0)
+        else:
+            background = wx.Brush(window.GetBackgroundColour(), wx.SOLID)
+        
+        bmp = RenderToBitmap(text, background)
+        wx.StaticBitmap.__init__(self, window, id, bmp, *args, **kargs)
+
+
+# Old names for backward compatibiliry
+getExtent = GetExtent
+renderToBitmap = RenderToBitmap
+renderToDC = RenderToDC
+
+
+# Test Driver
+
+def test():
+    app = wx.PyApp()
+    box = wx.BoxSizer(wx.VERTICAL)
+    frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition)
+    frame.SetBackgroundColour("light grey")
+    sft = StaticFancyText(frame, -1, __doc__)
+    box.Add(sft, 1, wx.EXPAND)
+    frame.SetSizer(box)
+    frame.SetAutoLayout(True)
+    box.Fit(frame)
+    box.SetSizeHints(frame)
+    frame.Show()
     app.MainLoop()
+
+if __name__ == "__main__":    
+    test()
+
+