home *** CD-ROM | disk | FTP | other *** search
/ OS/2 Shareware BBS: 10 Tools / 10-Tools.zip / pyos2bin.zip / Demo / tkinter / www / fmt.py next >
Text File  |  1995-01-10  |  15KB  |  628 lines

  1. # Text formatting abstractions
  2.  
  3.  
  4. import string
  5. import Para
  6.  
  7.  
  8. # A formatter back-end object has one method that is called by the formatter:
  9. # addpara(p), where p is a paragraph object.  For example:
  10.  
  11.  
  12. # Formatter back-end to do nothing at all with the paragraphs
  13. class NullBackEnd:
  14.     #
  15.     def __init__(self):
  16.         pass
  17.     #
  18.     def addpara(self, p):
  19.         pass
  20.     #
  21.     def bgn_anchor(self, id):
  22.         pass
  23.     #
  24.     def end_anchor(self, id):
  25.         pass
  26.  
  27.  
  28. # Formatter back-end to collect the paragraphs in a list
  29. class SavingBackEnd(NullBackEnd):
  30.     #
  31.     def __init__(self):
  32.         self.paralist = []
  33.     #
  34.     def addpara(self, p):
  35.         self.paralist.append(p)
  36.     #
  37.     def hitcheck(self, h, v):
  38.         hits = []
  39.         for p in self.paralist:
  40.             if p.top <= v <= p.bottom:
  41.                 for id in p.hitcheck(h, v):
  42.                     if id not in hits:
  43.                         hits.append(id)
  44.         return hits
  45.     #
  46.     def extract(self):
  47.         text = ''
  48.         for p in self.paralist:
  49.             text = text + (p.extract())
  50.         return text
  51.     #
  52.     def extractpart(self, long1, long2):
  53.         if long1 > long2: long1, long2 = long2, long1
  54.         para1, pos1 = long1
  55.         para2, pos2 = long2
  56.         text = ''
  57.         while para1 < para2:
  58.             ptext = self.paralist[para1].extract()
  59.             text = text + ptext[pos1:]
  60.             pos1 = 0
  61.             para1 = para1 + 1
  62.         ptext = self.paralist[para2].extract()
  63.         return text + ptext[pos1:pos2]
  64.     #
  65.     def whereis(self, d, h, v):
  66.         total = 0
  67.         for i in range(len(self.paralist)):
  68.             p = self.paralist[i]
  69.             result = p.whereis(d, h, v)
  70.             if result <> None:
  71.                 return i, result
  72.         return None
  73.     #
  74.     def roundtowords(self, long1, long2):
  75.         i, offset = long1
  76.         text = self.paralist[i].extract()
  77.         while offset > 0 and text[offset-1] <> ' ': offset = offset-1
  78.         long1 = i, offset
  79.         #
  80.         i, offset = long2
  81.         text = self.paralist[i].extract()
  82.         n = len(text)
  83.         while offset < n-1 and text[offset] <> ' ': offset = offset+1
  84.         long2 = i, offset
  85.         #
  86.         return long1, long2
  87.     #
  88.     def roundtoparagraphs(self, long1, long2):
  89.         long1 = long1[0], 0
  90.         long2 = long2[0], len(self.paralist[long2[0]].extract())
  91.         return long1, long2
  92.  
  93.  
  94. # Formatter back-end to send the text directly to the drawing object
  95. class WritingBackEnd(NullBackEnd):
  96.     #
  97.     def __init__(self, d, width):
  98.         self.d = d
  99.         self.width = width
  100.         self.lineno = 0
  101.     #
  102.     def addpara(self, p):
  103.         self.lineno = p.render(self.d, 0, self.lineno, self.width)
  104.  
  105.  
  106. # A formatter receives a stream of formatting instructions and assembles
  107. # these into a stream of paragraphs on to a back-end.  The assembly is
  108. # parametrized by a text measurement object, which must match the output
  109. # operations of the back-end.  The back-end is responsible for splitting
  110. # paragraphs up in lines of a given maximum width.  (This is done because
  111. # in a windowing environment, when the window size changes, there is no
  112. # need to redo the assembly into paragraphs, but the splitting into lines
  113. # must be done taking the new window size into account.)
  114.  
  115.  
  116. # Formatter base class.  Initialize it with a text measurement object,
  117. # which is used for text measurements, and a back-end object,
  118. # which receives the completed paragraphs.  The formatting methods are:
  119. # setfont(font)
  120. # setleftindent(nspaces)
  121. # setjust(type) where type is 'l', 'c', 'r', or 'lr'
  122. # flush()
  123. # vspace(nlines)
  124. # needvspace(nlines)
  125. # addword(word, nspaces)
  126. class BaseFormatter:
  127.     #
  128.     def __init__(self, d, b):
  129.         # Drawing object used for text measurements
  130.         self.d = d
  131.         #
  132.         # BackEnd object receiving completed paragraphs
  133.         self.b = b
  134.         #
  135.         # Parameters of the formatting model
  136.         self.leftindent = 0
  137.         self.just = 'l'
  138.         self.font = None
  139.         self.blanklines = 0
  140.         #
  141.         # Parameters derived from the current font
  142.         self.space = d.textwidth(' ')
  143.         self.line = d.lineheight()
  144.         self.ascent = d.baseline()
  145.         self.descent = self.line - self.ascent
  146.         #
  147.         # Parameter derived from the default font
  148.         self.n_space = self.space
  149.         #
  150.         # Current paragraph being built
  151.         self.para = None
  152.         self.nospace = 1
  153.         #
  154.         # Font to set on the next word
  155.         self.nextfont = None
  156.     #
  157.     def newpara(self):
  158.         return Para.Para()
  159.     #
  160.     def setfont(self, font):
  161.         if font == None: return
  162.         self.font = self.nextfont = font
  163.         d = self.d
  164.         d.setfont(font)
  165.         self.space = d.textwidth(' ')
  166.         self.line = d.lineheight()
  167.         self.ascent = d.baseline()
  168.         self.descent = self.line - self.ascent
  169.     #
  170.     def setleftindent(self, nspaces):
  171.         self.leftindent = int(self.n_space * nspaces)
  172.         if self.para:
  173.             hang = self.leftindent - self.para.indent_left
  174.             if hang > 0 and self.para.getlength() <= hang:
  175.                 self.para.makehangingtag(hang)
  176.                 self.nospace = 1
  177.             else:
  178.                 self.flush()
  179.     #
  180.     def setrightindent(self, nspaces):
  181.         self.rightindent = int(self.n_space * nspaces)
  182.         if self.para:
  183.             self.para.indent_right = self.rightindent
  184.             self.flush()
  185.     #
  186.     def setjust(self, just):
  187.         self.just = just
  188.         if self.para:
  189.             self.para.just = self.just
  190.     #
  191.     def flush(self):
  192.         if self.para:
  193.             self.b.addpara(self.para)
  194.             self.para = None
  195.             if self.font <> None:
  196.                 self.d.setfont(self.font)
  197.         self.nospace = 1
  198.     #
  199.     def vspace(self, nlines):
  200.         self.flush()
  201.         if nlines > 0:
  202.             self.para = self.newpara()
  203.             tuple = None, '', 0, 0, 0, int(nlines*self.line), 0
  204.             self.para.words.append(tuple)
  205.             self.flush()
  206.             self.blanklines = self.blanklines + nlines
  207.     #
  208.     def needvspace(self, nlines):
  209.         self.flush() # Just to be sure
  210.         if nlines > self.blanklines:
  211.             self.vspace(nlines - self.blanklines)
  212.     #
  213.     def addword(self, text, space):
  214.         if self.nospace and not text:
  215.             return
  216.         self.nospace = 0
  217.         self.blanklines = 0
  218.         if not self.para:
  219.             self.para = self.newpara()
  220.             self.para.indent_left = self.leftindent
  221.             self.para.just = self.just
  222.             self.nextfont = self.font
  223.         space = int(space * self.space)
  224.         self.para.words.append(self.nextfont, text, \
  225.             self.d.textwidth(text), space, space, \
  226.             self.ascent, self.descent)
  227.         self.nextfont = None
  228.     #
  229.     def bgn_anchor(self, id):
  230.         if not self.para:
  231.             self.nospace = 0
  232.             self.addword('', 0)
  233.         self.para.bgn_anchor(id)
  234.     #
  235.     def end_anchor(self, id):
  236.         if not self.para:
  237.             self.nospace = 0
  238.             self.addword('', 0)
  239.         self.para.end_anchor(id)
  240.     #
  241.     def hrule(self):
  242.         # Typically need to override this for bit-mapped displays
  243.         self.flush()
  244.         self.addword('-'*60, 0)
  245.         self.flush()
  246.  
  247.  
  248. # Measuring object for measuring text as viewed on a tty
  249. class NullMeasurer:
  250.     #
  251.     def __init__(self):
  252.         pass
  253.     #
  254.     def setfont(self, font):
  255.         pass
  256.     #
  257.     def textwidth(self, text):
  258.         return len(text)
  259.     #
  260.     def lineheight(self):
  261.         return 1
  262.     #
  263.     def baseline(self):
  264.         return 0
  265.  
  266.  
  267. # Drawing object for writing plain ASCII text to a file
  268. class FileWriter:
  269.     #
  270.     def __init__(self, fp):
  271.         self.fp = fp
  272.         self.lineno, self.colno = 0, 0
  273.     #
  274.     def setfont(self, font):
  275.         pass
  276.     #
  277.     def text(self, (h, v), str):
  278.         if not str: return
  279.         if '\n' in str:
  280.             raise ValueError, 'can\'t write \\n'
  281.         while self.lineno < v:
  282.             self.fp.write('\n')
  283.             self.colno, self.lineno = 0, self.lineno + 1
  284.         while self.lineno > v:
  285.             # XXX This should never happen...
  286.             self.fp.write('\033[A') # ANSI up arrow
  287.             self.lineno = self.lineno - 1
  288.         if self.colno < h:
  289.             self.fp.write(' ' * (h - self.colno))
  290.         elif self.colno > h:
  291.             self.fp.write('\b' * (self.colno - h))
  292.         self.colno = h
  293.         self.fp.write(str)
  294.         self.colno = h + len(str)
  295.  
  296.  
  297. # Formatting class to do nothing at all with the data
  298. class NullFormatter(BaseFormatter):
  299.     #
  300.     def __init__(self):
  301.         d = NullMeasurer()
  302.         b = NullBackEnd()
  303.         BaseFormatter.__init__(self, d, b)
  304.  
  305.  
  306. # Formatting class to write directly to a file
  307. class WritingFormatter(BaseFormatter):
  308.     #
  309.     def __init__(self, fp, width):
  310.         dm = NullMeasurer()
  311.         dw = FileWriter(fp)
  312.         b = WritingBackEnd(dw, width)
  313.         BaseFormatter.__init__(self, dm, b)
  314.         self.blanklines = 1
  315.     #
  316.     # Suppress multiple blank lines
  317.     def needvspace(self, nlines):
  318.         BaseFormatter.needvspace(self, min(1, nlines))
  319.  
  320.  
  321. # A "FunnyFormatter" writes ASCII text with a twist: *bold words*,
  322. # _italic text_ and _underlined words_, and `quoted text'.
  323. # It assumes that the fonts are 'r', 'i', 'b', 'u', 'q': (roman,
  324. # italic, bold, underline, quote).
  325. # Moreover, if the font is in upper case, the text is converted to
  326. # UPPER CASE.
  327. class FunnyFormatter(WritingFormatter):
  328.     #
  329.     def flush(self):
  330.         if self.para: finalize(self.para)
  331.         WritingFormatter.flush(self)
  332.  
  333.  
  334. # Surrounds *bold words* and _italic text_ in a paragraph with
  335. # appropriate markers, fixing the size (assuming these characters'
  336. # width is 1).
  337. openchar = \
  338.     {'b':'*', 'i':'_', 'u':'_', 'q':'`', 'B':'*', 'I':'_', 'U':'_', 'Q':'`'}
  339. closechar = \
  340.     {'b':'*', 'i':'_', 'u':'_', 'q':'\'', 'B':'*', 'I':'_', 'U':'_', 'Q':'\''}
  341. def finalize(para):
  342.     oldfont = curfont = 'r'
  343.     para.words.append('r', '', 0, 0, 0, 0) # temporary, deleted at end
  344.     for i in range(len(para.words)):
  345.         fo, te, wi = para.words[i][:3]
  346.         if fo <> None: curfont = fo
  347.         if curfont <> oldfont:
  348.             if closechar.has_key(oldfont):
  349.                 c = closechar[oldfont]
  350.                 j = i-1
  351.                 while j > 0 and para.words[j][1] == '': j = j-1
  352.                 fo1, te1, wi1 = para.words[j][:3]
  353.                 te1 = te1 + c
  354.                 wi1 = wi1 + len(c)
  355.                 para.words[j] = (fo1, te1, wi1) + \
  356.                     para.words[j][3:]
  357.             if openchar.has_key(curfont) and te:
  358.                 c = openchar[curfont]
  359.                 te = c + te
  360.                 wi = len(c) + wi
  361.                 para.words[i] = (fo, te, wi) + \
  362.                     para.words[i][3:]
  363.             if te: oldfont = curfont
  364.             else: oldfont = 'r'
  365.         if curfont in string.uppercase:
  366.             te = string.upper(te)
  367.             para.words[i] = (fo, te, wi) + para.words[i][3:]
  368.     del para.words[-1]
  369.  
  370.  
  371. # Formatter back-end to draw the text in a window.
  372. # This has an option to draw while the paragraphs are being added,
  373. # to minimize the delay before the user sees anything.
  374. # This manages the entire "document" of the window.
  375. class StdwinBackEnd(SavingBackEnd):
  376.     #
  377.     def __init__(self, window, drawnow):
  378.         self.window = window
  379.         self.drawnow = drawnow
  380.         self.width = window.getwinsize()[0]
  381.         self.selection = None
  382.         self.height = 0
  383.         window.setorigin(0, 0)
  384.         window.setdocsize(0, 0)
  385.         self.d = window.begindrawing()
  386.         SavingBackEnd.__init__(self)
  387.     #
  388.     def finish(self):
  389.         self.d.close()
  390.         self.d = None
  391.         self.window.setdocsize(0, self.height)
  392.     #
  393.     def addpara(self, p):
  394.         self.paralist.append(p)
  395.         if self.drawnow:
  396.             self.height = \
  397.                 p.render(self.d, 0, self.height, self.width)
  398.         else:
  399.             p.layout(self.width)
  400.             p.left = 0
  401.             p.top = self.height
  402.             p.right = self.width
  403.             p.bottom = self.height + p.height
  404.             self.height = p.bottom
  405.     #
  406.     def resize(self):
  407.         self.window.change((0, 0), (self.width, self.height))
  408.         self.width = self.window.getwinsize()[0]
  409.         self.height = 0
  410.         for p in self.paralist:
  411.             p.layout(self.width)
  412.             p.left = 0
  413.             p.top = self.height
  414.             p.right = self.width
  415.             p.bottom = self.height + p.height
  416.             self.height = p.bottom
  417.         self.window.change((0, 0), (self.width, self.height))
  418.         self.window.setdocsize(0, self.height)
  419.     #
  420.     def redraw(self, area):
  421.         d = self.window.begindrawing()
  422.         (left, top), (right, bottom) = area
  423.         d.erase(area)
  424.         d.cliprect(area)
  425.         for p in self.paralist:
  426.             if top < p.bottom and p.top < bottom:
  427.                 v = p.render(d, p.left, p.top, p.right)
  428.         if self.selection:
  429.             self.invert(d, self.selection)
  430.         d.close()
  431.     #
  432.     def setselection(self, new):
  433.         if new:
  434.             long1, long2 = new
  435.             pos1 = long1[:3]
  436.             pos2 = long2[:3]
  437.             new = pos1, pos2
  438.         if new <> self.selection:
  439.             d = self.window.begindrawing()
  440.             if self.selection:
  441.                 self.invert(d, self.selection)
  442.             if new:
  443.                 self.invert(d, new)
  444.             d.close()
  445.             self.selection = new
  446.     #
  447.     def getselection(self):
  448.         return self.selection
  449.     #
  450.     def extractselection(self):
  451.         if self.selection:
  452.             a, b = self.selection
  453.             return self.extractpart(a, b)
  454.         else:
  455.             return None
  456.     #
  457.     def invert(self, d, region):
  458.         long1, long2 = region
  459.         if long1 > long2: long1, long2 = long2, long1
  460.         para1, pos1 = long1
  461.         para2, pos2 = long2
  462.         while para1 < para2:
  463.             self.paralist[para1].invert(d, pos1, None)
  464.             pos1 = None
  465.             para1 = para1 + 1
  466.         self.paralist[para2].invert(d, pos1, pos2)
  467.     #
  468.     def search(self, prog):
  469.         import regex, string
  470.         if type(prog) == type(''):
  471.             prog = regex.compile(string.lower(prog))
  472.         if self.selection:
  473.             iold = self.selection[0][0]
  474.         else:
  475.             iold = -1
  476.         hit = None
  477.         for i in range(len(self.paralist)):
  478.             if i == iold or i < iold and hit:
  479.                 continue
  480.             p = self.paralist[i]
  481.             text = string.lower(p.extract())
  482.             if prog.search(text) >= 0:
  483.                 a, b = prog.regs[0]
  484.                 long1 = i, a
  485.                 long2 = i, b
  486.                 hit = long1, long2
  487.                 if i > iold:
  488.                     break
  489.         if hit:
  490.             self.setselection(hit)
  491.             i = hit[0][0]
  492.             p = self.paralist[i]
  493.             self.window.show((p.left, p.top), (p.right, p.bottom))
  494.             return 1
  495.         else:
  496.             return 0
  497.     #
  498.     def showanchor(self, id):
  499.         for i in range(len(self.paralist)):
  500.             p = self.paralist[i]
  501.             if p.hasanchor(id):
  502.                 long1 = i, 0
  503.                 long2 = i, len(p.extract())
  504.                 hit = long1, long2
  505.                 self.setselection(hit)
  506.                 self.window.show( \
  507.                     (p.left, p.top), (p.right, p.bottom))
  508.                 break
  509.  
  510.  
  511. # GL extensions
  512.  
  513. class GLFontCache:
  514.     #
  515.     def __init__(self):
  516.         self.reset()
  517.         self.setfont('')
  518.     #
  519.     def reset(self):
  520.         self.fontkey = None
  521.         self.fonthandle = None
  522.         self.fontinfo = None
  523.         self.fontcache = {}
  524.     #
  525.     def close(self):
  526.         self.reset()
  527.     #
  528.     def setfont(self, fontkey):
  529.         if fontkey == '':
  530.             fontkey = 'Times-Roman 12'
  531.         elif ' ' not in fontkey:
  532.             fontkey = fontkey + ' 12'
  533.         if fontkey == self.fontkey:
  534.             return
  535.         if self.fontcache.has_key(fontkey):
  536.             handle = self.fontcache[fontkey]
  537.         else:
  538.             import string
  539.             i = string.index(fontkey, ' ')
  540.             name, sizestr = fontkey[:i], fontkey[i:]
  541.             size = eval(sizestr)
  542.             key1 = name + ' 1'
  543.             key = name + ' ' + `size`
  544.             # NB key may differ from fontkey!
  545.             if self.fontcache.has_key(key):
  546.                 handle = self.fontcache[key]
  547.             else:
  548.                 if self.fontcache.has_key(key1):
  549.                     handle = self.fontcache[key1]
  550.                 else:
  551.                     import fm
  552.                     handle = fm.findfont(name)
  553.                     self.fontcache[key1] = handle
  554.                 handle = handle.scalefont(size)
  555.                 self.fontcache[fontkey] = \
  556.                     self.fontcache[key] = handle
  557.         self.fontkey = fontkey
  558.         if self.fonthandle <> handle:
  559.             self.fonthandle = handle
  560.             self.fontinfo = handle.getfontinfo()
  561.             handle.setfont()
  562.  
  563.  
  564. class GLMeasurer(GLFontCache):
  565.     #
  566.     def textwidth(self, text):
  567.         return self.fonthandle.getstrwidth(text)
  568.     #
  569.     def baseline(self):
  570.         return self.fontinfo[6] - self.fontinfo[3]
  571.     #
  572.     def lineheight(self):
  573.         return self.fontinfo[6]
  574.  
  575.  
  576. class GLWriter(GLFontCache):
  577.     #
  578.     # NOTES:
  579.     # (1) Use gl.ortho2 to use X pixel coordinates!
  580.     #
  581.     def text(self, (h, v), text):
  582.         import gl, fm
  583.         gl.cmov2i(h, v + self.fontinfo[6] - self.fontinfo[3])
  584.         fm.prstr(text)
  585.     #
  586.     def setfont(self, fontkey):
  587.         oldhandle = self.fonthandle
  588.         GLFontCache.setfont(fontkey)
  589.         if self.fonthandle <> oldhandle:
  590.             handle.setfont()
  591.  
  592.  
  593. class GLMeasurerWriter(GLMeasurer, GLWriter):
  594.     pass
  595.  
  596.  
  597. class GLBackEnd(SavingBackEnd):
  598.     #
  599.     def __init__(self, wid):
  600.         import gl
  601.         gl.winset(wid)
  602.         self.wid = wid
  603.         self.width = gl.getsize()[1]
  604.         self.height = 0
  605.         self.d = GLMeasurerWriter()
  606.         SavingBackEnd.__init__(self)
  607.     #
  608.     def finish(self):
  609.         pass
  610.     #
  611.     def addpara(self, p):
  612.         self.paralist.append(p)
  613.         self.height = p.render(self.d, 0, self.height, self.width)
  614.     #
  615.     def redraw(self):
  616.         import gl
  617.         gl.winset(self.wid)
  618.         width = gl.getsize()[1]
  619.         if width <> self.width:
  620.             setdocsize = 1
  621.             self.width = width
  622.             for p in self.paralist:
  623.                 p.top = p.bottom = None
  624.         d = self.d
  625.         v = 0
  626.         for p in self.paralist:
  627.             v = p.render(d, 0, v, width)
  628.