]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/fancytext.py
Add GetHDC back
[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 """<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