]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/fancytext.py
"wxWindows" --> "wxWidgets"
[wxWidgets.git] / wxPython / wx / lib / fancytext.py
1 # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net)
2 #
3 # o Updated for 2.5 compatability.
4 #
5
6 """
7 FancyText -- methods for rendering XML specified text
8
9 This module exports four main methods::
10
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)
15
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.
19
20 In addition, the module exports one class::
21
22 class StaticFancyText(self, window, id, text, background, ...)
23
24 This class works similar to StaticText except it interprets its text
25 as FancyText.
26
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*).
32
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()
38
39 """
40
41 # Copyright 2001-2003 Timothy Hochberg
42 # Use as you see fit. No warantees, I cannot be held responsible, etc.
43
44 import copy
45 import math
46 import sys
47
48 import wx
49 import xml.parsers.expat
50
51 __all__ = "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText"
52
53 if sys.platform == "win32":
54 _greekEncoding = str(wx.FONTENCODING_CP1253)
55 else:
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
73 def iround(number):
74 return int(round(number))
75
76 def iceil(number):
77 return int(math.ceil(number))
78
79 class 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]
204 return wx.TheFontList.FindOrCreateFont(font.get("size", self.defaultSize),
205 font.get("family", self.defaultFamily),
206 font.get("style", self.defaultStyle),
207 font.get("weight", self.defaultWeight),
208 encoding = font.get("encoding", self.defaultEncoding))
209
210 def getCurrentColor(self):
211 font = self.fonts[-1]
212 return wx.TheColourDatabase.FindColour(font.get("color", self.defaultColor))
213
214 def getCurrentPen(self):
215 return wx.ThePenList.FindOrCreatePen(self.getCurrentColor(), 1, wx.SOLID)
216
217 def renderCharacterData(self, data, x, y):
218 raise NotImplementedError()
219
220
221 def _addGreek():
222 alpha = 0xE1
223 Alpha = 0xC1
224 def end(self):
225 pass
226 for i, name in enumerate(_greek_letters):
227 def start(self, attrs, code=chr(alpha+i)):
228 self.start_font({"encoding" : _greekEncoding})
229 self.characterData(code)
230 self.end_font()
231 setattr(Renderer, "start_%s" % name, start)
232 setattr(Renderer, "end_%s" % name, end)
233 if name == "altsigma":
234 continue # There is no capital for altsigma
235 def start(self, attrs, code=chr(Alpha+i)):
236 self.start_font({"encoding" : _greekEncoding})
237 self.characterData(code)
238 self.end_font()
239 setattr(Renderer, "start_%s" % name.capitalize(), start)
240 setattr(Renderer, "end_%s" % name.capitalize(), end)
241 _addGreek()
242
243
244
245 class SizeRenderer(Renderer):
246 """Processes text as if rendering it, but just computes the size."""
247
248 def __init__(self, dc=None):
249 Renderer.__init__(self, dc, 0, 0)
250
251 def renderCharacterData(self, data, x, y):
252 pass
253
254 def start_angle(self, attrs):
255 self.characterData("M")
256
257 def start_infinity(self, attrs):
258 width, height = self.dc.GetTextExtent("M")
259 width = max(width, 10)
260 height = max(height, width / 2)
261 self.updateDims(width, height, 0, 0)
262
263 def start_times(self, attrs):
264 self.characterData("M")
265
266 def start_in(self, attrs):
267 self.characterData("M")
268
269 def start_times(self, attrs):
270 self.characterData("M")
271
272
273 class DCRenderer(Renderer):
274 """Renders text to a wxPython device context DC."""
275
276 def renderCharacterData(self, data, x, y):
277 self.dc.SetTextForeground(self.getCurrentColor())
278 self.dc.DrawText(data, x, y)
279
280 def start_angle(self, attrs):
281 self.dc.SetFont(self.getCurrentFont())
282 self.dc.SetPen(self.getCurrentPen())
283 width, height, descent, leading = self.dc.GetFullTextExtent("M")
284 y = self.y + self.offsets[-1]
285 self.dc.DrawLine(iround(self.x), iround(y), iround( self.x+width), iround(y))
286 self.dc.DrawLine(iround(self.x), iround(y), iround(self.x+width), iround(y-width))
287 self.updateDims(width, height, descent, leading)
288
289
290 def start_infinity(self, attrs):
291 self.dc.SetFont(self.getCurrentFont())
292 self.dc.SetPen(self.getCurrentPen())
293 width, height, descent, leading = self.dc.GetFullTextExtent("M")
294 width = max(width, 10)
295 height = max(height, width / 2)
296 self.dc.SetPen(wx.Pen(self.getCurrentColor(), max(1, width/10)))
297 self.dc.SetBrush(wx.TRANSPARENT_BRUSH)
298 y = self.y + self.offsets[-1]
299 r = iround( 0.95 * width / 4)
300 xc = (2*self.x + width) / 2
301 yc = iround(y-1.5*r)
302 self.dc.DrawCircle(xc - r, yc, r)
303 self.dc.DrawCircle(xc + r, yc, r)
304 self.updateDims(width, height, 0, 0)
305
306 def start_times(self, attrs):
307 self.dc.SetFont(self.getCurrentFont())
308 self.dc.SetPen(self.getCurrentPen())
309 width, height, descent, leading = self.dc.GetFullTextExtent("M")
310 y = self.y + self.offsets[-1]
311 width *= 0.8
312 width = iround(width+.5)
313 self.dc.SetPen(wx.Pen(self.getCurrentColor(), 1))
314 self.dc.DrawLine(iround(self.x), iround(y-width), iround(self.x+width-1), iround(y-1))
315 self.dc.DrawLine(iround(self.x), iround(y-2), iround(self.x+width-1), iround(y-width-1))
316 self.updateDims(width, height, 0, 0)
317
318
319 def RenderToRenderer(str, renderer, enclose=True):
320 try:
321 if enclose:
322 str = '<?xml version="1.0"?><FancyText>%s</FancyText>' % str
323 p = xml.parsers.expat.ParserCreate()
324 p.returns_unicode = 0
325 p.StartElementHandler = renderer.startElement
326 p.EndElementHandler = renderer.endElement
327 p.CharacterDataHandler = renderer.characterData
328 p.Parse(str, 1)
329 except xml.parsers.expat.error, err:
330 raise ValueError('error parsing text text "%s": %s' % (str, err))
331
332
333 # Public interface
334
335
336 def GetExtent(str, dc=None, enclose=True):
337 "Return the extent of str"
338 renderer = SizeRenderer(dc)
339 RenderToRenderer(str, renderer, enclose)
340 return iceil(renderer.width), iceil(renderer.height) # XXX round up
341
342
343 def GetFullExtent(str, dc=None, enclose=True):
344 renderer = SizeRenderer(dc)
345 RenderToRenderer(str, renderer, enclose)
346 return iceil(renderer.width), iceil(renderer.height), -iceil(renderer.minY) # XXX round up
347
348
349 def RenderToBitmap(str, background=None, enclose=1):
350 "Return str rendered on a minumum size bitmap"
351 dc = wx.MemoryDC()
352 width, height, dy = GetFullExtent(str, dc, enclose)
353 bmp = wx.EmptyBitmap(width, height)
354 dc.SelectObject(bmp)
355 if background is None:
356 dc.SetBackground(wx.WHITE_BRUSH)
357 else:
358 dc.SetBackground(background)
359 dc.Clear()
360 renderer = DCRenderer(dc, y=dy)
361 dc.BeginDrawing()
362 RenderToRenderer(str, renderer, enclose)
363 dc.EndDrawing()
364 dc.SelectObject(wx.NullBitmap)
365 if background is None:
366 img = wx.ImageFromBitmap(bmp)
367 bg = dc.GetBackground().GetColour()
368 img.SetMaskColour(bg.Red(), bg.Green(), bg.Blue())
369 bmp = img.ConvertToBitmap()
370 return bmp
371
372
373 def RenderToDC(str, dc, x, y, enclose=1):
374 "Render str onto a wxDC at (x,y)"
375 width, height, dy = GetFullExtent(str, dc)
376 renderer = DCRenderer(dc, x, y+dy)
377 RenderToRenderer(str, renderer, enclose)
378
379
380 class StaticFancyText(wx.StaticBitmap):
381 def __init__(self, window, id, text, *args, **kargs):
382 args = list(args)
383 kargs.setdefault('name', 'staticFancyText')
384 if 'background' in kargs:
385 background = kargs.pop('background')
386 elif args:
387 background = args.pop(0)
388 else:
389 background = wx.Brush(window.GetBackgroundColour(), wx.SOLID)
390
391 bmp = RenderToBitmap(text, background)
392 wx.StaticBitmap.__init__(self, window, id, bmp, *args, **kargs)
393
394
395 # Old names for backward compatibiliry
396 getExtent = GetExtent
397 renderToBitmap = RenderToBitmap
398 renderToDC = RenderToDC
399
400
401 # Test Driver
402
403 def test():
404 testText = \
405 """<font weight="bold" size="16">FancyText</font> -- <font style="italic" size="16">methods for rendering XML specified text</font>
406 <font family="swiss" size="12">
407 This module exports four main methods::
408 <font family="fixed" style="slant">
409 def GetExtent(str, dc=None, enclose=True)
410 def GetFullExtent(str, dc=None, enclose=True)
411 def RenderToBitmap(str, background=None, enclose=True)
412 def RenderToDC(str, dc, x, y, enclose=True)
413 </font>
414 In all cases, 'str' is an XML string. Note that start and end tags
415 are only required if *enclose* is set to False. In this case the
416 text should be wrapped in FancyText tags.
417
418 In addition, the module exports one class::
419 <font family="fixed" style="slant">
420 class StaticFancyText(self, window, id, text, background, ...)
421 </font>
422 This class works similar to StaticText except it interprets its text
423 as FancyText.
424
425 The text can support<sup>superscripts</sup> and <sub>subscripts</sub>, text
426 in different <font size="20">sizes</font>, <font color="blue">colors</font>, <font style="italic">styles</font>, <font weight="bold">weights</font> and
427 <font family="script">families</font>. It also supports a limited set of symbols,
428 currently <times/>, <infinity/>, <angle/> as well as greek letters in both
429 upper case (<Alpha/><Beta/>...<Omega/>) and lower case (<alpha/><beta/>...<omega/>).
430
431 We can use doctest/guitest to display this string in all its marked up glory.
432 <font family="fixed">
433 >>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition)
434 >>> sft = StaticFancyText(frame, -1, __doc__, wx.Brush("light grey", wx.SOLID))
435 >>> frame.SetClientSize(sft.GetSize())
436 >>> didit = frame.Show()
437 >>> from guitest import PauseTests; PauseTests()
438
439 </font></font>
440 The End"""
441
442 app = wx.PySimpleApp()
443 box = wx.BoxSizer(wx.VERTICAL)
444 frame = wx.Frame(None, -1, "FancyText demo", wx.DefaultPosition)
445 frame.SetBackgroundColour("light grey")
446 sft = StaticFancyText(frame, -1, testText)
447 box.Add(sft, 1, wx.EXPAND)
448 frame.SetSizer(box)
449 frame.SetAutoLayout(True)
450 box.Fit(frame)
451 box.SetSizeHints(frame)
452 frame.Show()
453 app.MainLoop()
454
455 if __name__ == "__main__":
456 test()
457
458