Made Index entries clickable.
authorjonas
Tue, 02 Jun 2009 11:33:20 +0000
changeset 3162 77afde998aa9
parent 3161 5717a3003dc3
child 3163 1d8b6e22b07f
Made Index entries clickable.
src/reportlab/platypus/tableofcontents.py
tests/test_platypus_index.py
--- a/src/reportlab/platypus/tableofcontents.py	Thu May 28 09:34:43 2009 +0000
+++ b/src/reportlab/platypus/tableofcontents.py	Tue Jun 02 11:33:20 2009 +0000
@@ -52,6 +52,12 @@
 from reportlab.platypus.tables import TableStyle, Table
 from reportlab.platypus.flowables import Spacer, Flowable
 from reportlab.pdfbase.pdfmetrics import stringWidth
+from reportlab.pdfgen import canvas
+from xml.sax.saxutils import escape, quoteattr, unescape
+
+def unquote(txt):
+    return unescape(txt, {"'": "'", """: '"'})
+
 try:
     set
 except:
@@ -59,14 +65,23 @@
         def add(self,x):
             if x not in self:
                 list.append(self,x)
-
-def drawPageNumbers(canvas, style, pagestr, availWidth, availHeight, dot=' . '):
+    
+def _getArgs(*args,**kw):
+    return args, kw
+    
+def _evalArgs(data):
+    if data[0]!='(': data = '(%s)' % data
+    return eval('_getArgs'+data)
+    
+def drawPageNumbers(canvas, style, pages, availWidth, availHeight, dot=' . '):
     '''
     Draws pagestr on the canvas using the given style.
     If dot is None, pagestr is drawn at the current position in the canvas.
     If dot is a string, pagestr is drawn right-aligned. If the string is not empty,
     the gap is filled with it.
     '''
+    pages.sort(cmp=lambda a,b: cmp(a[0], b[0]))
+    pagestr = ', '.join((str(p) for p, _ in pages))
     x, y = canvas._curr_tx_info['cur_x'], canvas._curr_tx_info['cur_y']
     pagestrw = stringWidth(pagestr, style.fontName, style.fontSize)
     if isinstance(dot, basestring):
@@ -76,10 +91,12 @@
         else:
             dotsn = dotw = 0
         text = '%s%s' % (dotsn * dot, pagestr)
-        newx = availWidth-dotsn*dotw-pagestrw
+        newx = availWidth - dotsn*dotw - pagestrw
+        pagex = availWidth - pagestrw
     elif dot is None:
         text = ',  ' + pagestr
         newx = x
+        pagex = newx
     else:
         raise TypeError('Argument dot should either be None or an instance of basestring.')
 
@@ -88,6 +105,14 @@
     tx.setFillColor(style.textColor)
     tx.textLine(text)
     canvas.drawText(tx)
+   
+    commaw = stringWidth(', ', style.fontName, style.fontSize)
+    for p, key in pages:
+        if not key:
+            continue
+        w = stringWidth(str(p), style.fontName, style.fontSize)
+        canvas.linkRect('', key, (pagex, y, pagex+w, y+style.leading), relative=1)
+        pagex += w + commaw
 
 # Default paragraph styles for tables of contents.
 # (This could also be generated automatically or even
@@ -209,7 +234,7 @@
                 dot = ' . ' 
             else: 
                 dot = ''
-            drawPageNumbers(canvas, style, str(page), availWidth, availHeight, dot)
+            drawPageNumbers(canvas, style, [(page, None)], availWidth, availHeight, dot)
         self.canv.drawTOCEntryEnd = drawTOCEntryEnd
 
         tableData = []
@@ -266,6 +291,31 @@
         self.textStyle = style
         self.tableStyle = tableStyle or defaultTableStyle
         self.dot = dot
+    
+    def getCanvasMaker(self, canvasmaker=canvas.Canvas):
+        def cb(canv,kind,label):
+            label = unquote(label)
+            try:
+                args, kwargs = _evalArgs(label)
+                if kwargs:
+                    raise SyntaxError()
+            except (NameError, SyntaxError):
+                pass
+            else:
+                label = args
+            key = 'idx_%s_p_%s' % (','.join(label), canv.getPageNumber())
+            
+            info = canv._curr_tx_info
+            canv.bookmarkHorizontal(key, info['cur_x'], info['cur_y'] + info['leading'])
+            self.addEntry(label, canv.getPageNumber(), key)
+        
+        def newcanvasmaker(*args, **kwargs):
+            from reportlab.pdfgen import canvas
+            c = canvasmaker(*args, **kwargs)
+            c._indexAdd = cb
+            return c
+        
+        return newcanvasmaker
 
     def isIndexing(self):
         return 1
@@ -290,9 +340,9 @@
             (text, pageNum) = stuff
             self.addEntry(text, pageNum)
 
-    def addEntry(self, text, pageNum):
+    def addEntry(self, text, pageNum, key=None):
         """Allows incremental buildup"""
-        self._entries.setdefault(makeTuple(text),set([])).add(pageNum)
+        self._entries.setdefault(makeTuple(text),set([])).add((pageNum, key))
 
     def split(self, availWidth, availHeight):
         """At this stage we do not care about splitting the entries,
@@ -302,7 +352,7 @@
         """
         return self._flowable.splitOn(self.canv,availWidth, availHeight)
 
-    def _getlastEntries(self, dummy=[(['Placeholder for index'],[0,1,2])]):
+    def _getlastEntries(self, dummy=[(['Placeholder for index'],enumerate((None,)*3))]):
         '''Return the last run's entries!  If there are none, returns dummy.'''
         if not self._lastEntries:
             if self._entries:
@@ -317,7 +367,8 @@
         def drawIndexEntryEnd(canvas, kind, label):
             '''Callback to draw dots and page numbers after each entry.'''
             style = self.getLevelStyle(0)
-            drawPageNumbers(canvas, style, label, availWidth, availHeight, self.dot)
+            pages = eval(unquote(label))
+            drawPageNumbers(canvas, style, pages, availWidth, availHeight, self.dot)
         self.canv.drawIndexEntryEnd = drawIndexEntryEnd
 
         tableData = []
@@ -328,7 +379,7 @@
             if diff:
                 lastTexts = texts
                 texts = texts[i:]
-            texts[-1] = '%s<onDraw name="drawIndexEntryEnd" label="%s"/>' % (texts[-1], ', '.join(map(str, pageNumbers)))
+            texts[-1] = '%s<onDraw name="drawIndexEntryEnd" label=%s/>' % (texts[-1], quoteattr(repr(list(pageNumbers))))
             for text in texts:
                 style = self.getLevelStyle(i)
                 para = Paragraph(text, style)
@@ -387,7 +438,8 @@
         def drawIndexEntryEnd(canvas, kind, label):
             '''Callback to draw dots and page numbers after each entry.'''
             style = self.getLevelStyle(1)
-            drawPageNumbers(canvas, style, label, availWidth, availHeight, self.dot)
+            pages = eval(unquote(label))
+            drawPageNumbers(canvas, style, pages, availWidth, availHeight, self.dot)
         self.canv.drawIndexEntryEnd = drawIndexEntryEnd
 
         alpha = ''
@@ -408,7 +460,7 @@
             if diff:
                 lastTexts = texts
                 texts = texts[i:]
-            texts[-1] = '%s<onDraw name="drawIndexEntryEnd" label="%s"/>' % (texts[-1], ', '.join(map(str, pageNumbers)))
+            texts[-1] = '%s<onDraw name="drawIndexEntryEnd" label=%s/>' % (texts[-1], quoteattr(repr(list(pageNumbers))))
             for text in texts:
                 style = self.getLevelStyle(i+1)
                 para = Paragraph(text, style)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_platypus_index.py	Tue Jun 02 11:33:20 2009 +0000
@@ -0,0 +1,108 @@
+#Copyright ReportLab Europe Ltd. 2000-2008
+#see license.txt for license details
+"""Tests for the Platypus SimpleIndex and AlphabeticIndex classes.
+"""
+__version__='''$Id$'''
+from reportlab.lib.testutils import setOutDir,makeSuiteForClasses, outputfile, printLocation
+setOutDir(__name__)
+import sys, os
+from os.path import join, basename, splitext
+from math import sqrt
+import unittest
+from reportlab.lib.units import inch, cm
+from reportlab.lib.pagesizes import A4
+from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
+from reportlab.platypus.paragraph import Paragraph
+from reportlab.platypus.xpreformatted import XPreformatted
+from reportlab.platypus.frames import Frame
+from reportlab.platypus.doctemplate \
+     import PageTemplate, BaseDocTemplate
+from reportlab.platypus.tableofcontents import SimpleIndex, AlphabeticIndex
+from reportlab.lib import randomtext
+import re
+from xml.sax.saxutils import quoteattr
+
+def myMainPageFrame(canvas, doc):
+    "The page frame used for all PDF documents."
+
+    canvas.saveState()
+
+    canvas.rect(2.5*cm, 2.5*cm, 15*cm, 25*cm)
+    canvas.setFont('Times-Roman', 12)
+    pageNumber = canvas.getPageNumber()
+    canvas.drawString(10*cm, cm, str(pageNumber))
+
+    canvas.restoreState()
+
+
+class MyDocTemplate(BaseDocTemplate):
+    "The document template used for all PDF documents."
+
+    _invalidInitArgs = ('pageTemplates',)
+
+    def __init__(self, filename, **kw):
+        frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1')
+        self.allowSplitting = 0
+        apply(BaseDocTemplate.__init__, (self, filename), kw)
+        template = PageTemplate('normal', [frame1], myMainPageFrame)
+        self.addPageTemplates(template)
+
+
+    def afterFlowable(self, flowable):
+        "Registers TOC entries."
+
+        if flowable.__class__.__name__ == 'Paragraph':
+            styleName = flowable.style.name
+            if styleName[:7] == 'Heading':
+                key = str(hash(flowable))
+                self.canv.bookmarkPage(key)
+
+                # Register TOC entries.
+                level = int(styleName[7:])
+                text = flowable.getPlainText()
+                pageNum = self.page
+                # Try calling this with and without a key to test both
+                # Entries of every second level will have links, others won't
+                if level % 2 == 1:
+                    self.notify('TOCEntry', (level, text, pageNum, key))
+                else:
+                    self.notify('TOCEntry', (level, text, pageNum))
+
+def makeBodyStyle():
+    "Body text style - the default will do"
+    return ParagraphStyle('body', spaceBefore=20)
+    
+class IndexTestCase(unittest.TestCase):
+    "Test (Simple|Alphabetic)Index classes (eyeball-test)."
+
+    def test0(self):
+        # Build story.
+        
+        for cls in SimpleIndex, AlphabeticIndex:
+            path = outputfile('test_platypus_%s.pdf' % cls.__name__.lower())
+            doc = MyDocTemplate(path)
+            story = []
+            styleSheet = getSampleStyleSheet()
+            bt = styleSheet['BodyText']
+    
+            description = '<font color=red>%s</font>' % self.test0.__doc__
+            story.append(XPreformatted(description, bt))
+            index = cls(None, ' . ')
+    
+            for i in range(20):
+                words = randomtext.randomText(randomtext.PYTHON, 5).split(' ')
+                txt = ' '.join((('<onDraw name="_indexAdd" label=%s/>%s' % (quoteattr(repr(w)), w) if len(w) > 5 else w) for w in words))
+                para = Paragraph(txt, makeBodyStyle())
+                story.append(para)
+            story.append(index)
+    
+            doc.multiBuild(story, canvasmaker=index.getCanvasMaker())
+
+def makeSuite():
+    return makeSuiteForClasses(IndexTestCase)
+
+
+#noruntests
+if __name__ == "__main__":
+    unittest.TextTestRunner().run(makeSuite())
+    printLocation()