]>
Commit | Line | Data |
---|---|---|
1 | # 12/02/2003 - Jeff Grimmett (grimmtooth@softhome.net) | |
2 | # | |
3 | # o Updated for 2.5 compatability. | |
4 | # | |
5 | ||
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) | |
14 | </font> | |
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. | |
18 | ||
19 | In addition, the module exports one class:: | |
20 | <font family="fixed" style="slant"> | |
21 | class StaticFancyText(self, window, id, text, background, ...) | |
22 | </font> | |
23 | This class works similar to StaticText except it interprets its text | |
24 | as FancyText. | |
25 | ||
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/>). | |
31 | ||
32 | We can use doctest/guitest to display this string in all its marked up glory. | |
33 | <font family="fixed"> | |
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() | |
39 | ||
40 | </font></font> | |
41 | The End""" | |
42 | ||
43 | # Copyright 2001-2003 Timothy Hochberg | |
44 | # Use as you see fit. No warantees, I cannot be held responsible, etc. | |
45 | ||
46 | import copy | |
47 | import math | |
48 | import sys | |
49 | ||
50 | import wx | |
51 | import xml.parsers.expat | |
52 | ||
53 | __all__ = "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText" | |
54 | ||
55 | if sys.platform == "win32": | |
56 | _greekEncoding = str(wx.FONTENCODING_CP1253) | |
57 | else: | |
58 | _greekEncoding = str(wx.FONTENCODING_ISO8859_7) | |
59 | ||
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} | |
64 | ||
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 | |
68 | # Renderer. | |
69 | ||
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") | |
74 | ||
75 | def iround(number): | |
76 | return int(round(number)) | |
77 | ||
78 | def iceil(number): | |
79 | return int(math.ceil(number)) | |
80 | ||
81 | class Renderer: | |
82 | """Class for rendering XML marked up text. | |
83 | ||
84 | See the module docstring for a description of the markup. | |
85 | ||
86 | This class must be subclassed and the methods the methods that do | |
87 | the drawing overridden for a particular output device. | |
88 | ||
89 | """ | |
90 | defaultSize = None | |
91 | defaultFamily = wx.DEFAULT | |
92 | defaultStyle = wx.NORMAL | |
93 | defaultWeight = wx.NORMAL | |
94 | defaultEncoding = None | |
95 | defaultColor = "black" | |
96 | ||
97 | def __init__(self, dc=None, x=0, y=None): | |
98 | if dc == None: | |
99 | dc = wx.MemoryDC() | |
100 | self.dc = dc | |
101 | self.offsets = [0] | |
102 | self.fonts = [{}] | |
103 | self.width = self.height = 0 | |
104 | self.x = x | |
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() | |
110 | ||
111 | def getY(self): | |
112 | if self._y is None: | |
113 | self.minY = self.maxY = self._y = self.dc.GetTextExtent("M")[1] | |
114 | return self._y | |
115 | def setY(self, value): | |
116 | self._y = y | |
117 | y = property(getY, setY) | |
118 | ||
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) | |
124 | ||
125 | def endElement(self, name): | |
126 | methname = "end_" + name | |
127 | if hasattr(self, methname): | |
128 | getattr(self, methname)() | |
129 | elif hasattr(self, "start_" + name): | |
130 | pass | |
131 | else: | |
132 | raise ValueError("XML tag '%s' not supported" % methname) | |
133 | ||
134 | def characterData(self, data): | |
135 | self.dc.SetFont(self.getCurrentFont()) | |
136 | for i, chunk in enumerate(data.split('\n')): | |
137 | if i: | |
138 | self.x = 0 | |
139 | self.y = self.mayY = self.maxY + self.dc.GetTextExtent("M")[1] | |
140 | if chunk: | |
141 | width, height, descent, extl = self.dc.GetFullTextExtent(chunk) | |
142 | self.renderCharacterData(data, iround(self.x), iround(self.y + self.offsets[-1] - height + descent)) | |
143 | else: | |
144 | width = height = descent = extl = 0 | |
145 | self.updateDims(width, height, descent, extl) | |
146 | ||
147 | def updateDims(self, width, height, descent, externalLeading): | |
148 | self.x += width | |
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 | |
153 | ||
154 | def start_FancyText(self, attrs): | |
155 | pass | |
156 | start_wxFancyText = start_FancyText # For backward compatibility | |
157 | ||
158 | def start_font(self, attrs): | |
159 | for key, value in attrs.items(): | |
160 | if key == "size": | |
161 | value = int(value) | |
162 | elif key == "family": | |
163 | value = _families[value] | |
164 | elif key == "style": | |
165 | value = _styles[value] | |
166 | elif key == "weight": | |
167 | value = _weights[value] | |
168 | elif key == "encoding": | |
169 | value = int(value) | |
170 | elif key == "color": | |
171 | pass | |
172 | else: | |
173 | raise ValueError("unknown font attribute '%s'" % key) | |
174 | attrs[key] = value | |
175 | font = copy.copy(self.fonts[-1]) | |
176 | font.update(attrs) | |
177 | self.fonts.append(font) | |
178 | ||
179 | def end_font(self): | |
180 | self.fonts.pop() | |
181 | ||
182 | def start_sub(self, attrs): | |
183 | if attrs.keys(): | |
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}) | |
188 | ||
189 | def end_sub(self): | |
190 | self.fonts.pop() | |
191 | self.offsets.pop() | |
192 | ||
193 | def start_sup(self, attrs): | |
194 | if attrs.keys(): | |
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}) | |
199 | ||
200 | def end_sup(self): | |
201 | self.fonts.pop() | |
202 | self.offsets.pop() | |
203 | ||
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)) | |
211 | ||
212 | def getCurrentColor(self): | |
213 | font = self.fonts[-1] | |
214 | return wx.TheColourDatabase.FindColour(font.get("color", self.defaultColor)) | |
215 | ||
216 | def getCurrentPen(self): | |
217 | return wx.ThePenList.FindOrCreatePen(self.getCurrentColor(), 1, wx.SOLID) | |
218 | ||
219 | def renderCharacterData(self, data, x, y): | |
220 | raise NotImplementedError() | |
221 | ||
222 | ||
223 | def _addGreek(): | |
224 | alpha = 0xE1 | |
225 | Alpha = 0xC1 | |
226 | def end(self): | |
227 | pass | |
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) | |
232 | self.end_font() | |
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) | |
240 | self.end_font() | |
241 | setattr(Renderer, "start_%s" % name.capitalize(), start) | |
242 | setattr(Renderer, "end_%s" % name.capitalize(), end) | |
243 | _addGreek() | |
244 | ||
245 | ||
246 | ||
247 | class SizeRenderer(Renderer): | |
248 | """Processes text as if rendering it, but just computes the size.""" | |
249 | ||
250 | def __init__(self, dc=None): | |
251 | Renderer.__init__(self, dc, 0, 0) | |
252 | ||
253 | def renderCharacterData(self, data, x, y): | |
254 | pass | |
255 | ||
256 | def start_angle(self, attrs): | |
257 | self.characterData("M") | |
258 | ||
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) | |
264 | ||
265 | def start_times(self, attrs): | |
266 | self.characterData("M") | |
267 | ||
268 | def start_in(self, attrs): | |
269 | self.characterData("M") | |
270 | ||
271 | def start_times(self, attrs): | |
272 | self.characterData("M") | |
273 | ||
274 | ||
275 | class DCRenderer(Renderer): | |
276 | """Renders text to a wxPython device context DC.""" | |
277 | ||
278 | def renderCharacterData(self, data, x, y): | |
279 | self.dc.SetTextForeground(self.getCurrentColor()) | |
280 | self.dc.DrawText(data, x, y) | |
281 | ||
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) | |
290 | ||
291 | ||
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 | |
303 | yc = iround(y-1.5*r) | |
304 | self.dc.DrawCircle(xc - r, yc, r) | |
305 | self.dc.DrawCircle(xc + r, yc, r) | |
306 | self.updateDims(width, height, 0, 0) | |
307 | ||
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] | |
313 | width *= 0.8 | |
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) | |
319 | ||
320 | ||
321 | def RenderToRenderer(str, renderer, enclose=True): | |
322 | try: | |
323 | if enclose: | |
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 | |
330 | p.Parse(str, 1) | |
331 | except xml.parsers.expat.error, err: | |
332 | raise ValueError('error parsing text text "%s": %s' % (str, err)) | |
333 | ||
334 | ||
335 | # Public interface | |
336 | ||
337 | ||
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 | |
343 | ||
344 | ||
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 | |
349 | ||
350 | ||
351 | def RenderToBitmap(str, background=None, enclose=1): | |
352 | "Return str rendered on a minumum size bitmap" | |
353 | dc = wx.MemoryDC() | |
354 | width, height, dy = GetFullExtent(str, dc, enclose) | |
355 | bmp = wx.EmptyBitmap(width, height) | |
356 | dc.SelectObject(bmp) | |
357 | if background is None: | |
358 | dc.SetBackground(wx.WHITE_BRUSH) | |
359 | else: | |
360 | dc.SetBackground(background) | |
361 | dc.Clear() | |
362 | renderer = DCRenderer(dc, y=dy) | |
363 | dc.BeginDrawing() | |
364 | RenderToRenderer(str, renderer, enclose) | |
365 | dc.EndDrawing() | |
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() | |
372 | return bmp | |
373 | ||
374 | ||
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) | |
380 | ||
381 | ||
382 | class StaticFancyText(wx.StaticBitmap): | |
383 | def __init__(self, window, id, text, *args, **kargs): | |
384 | args = list(args) | |
385 | kargs.setdefault('name', 'staticFancyText') | |
386 | if 'background' in kargs: | |
387 | background = kargs.pop('background') | |
388 | elif args: | |
389 | background = args.pop(0) | |
390 | else: | |
391 | background = wx.Brush(window.GetBackgroundColour(), wx.SOLID) | |
392 | ||
393 | bmp = RenderToBitmap(text, background) | |
394 | wx.StaticBitmap.__init__(self, window, id, bmp, *args, **kargs) | |
395 | ||
396 | ||
397 | # Old names for backward compatibiliry | |
398 | getExtent = GetExtent | |
399 | renderToBitmap = RenderToBitmap | |
400 | renderToDC = RenderToDC | |
401 | ||
402 | ||
403 | # Test Driver | |
404 | ||
405 | def test(): | |
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) | |
412 | frame.SetSizer(box) | |
413 | frame.SetAutoLayout(True) | |
414 | box.Fit(frame) | |
415 | box.SetSizeHints(frame) | |
416 | frame.Show() | |
417 | app.MainLoop() | |
418 | ||
419 | if __name__ == "__main__": | |
420 | test() | |
421 | ||
422 |