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__":