# 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 <> 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) <> 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 beying 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) == 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) == 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 <> 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]) == 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) == Int: ok = anchorfunc(self, tuple, word, \ h, v) if ok <> 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 <> 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] <> 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) <> 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 <> 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 <> 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 == 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 <> 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 == None: pos1 = self.left, self.top, self.top, self.top else: pos1 = self.screenpos(d, pos1) if pos2 == 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)) # Test class Para # XXX This was last used on the Mac, hence the weird fonts... def test(): import stdwin from stdwinevents import * words = 'The', 'quick', 'brown', 'fox', 'jumps', 'over', \ 'the', 'lazy', 'dog.' paralist = [] for just in 'l', 'r', 'lr', 'c': p = Para() p.just = just p.addword(stdwin, ('New York', 'p', 12), words[0], 1, 1) for word in words[1:-1]: p.addword(stdwin, None, word, 1, 1) p.addword(stdwin, None, words[-1], 2, 4) p.addword(stdwin, ('New York', 'b', 18), 'Bye!', 0, 0) p.addword(stdwin, ('New York', 'p', 10), 'Bye!', 0, 0) paralist.append(p) window = stdwin.open('Para.test()') start = stop = selpara = None while 1: etype, win, detail = stdwin.getevent() if etype == WE_CLOSE: break if etype == WE_SIZE: window.change((0, 0), (1000, 1000)) if etype == WE_DRAW: width, height = window.getwinsize() d = None try: d = window.begindrawing() d.cliprect(detail) d.erase(detail) v = 0 for p in paralist: v = p.render(d, 0, v, width) if p == selpara and \ start <> None and stop <> None: p.invert(d, start, stop) finally: if d: d.close() if etype == WE_MOUSE_DOWN: if selpara and start <> None and stop <> None: d = window.begindrawing() selpara.invert(d, start, stop) d.close() start = stop = selpara = None mouseh, mousev = detail[0] for p in paralist: start = p.whereis(stdwin, mouseh, mousev) if start <> None: selpara = p break if etype == WE_MOUSE_UP and start <> None and selpara: mouseh, mousev = detail[0] stop = selpara.whereis(stdwin, mouseh, mousev) if stop == None: start = selpara = None else: if start > stop: start, stop = stop, start d = window.begindrawing() selpara.invert(d, start, stop) d.close() window.close()