]>
Commit | Line | Data |
---|---|---|
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 |