Commit | Line | Data |
---|---|---|
86530b38 AT |
1 | # Text formatting abstractions |
2 | # Note -- this module is obsolete, it's too slow anyway | |
3 | ||
4 | ||
5 | # Oft-used type object | |
6 | Int = type(0) | |
7 | ||
8 | ||
9 | # Represent a paragraph. This is a list of words with associated | |
10 | # font and size information, plus indents and justification for the | |
11 | # entire paragraph. | |
12 | # Once the words have been added to a paragraph, it can be laid out | |
13 | # for different line widths. Once laid out, it can be rendered at | |
14 | # different screen locations. Once rendered, it can be queried | |
15 | # for mouse hits, and parts of the text can be highlighted | |
16 | class Para: | |
17 | # | |
18 | def __init__(self): | |
19 | self.words = [] # The words | |
20 | self.just = 'l' # Justification: 'l', 'r', 'lr' or 'c' | |
21 | self.indent_left = self.indent_right = self.indent_hang = 0 | |
22 | # Final lay-out parameters, may change | |
23 | self.left = self.top = self.right = self.bottom = \ | |
24 | self.width = self.height = self.lines = None | |
25 | # | |
26 | # Add a word, computing size information for it. | |
27 | # Words may also be added manually by appending to self.words | |
28 | # Each word should be a 7-tuple: | |
29 | # (font, text, width, space, stretch, ascent, descent) | |
30 | def addword(self, d, font, text, space, stretch): | |
31 | if font is not None: | |
32 | d.setfont(font) | |
33 | width = d.textwidth(text) | |
34 | ascent = d.baseline() | |
35 | descent = d.lineheight() - ascent | |
36 | spw = d.textwidth(' ') | |
37 | space = space * spw | |
38 | stretch = stretch * spw | |
39 | tuple = (font, text, width, space, stretch, ascent, descent) | |
40 | self.words.append(tuple) | |
41 | # | |
42 | # Hooks to begin and end anchors -- insert numbers in the word list! | |
43 | def bgn_anchor(self, id): | |
44 | self.words.append(id) | |
45 | # | |
46 | def end_anchor(self, id): | |
47 | self.words.append(0) | |
48 | # | |
49 | # Return the total length (width) of the text added so far, in pixels | |
50 | def getlength(self): | |
51 | total = 0 | |
52 | for word in self.words: | |
53 | if type(word) is not Int: | |
54 | total = total + word[2] + word[3] | |
55 | return total | |
56 | # | |
57 | # Tab to a given position (relative to the current left indent): | |
58 | # remove all stretch, add fixed space up to the new indent. | |
59 | # If the current position is already at the tab stop, | |
60 | # don't add any new space (but still remove the stretch) | |
61 | def tabto(self, tab): | |
62 | total = 0 | |
63 | as, de = 1, 0 | |
64 | for i in range(len(self.words)): | |
65 | word = self.words[i] | |
66 | if type(word) is Int: continue | |
67 | (fo, te, wi, sp, st, as, de) = word | |
68 | self.words[i] = (fo, te, wi, sp, 0, as, de) | |
69 | total = total + wi + sp | |
70 | if total < tab: | |
71 | self.words.append((None, '', 0, tab-total, 0, as, de)) | |
72 | # | |
73 | # Make a hanging tag: tab to hang, increment indent_left by hang, | |
74 | # and reset indent_hang to -hang | |
75 | def makehangingtag(self, hang): | |
76 | self.tabto(hang) | |
77 | self.indent_left = self.indent_left + hang | |
78 | self.indent_hang = -hang | |
79 | # | |
80 | # Decide where the line breaks will be given some screen width | |
81 | def layout(self, linewidth): | |
82 | self.width = linewidth | |
83 | height = 0 | |
84 | self.lines = lines = [] | |
85 | avail1 = self.width - self.indent_left - self.indent_right | |
86 | avail = avail1 - self.indent_hang | |
87 | words = self.words | |
88 | i = 0 | |
89 | n = len(words) | |
90 | lastfont = None | |
91 | while i < n: | |
92 | firstfont = lastfont | |
93 | charcount = 0 | |
94 | width = 0 | |
95 | stretch = 0 | |
96 | ascent = 0 | |
97 | descent = 0 | |
98 | lsp = 0 | |
99 | j = i | |
100 | while i < n: | |
101 | word = words[i] | |
102 | if type(word) is Int: | |
103 | if word > 0 and width >= avail: | |
104 | break | |
105 | i = i+1 | |
106 | continue | |
107 | fo, te, wi, sp, st, as, de = word | |
108 | if width + wi > avail and width > 0 and wi > 0: | |
109 | break | |
110 | if fo is not None: | |
111 | lastfont = fo | |
112 | if width == 0: | |
113 | firstfont = fo | |
114 | charcount = charcount + len(te) + (sp > 0) | |
115 | width = width + wi + sp | |
116 | lsp = sp | |
117 | stretch = stretch + st | |
118 | lst = st | |
119 | ascent = max(ascent, as) | |
120 | descent = max(descent, de) | |
121 | i = i+1 | |
122 | while i > j and type(words[i-1]) is Int and \ | |
123 | words[i-1] > 0: i = i-1 | |
124 | width = width - lsp | |
125 | if i < n: | |
126 | stretch = stretch - lst | |
127 | else: | |
128 | stretch = 0 | |
129 | tuple = i-j, firstfont, charcount, width, stretch, \ | |
130 | ascent, descent | |
131 | lines.append(tuple) | |
132 | height = height + ascent + descent | |
133 | avail = avail1 | |
134 | self.height = height | |
135 | # | |
136 | # Call a function for all words in a line | |
137 | def visit(self, wordfunc, anchorfunc): | |
138 | avail1 = self.width - self.indent_left - self.indent_right | |
139 | avail = avail1 - self.indent_hang | |
140 | v = self.top | |
141 | i = 0 | |
142 | for tuple in self.lines: | |
143 | wordcount, firstfont, charcount, width, stretch, \ | |
144 | ascent, descent = tuple | |
145 | h = self.left + self.indent_left | |
146 | if i == 0: h = h + self.indent_hang | |
147 | extra = 0 | |
148 | if self.just == 'r': h = h + avail - width | |
149 | elif self.just == 'c': h = h + (avail - width) / 2 | |
150 | elif self.just == 'lr' and stretch > 0: | |
151 | extra = avail - width | |
152 | v2 = v + ascent + descent | |
153 | for j in range(i, i+wordcount): | |
154 | word = self.words[j] | |
155 | if type(word) is Int: | |
156 | ok = anchorfunc(self, tuple, word, \ | |
157 | h, v) | |
158 | if ok is not None: return ok | |
159 | continue | |
160 | fo, te, wi, sp, st, as, de = word | |
161 | if extra > 0 and stretch > 0: | |
162 | ex = extra * st / stretch | |
163 | extra = extra - ex | |
164 | stretch = stretch - st | |
165 | else: | |
166 | ex = 0 | |
167 | h2 = h + wi + sp + ex | |
168 | ok = wordfunc(self, tuple, word, h, v, \ | |
169 | h2, v2, (j==i), (j==i+wordcount-1)) | |
170 | if ok is not None: return ok | |
171 | h = h2 | |
172 | v = v2 | |
173 | i = i + wordcount | |
174 | avail = avail1 | |
175 | # | |
176 | # Render a paragraph in "drawing object" d, using the rectangle | |
177 | # given by (left, top, right) with an unspecified bottom. | |
178 | # Return the computed bottom of the text. | |
179 | def render(self, d, left, top, right): | |
180 | if self.width != right-left: | |
181 | self.layout(right-left) | |
182 | self.left = left | |
183 | self.top = top | |
184 | self.right = right | |
185 | self.bottom = self.top + self.height | |
186 | self.anchorid = 0 | |
187 | try: | |
188 | self.d = d | |
189 | self.visit(self.__class__._renderword, \ | |
190 | self.__class__._renderanchor) | |
191 | finally: | |
192 | self.d = None | |
193 | return self.bottom | |
194 | # | |
195 | def _renderword(self, tuple, word, h, v, h2, v2, isfirst, islast): | |
196 | if word[0] is not None: self.d.setfont(word[0]) | |
197 | baseline = v + tuple[5] | |
198 | self.d.text((h, baseline - word[5]), word[1]) | |
199 | if self.anchorid > 0: | |
200 | self.d.line((h, baseline+2), (h2, baseline+2)) | |
201 | # | |
202 | def _renderanchor(self, tuple, word, h, v): | |
203 | self.anchorid = word | |
204 | # | |
205 | # Return which anchor(s) was hit by the mouse | |
206 | def hitcheck(self, mouseh, mousev): | |
207 | self.mouseh = mouseh | |
208 | self.mousev = mousev | |
209 | self.anchorid = 0 | |
210 | self.hits = [] | |
211 | self.visit(self.__class__._hitcheckword, \ | |
212 | self.__class__._hitcheckanchor) | |
213 | return self.hits | |
214 | # | |
215 | def _hitcheckword(self, tuple, word, h, v, h2, v2, isfirst, islast): | |
216 | if self.anchorid > 0 and h <= self.mouseh <= h2 and \ | |
217 | v <= self.mousev <= v2: | |
218 | self.hits.append(self.anchorid) | |
219 | # | |
220 | def _hitcheckanchor(self, tuple, word, h, v): | |
221 | self.anchorid = word | |
222 | # | |
223 | # Return whether the given anchor id is present | |
224 | def hasanchor(self, id): | |
225 | return id in self.words or -id in self.words | |
226 | # | |
227 | # Extract the raw text from the word list, substituting one space | |
228 | # for non-empty inter-word space, and terminating with '\n' | |
229 | def extract(self): | |
230 | text = '' | |
231 | for w in self.words: | |
232 | if type(w) is not Int: | |
233 | word = w[1] | |
234 | if w[3]: word = word + ' ' | |
235 | text = text + word | |
236 | return text + '\n' | |
237 | # | |
238 | # Return which character position was hit by the mouse, as | |
239 | # an offset in the entire text as returned by extract(). | |
240 | # Return None if the mouse was not in this paragraph | |
241 | def whereis(self, d, mouseh, mousev): | |
242 | if mousev < self.top or mousev > self.bottom: | |
243 | return None | |
244 | self.mouseh = mouseh | |
245 | self.mousev = mousev | |
246 | self.lastfont = None | |
247 | self.charcount = 0 | |
248 | try: | |
249 | self.d = d | |
250 | return self.visit(self.__class__._whereisword, \ | |
251 | self.__class__._whereisanchor) | |
252 | finally: | |
253 | self.d = None | |
254 | # | |
255 | def _whereisword(self, tuple, word, h1, v1, h2, v2, isfirst, islast): | |
256 | fo, te, wi, sp, st, as, de = word | |
257 | if fo is not None: self.lastfont = fo | |
258 | h = h1 | |
259 | if isfirst: h1 = 0 | |
260 | if islast: h2 = 999999 | |
261 | if not (v1 <= self.mousev <= v2 and h1 <= self.mouseh <= h2): | |
262 | self.charcount = self.charcount + len(te) + (sp > 0) | |
263 | return | |
264 | if self.lastfont is not None: | |
265 | self.d.setfont(self.lastfont) | |
266 | cc = 0 | |
267 | for c in te: | |
268 | cw = self.d.textwidth(c) | |
269 | if self.mouseh <= h + cw/2: | |
270 | return self.charcount + cc | |
271 | cc = cc+1 | |
272 | h = h+cw | |
273 | self.charcount = self.charcount + cc | |
274 | if self.mouseh <= (h+h2) / 2: | |
275 | return self.charcount | |
276 | else: | |
277 | return self.charcount + 1 | |
278 | # | |
279 | def _whereisanchor(self, tuple, word, h, v): | |
280 | pass | |
281 | # | |
282 | # Return screen position corresponding to position in paragraph. | |
283 | # Return tuple (h, vtop, vbaseline, vbottom). | |
284 | # This is more or less the inverse of whereis() | |
285 | def screenpos(self, d, pos): | |
286 | if pos < 0: | |
287 | ascent, descent = self.lines[0][5:7] | |
288 | return self.left, self.top, self.top + ascent, \ | |
289 | self.top + ascent + descent | |
290 | self.pos = pos | |
291 | self.lastfont = None | |
292 | try: | |
293 | self.d = d | |
294 | ok = self.visit(self.__class__._screenposword, \ | |
295 | self.__class__._screenposanchor) | |
296 | finally: | |
297 | self.d = None | |
298 | if ok is None: | |
299 | ascent, descent = self.lines[-1][5:7] | |
300 | ok = self.right, self.bottom - ascent - descent, \ | |
301 | self.bottom - descent, self.bottom | |
302 | return ok | |
303 | # | |
304 | def _screenposword(self, tuple, word, h1, v1, h2, v2, isfirst, islast): | |
305 | fo, te, wi, sp, st, as, de = word | |
306 | if fo is not None: self.lastfont = fo | |
307 | cc = len(te) + (sp > 0) | |
308 | if self.pos > cc: | |
309 | self.pos = self.pos - cc | |
310 | return | |
311 | if self.pos < cc: | |
312 | self.d.setfont(self.lastfont) | |
313 | h = h1 + self.d.textwidth(te[:self.pos]) | |
314 | else: | |
315 | h = h2 | |
316 | ascent, descent = tuple[5:7] | |
317 | return h, v1, v1+ascent, v2 | |
318 | # | |
319 | def _screenposanchor(self, tuple, word, h, v): | |
320 | pass | |
321 | # | |
322 | # Invert the stretch of text between pos1 and pos2. | |
323 | # If pos1 is None, the beginning is implied; | |
324 | # if pos2 is None, the end is implied. | |
325 | # Undoes its own effect when called again with the same arguments | |
326 | def invert(self, d, pos1, pos2): | |
327 | if pos1 is None: | |
328 | pos1 = self.left, self.top, self.top, self.top | |
329 | else: | |
330 | pos1 = self.screenpos(d, pos1) | |
331 | if pos2 is None: | |
332 | pos2 = self.right, self.bottom,self.bottom,self.bottom | |
333 | else: | |
334 | pos2 = self.screenpos(d, pos2) | |
335 | h1, top1, baseline1, bottom1 = pos1 | |
336 | h2, top2, baseline2, bottom2 = pos2 | |
337 | if bottom1 <= top2: | |
338 | d.invert((h1, top1), (self.right, bottom1)) | |
339 | h1 = self.left | |
340 | if bottom1 < top2: | |
341 | d.invert((h1, bottom1), (self.right, top2)) | |
342 | top1, bottom1 = top2, bottom2 | |
343 | d.invert((h1, top1), (h2, bottom2)) |