Added 'description' class attribute to every command class (to help the
[python/dscho.git] / Demo / tkinter / www / fmt.py
blobb5ca33c77f03db7c8ce138e001ce543f282e8672
1 # Text formatting abstractions
4 import string
5 import Para
8 # A formatter back-end object has one method that is called by the formatter:
9 # addpara(p), where p is a paragraph object. For example:
12 # Formatter back-end to do nothing at all with the paragraphs
13 class NullBackEnd:
15 def __init__(self):
16 pass
18 def addpara(self, p):
19 pass
21 def bgn_anchor(self, id):
22 pass
24 def end_anchor(self, id):
25 pass
28 # Formatter back-end to collect the paragraphs in a list
29 class SavingBackEnd(NullBackEnd):
31 def __init__(self):
32 self.paralist = []
34 def addpara(self, p):
35 self.paralist.append(p)
37 def hitcheck(self, h, v):
38 hits = []
39 for p in self.paralist:
40 if p.top <= v <= p.bottom:
41 for id in p.hitcheck(h, v):
42 if id not in hits:
43 hits.append(id)
44 return hits
46 def extract(self):
47 text = ''
48 for p in self.paralist:
49 text = text + (p.extract())
50 return text
52 def extractpart(self, long1, long2):
53 if long1 > long2: long1, long2 = long2, long1
54 para1, pos1 = long1
55 para2, pos2 = long2
56 text = ''
57 while para1 < para2:
58 ptext = self.paralist[para1].extract()
59 text = text + ptext[pos1:]
60 pos1 = 0
61 para1 = para1 + 1
62 ptext = self.paralist[para2].extract()
63 return text + ptext[pos1:pos2]
65 def whereis(self, d, h, v):
66 total = 0
67 for i in range(len(self.paralist)):
68 p = self.paralist[i]
69 result = p.whereis(d, h, v)
70 if result <> None:
71 return i, result
72 return None
74 def roundtowords(self, long1, long2):
75 i, offset = long1
76 text = self.paralist[i].extract()
77 while offset > 0 and text[offset-1] <> ' ': offset = offset-1
78 long1 = i, offset
80 i, offset = long2
81 text = self.paralist[i].extract()
82 n = len(text)
83 while offset < n-1 and text[offset] <> ' ': offset = offset+1
84 long2 = i, offset
86 return long1, long2
88 def roundtoparagraphs(self, long1, long2):
89 long1 = long1[0], 0
90 long2 = long2[0], len(self.paralist[long2[0]].extract())
91 return long1, long2
94 # Formatter back-end to send the text directly to the drawing object
95 class WritingBackEnd(NullBackEnd):
97 def __init__(self, d, width):
98 self.d = d
99 self.width = width
100 self.lineno = 0
102 def addpara(self, p):
103 self.lineno = p.render(self.d, 0, self.lineno, self.width)
106 # A formatter receives a stream of formatting instructions and assembles
107 # these into a stream of paragraphs on to a back-end. The assembly is
108 # parametrized by a text measurement object, which must match the output
109 # operations of the back-end. The back-end is responsible for splitting
110 # paragraphs up in lines of a given maximum width. (This is done because
111 # in a windowing environment, when the window size changes, there is no
112 # need to redo the assembly into paragraphs, but the splitting into lines
113 # must be done taking the new window size into account.)
116 # Formatter base class. Initialize it with a text measurement object,
117 # which is used for text measurements, and a back-end object,
118 # which receives the completed paragraphs. The formatting methods are:
119 # setfont(font)
120 # setleftindent(nspaces)
121 # setjust(type) where type is 'l', 'c', 'r', or 'lr'
122 # flush()
123 # vspace(nlines)
124 # needvspace(nlines)
125 # addword(word, nspaces)
126 class BaseFormatter:
128 def __init__(self, d, b):
129 # Drawing object used for text measurements
130 self.d = d
132 # BackEnd object receiving completed paragraphs
133 self.b = b
135 # Parameters of the formatting model
136 self.leftindent = 0
137 self.just = 'l'
138 self.font = None
139 self.blanklines = 0
141 # Parameters derived from the current font
142 self.space = d.textwidth(' ')
143 self.line = d.lineheight()
144 self.ascent = d.baseline()
145 self.descent = self.line - self.ascent
147 # Parameter derived from the default font
148 self.n_space = self.space
150 # Current paragraph being built
151 self.para = None
152 self.nospace = 1
154 # Font to set on the next word
155 self.nextfont = None
157 def newpara(self):
158 return Para.Para()
160 def setfont(self, font):
161 if font == None: return
162 self.font = self.nextfont = font
163 d = self.d
164 d.setfont(font)
165 self.space = d.textwidth(' ')
166 self.line = d.lineheight()
167 self.ascent = d.baseline()
168 self.descent = self.line - self.ascent
170 def setleftindent(self, nspaces):
171 self.leftindent = int(self.n_space * nspaces)
172 if self.para:
173 hang = self.leftindent - self.para.indent_left
174 if hang > 0 and self.para.getlength() <= hang:
175 self.para.makehangingtag(hang)
176 self.nospace = 1
177 else:
178 self.flush()
180 def setrightindent(self, nspaces):
181 self.rightindent = int(self.n_space * nspaces)
182 if self.para:
183 self.para.indent_right = self.rightindent
184 self.flush()
186 def setjust(self, just):
187 self.just = just
188 if self.para:
189 self.para.just = self.just
191 def flush(self):
192 if self.para:
193 self.b.addpara(self.para)
194 self.para = None
195 if self.font <> None:
196 self.d.setfont(self.font)
197 self.nospace = 1
199 def vspace(self, nlines):
200 self.flush()
201 if nlines > 0:
202 self.para = self.newpara()
203 tuple = None, '', 0, 0, 0, int(nlines*self.line), 0
204 self.para.words.append(tuple)
205 self.flush()
206 self.blanklines = self.blanklines + nlines
208 def needvspace(self, nlines):
209 self.flush() # Just to be sure
210 if nlines > self.blanklines:
211 self.vspace(nlines - self.blanklines)
213 def addword(self, text, space):
214 if self.nospace and not text:
215 return
216 self.nospace = 0
217 self.blanklines = 0
218 if not self.para:
219 self.para = self.newpara()
220 self.para.indent_left = self.leftindent
221 self.para.just = self.just
222 self.nextfont = self.font
223 space = int(space * self.space)
224 self.para.words.append(self.nextfont, text, \
225 self.d.textwidth(text), space, space, \
226 self.ascent, self.descent)
227 self.nextfont = None
229 def bgn_anchor(self, id):
230 if not self.para:
231 self.nospace = 0
232 self.addword('', 0)
233 self.para.bgn_anchor(id)
235 def end_anchor(self, id):
236 if not self.para:
237 self.nospace = 0
238 self.addword('', 0)
239 self.para.end_anchor(id)
241 def hrule(self):
242 # Typically need to override this for bit-mapped displays
243 self.flush()
244 self.addword('-'*60, 0)
245 self.flush()
248 # Measuring object for measuring text as viewed on a tty
249 class NullMeasurer:
251 def __init__(self):
252 pass
254 def setfont(self, font):
255 pass
257 def textwidth(self, text):
258 return len(text)
260 def lineheight(self):
261 return 1
263 def baseline(self):
264 return 0
267 # Drawing object for writing plain ASCII text to a file
268 class FileWriter:
270 def __init__(self, fp):
271 self.fp = fp
272 self.lineno, self.colno = 0, 0
274 def setfont(self, font):
275 pass
277 def text(self, (h, v), str):
278 if not str: return
279 if '\n' in str:
280 raise ValueError, 'can\'t write \\n'
281 while self.lineno < v:
282 self.fp.write('\n')
283 self.colno, self.lineno = 0, self.lineno + 1
284 while self.lineno > v:
285 # XXX This should never happen...
286 self.fp.write('\033[A') # ANSI up arrow
287 self.lineno = self.lineno - 1
288 if self.colno < h:
289 self.fp.write(' ' * (h - self.colno))
290 elif self.colno > h:
291 self.fp.write('\b' * (self.colno - h))
292 self.colno = h
293 self.fp.write(str)
294 self.colno = h + len(str)
297 # Formatting class to do nothing at all with the data
298 class NullFormatter(BaseFormatter):
300 def __init__(self):
301 d = NullMeasurer()
302 b = NullBackEnd()
303 BaseFormatter.__init__(self, d, b)
306 # Formatting class to write directly to a file
307 class WritingFormatter(BaseFormatter):
309 def __init__(self, fp, width):
310 dm = NullMeasurer()
311 dw = FileWriter(fp)
312 b = WritingBackEnd(dw, width)
313 BaseFormatter.__init__(self, dm, b)
314 self.blanklines = 1
316 # Suppress multiple blank lines
317 def needvspace(self, nlines):
318 BaseFormatter.needvspace(self, min(1, nlines))
321 # A "FunnyFormatter" writes ASCII text with a twist: *bold words*,
322 # _italic text_ and _underlined words_, and `quoted text'.
323 # It assumes that the fonts are 'r', 'i', 'b', 'u', 'q': (roman,
324 # italic, bold, underline, quote).
325 # Moreover, if the font is in upper case, the text is converted to
326 # UPPER CASE.
327 class FunnyFormatter(WritingFormatter):
329 def flush(self):
330 if self.para: finalize(self.para)
331 WritingFormatter.flush(self)
334 # Surrounds *bold words* and _italic text_ in a paragraph with
335 # appropriate markers, fixing the size (assuming these characters'
336 # width is 1).
337 openchar = \
338 {'b':'*', 'i':'_', 'u':'_', 'q':'`', 'B':'*', 'I':'_', 'U':'_', 'Q':'`'}
339 closechar = \
340 {'b':'*', 'i':'_', 'u':'_', 'q':'\'', 'B':'*', 'I':'_', 'U':'_', 'Q':'\''}
341 def finalize(para):
342 oldfont = curfont = 'r'
343 para.words.append('r', '', 0, 0, 0, 0) # temporary, deleted at end
344 for i in range(len(para.words)):
345 fo, te, wi = para.words[i][:3]
346 if fo <> None: curfont = fo
347 if curfont <> oldfont:
348 if closechar.has_key(oldfont):
349 c = closechar[oldfont]
350 j = i-1
351 while j > 0 and para.words[j][1] == '': j = j-1
352 fo1, te1, wi1 = para.words[j][:3]
353 te1 = te1 + c
354 wi1 = wi1 + len(c)
355 para.words[j] = (fo1, te1, wi1) + \
356 para.words[j][3:]
357 if openchar.has_key(curfont) and te:
358 c = openchar[curfont]
359 te = c + te
360 wi = len(c) + wi
361 para.words[i] = (fo, te, wi) + \
362 para.words[i][3:]
363 if te: oldfont = curfont
364 else: oldfont = 'r'
365 if curfont in string.uppercase:
366 te = string.upper(te)
367 para.words[i] = (fo, te, wi) + para.words[i][3:]
368 del para.words[-1]
371 # Formatter back-end to draw the text in a window.
372 # This has an option to draw while the paragraphs are being added,
373 # to minimize the delay before the user sees anything.
374 # This manages the entire "document" of the window.
375 class StdwinBackEnd(SavingBackEnd):
377 def __init__(self, window, drawnow):
378 self.window = window
379 self.drawnow = drawnow
380 self.width = window.getwinsize()[0]
381 self.selection = None
382 self.height = 0
383 window.setorigin(0, 0)
384 window.setdocsize(0, 0)
385 self.d = window.begindrawing()
386 SavingBackEnd.__init__(self)
388 def finish(self):
389 self.d.close()
390 self.d = None
391 self.window.setdocsize(0, self.height)
393 def addpara(self, p):
394 self.paralist.append(p)
395 if self.drawnow:
396 self.height = \
397 p.render(self.d, 0, self.height, self.width)
398 else:
399 p.layout(self.width)
400 p.left = 0
401 p.top = self.height
402 p.right = self.width
403 p.bottom = self.height + p.height
404 self.height = p.bottom
406 def resize(self):
407 self.window.change((0, 0), (self.width, self.height))
408 self.width = self.window.getwinsize()[0]
409 self.height = 0
410 for p in self.paralist:
411 p.layout(self.width)
412 p.left = 0
413 p.top = self.height
414 p.right = self.width
415 p.bottom = self.height + p.height
416 self.height = p.bottom
417 self.window.change((0, 0), (self.width, self.height))
418 self.window.setdocsize(0, self.height)
420 def redraw(self, area):
421 d = self.window.begindrawing()
422 (left, top), (right, bottom) = area
423 d.erase(area)
424 d.cliprect(area)
425 for p in self.paralist:
426 if top < p.bottom and p.top < bottom:
427 v = p.render(d, p.left, p.top, p.right)
428 if self.selection:
429 self.invert(d, self.selection)
430 d.close()
432 def setselection(self, new):
433 if new:
434 long1, long2 = new
435 pos1 = long1[:3]
436 pos2 = long2[:3]
437 new = pos1, pos2
438 if new <> self.selection:
439 d = self.window.begindrawing()
440 if self.selection:
441 self.invert(d, self.selection)
442 if new:
443 self.invert(d, new)
444 d.close()
445 self.selection = new
447 def getselection(self):
448 return self.selection
450 def extractselection(self):
451 if self.selection:
452 a, b = self.selection
453 return self.extractpart(a, b)
454 else:
455 return None
457 def invert(self, d, region):
458 long1, long2 = region
459 if long1 > long2: long1, long2 = long2, long1
460 para1, pos1 = long1
461 para2, pos2 = long2
462 while para1 < para2:
463 self.paralist[para1].invert(d, pos1, None)
464 pos1 = None
465 para1 = para1 + 1
466 self.paralist[para2].invert(d, pos1, pos2)
468 def search(self, prog):
469 import regex, string
470 if type(prog) == type(''):
471 prog = regex.compile(string.lower(prog))
472 if self.selection:
473 iold = self.selection[0][0]
474 else:
475 iold = -1
476 hit = None
477 for i in range(len(self.paralist)):
478 if i == iold or i < iold and hit:
479 continue
480 p = self.paralist[i]
481 text = string.lower(p.extract())
482 if prog.search(text) >= 0:
483 a, b = prog.regs[0]
484 long1 = i, a
485 long2 = i, b
486 hit = long1, long2
487 if i > iold:
488 break
489 if hit:
490 self.setselection(hit)
491 i = hit[0][0]
492 p = self.paralist[i]
493 self.window.show((p.left, p.top), (p.right, p.bottom))
494 return 1
495 else:
496 return 0
498 def showanchor(self, id):
499 for i in range(len(self.paralist)):
500 p = self.paralist[i]
501 if p.hasanchor(id):
502 long1 = i, 0
503 long2 = i, len(p.extract())
504 hit = long1, long2
505 self.setselection(hit)
506 self.window.show( \
507 (p.left, p.top), (p.right, p.bottom))
508 break
511 # GL extensions
513 class GLFontCache:
515 def __init__(self):
516 self.reset()
517 self.setfont('')
519 def reset(self):
520 self.fontkey = None
521 self.fonthandle = None
522 self.fontinfo = None
523 self.fontcache = {}
525 def close(self):
526 self.reset()
528 def setfont(self, fontkey):
529 if fontkey == '':
530 fontkey = 'Times-Roman 12'
531 elif ' ' not in fontkey:
532 fontkey = fontkey + ' 12'
533 if fontkey == self.fontkey:
534 return
535 if self.fontcache.has_key(fontkey):
536 handle = self.fontcache[fontkey]
537 else:
538 import string
539 i = string.index(fontkey, ' ')
540 name, sizestr = fontkey[:i], fontkey[i:]
541 size = eval(sizestr)
542 key1 = name + ' 1'
543 key = name + ' ' + `size`
544 # NB key may differ from fontkey!
545 if self.fontcache.has_key(key):
546 handle = self.fontcache[key]
547 else:
548 if self.fontcache.has_key(key1):
549 handle = self.fontcache[key1]
550 else:
551 import fm
552 handle = fm.findfont(name)
553 self.fontcache[key1] = handle
554 handle = handle.scalefont(size)
555 self.fontcache[fontkey] = \
556 self.fontcache[key] = handle
557 self.fontkey = fontkey
558 if self.fonthandle <> handle:
559 self.fonthandle = handle
560 self.fontinfo = handle.getfontinfo()
561 handle.setfont()
564 class GLMeasurer(GLFontCache):
566 def textwidth(self, text):
567 return self.fonthandle.getstrwidth(text)
569 def baseline(self):
570 return self.fontinfo[6] - self.fontinfo[3]
572 def lineheight(self):
573 return self.fontinfo[6]
576 class GLWriter(GLFontCache):
578 # NOTES:
579 # (1) Use gl.ortho2 to use X pixel coordinates!
581 def text(self, (h, v), text):
582 import gl, fm
583 gl.cmov2i(h, v + self.fontinfo[6] - self.fontinfo[3])
584 fm.prstr(text)
586 def setfont(self, fontkey):
587 oldhandle = self.fonthandle
588 GLFontCache.setfont(fontkey)
589 if self.fonthandle <> oldhandle:
590 handle.setfont()
593 class GLMeasurerWriter(GLMeasurer, GLWriter):
594 pass
597 class GLBackEnd(SavingBackEnd):
599 def __init__(self, wid):
600 import gl
601 gl.winset(wid)
602 self.wid = wid
603 self.width = gl.getsize()[1]
604 self.height = 0
605 self.d = GLMeasurerWriter()
606 SavingBackEnd.__init__(self)
608 def finish(self):
609 pass
611 def addpara(self, p):
612 self.paralist.append(p)
613 self.height = p.render(self.d, 0, self.height, self.width)
615 def redraw(self):
616 import gl
617 gl.winset(self.wid)
618 width = gl.getsize()[1]
619 if width <> self.width:
620 setdocsize = 1
621 self.width = width
622 for p in self.paralist:
623 p.top = p.bottom = None
624 d = self.d
625 v = 0
626 for p in self.paralist:
627 v = p.render(d, 0, v, width)