1 # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) 
   3 # o Updated for 2.5 compatability. 
   7 FancyText -- methods for rendering XML specified text 
   9 This module exports four main methods:: 
  11     def GetExtent(str, dc=None, enclose=True) 
  12     def GetFullExtent(str, dc=None, enclose=True) 
  13     def RenderToBitmap(str, background=None, enclose=True) 
  14     def RenderToDC(str, dc, x, y, enclose=True) 
  16 In all cases, 'str' is an XML string. Note that start and end tags are 
  17 only required if *enclose* is set to False. In this case the text 
  18 should be wrapped in FancyText tags. 
  20 In addition, the module exports one class:: 
  22     class StaticFancyText(self, window, id, text, background, ...) 
  24 This class works similar to StaticText except it interprets its text 
  27 The text can support superscripts and subscripts, text in different 
  28 sizes, colors, styles, weights and families. It also supports a 
  29 limited set of symbols, currently *times*, *infinity*, *angle* as well 
  30 as greek letters in both upper case (*Alpha* *Beta*... *Omega*) and 
  31 lower case (*alpha* *beta*... *omega*). 
  33 >>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition) 
  34 >>> sft = StaticFancyText(frame, -1, testText, wx.Brush("light grey", wx.SOLID)) 
  35 >>> frame.SetClientSize(sft.GetSize()) 
  36 >>> didit = frame.Show() 
  37 >>> from guitest import PauseTests; PauseTests() 
  41 # Copyright 2001-2003 Timothy Hochberg 
  42 # Use as you see fit. No warantees, I cannot be held responsible, etc. 
  49 import xml
.parsers
.expat
 
  51 __all__ 
= "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText" 
  53 if sys
.platform 
== "win32": 
  54     _greekEncoding 
= str(wx
.FONTENCODING_CP1253
) 
  56     _greekEncoding 
= str(wx
.FONTENCODING_ISO8859_7
) 
  58 _families 
= {"fixed" : wx
.FIXED
, "default" : wx
.DEFAULT
, "decorative" : wx
.DECORATIVE
, "roman" : wx
.ROMAN
, 
  59                 "script" : wx
.SCRIPT
, "swiss" : wx
.SWISS
, "modern" : wx
.MODERN
} 
  60 _styles 
= {"normal" : wx.NORMAL, "slant" : wx.SLANT, "italic" : wx.ITALIC}
 
  61 _weights 
= {"normal" : wx.NORMAL, "light" : wx.LIGHT, "bold" : wx.BOLD}
 
  63 # The next three classes: Renderer, SizeRenderer and DCRenderer are 
  64 # what you will need to override to extend the XML language. All of 
  65 # the font stuff as well as the subscript and superscript stuff are in  
  68 _greek_letters 
= ("alpha", "beta", "gamma", "delta", "epsilon",  "zeta", 
  69                  "eta", "theta", "iota", "kappa", "lambda", "mu", "nu", 
  70                  "xi", "omnikron", "pi", "rho", "altsigma", "sigma", "tau", "upsilon", 
  71                  "phi", "chi", "psi", "omega") 
  74     return int(round(number
)) 
  77     return int(math
.ceil(number
)) 
  80     """Class for rendering XML marked up text. 
  82     See the module docstring for a description of the markup. 
  84     This class must be subclassed and the methods the methods that do 
  85     the drawing overridden for a particular output device. 
  89     defaultFamily 
= wx
.DEFAULT
 
  90     defaultStyle 
= wx
.NORMAL
 
  91     defaultWeight 
= wx
.NORMAL
 
  92     defaultEncoding 
= None 
  93     defaultColor 
= "black" 
  95     def __init__(self
, dc
=None, x
=0, y
=None): 
 101         self
.width 
= self
.height 
= 0 
 103         self
.minY 
= self
.maxY 
= self
._y 
= y
 
 104         if Renderer
.defaultSize 
is None: 
 105             Renderer
.defaultSize 
= wx
.NORMAL_FONT
.GetPointSize() 
 106         if Renderer
.defaultEncoding 
is None: 
 107             Renderer
.defaultEncoding 
= wx
.Font_GetDefaultEncoding() 
 111             self
.minY 
= self
.maxY 
= self
._y 
= self
.dc
.GetTextExtent("M")[1] 
 113     def setY(self
, value
): 
 115     y 
= property(getY
, setY
) 
 117     def startElement(self
, name
, attrs
): 
 118         method 
= "start_" + name
 
 119         if not hasattr(self
, method
): 
 120             raise ValueError("XML tag '%s' not supported" % name
) 
 121         getattr(self
, method
)(attrs
) 
 123     def endElement(self
, name
): 
 124         methname 
= "end_" + name
 
 125         if hasattr(self
, methname
): 
 126             getattr(self
, methname
)() 
 127         elif hasattr(self
, "start_" + name
): 
 130             raise ValueError("XML tag '%s' not supported" % methname
) 
 132     def characterData(self
, data
): 
 133         self
.dc
.SetFont(self
.getCurrentFont()) 
 134         for i
, chunk 
in enumerate(data
.split('\n')): 
 137                 self
.y 
= self
.mayY 
= self
.maxY 
+ self
.dc
.GetTextExtent("M")[1] 
 139                 width
, height
, descent
, extl 
= self
.dc
.GetFullTextExtent(chunk
) 
 140                 self
.renderCharacterData(data
, iround(self
.x
), iround(self
.y 
+ self
.offsets
[-1] - height 
+ descent
)) 
 142                 width 
= height 
= descent 
= extl 
= 0 
 143             self
.updateDims(width
, height
, descent
, extl
) 
 145     def updateDims(self
, width
, height
, descent
, externalLeading
): 
 147         self
.width 
= max(self
.x
, self
.width
) 
 148         self
.minY 
= min(self
.minY
, self
.y
+self
.offsets
[-1]-height
+descent
) 
 149         self
.maxY 
= max(self
.maxY
, self
.y
+self
.offsets
[-1]+descent
) 
 150         self
.height 
= self
.maxY 
- self
.minY
 
 152     def start_FancyText(self
, attrs
): 
 154     start_wxFancyText 
= start_FancyText 
# For backward compatibility 
 156     def start_font(self
, attrs
): 
 157         for key
, value 
in attrs
.items(): 
 160             elif key 
== "family": 
 161                 value 
= _families
[value
] 
 163                 value 
= _styles
[value
] 
 164             elif key 
== "weight": 
 165                 value 
= _weights
[value
] 
 166             elif key 
== "encoding": 
 171                 raise ValueError("unknown font attribute '%s'" % key
) 
 173         font 
= copy
.copy(self
.fonts
[-1]) 
 175         self
.fonts
.append(font
) 
 180     def start_sub(self
, attrs
): 
 182             raise ValueError("<sub> does not take attributes") 
 183         font 
= self
.getCurrentFont() 
 184         self
.offsets
.append(self
.offsets
[-1] + self
.dc
.GetFullTextExtent("M", font
)[1]*0.5) 
 185         self
.start_font({"size" : font.GetPointSize() * 0.8}
) 
 191     def start_sup(self
, attrs
): 
 193             raise ValueError("<sup> does not take attributes") 
 194         font 
= self
.getCurrentFont() 
 195         self
.offsets
.append(self
.offsets
[-1] - self
.dc
.GetFullTextExtent("M", font
)[1]*0.3) 
 196         self
.start_font({"size" : font.GetPointSize() * 0.8}
) 
 202     def getCurrentFont(self
): 
 203         font 
= self
.fonts
[-1] 
 204         return wx
.Font(font
.get("size", self
.defaultSize
), 
 205                        font
.get("family", self
.defaultFamily
), 
 206                        font
.get("style", self
.defaultStyle
), 
 207                        font
.get("weight",self
.defaultWeight
), 
 209                        font
.get("encoding", self
.defaultEncoding
)) 
 211     def getCurrentColor(self
): 
 212         font 
= self
.fonts
[-1] 
 213         return wx
.TheColourDatabase
.FindColour(font
.get("color", self
.defaultColor
)) 
 215     def getCurrentPen(self
): 
 216         return wx
.Pen(self
.getCurrentColor(), 1, wx
.SOLID
) 
 218     def renderCharacterData(self
, data
, x
, y
): 
 219         raise NotImplementedError() 
 227     for i
, name 
in enumerate(_greek_letters
): 
 228         def start(self
, attrs
, code
=chr(alpha
+i
)): 
 229             self
.start_font({"encoding" : _greekEncoding}
) 
 230             self
.characterData(code
) 
 232         setattr(Renderer
, "start_%s" % name
, start
) 
 233         setattr(Renderer
, "end_%s" % name
, end
) 
 234         if name 
== "altsigma": 
 235             continue # There is no capital for altsigma 
 236         def start(self
, attrs
, code
=chr(Alpha
+i
)): 
 237             self
.start_font({"encoding" : _greekEncoding}
) 
 238             self
.characterData(code
) 
 240         setattr(Renderer
, "start_%s" % name
.capitalize(), start
) 
 241         setattr(Renderer
, "end_%s" % name
.capitalize(), end
) 
 246 class SizeRenderer(Renderer
): 
 247     """Processes text as if rendering it, but just computes the size.""" 
 249     def __init__(self
, dc
=None): 
 250         Renderer
.__init
__(self
, dc
, 0, 0) 
 252     def renderCharacterData(self
, data
, x
, y
): 
 255     def start_angle(self
, attrs
): 
 256         self
.characterData("M") 
 258     def start_infinity(self
, attrs
): 
 259         width
, height 
= self
.dc
.GetTextExtent("M") 
 260         width 
= max(width
, 10) 
 261         height 
= max(height
, width 
/ 2) 
 262         self
.updateDims(width
, height
, 0, 0) 
 264     def start_times(self
, attrs
): 
 265         self
.characterData("M") 
 267     def start_in(self
, attrs
): 
 268         self
.characterData("M") 
 270     def start_times(self
, attrs
): 
 271         self
.characterData("M")         
 274 class DCRenderer(Renderer
): 
 275     """Renders text to a wxPython device context DC.""" 
 277     def renderCharacterData(self
, data
, x
, y
): 
 278         self
.dc
.SetTextForeground(self
.getCurrentColor()) 
 279         self
.dc
.DrawText(data
, x
, y
) 
 281     def start_angle(self
, attrs
): 
 282         self
.dc
.SetFont(self
.getCurrentFont()) 
 283         self
.dc
.SetPen(self
.getCurrentPen()) 
 284         width
, height
, descent
, leading 
= self
.dc
.GetFullTextExtent("M") 
 285         y 
= self
.y 
+ self
.offsets
[-1] 
 286         self
.dc
.DrawLine(iround(self
.x
), iround(y
), iround( self
.x
+width
), iround(y
)) 
 287         self
.dc
.DrawLine(iround(self
.x
), iround(y
), iround(self
.x
+width
), iround(y
-width
)) 
 288         self
.updateDims(width
, height
, descent
, leading
) 
 291     def start_infinity(self
, attrs
): 
 292         self
.dc
.SetFont(self
.getCurrentFont()) 
 293         self
.dc
.SetPen(self
.getCurrentPen()) 
 294         width
, height
, descent
, leading 
= self
.dc
.GetFullTextExtent("M") 
 295         width 
= max(width
, 10) 
 296         height 
= max(height
, width 
/ 2) 
 297         self
.dc
.SetPen(wx
.Pen(self
.getCurrentColor(), max(1, width
/10))) 
 298         self
.dc
.SetBrush(wx
.TRANSPARENT_BRUSH
) 
 299         y 
= self
.y 
+ self
.offsets
[-1] 
 300         r 
= iround( 0.95 * width 
/ 4) 
 301         xc 
= (2*self
.x 
+ width
) / 2 
 303         self
.dc
.DrawCircle(xc 
- r
, yc
, r
) 
 304         self
.dc
.DrawCircle(xc 
+ r
, yc
, r
) 
 305         self
.updateDims(width
, height
, 0, 0) 
 307     def start_times(self
, attrs
): 
 308         self
.dc
.SetFont(self
.getCurrentFont()) 
 309         self
.dc
.SetPen(self
.getCurrentPen()) 
 310         width
, height
, descent
, leading 
= self
.dc
.GetFullTextExtent("M") 
 311         y 
= self
.y 
+ self
.offsets
[-1] 
 313         width 
= iround(width
+.5) 
 314         self
.dc
.SetPen(wx
.Pen(self
.getCurrentColor(), 1)) 
 315         self
.dc
.DrawLine(iround(self
.x
), iround(y
-width
), iround(self
.x
+width
-1), iround(y
-1)) 
 316         self
.dc
.DrawLine(iround(self
.x
), iround(y
-2), iround(self
.x
+width
-1), iround(y
-width
-1)) 
 317         self
.updateDims(width
, height
, 0, 0) 
 320 def RenderToRenderer(str, renderer
, enclose
=True): 
 323             str = '<?xml version="1.0"?><FancyText>%s</FancyText>' % str 
 324         p 
= xml
.parsers
.expat
.ParserCreate() 
 325         p
.returns_unicode 
= 0 
 326         p
.StartElementHandler 
= renderer
.startElement
 
 327         p
.EndElementHandler 
= renderer
.endElement
 
 328         p
.CharacterDataHandler 
= renderer
.characterData
 
 330     except xml
.parsers
.expat
.error
, err
: 
 331         raise ValueError('error parsing text text "%s": %s' % (str, err
))  
 337 def GetExtent(str, dc
=None, enclose
=True): 
 338     "Return the extent of str" 
 339     renderer 
= SizeRenderer(dc
) 
 340     RenderToRenderer(str, renderer
, enclose
) 
 341     return iceil(renderer
.width
), iceil(renderer
.height
) # XXX round up 
 344 def GetFullExtent(str, dc
=None, enclose
=True): 
 345     renderer 
= SizeRenderer(dc
) 
 346     RenderToRenderer(str, renderer
, enclose
) 
 347     return iceil(renderer
.width
), iceil(renderer
.height
), -iceil(renderer
.minY
) # XXX round up 
 350 def RenderToBitmap(str, background
=None, enclose
=1): 
 351     "Return str rendered on a minumum size bitmap" 
 353     # Chicken and egg problem, we need a bitmap in the DC in order to 
 354     # measure how big the bitmap should be... 
 355     dc
.SelectObject(wx
.EmptyBitmap(1,1)) 
 356     width
, height
, dy 
= GetFullExtent(str, dc
, enclose
) 
 357     bmp 
= wx
.EmptyBitmap(width
, height
) 
 359     if background 
is None: 
 360         dc
.SetBackground(wx
.WHITE_BRUSH
) 
 362         dc
.SetBackground(background
)  
 364     renderer 
= DCRenderer(dc
, y
=dy
) 
 366     RenderToRenderer(str, renderer
, enclose
) 
 368     dc
.SelectObject(wx
.NullBitmap
) 
 369     if background 
is None: 
 370         img 
= wx
.ImageFromBitmap(bmp
) 
 371         bg 
= dc
.GetBackground().GetColour() 
 372         img
.SetMaskColour(bg
.Red(), bg
.Green(), bg
.Blue()) 
 373         bmp 
= img
.ConvertToBitmap() 
 377 def RenderToDC(str, dc
, x
, y
, enclose
=1): 
 378     "Render str onto a wxDC at (x,y)" 
 379     width
, height
, dy 
= GetFullExtent(str, dc
) 
 380     renderer 
= DCRenderer(dc
, x
, y
+dy
) 
 381     RenderToRenderer(str, renderer
, enclose
) 
 384 class StaticFancyText(wx
.StaticBitmap
): 
 385     def __init__(self
, window
, id, text
, *args
, **kargs
): 
 387         kargs
.setdefault('name', 'staticFancyText') 
 388         if 'background' in kargs
: 
 389             background 
= kargs
.pop('background') 
 391             background 
= args
.pop(0) 
 393             background 
= wx
.Brush(window
.GetBackgroundColour(), wx
.SOLID
) 
 395         bmp 
= RenderToBitmap(text
, background
) 
 396         wx
.StaticBitmap
.__init
__(self
, window
, id, bmp
, *args
, **kargs
) 
 399 # Old names for backward compatibiliry 
 400 getExtent 
= GetExtent
 
 401 renderToBitmap 
= RenderToBitmap
 
 402 renderToDC 
= RenderToDC
 
 409 """<font weight="bold" size="16">FancyText</font> -- <font style="italic" size="16">methods for rendering XML specified text</font> 
 410 <font family="swiss" size="12"> 
 411 This module exports four main methods:: 
 412 <font family="fixed" style="slant"> 
 413     def GetExtent(str, dc=None, enclose=True) 
 414     def GetFullExtent(str, dc=None, enclose=True) 
 415     def RenderToBitmap(str, background=None, enclose=True) 
 416     def RenderToDC(str, dc, x, y, enclose=True) 
 418 In all cases, 'str' is an XML string. Note that start and end tags 
 419 are only required if *enclose* is set to False. In this case the  
 420 text should be wrapped in FancyText tags. 
 422 In addition, the module exports one class:: 
 423 <font family="fixed" style="slant"> 
 424     class StaticFancyText(self, window, id, text, background, ...) 
 426 This class works similar to StaticText except it interprets its text  
 429 The text can support<sup>superscripts</sup> and <sub>subscripts</sub>, text 
 430 in different <font size="20">sizes</font>, <font color="blue">colors</font>, <font style="italic">styles</font>, <font weight="bold">weights</font> and 
 431 <font family="script">families</font>. It also supports a limited set of symbols, 
 432 currently <times/>, <infinity/>, <angle/> as well as greek letters in both 
 433 upper case (<Alpha/><Beta/>...<Omega/>) and lower case (<alpha/><beta/>...<omega/>). 
 435 We can use doctest/guitest to display this string in all its marked up glory. 
 436 <font family="fixed"> 
 437 >>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition) 
 438 >>> sft = StaticFancyText(frame, -1, __doc__, wx.Brush("light grey", wx.SOLID)) 
 439 >>> frame.SetClientSize(sft.GetSize()) 
 440 >>> didit = frame.Show() 
 441 >>> from guitest import PauseTests; PauseTests() 
 446     app 
= wx
.PySimpleApp() 
 447     box 
= wx
.BoxSizer(wx
.VERTICAL
) 
 448     frame 
= wx
.Frame(None, -1, "FancyText demo", wx
.DefaultPosition
) 
 449     frame
.SetBackgroundColour("light grey") 
 450     sft 
= StaticFancyText(frame
, -1, testText
) 
 451     box
.Add(sft
, 1, wx
.EXPAND
) 
 453     frame
.SetAutoLayout(True) 
 455     box
.SetSizeHints(frame
) 
 459 if __name__ 
== "__main__":