1 # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net)
3 # o Updated for 2.5 compatability.
6 """<font weight="bold" size="16">FancyText</font> -- <font style="italic" size="16">methods for rendering XML specified text</font>
7 <font family="swiss" size="12">
8 This module exports four main methods::
9 <font family="fixed" style="slant">
10 def GetExtent(str, dc=None, enclose=True)
11 def GetFullExtent(str, dc=None, enclose=True)
12 def RenderToBitmap(str, background=None, enclose=True)
13 def RenderToDC(str, dc, x, y, enclose=True)
15 In all cases, 'str' is an XML string. Note that start and end tags
16 are only required if *enclose* is set to False. In this case the
17 text should be wrapped in FancyText tags.
19 In addition, the module exports one class::
20 <font family="fixed" style="slant">
21 class StaticFancyText(self, window, id, text, background, ...)
23 This class works similar to StaticText except it interprets its text
26 The text can support<sup>superscripts</sup> and <sub>subscripts</sub>, text
27 in different <font size="20">sizes</font>, <font color="blue">colors</font>, <font style="italic">styles</font>, <font weight="bold">weights</font> and
28 <font family="script">families</font>. It also supports a limited set of symbols,
29 currently <times/>, <infinity/>, <angle/> as well as greek letters in both
30 upper case (<Alpha/><Beta/>...<Omega/>) and lower case (<alpha/><beta/>...<omega/>).
32 We can use doctest/guitest to display this string in all its marked up glory.
34 >>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition)
35 >>> sft = StaticFancyText(frame, -1, __doc__, wx.Brush("light grey", wx.SOLID))
36 >>> frame.SetClientSize(sft.GetSize())
37 >>> didit = frame.Show()
38 >>> from guitest import PauseTests; PauseTests()
43 # Copyright 2001-2003 Timothy Hochberg
44 # Use as you see fit. No warantees, I cannot be held responsible, etc.
51 import xml
.parsers
.expat
53 __all__
= "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText"
55 if sys
.platform
== "win32":
56 _greekEncoding
= str(wx
.FONTENCODING_CP1253
)
58 _greekEncoding
= str(wx
.FONTENCODING_ISO8859_7
)
60 _families
= {"fixed" : wx
.FIXED
, "default" : wx
.DEFAULT
, "decorative" : wx
.DECORATIVE
, "roman" : wx
.ROMAN
,
61 "script" : wx
.SCRIPT
, "swiss" : wx
.SWISS
, "modern" : wx
.MODERN
}
62 _styles
= {"normal" : wx.NORMAL, "slant" : wx.SLANT, "italic" : wx.ITALIC}
63 _weights
= {"normal" : wx.NORMAL, "light" : wx.LIGHT, "bold" : wx.BOLD}
65 # The next three classes: Renderer, SizeRenderer and DCRenderer are
66 # what you will need to override to extend the XML language. All of
67 # the font stuff as well as the subscript and superscript stuff are in
70 _greek_letters
= ("alpha", "beta", "gamma", "delta", "epsilon", "zeta",
71 "eta", "theta", "iota", "kappa", "lambda", "mu", "nu",
72 "xi", "omnikron", "pi", "rho", "altsigma", "sigma", "tau", "upsilon",
73 "phi", "chi", "psi", "omega")
76 return int(round(number
))
79 return int(math
.ceil(number
))
82 """Class for rendering XML marked up text.
84 See the module docstring for a description of the markup.
86 This class must be subclassed and the methods the methods that do
87 the drawing overridden for a particular output device.
91 defaultFamily
= wx
.DEFAULT
92 defaultStyle
= wx
.NORMAL
93 defaultWeight
= wx
.NORMAL
94 defaultEncoding
= None
95 defaultColor
= "black"
97 def __init__(self
, dc
=None, x
=0, y
=None):
103 self
.width
= self
.height
= 0
105 self
.minY
= self
.maxY
= self
._y
= y
106 if Renderer
.defaultSize
is None:
107 Renderer
.defaultSize
= wx
.NORMAL_FONT
.GetPointSize()
108 if Renderer
.defaultEncoding
is None:
109 Renderer
.defaultEncoding
= wx
.Font_GetDefaultEncoding()
113 self
.minY
= self
.maxY
= self
._y
= self
.dc
.GetTextExtent("M")[1]
115 def setY(self
, value
):
117 y
= property(getY
, setY
)
119 def startElement(self
, name
, attrs
):
120 method
= "start_" + name
121 if not hasattr(self
, method
):
122 raise ValueError("XML tag '%s' not supported" % name
)
123 getattr(self
, method
)(attrs
)
125 def endElement(self
, name
):
126 methname
= "end_" + name
127 if hasattr(self
, methname
):
128 getattr(self
, methname
)()
129 elif hasattr(self
, "start_" + name
):
132 raise ValueError("XML tag '%s' not supported" % methname
)
134 def characterData(self
, data
):
135 self
.dc
.SetFont(self
.getCurrentFont())
136 for i
, chunk
in enumerate(data
.split('\n')):
139 self
.y
= self
.mayY
= self
.maxY
+ self
.dc
.GetTextExtent("M")[1]
141 width
, height
, descent
, extl
= self
.dc
.GetFullTextExtent(chunk
)
142 self
.renderCharacterData(data
, iround(self
.x
), iround(self
.y
+ self
.offsets
[-1] - height
+ descent
))
144 width
= height
= descent
= extl
= 0
145 self
.updateDims(width
, height
, descent
, extl
)
147 def updateDims(self
, width
, height
, descent
, externalLeading
):
149 self
.width
= max(self
.x
, self
.width
)
150 self
.minY
= min(self
.minY
, self
.y
+self
.offsets
[-1]-height
+descent
)
151 self
.maxY
= max(self
.maxY
, self
.y
+self
.offsets
[-1]+descent
)
152 self
.height
= self
.maxY
- self
.minY
154 def start_FancyText(self
, attrs
):
156 start_wxFancyText
= start_FancyText
# For backward compatibility
158 def start_font(self
, attrs
):
159 for key
, value
in attrs
.items():
162 elif key
== "family":
163 value
= _families
[value
]
165 value
= _styles
[value
]
166 elif key
== "weight":
167 value
= _weights
[value
]
168 elif key
== "encoding":
173 raise ValueError("unknown font attribute '%s'" % key
)
175 font
= copy
.copy(self
.fonts
[-1])
177 self
.fonts
.append(font
)
182 def start_sub(self
, attrs
):
184 raise ValueError("<sub> does not take attributes")
185 font
= self
.getCurrentFont()
186 self
.offsets
.append(self
.offsets
[-1] + self
.dc
.GetFullTextExtent("M", font
)[1]*0.5)
187 self
.start_font({"size" : font.GetPointSize() * 0.8}
)
193 def start_sup(self
, attrs
):
195 raise ValueError("<sup> does not take attributes")
196 font
= self
.getCurrentFont()
197 self
.offsets
.append(self
.offsets
[-1] - self
.dc
.GetFullTextExtent("M", font
)[1]*0.3)
198 self
.start_font({"size" : font.GetPointSize() * 0.8}
)
204 def getCurrentFont(self
):
205 font
= self
.fonts
[-1]
206 return wx
.TheFontList
.FindOrCreateFont(font
.get("size", self
.defaultSize
),
207 font
.get("family", self
.defaultFamily
),
208 font
.get("style", self
.defaultStyle
),
209 font
.get("weight", self
.defaultWeight
),
210 encoding
= font
.get("encoding", self
.defaultEncoding
))
212 def getCurrentColor(self
):
213 font
= self
.fonts
[-1]
214 return wx
.TheColourDatabase
.FindColour(font
.get("color", self
.defaultColor
))
216 def getCurrentPen(self
):
217 return wx
.ThePenList
.FindOrCreatePen(self
.getCurrentColor(), 1, wx
.SOLID
)
219 def renderCharacterData(self
, data
, x
, y
):
220 raise NotImplementedError()
228 for i
, name
in enumerate(_greek_letters
):
229 def start(self
, attrs
, code
=chr(alpha
+i
)):
230 self
.start_font({"encoding" : _greekEncoding}
)
231 self
.characterData(code
)
233 setattr(Renderer
, "start_%s" % name
, start
)
234 setattr(Renderer
, "end_%s" % name
, end
)
235 if name
== "altsigma":
236 continue # There is no capital for altsigma
237 def start(self
, attrs
, code
=chr(Alpha
+i
)):
238 self
.start_font({"encoding" : _greekEncoding}
)
239 self
.characterData(code
)
241 setattr(Renderer
, "start_%s" % name
.capitalize(), start
)
242 setattr(Renderer
, "end_%s" % name
.capitalize(), end
)
247 class SizeRenderer(Renderer
):
248 """Processes text as if rendering it, but just computes the size."""
250 def __init__(self
, dc
=None):
251 Renderer
.__init
__(self
, dc
, 0, 0)
253 def renderCharacterData(self
, data
, x
, y
):
256 def start_angle(self
, attrs
):
257 self
.characterData("M")
259 def start_infinity(self
, attrs
):
260 width
, height
= self
.dc
.GetTextExtent("M")
261 width
= max(width
, 10)
262 height
= max(height
, width
/ 2)
263 self
.updateDims(width
, height
, 0, 0)
265 def start_times(self
, attrs
):
266 self
.characterData("M")
268 def start_in(self
, attrs
):
269 self
.characterData("M")
271 def start_times(self
, attrs
):
272 self
.characterData("M")
275 class DCRenderer(Renderer
):
276 """Renders text to a wxPython device context DC."""
278 def renderCharacterData(self
, data
, x
, y
):
279 self
.dc
.SetTextForeground(self
.getCurrentColor())
280 self
.dc
.DrawText(data
, x
, y
)
282 def start_angle(self
, attrs
):
283 self
.dc
.SetFont(self
.getCurrentFont())
284 self
.dc
.SetPen(self
.getCurrentPen())
285 width
, height
, descent
, leading
= self
.dc
.GetFullTextExtent("M")
286 y
= self
.y
+ self
.offsets
[-1]
287 self
.dc
.DrawLine(iround(self
.x
), iround(y
), iround( self
.x
+width
), iround(y
))
288 self
.dc
.DrawLine(iround(self
.x
), iround(y
), iround(self
.x
+width
), iround(y
-width
))
289 self
.updateDims(width
, height
, descent
, leading
)
292 def start_infinity(self
, attrs
):
293 self
.dc
.SetFont(self
.getCurrentFont())
294 self
.dc
.SetPen(self
.getCurrentPen())
295 width
, height
, descent
, leading
= self
.dc
.GetFullTextExtent("M")
296 width
= max(width
, 10)
297 height
= max(height
, width
/ 2)
298 self
.dc
.SetPen(wx
.Pen(self
.getCurrentColor(), max(1, width
/10)))
299 self
.dc
.SetBrush(wx
.TRANSPARENT_BRUSH
)
300 y
= self
.y
+ self
.offsets
[-1]
301 r
= iround( 0.95 * width
/ 4)
302 xc
= (2*self
.x
+ width
) / 2
304 self
.dc
.DrawCircle(xc
- r
, yc
, r
)
305 self
.dc
.DrawCircle(xc
+ r
, yc
, r
)
306 self
.updateDims(width
, height
, 0, 0)
308 def start_times(self
, attrs
):
309 self
.dc
.SetFont(self
.getCurrentFont())
310 self
.dc
.SetPen(self
.getCurrentPen())
311 width
, height
, descent
, leading
= self
.dc
.GetFullTextExtent("M")
312 y
= self
.y
+ self
.offsets
[-1]
314 width
= iround(width
+.5)
315 self
.dc
.SetPen(wx
.Pen(self
.getCurrentColor(), 1))
316 self
.dc
.DrawLine(iround(self
.x
), iround(y
-width
), iround(self
.x
+width
-1), iround(y
-1))
317 self
.dc
.DrawLine(iround(self
.x
), iround(y
-2), iround(self
.x
+width
-1), iround(y
-width
-1))
318 self
.updateDims(width
, height
, 0, 0)
321 def RenderToRenderer(str, renderer
, enclose
=True):
324 str = '<?xml version="1.0"?><FancyText>%s</FancyText>' % str
325 p
= xml
.parsers
.expat
.ParserCreate()
326 p
.returns_unicode
= 0
327 p
.StartElementHandler
= renderer
.startElement
328 p
.EndElementHandler
= renderer
.endElement
329 p
.CharacterDataHandler
= renderer
.characterData
331 except xml
.parsers
.expat
.error
, err
:
332 raise ValueError('error parsing text text "%s": %s' % (str, err
))
338 def GetExtent(str, dc
=None, enclose
=True):
339 "Return the extent of str"
340 renderer
= SizeRenderer(dc
)
341 RenderToRenderer(str, renderer
, enclose
)
342 return iceil(renderer
.width
), iceil(renderer
.height
) # XXX round up
345 def GetFullExtent(str, dc
=None, enclose
=True):
346 renderer
= SizeRenderer(dc
)
347 RenderToRenderer(str, renderer
, enclose
)
348 return iceil(renderer
.width
), iceil(renderer
.height
), -iceil(renderer
.minY
) # XXX round up
351 def RenderToBitmap(str, background
=None, enclose
=1):
352 "Return str rendered on a minumum size bitmap"
354 width
, height
, dy
= GetFullExtent(str, dc
, enclose
)
355 bmp
= wx
.EmptyBitmap(width
, height
)
357 if background
is None:
358 dc
.SetBackground(wx
.WHITE_BRUSH
)
360 dc
.SetBackground(background
)
362 renderer
= DCRenderer(dc
, y
=dy
)
364 RenderToRenderer(str, renderer
, enclose
)
366 dc
.SelectObject(wx
.NullBitmap
)
367 if background
is None:
368 img
= wx
.ImageFromBitmap(bmp
)
369 bg
= dc
.GetBackground().GetColour()
370 img
.SetMaskColour(bg
.Red(), bg
.Green(), bg
.Blue())
371 bmp
= img
.ConvertToBitmap()
375 def RenderToDC(str, dc
, x
, y
, enclose
=1):
376 "Render str onto a wxDC at (x,y)"
377 width
, height
, dy
= GetFullExtent(str, dc
)
378 renderer
= DCRenderer(dc
, x
, y
+dy
)
379 RenderToRenderer(str, renderer
, enclose
)
382 class StaticFancyText(wx
.StaticBitmap
):
383 def __init__(self
, window
, id, text
, *args
, **kargs
):
385 kargs
.setdefault('name', 'staticFancyText')
386 if 'background' in kargs
:
387 background
= kargs
.pop('background')
389 background
= args
.pop(0)
391 background
= wx
.Brush(window
.GetBackgroundColour(), wx
.SOLID
)
393 bmp
= RenderToBitmap(text
, background
)
394 wx
.StaticBitmap
.__init
__(self
, window
, id, bmp
, *args
, **kargs
)
397 # Old names for backward compatibiliry
398 getExtent
= GetExtent
399 renderToBitmap
= RenderToBitmap
400 renderToDC
= RenderToDC
406 app
= wx
.PySimpleApp()
407 box
= wx
.BoxSizer(wx
.VERTICAL
)
408 frame
= wx
.Frame(None, -1, "FancyText demo", wx
.DefaultPosition
)
409 frame
.SetBackgroundColour("light grey")
410 sft
= StaticFancyText(frame
, -1, __doc__
)
411 box
.Add(sft
, 1, wx
.EXPAND
)
413 frame
.SetAutoLayout(True)
415 box
.SetSizeHints(frame
)
419 if __name__
== "__main__":