]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/fancytext.py
more informative assert message
[wxWidgets.git] / wxPython / wx / lib / fancytext.py
CommitLineData
b881fc78
RD
1# 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net)
2#
3# o Updated for 2.5 compatability.
4#
5
fdc775af
RD
6"""
7FancyText -- methods for rendering XML specified text
8
d14a1e28 9This module exports four main methods::
fdc775af 10
d14a1e28
RD
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)
fdc775af
RD
15
16In all cases, 'str' is an XML string. Note that start and end tags are
17only required if *enclose* is set to False. In this case the text
18should be wrapped in FancyText tags.
d14a1e28
RD
19
20In addition, the module exports one class::
fdc775af 21
d14a1e28 22 class StaticFancyText(self, window, id, text, background, ...)
fdc775af
RD
23
24This class works similar to StaticText except it interprets its text
d14a1e28
RD
25as FancyText.
26
fdc775af
RD
27The text can support superscripts and subscripts, text in different
28sizes, colors, styles, weights and families. It also supports a
29limited set of symbols, currently *times*, *infinity*, *angle* as well
30as greek letters in both upper case (*Alpha* *Beta*... *Omega*) and
31lower case (*alpha* *beta*... *omega*).
d14a1e28 32
d14a1e28 33>>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition)
fdc775af 34>>> sft = StaticFancyText(frame, -1, testText, wx.Brush("light grey", wx.SOLID))
d14a1e28
RD
35>>> frame.SetClientSize(sft.GetSize())
36>>> didit = frame.Show()
37>>> from guitest import PauseTests; PauseTests()
38
fdc775af 39"""
b881fc78 40
d14a1e28 41# Copyright 2001-2003 Timothy Hochberg
b881fc78
RD
42# Use as you see fit. No warantees, I cannot be held responsible, etc.
43
d14a1e28
RD
44import copy
45import math
46import sys
b881fc78 47
d14a1e28
RD
48import wx
49import xml.parsers.expat
50
51__all__ = "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText"
52
53if sys.platform == "win32":
54 _greekEncoding = str(wx.FONTENCODING_CP1253)
55else:
56 _greekEncoding = str(wx.FONTENCODING_ISO8859_7)
57
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}
62
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
66# Renderer.
67
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")
72
73def iround(number):
74 return int(round(number))
75
76def iceil(number):
77 return int(math.ceil(number))
78
79class Renderer:
80 """Class for rendering XML marked up text.
81
82 See the module docstring for a description of the markup.
83
84 This class must be subclassed and the methods the methods that do
85 the drawing overridden for a particular output device.
86
87 """
88 defaultSize = None
89 defaultFamily = wx.DEFAULT
90 defaultStyle = wx.NORMAL
91 defaultWeight = wx.NORMAL
92 defaultEncoding = None
93 defaultColor = "black"
94
95 def __init__(self, dc=None, x=0, y=None):
96 if dc == None:
97 dc = wx.MemoryDC()
98 self.dc = dc
99 self.offsets = [0]
100 self.fonts = [{}]
101 self.width = self.height = 0
102 self.x = x
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()
108
109 def getY(self):
110 if self._y is None:
111 self.minY = self.maxY = self._y = self.dc.GetTextExtent("M")[1]
112 return self._y
113 def setY(self, value):
114 self._y = y
115 y = property(getY, setY)
116
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)
122
123 def endElement(self, name):
124 methname = "end_" + name
125 if hasattr(self, methname):
126 getattr(self, methname)()
127 elif hasattr(self, "start_" + name):
128 pass
129 else:
130 raise ValueError("XML tag '%s' not supported" % methname)
131
132 def characterData(self, data):
133 self.dc.SetFont(self.getCurrentFont())
134 for i, chunk in enumerate(data.split('\n')):
135 if i:
136 self.x = 0
137 self.y = self.mayY = self.maxY + self.dc.GetTextExtent("M")[1]
138 if chunk:
139 width, height, descent, extl = self.dc.GetFullTextExtent(chunk)
140 self.renderCharacterData(data, iround(self.x), iround(self.y + self.offsets[-1] - height + descent))
141 else:
142 width = height = descent = extl = 0
143 self.updateDims(width, height, descent, extl)
144
145 def updateDims(self, width, height, descent, externalLeading):
146 self.x += width
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
151
152 def start_FancyText(self, attrs):
153 pass
154 start_wxFancyText = start_FancyText # For backward compatibility
155
156 def start_font(self, attrs):
157 for key, value in attrs.items():
158 if key == "size":
159 value = int(value)
160 elif key == "family":
161 value = _families[value]
162 elif key == "style":
163 value = _styles[value]
164 elif key == "weight":
165 value = _weights[value]
166 elif key == "encoding":
167 value = int(value)
168 elif key == "color":
169 pass
170 else:
171 raise ValueError("unknown font attribute '%s'" % key)
172 attrs[key] = value
173 font = copy.copy(self.fonts[-1])
174 font.update(attrs)
175 self.fonts.append(font)
176
177 def end_font(self):
178 self.fonts.pop()
179
180 def start_sub(self, attrs):
181 if attrs.keys():
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})
186
187 def end_sub(self):
188 self.fonts.pop()
189 self.offsets.pop()
190
191 def start_sup(self, attrs):
192 if attrs.keys():
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})
197
198 def end_sup(self):
199 self.fonts.pop()
200 self.offsets.pop()
201
202 def getCurrentFont(self):
203 font = self.fonts[-1]
9751f1a8
RD
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),
208 False, "",
209 font.get("encoding", self.defaultEncoding))
d14a1e28
RD
210
211 def getCurrentColor(self):
212 font = self.fonts[-1]
213 return wx.TheColourDatabase.FindColour(font.get("color", self.defaultColor))
214
215 def getCurrentPen(self):
9751f1a8 216 return wx.Pen(self.getCurrentColor(), 1, wx.SOLID)
d14a1e28
RD
217
218 def renderCharacterData(self, data, x, y):
219 raise NotImplementedError()
220
221
222def _addGreek():
223 alpha = 0xE1
224 Alpha = 0xC1
225 def end(self):
226 pass
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)
231 self.end_font()
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)
239 self.end_font()
240 setattr(Renderer, "start_%s" % name.capitalize(), start)
241 setattr(Renderer, "end_%s" % name.capitalize(), end)
242_addGreek()
243
244
245
246class SizeRenderer(Renderer):
247 """Processes text as if rendering it, but just computes the size."""
248
249 def __init__(self, dc=None):
250 Renderer.__init__(self, dc, 0, 0)
251
252 def renderCharacterData(self, data, x, y):
253 pass
254
255 def start_angle(self, attrs):
256 self.characterData("M")
257
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)
263
264 def start_times(self, attrs):
265 self.characterData("M")
266
267 def start_in(self, attrs):
268 self.characterData("M")
269
270 def start_times(self, attrs):
271 self.characterData("M")
272
273
274class DCRenderer(Renderer):
275 """Renders text to a wxPython device context DC."""
276
277 def renderCharacterData(self, data, x, y):
278 self.dc.SetTextForeground(self.getCurrentColor())
d7403ad2 279 self.dc.DrawText(data, x, y)
d14a1e28
RD
280
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]
d7403ad2
RD
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))
d14a1e28
RD
288 self.updateDims(width, height, descent, leading)
289
290
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
302 yc = iround(y-1.5*r)
d7403ad2
RD
303 self.dc.DrawCircle(xc - r, yc, r)
304 self.dc.DrawCircle(xc + r, yc, r)
d14a1e28
RD
305 self.updateDims(width, height, 0, 0)
306
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]
312 width *= 0.8
313 width = iround(width+.5)
314 self.dc.SetPen(wx.Pen(self.getCurrentColor(), 1))
d7403ad2
RD
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))
d14a1e28
RD
317 self.updateDims(width, height, 0, 0)
318
319
320def RenderToRenderer(str, renderer, enclose=True):
321 try:
322 if enclose:
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
329 p.Parse(str, 1)
330 except xml.parsers.expat.error, err:
331 raise ValueError('error parsing text text "%s": %s' % (str, err))
332
333
334# Public interface
335
336
337def 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
342
343
344def 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
348
349
350def RenderToBitmap(str, background=None, enclose=1):
351 "Return str rendered on a minumum size bitmap"
352 dc = wx.MemoryDC()
b8aab4d2
RD
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))
d14a1e28
RD
356 width, height, dy = GetFullExtent(str, dc, enclose)
357 bmp = wx.EmptyBitmap(width, height)
358 dc.SelectObject(bmp)
359 if background is None:
360 dc.SetBackground(wx.WHITE_BRUSH)
361 else:
362 dc.SetBackground(background)
363 dc.Clear()
364 renderer = DCRenderer(dc, y=dy)
365 dc.BeginDrawing()
366 RenderToRenderer(str, renderer, enclose)
367 dc.EndDrawing()
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()
374 return bmp
375
376
377def 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)
382
383
384class StaticFancyText(wx.StaticBitmap):
385 def __init__(self, window, id, text, *args, **kargs):
386 args = list(args)
387 kargs.setdefault('name', 'staticFancyText')
388 if 'background' in kargs:
389 background = kargs.pop('background')
390 elif args:
391 background = args.pop(0)
392 else:
393 background = wx.Brush(window.GetBackgroundColour(), wx.SOLID)
394
395 bmp = RenderToBitmap(text, background)
396 wx.StaticBitmap.__init__(self, window, id, bmp, *args, **kargs)
397
398
399# Old names for backward compatibiliry
400getExtent = GetExtent
401renderToBitmap = RenderToBitmap
402renderToDC = RenderToDC
403
404
405# Test Driver
406
407def test():
4c53d530 408 testText = \
fdc775af
RD
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">
411This 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)
417</font>
418In all cases, 'str' is an XML string. Note that start and end tags
419are only required if *enclose* is set to False. In this case the
420text should be wrapped in FancyText tags.
421
422In addition, the module exports one class::
423<font family="fixed" style="slant">
424 class StaticFancyText(self, window, id, text, background, ...)
425</font>
426This class works similar to StaticText except it interprets its text
427as FancyText.
428
429The text can support<sup>superscripts</sup> and <sub>subscripts</sub>, text
430in 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,
432currently <times/>, <infinity/>, <angle/> as well as greek letters in both
433upper case (<Alpha/><Beta/>...<Omega/>) and lower case (<alpha/><beta/>...<omega/>).
434
435We 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()
442
443</font></font>
444The End"""
445
b881fc78 446 app = wx.PySimpleApp()
d14a1e28 447 box = wx.BoxSizer(wx.VERTICAL)
b881fc78 448 frame = wx.Frame(None, -1, "FancyText demo", wx.DefaultPosition)
d14a1e28 449 frame.SetBackgroundColour("light grey")
fdc775af 450 sft = StaticFancyText(frame, -1, testText)
d14a1e28
RD
451 box.Add(sft, 1, wx.EXPAND)
452 frame.SetSizer(box)
453 frame.SetAutoLayout(True)
454 box.Fit(frame)
455 box.SetSizeHints(frame)
456 frame.Show()
457 app.MainLoop()
458
459if __name__ == "__main__":
460 test()
1fded56b 461
1fded56b 462