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