blob: c0963069e0198472036a687d77ff325a474b1226 [file] [log] [blame]
Guido van Rossum7c750e11995-02-27 13:16:55 +00001# Text formatting abstractions
2
3
4import string
5import Para
6
7
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:
10
11
12# Formatter back-end to do nothing at all with the paragraphs
13class NullBackEnd:
14 #
15 def __init__(self):
16 pass
17 #
18 def addpara(self, p):
19 pass
20 #
21 def bgn_anchor(self, id):
22 pass
23 #
24 def end_anchor(self, id):
25 pass
26
27
28# Formatter back-end to collect the paragraphs in a list
29class SavingBackEnd(NullBackEnd):
30 #
31 def __init__(self):
32 self.paralist = []
33 #
34 def addpara(self, p):
35 self.paralist.append(p)
36 #
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
45 #
46 def extract(self):
47 text = ''
48 for p in self.paralist:
49 text = text + (p.extract())
50 return text
51 #
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]
64 #
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
73 #
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
79 #
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
85 #
86 return long1, long2
87 #
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
92
93
94# Formatter back-end to send the text directly to the drawing object
95class WritingBackEnd(NullBackEnd):
96 #
97 def __init__(self, d, width):
98 self.d = d
99 self.width = width
100 self.lineno = 0
101 #
102 def addpara(self, p):
103 self.lineno = p.render(self.d, 0, self.lineno, self.width)
104
105
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.)
114
115
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)
126class BaseFormatter:
127 #
128 def __init__(self, d, b):
129 # Drawing object used for text measurements
130 self.d = d
131 #
132 # BackEnd object receiving completed paragraphs
133 self.b = b
134 #
135 # Parameters of the formatting model
136 self.leftindent = 0
137 self.just = 'l'
138 self.font = None
139 self.blanklines = 0
140 #
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
146 #
147 # Parameter derived from the default font
148 self.n_space = self.space
149 #
150 # Current paragraph being built
151 self.para = None
152 self.nospace = 1
153 #
154 # Font to set on the next word
155 self.nextfont = None
156 #
157 def newpara(self):
158 return Para.Para()
159 #
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
169 #
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()
179 #
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()
185 #
186 def setjust(self, just):
187 self.just = just
188 if self.para:
189 self.para.just = self.just
190 #
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
198 #
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
207 #
208 def needvspace(self, nlines):
209 self.flush() # Just to be sure
210 if nlines > self.blanklines:
211 self.vspace(nlines - self.blanklines)
212 #
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
228 #
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)
234 #
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)
240
241
242# Measuring object for measuring text as viewed on a tty
243class NullMeasurer:
244 #
245 def __init__(self):
246 pass
247 #
248 def setfont(self, font):
249 pass
250 #
251 def textwidth(self, text):
252 return len(text)
253 #
254 def lineheight(self):
255 return 1
256 #
257 def baseline(self):
258 return 0
259
260
261# Drawing object for writing plain ASCII text to a file
262class FileWriter:
263 #
264 def __init__(self, fp):
265 self.fp = fp
266 self.lineno, self.colno = 0, 0
267 #
268 def setfont(self, font):
269 pass
270 #
271 def text(self, (h, v), str):
272 if not str: return
273 if '\n' in str:
274 raise ValueError, 'can\'t write \\n'
275 while self.lineno < v:
276 self.fp.write('\n')
277 self.colno, self.lineno = 0, self.lineno + 1
278 while self.lineno > v:
279 # XXX This should never happen...
280 self.fp.write('\033[A') # ANSI up arrow
281 self.lineno = self.lineno - 1
282 if self.colno < h:
283 self.fp.write(' ' * (h - self.colno))
284 elif self.colno > h:
285 self.fp.write('\b' * (self.colno - h))
286 self.colno = h
287 self.fp.write(str)
288 self.colno = h + len(str)
289
290
291# Formatting class to do nothing at all with the data
292class NullFormatter(BaseFormatter):
293 #
294 def __init__(self):
295 d = NullMeasurer()
296 b = NullBackEnd()
297 BaseFormatter.__init__(self, d, b)
298
299
300# Formatting class to write directly to a file
301class WritingFormatter(BaseFormatter):
302 #
303 def __init__(self, fp, width):
304 dm = NullMeasurer()
305 dw = FileWriter(fp)
306 b = WritingBackEnd(dw, width)
307 BaseFormatter.__init__(self, dm, b)
308 self.blanklines = 1
309 #
310 # Suppress multiple blank lines
311 def needvspace(self, nlines):
312 BaseFormatter.needvspace(self, min(1, nlines))
313
314
315# A "FunnyFormatter" writes ASCII text with a twist: *bold words*,
316# _italic text_ and _underlined words_, and `quoted text'.
317# It assumes that the fonts are 'r', 'i', 'b', 'u', 'q': (roman,
318# italic, bold, underline, quote).
319# Moreover, if the font is in upper case, the text is converted to
320# UPPER CASE.
321class FunnyFormatter(WritingFormatter):
322 #
323 def flush(self):
324 if self.para: finalize(self.para)
325 WritingFormatter.flush(self)
326
327
328# Surrounds *bold words* and _italic text_ in a paragraph with
329# appropriate markers, fixing the size (assuming these characters'
330# width is 1).
331openchar = \
332 {'b':'*', 'i':'_', 'u':'_', 'q':'`', 'B':'*', 'I':'_', 'U':'_', 'Q':'`'}
333closechar = \
334 {'b':'*', 'i':'_', 'u':'_', 'q':'\'', 'B':'*', 'I':'_', 'U':'_', 'Q':'\''}
335def finalize(para):
336 oldfont = curfont = 'r'
337 para.words.append('r', '', 0, 0, 0, 0) # temporary, deleted at end
338 for i in range(len(para.words)):
339 fo, te, wi = para.words[i][:3]
340 if fo <> None: curfont = fo
341 if curfont <> oldfont:
342 if closechar.has_key(oldfont):
343 c = closechar[oldfont]
344 j = i-1
345 while j > 0 and para.words[j][1] == '': j = j-1
346 fo1, te1, wi1 = para.words[j][:3]
347 te1 = te1 + c
348 wi1 = wi1 + len(c)
349 para.words[j] = (fo1, te1, wi1) + \
350 para.words[j][3:]
351 if openchar.has_key(curfont) and te:
352 c = openchar[curfont]
353 te = c + te
354 wi = len(c) + wi
355 para.words[i] = (fo, te, wi) + \
356 para.words[i][3:]
357 if te: oldfont = curfont
358 else: oldfont = 'r'
359 if curfont in string.uppercase:
360 te = string.upper(te)
361 para.words[i] = (fo, te, wi) + para.words[i][3:]
362 del para.words[-1]
363
364
365# Formatter back-end to draw the text in a window.
366# This has an option to draw while the paragraphs are being added,
367# to minimize the delay before the user sees anything.
368# This manages the entire "document" of the window.
369class StdwinBackEnd(SavingBackEnd):
370 #
371 def __init__(self, window, drawnow):
372 self.window = window
373 self.drawnow = drawnow
374 self.width = window.getwinsize()[0]
375 self.selection = None
376 self.height = 0
377 window.setorigin(0, 0)
378 window.setdocsize(0, 0)
379 self.d = window.begindrawing()
380 SavingBackEnd.__init__(self)
381 #
382 def finish(self):
383 self.d.close()
384 self.d = None
385 self.window.setdocsize(0, self.height)
386 #
387 def addpara(self, p):
388 self.paralist.append(p)
389 if self.drawnow:
390 self.height = \
391 p.render(self.d, 0, self.height, self.width)
392 else:
393 p.layout(self.width)
394 p.left = 0
395 p.top = self.height
396 p.right = self.width
397 p.bottom = self.height + p.height
398 self.height = p.bottom
399 #
400 def resize(self):
401 self.window.change((0, 0), (self.width, self.height))
402 self.width = self.window.getwinsize()[0]
403 self.height = 0
404 for p in self.paralist:
405 p.layout(self.width)
406 p.left = 0
407 p.top = self.height
408 p.right = self.width
409 p.bottom = self.height + p.height
410 self.height = p.bottom
411 self.window.change((0, 0), (self.width, self.height))
412 self.window.setdocsize(0, self.height)
413 #
414 def redraw(self, area):
415 d = self.window.begindrawing()
416 (left, top), (right, bottom) = area
417 d.erase(area)
418 d.cliprect(area)
419 for p in self.paralist:
420 if top < p.bottom and p.top < bottom:
421 v = p.render(d, p.left, p.top, p.right)
422 if self.selection:
423 self.invert(d, self.selection)
424 d.close()
425 #
426 def setselection(self, new):
427 if new:
428 long1, long2 = new
429 pos1 = long1[:3]
430 pos2 = long2[:3]
431 new = pos1, pos2
432 if new <> self.selection:
433 d = self.window.begindrawing()
434 if self.selection:
435 self.invert(d, self.selection)
436 if new:
437 self.invert(d, new)
438 d.close()
439 self.selection = new
440 #
441 def getselection(self):
442 return self.selection
443 #
444 def extractselection(self):
445 if self.selection:
446 a, b = self.selection
447 return self.extractpart(a, b)
448 else:
449 return None
450 #
451 def invert(self, d, region):
452 long1, long2 = region
453 if long1 > long2: long1, long2 = long2, long1
454 para1, pos1 = long1
455 para2, pos2 = long2
456 while para1 < para2:
457 self.paralist[para1].invert(d, pos1, None)
458 pos1 = None
459 para1 = para1 + 1
460 self.paralist[para2].invert(d, pos1, pos2)
461 #
462 def search(self, prog):
463 import regex, string
464 if type(prog) == type(''):
465 prog = regex.compile(string.lower(prog))
466 if self.selection:
467 iold = self.selection[0][0]
468 else:
469 iold = -1
470 hit = None
471 for i in range(len(self.paralist)):
472 if i == iold or i < iold and hit:
473 continue
474 p = self.paralist[i]
475 text = string.lower(p.extract())
476 if prog.search(text) >= 0:
477 a, b = prog.regs[0]
478 long1 = i, a
479 long2 = i, b
480 hit = long1, long2
481 if i > iold:
482 break
483 if hit:
484 self.setselection(hit)
485 i = hit[0][0]
486 p = self.paralist[i]
487 self.window.show((p.left, p.top), (p.right, p.bottom))
488 return 1
489 else:
490 return 0
491 #
492 def showanchor(self, id):
493 for i in range(len(self.paralist)):
494 p = self.paralist[i]
495 if p.hasanchor(id):
496 long1 = i, 0
497 long2 = i, len(p.extract())
498 hit = long1, long2
499 self.setselection(hit)
500 self.window.show( \
501 (p.left, p.top), (p.right, p.bottom))
502 break
503
504
505# GL extensions
506
507class GLFontCache:
508 #
509 def __init__(self):
510 self.reset()
511 self.setfont('')
512 #
513 def reset(self):
514 self.fontkey = None
515 self.fonthandle = None
516 self.fontinfo = None
517 self.fontcache = {}
518 #
519 def close(self):
520 self.reset()
521 #
522 def setfont(self, fontkey):
523 if fontkey == '':
524 fontkey = 'Times-Roman 12'
525 elif ' ' not in fontkey:
526 fontkey = fontkey + ' 12'
527 if fontkey == self.fontkey:
528 return
529 if self.fontcache.has_key(fontkey):
530 handle = self.fontcache[fontkey]
531 else:
532 import string
533 i = string.index(fontkey, ' ')
534 name, sizestr = fontkey[:i], fontkey[i:]
535 size = eval(sizestr)
536 key1 = name + ' 1'
537 key = name + ' ' + `size`
538 # NB key may differ from fontkey!
539 if self.fontcache.has_key(key):
540 handle = self.fontcache[key]
541 else:
542 if self.fontcache.has_key(key1):
543 handle = self.fontcache[key1]
544 else:
545 import fm
546 handle = fm.findfont(name)
547 self.fontcache[key1] = handle
548 handle = handle.scalefont(size)
549 self.fontcache[fontkey] = \
550 self.fontcache[key] = handle
551 self.fontkey = fontkey
552 if self.fonthandle <> handle:
553 self.fonthandle = handle
554 self.fontinfo = handle.getfontinfo()
555 handle.setfont()
556
557
558class GLMeasurer(GLFontCache):
559 #
560 def textwidth(self, text):
561 return self.fonthandle.getstrwidth(text)
562 #
563 def baseline(self):
564 return self.fontinfo[6] - self.fontinfo[3]
565 #
566 def lineheight(self):
567 return self.fontinfo[6]
568
569
570class GLWriter(GLFontCache):
571 #
572 # NOTES:
573 # (1) Use gl.ortho2 to use X pixel coordinates!
574 #
575 def text(self, (h, v), text):
576 import gl, fm
577 gl.cmov2i(h, v + self.fontinfo[6] - self.fontinfo[3])
578 fm.prstr(text)
579 #
580 def setfont(self, fontkey):
581 oldhandle = self.fonthandle
582 GLFontCache.setfont(fontkey)
583 if self.fonthandle <> oldhandle:
584 handle.setfont()
585
586
587class GLMeasurerWriter(GLMeasurer, GLWriter):
588 pass
589
590
591class GLBackEnd(SavingBackEnd):
592 #
593 def __init__(self, wid):
594 import gl
595 gl.winset(wid)
596 self.wid = wid
597 self.width = gl.getsize()[1]
598 self.height = 0
599 self.d = GLMeasurerWriter()
600 SavingBackEnd.__init__(self)
601 #
602 def finish(self):
603 pass
604 #
605 def addpara(self, p):
606 self.paralist.append(p)
607 self.height = p.render(self.d, 0, self.height, self.width)
608 #
609 def redraw(self):
610 import gl
611 gl.winset(self.wid)
612 width = gl.getsize()[1]
613 if width <> self.width:
614 setdocsize = 1
615 self.width = width
616 for p in self.paralist:
617 p.top = p.bottom = None
618 d = self.d
619 v = 0
620 for p in self.paralist:
621 v = p.render(d, 0, v, width)