]>
Commit | Line | Data |
---|---|---|
1 | """<font weight="bold" size="16">FancyText</font> -- <font style="italic" size="16">methods for rendering XML specified text</font> | |
2 | <font family="swiss" size="12"> | |
3 | This module exports four main methods:: | |
4 | <font family="fixed" style="slant"> | |
5 | def GetExtent(str, dc=None, enclose=True) | |
6 | def GetFullExtent(str, dc=None, enclose=True) | |
7 | def RenderToBitmap(str, background=None, enclose=True) | |
8 | def RenderToDC(str, dc, x, y, enclose=True) | |
9 | </font> | |
10 | In all cases, 'str' is an XML string. Note that start and end tags | |
11 | are only required if *enclose* is set to False. In this case the | |
12 | text should be wrapped in FancyText tags. | |
13 | ||
14 | In addition, the module exports one class:: | |
15 | <font family="fixed" style="slant"> | |
16 | class StaticFancyText(self, window, id, text, background, ...) | |
17 | </font> | |
18 | This class works similar to StaticText except it interprets its text | |
19 | as FancyText. | |
20 | ||
21 | The text can support<sup>superscripts</sup> and <sub>subscripts</sub>, text | |
22 | in different <font size="20">sizes</font>, <font color="blue">colors</font>, <font style="italic">styles</font>, <font weight="bold">weights</font> and | |
23 | <font family="script">families</font>. It also supports a limited set of symbols, | |
24 | currently <times/>, <infinity/>, <angle/> as well as greek letters in both | |
25 | upper case (<Alpha/><Beta/>...<Omega/>) and lower case (<alpha/><beta/>...<omega/>). | |
26 | ||
27 | We can use doctest/guitest to display this string in all its marked up glory. | |
28 | <font family="fixed"> | |
29 | >>> frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition) | |
30 | >>> sft = StaticFancyText(frame, -1, __doc__, wx.Brush("light grey", wx.SOLID)) | |
31 | >>> frame.SetClientSize(sft.GetSize()) | |
32 | >>> didit = frame.Show() | |
33 | >>> from guitest import PauseTests; PauseTests() | |
34 | ||
35 | </font></font> | |
36 | The End""" | |
37 | # Copyright 2001-2003 Timothy Hochberg | |
38 | # Use as you see fit. No warantees, I cannot be help responsible, etc. | |
39 | import copy | |
40 | import math | |
41 | import sys | |
42 | import wx | |
43 | import xml.parsers.expat | |
44 | ||
45 | __all__ = "GetExtent", "GetFullExtent", "RenderToBitmap", "RenderToDC", "StaticFancyText" | |
46 | ||
47 | if sys.platform == "win32": | |
48 | _greekEncoding = str(wx.FONTENCODING_CP1253) | |
49 | else: | |
50 | _greekEncoding = str(wx.FONTENCODING_ISO8859_7) | |
51 | ||
52 | _families = {"fixed" : wx.FIXED, "default" : wx.DEFAULT, "decorative" : wx.DECORATIVE, "roman" : wx.ROMAN, | |
53 | "script" : wx.SCRIPT, "swiss" : wx.SWISS, "modern" : wx.MODERN} | |
54 | _styles = {"normal" : wx.NORMAL, "slant" : wx.SLANT, "italic" : wx.ITALIC} | |
55 | _weights = {"normal" : wx.NORMAL, "light" : wx.LIGHT, "bold" : wx.BOLD} | |
56 | ||
57 | # The next three classes: Renderer, SizeRenderer and DCRenderer are | |
58 | # what you will need to override to extend the XML language. All of | |
59 | # the font stuff as well as the subscript and superscript stuff are in | |
60 | # Renderer. | |
61 | ||
62 | _greek_letters = ("alpha", "beta", "gamma", "delta", "epsilon", "zeta", | |
63 | "eta", "theta", "iota", "kappa", "lambda", "mu", "nu", | |
64 | "xi", "omnikron", "pi", "rho", "altsigma", "sigma", "tau", "upsilon", | |
65 | "phi", "chi", "psi", "omega") | |
66 | ||
67 | def iround(number): | |
68 | return int(round(number)) | |
69 | ||
70 | def iceil(number): | |
71 | return int(math.ceil(number)) | |
72 | ||
73 | class Renderer: | |
74 | """Class for rendering XML marked up text. | |
75 | ||
76 | See the module docstring for a description of the markup. | |
77 | ||
78 | This class must be subclassed and the methods the methods that do | |
79 | the drawing overridden for a particular output device. | |
80 | ||
81 | """ | |
82 | defaultSize = None | |
83 | defaultFamily = wx.DEFAULT | |
84 | defaultStyle = wx.NORMAL | |
85 | defaultWeight = wx.NORMAL | |
86 | defaultEncoding = None | |
87 | defaultColor = "black" | |
88 | ||
89 | def __init__(self, dc=None, x=0, y=None): | |
90 | if dc == None: | |
91 | dc = wx.MemoryDC() | |
92 | self.dc = dc | |
93 | self.offsets = [0] | |
94 | self.fonts = [{}] | |
95 | self.width = self.height = 0 | |
96 | self.x = x | |
97 | self.minY = self.maxY = self._y = y | |
98 | if Renderer.defaultSize is None: | |
99 | Renderer.defaultSize = wx.NORMAL_FONT.GetPointSize() | |
100 | if Renderer.defaultEncoding is None: | |
101 | Renderer.defaultEncoding = wx.Font_GetDefaultEncoding() | |
102 | ||
103 | def getY(self): | |
104 | if self._y is None: | |
105 | self.minY = self.maxY = self._y = self.dc.GetTextExtent("M")[1] | |
106 | return self._y | |
107 | def setY(self, value): | |
108 | self._y = y | |
109 | y = property(getY, setY) | |
110 | ||
111 | def startElement(self, name, attrs): | |
112 | method = "start_" + name | |
113 | if not hasattr(self, method): | |
114 | raise ValueError("XML tag '%s' not supported" % name) | |
115 | getattr(self, method)(attrs) | |
116 | ||
117 | def endElement(self, name): | |
118 | methname = "end_" + name | |
119 | if hasattr(self, methname): | |
120 | getattr(self, methname)() | |
121 | elif hasattr(self, "start_" + name): | |
122 | pass | |
123 | else: | |
124 | raise ValueError("XML tag '%s' not supported" % methname) | |
125 | ||
126 | def characterData(self, data): | |
127 | self.dc.SetFont(self.getCurrentFont()) | |
128 | for i, chunk in enumerate(data.split('\n')): | |
129 | if i: | |
130 | self.x = 0 | |
131 | self.y = self.mayY = self.maxY + self.dc.GetTextExtent("M")[1] | |
132 | if chunk: | |
133 | width, height, descent, extl = self.dc.GetFullTextExtent(chunk) | |
134 | self.renderCharacterData(data, iround(self.x), iround(self.y + self.offsets[-1] - height + descent)) | |
135 | else: | |
136 | width = height = descent = extl = 0 | |
137 | self.updateDims(width, height, descent, extl) | |
138 | ||
139 | def updateDims(self, width, height, descent, externalLeading): | |
140 | self.x += width | |
141 | self.width = max(self.x, self.width) | |
142 | self.minY = min(self.minY, self.y+self.offsets[-1]-height+descent) | |
143 | self.maxY = max(self.maxY, self.y+self.offsets[-1]+descent) | |
144 | self.height = self.maxY - self.minY | |
145 | ||
146 | def start_FancyText(self, attrs): | |
147 | pass | |
148 | start_wxFancyText = start_FancyText # For backward compatibility | |
149 | ||
150 | def start_font(self, attrs): | |
151 | for key, value in attrs.items(): | |
152 | if key == "size": | |
153 | value = int(value) | |
154 | elif key == "family": | |
155 | value = _families[value] | |
156 | elif key == "style": | |
157 | value = _styles[value] | |
158 | elif key == "weight": | |
159 | value = _weights[value] | |
160 | elif key == "encoding": | |
161 | value = int(value) | |
162 | elif key == "color": | |
163 | pass | |
164 | else: | |
165 | raise ValueError("unknown font attribute '%s'" % key) | |
166 | attrs[key] = value | |
167 | font = copy.copy(self.fonts[-1]) | |
168 | font.update(attrs) | |
169 | self.fonts.append(font) | |
170 | ||
171 | def end_font(self): | |
172 | self.fonts.pop() | |
173 | ||
174 | def start_sub(self, attrs): | |
175 | if attrs.keys(): | |
176 | raise ValueError("<sub> does not take attributes") | |
177 | font = self.getCurrentFont() | |
178 | self.offsets.append(self.offsets[-1] + self.dc.GetFullTextExtent("M", font)[1]*0.5) | |
179 | self.start_font({"size" : font.GetPointSize() * 0.8}) | |
180 | ||
181 | def end_sub(self): | |
182 | self.fonts.pop() | |
183 | self.offsets.pop() | |
184 | ||
185 | def start_sup(self, attrs): | |
186 | if attrs.keys(): | |
187 | raise ValueError("<sup> does not take attributes") | |
188 | font = self.getCurrentFont() | |
189 | self.offsets.append(self.offsets[-1] - self.dc.GetFullTextExtent("M", font)[1]*0.3) | |
190 | self.start_font({"size" : font.GetPointSize() * 0.8}) | |
191 | ||
192 | def end_sup(self): | |
193 | self.fonts.pop() | |
194 | self.offsets.pop() | |
195 | ||
196 | def getCurrentFont(self): | |
197 | font = self.fonts[-1] | |
198 | return wx.TheFontList.FindOrCreateFont(font.get("size", self.defaultSize), | |
199 | font.get("family", self.defaultFamily), | |
200 | font.get("style", self.defaultStyle), | |
201 | font.get("weight", self.defaultWeight), | |
202 | encoding = font.get("encoding", self.defaultEncoding)) | |
203 | ||
204 | def getCurrentColor(self): | |
205 | font = self.fonts[-1] | |
206 | return wx.TheColourDatabase.FindColour(font.get("color", self.defaultColor)) | |
207 | ||
208 | def getCurrentPen(self): | |
209 | return wx.ThePenList.FindOrCreatePen(self.getCurrentColor(), 1, wx.SOLID) | |
210 | ||
211 | def renderCharacterData(self, data, x, y): | |
212 | raise NotImplementedError() | |
213 | ||
214 | ||
215 | def _addGreek(): | |
216 | alpha = 0xE1 | |
217 | Alpha = 0xC1 | |
218 | def end(self): | |
219 | pass | |
220 | for i, name in enumerate(_greek_letters): | |
221 | def start(self, attrs, code=chr(alpha+i)): | |
222 | self.start_font({"encoding" : _greekEncoding}) | |
223 | self.characterData(code) | |
224 | self.end_font() | |
225 | setattr(Renderer, "start_%s" % name, start) | |
226 | setattr(Renderer, "end_%s" % name, end) | |
227 | if name == "altsigma": | |
228 | continue # There is no capital for altsigma | |
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.capitalize(), start) | |
234 | setattr(Renderer, "end_%s" % name.capitalize(), end) | |
235 | _addGreek() | |
236 | ||
237 | ||
238 | ||
239 | class SizeRenderer(Renderer): | |
240 | """Processes text as if rendering it, but just computes the size.""" | |
241 | ||
242 | def __init__(self, dc=None): | |
243 | Renderer.__init__(self, dc, 0, 0) | |
244 | ||
245 | def renderCharacterData(self, data, x, y): | |
246 | pass | |
247 | ||
248 | def start_angle(self, attrs): | |
249 | self.characterData("M") | |
250 | ||
251 | def start_infinity(self, attrs): | |
252 | width, height = self.dc.GetTextExtent("M") | |
253 | width = max(width, 10) | |
254 | height = max(height, width / 2) | |
255 | self.updateDims(width, height, 0, 0) | |
256 | ||
257 | def start_times(self, attrs): | |
258 | self.characterData("M") | |
259 | ||
260 | def start_in(self, attrs): | |
261 | self.characterData("M") | |
262 | ||
263 | def start_times(self, attrs): | |
264 | self.characterData("M") | |
265 | ||
266 | ||
267 | class DCRenderer(Renderer): | |
268 | """Renders text to a wxPython device context DC.""" | |
269 | ||
270 | def renderCharacterData(self, data, x, y): | |
271 | self.dc.SetTextForeground(self.getCurrentColor()) | |
272 | self.dc.DrawText(data, (x, y)) | |
273 | ||
274 | def start_angle(self, attrs): | |
275 | self.dc.SetFont(self.getCurrentFont()) | |
276 | self.dc.SetPen(self.getCurrentPen()) | |
277 | width, height, descent, leading = self.dc.GetFullTextExtent("M") | |
278 | y = self.y + self.offsets[-1] | |
279 | self.dc.DrawLine((iround(self.x), iround(y)), (iround( self.x+width), iround(y))) | |
280 | self.dc.DrawLine((iround(self.x), iround(y)), (iround(self.x+width), iround(y-width))) | |
281 | self.updateDims(width, height, descent, leading) | |
282 | ||
283 | ||
284 | def start_infinity(self, attrs): | |
285 | self.dc.SetFont(self.getCurrentFont()) | |
286 | self.dc.SetPen(self.getCurrentPen()) | |
287 | width, height, descent, leading = self.dc.GetFullTextExtent("M") | |
288 | width = max(width, 10) | |
289 | height = max(height, width / 2) | |
290 | self.dc.SetPen(wx.Pen(self.getCurrentColor(), max(1, width/10))) | |
291 | self.dc.SetBrush(wx.TRANSPARENT_BRUSH) | |
292 | y = self.y + self.offsets[-1] | |
293 | r = iround( 0.95 * width / 4) | |
294 | xc = (2*self.x + width) / 2 | |
295 | yc = iround(y-1.5*r) | |
296 | self.dc.DrawCircle((xc - r, yc), r) | |
297 | self.dc.DrawCircle((xc + r, yc), r) | |
298 | self.updateDims(width, height, 0, 0) | |
299 | ||
300 | def start_times(self, attrs): | |
301 | self.dc.SetFont(self.getCurrentFont()) | |
302 | self.dc.SetPen(self.getCurrentPen()) | |
303 | width, height, descent, leading = self.dc.GetFullTextExtent("M") | |
304 | y = self.y + self.offsets[-1] | |
305 | width *= 0.8 | |
306 | width = iround(width+.5) | |
307 | self.dc.SetPen(wx.Pen(self.getCurrentColor(), 1)) | |
308 | self.dc.DrawLine((iround(self.x), iround(y-width)), (iround(self.x+width-1), iround(y-1))) | |
309 | self.dc.DrawLine((iround(self.x), iround(y-2)), (iround(self.x+width-1), iround(y-width-1))) | |
310 | self.updateDims(width, height, 0, 0) | |
311 | ||
312 | ||
313 | def RenderToRenderer(str, renderer, enclose=True): | |
314 | try: | |
315 | if enclose: | |
316 | str = '<?xml version="1.0"?><FancyText>%s</FancyText>' % str | |
317 | p = xml.parsers.expat.ParserCreate() | |
318 | p.returns_unicode = 0 | |
319 | p.StartElementHandler = renderer.startElement | |
320 | p.EndElementHandler = renderer.endElement | |
321 | p.CharacterDataHandler = renderer.characterData | |
322 | p.Parse(str, 1) | |
323 | except xml.parsers.expat.error, err: | |
324 | raise ValueError('error parsing text text "%s": %s' % (str, err)) | |
325 | ||
326 | ||
327 | # Public interface | |
328 | ||
329 | ||
330 | def GetExtent(str, dc=None, enclose=True): | |
331 | "Return the extent of str" | |
332 | renderer = SizeRenderer(dc) | |
333 | RenderToRenderer(str, renderer, enclose) | |
334 | return iceil(renderer.width), iceil(renderer.height) # XXX round up | |
335 | ||
336 | ||
337 | def GetFullExtent(str, dc=None, enclose=True): | |
338 | renderer = SizeRenderer(dc) | |
339 | RenderToRenderer(str, renderer, enclose) | |
340 | return iceil(renderer.width), iceil(renderer.height), -iceil(renderer.minY) # XXX round up | |
341 | ||
342 | ||
343 | def RenderToBitmap(str, background=None, enclose=1): | |
344 | "Return str rendered on a minumum size bitmap" | |
345 | dc = wx.MemoryDC() | |
346 | width, height, dy = GetFullExtent(str, dc, enclose) | |
347 | bmp = wx.EmptyBitmap(width, height) | |
348 | dc.SelectObject(bmp) | |
349 | if background is None: | |
350 | dc.SetBackground(wx.WHITE_BRUSH) | |
351 | else: | |
352 | dc.SetBackground(background) | |
353 | dc.Clear() | |
354 | renderer = DCRenderer(dc, y=dy) | |
355 | dc.BeginDrawing() | |
356 | RenderToRenderer(str, renderer, enclose) | |
357 | dc.EndDrawing() | |
358 | dc.SelectObject(wx.NullBitmap) | |
359 | if background is None: | |
360 | img = wx.ImageFromBitmap(bmp) | |
361 | bg = dc.GetBackground().GetColour() | |
362 | img.SetMaskColour(bg.Red(), bg.Green(), bg.Blue()) | |
363 | bmp = img.ConvertToBitmap() | |
364 | return bmp | |
365 | ||
366 | ||
367 | def RenderToDC(str, dc, x, y, enclose=1): | |
368 | "Render str onto a wxDC at (x,y)" | |
369 | width, height, dy = GetFullExtent(str, dc) | |
370 | renderer = DCRenderer(dc, x, y+dy) | |
371 | RenderToRenderer(str, renderer, enclose) | |
372 | ||
373 | ||
374 | class StaticFancyText(wx.StaticBitmap): | |
375 | def __init__(self, window, id, text, *args, **kargs): | |
376 | args = list(args) | |
377 | kargs.setdefault('name', 'staticFancyText') | |
378 | if 'background' in kargs: | |
379 | background = kargs.pop('background') | |
380 | elif args: | |
381 | background = args.pop(0) | |
382 | else: | |
383 | background = wx.Brush(window.GetBackgroundColour(), wx.SOLID) | |
384 | ||
385 | bmp = RenderToBitmap(text, background) | |
386 | wx.StaticBitmap.__init__(self, window, id, bmp, *args, **kargs) | |
387 | ||
388 | ||
389 | # Old names for backward compatibiliry | |
390 | getExtent = GetExtent | |
391 | renderToBitmap = RenderToBitmap | |
392 | renderToDC = RenderToDC | |
393 | ||
394 | ||
395 | # Test Driver | |
396 | ||
397 | def test(): | |
398 | app = wx.PyApp() | |
399 | box = wx.BoxSizer(wx.VERTICAL) | |
400 | frame = wx.Frame(wx.NULL, -1, "FancyText demo", wx.DefaultPosition) | |
401 | frame.SetBackgroundColour("light grey") | |
402 | sft = StaticFancyText(frame, -1, __doc__) | |
403 | box.Add(sft, 1, wx.EXPAND) | |
404 | frame.SetSizer(box) | |
405 | frame.SetAutoLayout(True) | |
406 | box.Fit(frame) | |
407 | box.SetSizeHints(frame) | |
408 | frame.Show() | |
409 | app.MainLoop() | |
410 | ||
411 | if __name__ == "__main__": | |
412 | test() | |
413 | ||
414 |