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