1 # Text formatting abstractions
8 # Represent a paragraph. This is a list of words with associated
9 # font and size information, plus indents and justification for the
11 # Once the words have been added to a paragraph, it can be laid out
12 # for different line widths. Once laid out, it can be rendered at
13 # different screen locations. Once rendered, it can be queried
14 # for mouse hits, and parts of the text can be highlighted
18 self
.words
= [] # The words
19 self
.just
= 'l' # Justification: 'l', 'r', 'lr' or 'c'
20 self
.indent_left
= self
.indent_right
= self
.indent_hang
= 0
21 # Final lay-out parameters, may change
22 self
.left
= self
.top
= self
.right
= self
.bottom
= \
23 self
.width
= self
.height
= self
.lines
= None
25 # Add a word, computing size information for it.
26 # Words may also be added manually by appending to self.words
27 # Each word should be a 7-tuple:
28 # (font, text, width, space, stretch, ascent, descent)
29 def addword(self
, d
, font
, text
, space
, stretch
):
32 width
= d
.textwidth(text
)
34 descent
= d
.lineheight() - ascent
35 spw
= d
.textwidth(' ')
37 stretch
= stretch
* spw
38 tuple = (font
, text
, width
, space
, stretch
, ascent
, descent
)
39 self
.words
.append(tuple)
41 # Hooks to begin and end anchors -- insert numbers in the word list!
42 def bgn_anchor(self
, id):
45 def end_anchor(self
, id):
48 # Return the total length (width) of the text added so far, in pixels
51 for word
in self
.words
:
53 total
= total
+ word
[2] + word
[3]
56 # Tab to a given position (relative to the current left indent):
57 # remove all stretch, add fixed space up to the new indent.
58 # If the current position is already beying the tab stop,
59 # don't add any new space (but still remove the stretch)
63 for i
in range(len(self
.words
)):
65 if type(word
) == Int
: continue
66 fo
, te
, wi
, sp
, st
, as, de
= word
67 self
.words
[i
] = fo
, te
, wi
, sp
, 0, as, de
68 total
= total
+ wi
+ sp
70 self
.words
.append(None, '', 0, tab
-total
, 0, as, de
)
72 # Make a hanging tag: tab to hang, increment indent_left by hang,
73 # and reset indent_hang to -hang
74 def makehangingtag(self
, hang
):
76 self
.indent_left
= self
.indent_left
+ hang
77 self
.indent_hang
= -hang
79 # Decide where the line breaks will be given some screen width
80 def layout(self
, linewidth
):
81 self
.width
= linewidth
83 self
.lines
= lines
= []
84 avail1
= self
.width
- self
.indent_left
- self
.indent_right
85 avail
= avail1
- self
.indent_hang
101 if type(word
) == Int
:
102 if word
> 0 and width
>= avail
:
106 fo
, te
, wi
, sp
, st
, as, de
= word
107 if width
+ wi
> avail
and width
> 0 and wi
> 0:
113 charcount
= charcount
+ len(te
) + (sp
> 0)
114 width
= width
+ wi
+ sp
116 stretch
= stretch
+ st
118 ascent
= max(ascent
, as)
119 descent
= max(descent
, de
)
121 while i
> j
and type(words
[i
-1]) == Int
and \
122 words
[i
-1] > 0: i
= i
-1
125 stretch
= stretch
- lst
128 tuple = i
-j
, firstfont
, charcount
, width
, stretch
, \
131 height
= height
+ ascent
+ descent
135 # Call a function for all words in a line
136 def visit(self
, wordfunc
, anchorfunc
):
137 avail1
= self
.width
- self
.indent_left
- self
.indent_right
138 avail
= avail1
- self
.indent_hang
141 for tuple in self
.lines
:
142 wordcount
, firstfont
, charcount
, width
, stretch
, \
143 ascent
, descent
= tuple
144 h
= self
.left
+ self
.indent_left
145 if i
== 0: h
= h
+ self
.indent_hang
147 if self
.just
== 'r': h
= h
+ avail
- width
148 elif self
.just
== 'c': h
= h
+ (avail
- width
) / 2
149 elif self
.just
== 'lr' and stretch
> 0:
150 extra
= avail
- width
151 v2
= v
+ ascent
+ descent
152 for j
in range(i
, i
+wordcount
):
154 if type(word
) == Int
:
155 ok
= anchorfunc(self
, tuple, word
, \
157 if ok
<> None: return ok
159 fo
, te
, wi
, sp
, st
, as, de
= word
160 if extra
> 0 and stretch
> 0:
161 ex
= extra
* st
/ stretch
163 stretch
= stretch
- st
166 h2
= h
+ wi
+ sp
+ ex
167 ok
= wordfunc(self
, tuple, word
, h
, v
, \
168 h2
, v2
, (j
==i
), (j
==i
+wordcount
-1))
169 if ok
<> None: return ok
175 # Render a paragraph in "drawing object" d, using the rectangle
176 # given by (left, top, right) with an unspecified bottom.
177 # Return the computed bottom of the text.
178 def render(self
, d
, left
, top
, right
):
179 if self
.width
<> right
-left
:
180 self
.layout(right
-left
)
184 self
.bottom
= self
.top
+ self
.height
188 self
.visit(self
.__class
__._renderword
, \
189 self
.__class
__._renderanchor
)
194 def _renderword(self
, tuple, word
, h
, v
, h2
, v2
, isfirst
, islast
):
195 if word
[0] <> None: self
.d
.setfont(word
[0])
196 baseline
= v
+ tuple[5]
197 self
.d
.text((h
, baseline
- word
[5]), word
[1])
198 if self
.anchorid
> 0:
199 self
.d
.line((h
, baseline
+2), (h2
, baseline
+2))
201 def _renderanchor(self
, tuple, word
, h
, v
):
204 # Return which anchor(s) was hit by the mouse
205 def hitcheck(self
, mouseh
, mousev
):
210 self
.visit(self
.__class
__._hitcheckword
, \
211 self
.__class
__._hitcheckanchor
)
214 def _hitcheckword(self
, tuple, word
, h
, v
, h2
, v2
, isfirst
, islast
):
215 if self
.anchorid
> 0 and h
<= self
.mouseh
<= h2
and \
216 v
<= self
.mousev
<= v2
:
217 self
.hits
.append(self
.anchorid
)
219 def _hitcheckanchor(self
, tuple, word
, h
, v
):
222 # Return whether the given anchor id is present
223 def hasanchor(self
, id):
224 return id in self
.words
or -id in self
.words
226 # Extract the raw text from the word list, substituting one space
227 # for non-empty inter-word space, and terminating with '\n'
233 if w
[3]: word
= word
+ ' '
237 # Return which character position was hit by the mouse, as
238 # an offset in the entire text as returned by extract().
239 # Return None if the mouse was not in this paragraph
240 def whereis(self
, d
, mouseh
, mousev
):
241 if mousev
< self
.top
or mousev
> self
.bottom
:
249 return self
.visit(self
.__class
__._whereisword
, \
250 self
.__class
__._whereisanchor
)
254 def _whereisword(self
, tuple, word
, h1
, v1
, h2
, v2
, isfirst
, islast
):
255 fo
, te
, wi
, sp
, st
, as, de
= word
256 if fo
<> None: self
.lastfont
= fo
259 if islast
: h2
= 999999
260 if not (v1
<= self
.mousev
<= v2
and h1
<= self
.mouseh
<= h2
):
261 self
.charcount
= self
.charcount
+ len(te
) + (sp
> 0)
263 if self
.lastfont
<> None:
264 self
.d
.setfont(self
.lastfont
)
267 cw
= self
.d
.textwidth(c
)
268 if self
.mouseh
<= h
+ cw
/2:
269 return self
.charcount
+ cc
272 self
.charcount
= self
.charcount
+ cc
273 if self
.mouseh
<= (h
+h2
) / 2:
274 return self
.charcount
276 return self
.charcount
+ 1
278 def _whereisanchor(self
, tuple, word
, h
, v
):
281 # Return screen position corresponding to position in paragraph.
282 # Return tuple (h, vtop, vbaseline, vbottom).
283 # This is more or less the inverse of whereis()
284 def screenpos(self
, d
, pos
):
286 ascent
, descent
= self
.lines
[0][5:7]
287 return self
.left
, self
.top
, self
.top
+ ascent
, \
288 self
.top
+ ascent
+ descent
293 ok
= self
.visit(self
.__class
__._screenposword
, \
294 self
.__class
__._screenposanchor
)
298 ascent
, descent
= self
.lines
[-1][5:7]
299 ok
= self
.right
, self
.bottom
- ascent
- descent
, \
300 self
.bottom
- descent
, self
.bottom
303 def _screenposword(self
, tuple, word
, h1
, v1
, h2
, v2
, isfirst
, islast
):
304 fo
, te
, wi
, sp
, st
, as, de
= word
305 if fo
<> None: self
.lastfont
= fo
306 cc
= len(te
) + (sp
> 0)
308 self
.pos
= self
.pos
- cc
311 self
.d
.setfont(self
.lastfont
)
312 h
= h1
+ self
.d
.textwidth(te
[:self
.pos
])
315 ascent
, descent
= tuple[5:7]
316 return h
, v1
, v1
+ascent
, v2
318 def _screenposanchor(self
, tuple, word
, h
, v
):
321 # Invert the stretch of text between pos1 and pos2.
322 # If pos1 is None, the beginning is implied;
323 # if pos2 is None, the end is implied.
324 # Undoes its own effect when called again with the same arguments
325 def invert(self
, d
, pos1
, pos2
):
327 pos1
= self
.left
, self
.top
, self
.top
, self
.top
329 pos1
= self
.screenpos(d
, pos1
)
331 pos2
= self
.right
, self
.bottom
,self
.bottom
,self
.bottom
333 pos2
= self
.screenpos(d
, pos2
)
334 h1
, top1
, baseline1
, bottom1
= pos1
335 h2
, top2
, baseline2
, bottom2
= pos2
337 d
.invert((h1
, top1
), (self
.right
, bottom1
))
340 d
.invert((h1
, bottom1
), (self
.right
, top2
))
341 top1
, bottom1
= top2
, bottom2
342 d
.invert((h1
, top1
), (h2
, bottom2
))
346 # XXX This was last used on the Mac, hence the weird fonts...
349 from stdwinevents
import *
350 words
= 'The', 'quick', 'brown', 'fox', 'jumps', 'over', \
351 'the', 'lazy', 'dog.'
353 for just
in 'l', 'r', 'lr', 'c':
356 p
.addword(stdwin
, ('New York', 'p', 12), words
[0], 1, 1)
357 for word
in words
[1:-1]:
358 p
.addword(stdwin
, None, word
, 1, 1)
359 p
.addword(stdwin
, None, words
[-1], 2, 4)
360 p
.addword(stdwin
, ('New York', 'b', 18), 'Bye!', 0, 0)
361 p
.addword(stdwin
, ('New York', 'p', 10), 'Bye!', 0, 0)
363 window
= stdwin
.open('Para.test()')
364 start
= stop
= selpara
= None
366 etype
, win
, detail
= stdwin
.getevent()
367 if etype
== WE_CLOSE
:
370 window
.change((0, 0), (1000, 1000))
372 width
, height
= window
.getwinsize()
375 d
= window
.begindrawing()
380 v
= p
.render(d
, 0, v
, width
)
381 if p
== selpara
and \
382 start
<> None and stop
<> None:
383 p
.invert(d
, start
, stop
)
386 if etype
== WE_MOUSE_DOWN
:
387 if selpara
and start
<> None and stop
<> None:
388 d
= window
.begindrawing()
389 selpara
.invert(d
, start
, stop
)
391 start
= stop
= selpara
= None
392 mouseh
, mousev
= detail
[0]
394 start
= p
.whereis(stdwin
, mouseh
, mousev
)
398 if etype
== WE_MOUSE_UP
and start
<> None and selpara
:
399 mouseh
, mousev
= detail
[0]
400 stop
= selpara
.whereis(stdwin
, mouseh
, mousev
)
401 if stop
== None: start
= selpara
= None
404 start
, stop
= stop
, start
405 d
= window
.begindrawing()
406 selpara
.invert(d
, start
, stop
)