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