Commit | Line | Data |
---|---|---|
920dae64 AT |
1 | # Text formatting abstractions |
2 | # Note -- this module is obsolete, it's too slow anyway | |
3 | ||
4 | ||
5 | import string | |
6 | import 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 | |
14 | class 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 | |
30 | class 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 is not 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 | |
96 | class 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) | |
127 | class 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 is 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 is not 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 | |
244 | class 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 | |
263 | class 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 | |
293 | class 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 | |
302 | class 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. | |
322 | class 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). | |
332 | openchar = \ | |
333 | {'b':'*', 'i':'_', 'u':'_', 'q':'`', 'B':'*', 'I':'_', 'U':'_', 'Q':'`'} | |
334 | closechar = \ | |
335 | {'b':'*', 'i':'_', 'u':'_', 'q':'\'', 'B':'*', 'I':'_', 'U':'_', 'Q':'\''} | |
336 | def finalize(para): | |
337 | oldfont = curfont = 'r' | |
338 | para.words.append(('r', '', 0, 0, 0, 0)) # temporary, deleted at end | |
339 | for i in range(len(para.words)): | |
340 | fo, te, wi = para.words[i][:3] | |
341 | if fo is not 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. | |
370 | class 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 re, string | |
465 | if type(prog) is type(''): | |
466 | prog = re.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 | match = prog.search(text) | |
478 | if match: | |
479 | a, b = match.group(0) | |
480 | long1 = i, a | |
481 | long2 = i, b | |
482 | hit = long1, long2 | |
483 | if i > iold: | |
484 | break | |
485 | if hit: | |
486 | self.setselection(hit) | |
487 | i = hit[0][0] | |
488 | p = self.paralist[i] | |
489 | self.window.show((p.left, p.top), (p.right, p.bottom)) | |
490 | return 1 | |
491 | else: | |
492 | return 0 | |
493 | # | |
494 | def showanchor(self, id): | |
495 | for i in range(len(self.paralist)): | |
496 | p = self.paralist[i] | |
497 | if p.hasanchor(id): | |
498 | long1 = i, 0 | |
499 | long2 = i, len(p.extract()) | |
500 | hit = long1, long2 | |
501 | self.setselection(hit) | |
502 | self.window.show( | |
503 | (p.left, p.top), (p.right, p.bottom)) | |
504 | break | |
505 | ||
506 | ||
507 | # GL extensions | |
508 | ||
509 | class GLFontCache: | |
510 | # | |
511 | def __init__(self): | |
512 | self.reset() | |
513 | self.setfont('') | |
514 | # | |
515 | def reset(self): | |
516 | self.fontkey = None | |
517 | self.fonthandle = None | |
518 | self.fontinfo = None | |
519 | self.fontcache = {} | |
520 | # | |
521 | def close(self): | |
522 | self.reset() | |
523 | # | |
524 | def setfont(self, fontkey): | |
525 | if fontkey == '': | |
526 | fontkey = 'Times-Roman 12' | |
527 | elif ' ' not in fontkey: | |
528 | fontkey = fontkey + ' 12' | |
529 | if fontkey == self.fontkey: | |
530 | return | |
531 | if self.fontcache.has_key(fontkey): | |
532 | handle = self.fontcache[fontkey] | |
533 | else: | |
534 | import string | |
535 | i = string.index(fontkey, ' ') | |
536 | name, sizestr = fontkey[:i], fontkey[i:] | |
537 | size = eval(sizestr) | |
538 | key1 = name + ' 1' | |
539 | key = name + ' ' + `size` | |
540 | # NB key may differ from fontkey! | |
541 | if self.fontcache.has_key(key): | |
542 | handle = self.fontcache[key] | |
543 | else: | |
544 | if self.fontcache.has_key(key1): | |
545 | handle = self.fontcache[key1] | |
546 | else: | |
547 | import fm | |
548 | handle = fm.findfont(name) | |
549 | self.fontcache[key1] = handle | |
550 | handle = handle.scalefont(size) | |
551 | self.fontcache[fontkey] = \ | |
552 | self.fontcache[key] = handle | |
553 | self.fontkey = fontkey | |
554 | if self.fonthandle != handle: | |
555 | self.fonthandle = handle | |
556 | self.fontinfo = handle.getfontinfo() | |
557 | handle.setfont() | |
558 | ||
559 | ||
560 | class GLMeasurer(GLFontCache): | |
561 | # | |
562 | def textwidth(self, text): | |
563 | return self.fonthandle.getstrwidth(text) | |
564 | # | |
565 | def baseline(self): | |
566 | return self.fontinfo[6] - self.fontinfo[3] | |
567 | # | |
568 | def lineheight(self): | |
569 | return self.fontinfo[6] | |
570 | ||
571 | ||
572 | class GLWriter(GLFontCache): | |
573 | # | |
574 | # NOTES: | |
575 | # (1) Use gl.ortho2 to use X pixel coordinates! | |
576 | # | |
577 | def text(self, (h, v), text): | |
578 | import gl, fm | |
579 | gl.cmov2i(h, v + self.fontinfo[6] - self.fontinfo[3]) | |
580 | fm.prstr(text) | |
581 | # | |
582 | def setfont(self, fontkey): | |
583 | oldhandle = self.fonthandle | |
584 | GLFontCache.setfont(fontkey) | |
585 | if self.fonthandle != oldhandle: | |
586 | handle.setfont() | |
587 | ||
588 | ||
589 | class GLMeasurerWriter(GLMeasurer, GLWriter): | |
590 | pass | |
591 | ||
592 | ||
593 | class GLBackEnd(SavingBackEnd): | |
594 | # | |
595 | def __init__(self, wid): | |
596 | import gl | |
597 | gl.winset(wid) | |
598 | self.wid = wid | |
599 | self.width = gl.getsize()[1] | |
600 | self.height = 0 | |
601 | self.d = GLMeasurerWriter() | |
602 | SavingBackEnd.__init__(self) | |
603 | # | |
604 | def finish(self): | |
605 | pass | |
606 | # | |
607 | def addpara(self, p): | |
608 | self.paralist.append(p) | |
609 | self.height = p.render(self.d, 0, self.height, self.width) | |
610 | # | |
611 | def redraw(self): | |
612 | import gl | |
613 | gl.winset(self.wid) | |
614 | width = gl.getsize()[1] | |
615 | if width != self.width: | |
616 | setdocsize = 1 | |
617 | self.width = width | |
618 | for p in self.paralist: | |
619 | p.top = p.bottom = None | |
620 | d = self.d | |
621 | v = 0 | |
622 | for p in self.paralist: | |
623 | v = p.render(d, 0, v, width) |