| 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 | |