platypus: add support for FramSplitter flowable
authorrgbecker
Mon, 18 Aug 2008 14:42:09 +0000
changeset 2950 6c243412b06c
parent 2948 6864223fb014
child 2951 95785ed31090
platypus: add support for FramSplitter flowable
reportlab/platypus/doctemplate.py
reportlab/platypus/flowables.py
reportlab/platypus/frames.py
--- a/reportlab/platypus/doctemplate.py	Thu Aug 14 14:14:22 2008 +0000
+++ b/reportlab/platypus/doctemplate.py	Mon Aug 18 14:42:09 2008 +0000
@@ -204,7 +204,7 @@
 
     def frameAction(self,frame):
         if not frame._atTop:
-            frame._generated_content = [PageBreak()]
+            frame.add_generated_content(PageBreak())
 
 class NextPageTemplate(ActionFlowable):
     """When you get to the next page, use the template specified (change to two column, for example)  """
@@ -488,20 +488,19 @@
         ''' Handles the semantics of the end of a frame. This includes the selection of
             the next frame or if this is the last frame then invoke pageEnd.
         '''
-
         self._leftExtraIndent = self.frame._leftExtraIndent
         self._rightExtraIndent = self.frame._rightExtraIndent
 
+        f = self.frame
         if hasattr(self,'_nextFrameIndex'):
             self.frame = self.pageTemplate.frames[self._nextFrameIndex]
             self.frame._debug = self._debug
             del self._nextFrameIndex
             self.handle_frameBegin(resume)
-        elif hasattr(self.frame,'lastFrame') or self.frame is self.pageTemplate.frames[-1]:
+        elif hasattr(f,'lastFrame') or f is self.pageTemplate.frames[-1]:
             self.handle_pageEnd()
             self.frame = None
         else:
-            f = self.frame
             self.frame = self.pageTemplate.frames[self.pageTemplate.frames.index(f) + 1]
             self.frame._debug = self._debug
             self.handle_frameBegin()
@@ -541,7 +540,7 @@
             #ensure we start on the first one
             self._nextPageTemplateCycle = c.cyclicIterator()
         else:
-            raise TypeError, "argument pt should be string or integer or list"
+            raise TypeError("argument pt should be string or integer or list")
 
     def handle_nextFrame(self,fx,resume=0):
         '''On endFrame change to the frame with name or index fx'''
@@ -550,7 +549,7 @@
                 if f.id == fx:
                     self._nextFrameIndex = self.pageTemplate.frames.index(f)
                     return
-            raise ValueError, "can't find frame('%s')"%fx
+            raise ValueError("can't find frame('%s') in %r(%s) which has frames %r"%(fx,self.pageTemplate,self.pageTemplate.id,[(f,f.id) for f in self.pageTemplate.frames]))
         elif type(fx) is IntType:
             self._nextFrameIndex = fx
         else:
@@ -558,6 +557,7 @@
 
     def handle_currentFrame(self,fx,resume=0):
         '''change to the frame with name or index fx'''
+
         self.handle_nextFrame(fx,resume)
         self.handle_frameEnd(resume)
 
@@ -609,6 +609,13 @@
         finally:
             if frame: del f._frame
 
+    def _addGeneratedContent(self,flowables,frame):
+        S = getattr(frame,'_generated_content',None)
+        if S:
+            for i,f in enumerate(S):
+                flowables.insert(i,f)
+            del frame._generated_content
+
     def handle_flowable(self,flowables):
         '''try to handle one flowable from the front of list flowables.'''
 
@@ -634,31 +641,28 @@
             self.afterFlowable(f)
         else:
             frame = self.frame
+            canv = self.canv
             #try to fit it then draw it
-            if frame.add(f, self.canv, trySplit=self.allowSplitting):
+            if frame.add(f, canv, trySplit=self.allowSplitting):
                 if not isinstance(f,FrameActionFlowable):
                     self._curPageFlowableCount += 1
                     self.afterFlowable(f)
-                else:
-                    S = getattr(frame,'_generated_content',None)
-                    if S:
-                        for i,f in enumerate(S):
-                            flowables.insert(i,f)
-                        del frame._generated_content
+                self._addGeneratedContent(flowables,frame)
             else:
                 if self.allowSplitting:
                     # see if this is a splittable thing
-                    S = frame.split(f,self.canv)
+                    S = frame.split(f,canv)
                     n = len(S)
                 else:
                     n = 0
                 if n:
                     if not isinstance(S[0],(PageBreak,SlowPageBreak,ActionFlowable)):
-                        if frame.add(S[0], self.canv, trySplit=0):
+                        if frame.add(S[0], canv, trySplit=0):
                             self._curPageFlowableCount += 1
                             self.afterFlowable(S[0])
+                            self._addGeneratedContent(flowables,frame)
                         else:
-                            ident = "Splitting error(n==%d) on page %d in\n%s" % (n,self.page,self._fIdent(f,30,frame))
+                            ident = "Splitting error(n==%d) on page %d in\n%s" % (n,self.page,self._fIdent(f,60,frame))
                             #leave to keep apart from the raise
                             raise LayoutError(ident)
                         del S[0]
@@ -666,7 +670,7 @@
                         flowables.insert(i,f)   # put split flowables back on the list
                 else:
                     if hasattr(f,'_postponed'):
-                        ident = "Flowable %s too large on page %d" % (self._fIdent(f,30,frame), self.page)
+                        ident = "Flowable %s too large on page %d" % (self._fIdent(f,60,frame), self.page)
                         #leave to keep apart from the raise
                         raise LayoutError(ident)
                     # this ought to be cleared when they are finally drawn!
@@ -735,34 +739,41 @@
 
         #pagecatcher can drag in information from embedded PDFs and we want ours
         #to take priority, so cache and reapply our own info dictionary after the build.
-        self._savedInfo = self.canv._doc.info
+        canv = self.canv
+        self._savedInfo = canv._doc.info
         handled = 0
-        while len(flowables):
-            self.clean_hanging()
-            try:
-                first = flowables[0]
-                self.handle_flowable(flowables)
-                handled += 1
-            except:
-                #if it has trace info, add it to the traceback message.
-                if hasattr(first, '_traceInfo') and first._traceInfo:
-                    exc = sys.exc_info()[1]
-                    args = list(exc.args)
-                    tr = first._traceInfo
-                    args[0] += '\n(srcFile %s, line %d char %d to line %d char %d)' % (
-                        tr.srcFile,
-                        tr.startLineNo,
-                        tr.startLinePos,
-                        tr.endLineNo,
-                        tr.endLinePos
-                        )
-                    exc.args = tuple(args)
-                raise
-            if self._onProgress:
-                self._onProgress('PROGRESS',flowableCount - len(flowables))
+
+        try:
+            canv._doctemplate = self
+            while len(flowables):
+                self.clean_hanging()
+                try:
+                    first = flowables[0]
+                    self.handle_flowable(flowables)
+                    handled += 1
+                except:
+                    #if it has trace info, add it to the traceback message.
+                    if hasattr(first, '_traceInfo') and first._traceInfo:
+                        exc = sys.exc_info()[1]
+                        args = list(exc.args)
+                        tr = first._traceInfo
+                        args[0] += '\n(srcFile %s, line %d char %d to line %d char %d)' % (
+                            tr.srcFile,
+                            tr.startLineNo,
+                            tr.startLinePos,
+                            tr.endLineNo,
+                            tr.endLinePos
+                            )
+                        exc.args = tuple(args)
+                    raise
+                if self._onProgress:
+                    self._onProgress('PROGRESS',flowableCount - len(flowables))
+        finally:
+            del canv._doctemplate
+
 
         #reapply pagecatcher info
-        self.canv._doc.info = self._savedInfo
+        canv._doc.info = self._savedInfo
 
         self._endBuild()
         if self._onProgress:
--- a/reportlab/platypus/flowables.py	Thu Aug 14 14:14:22 2008 +0000
+++ b/reportlab/platypus/flowables.py	Mon Aug 18 14:42:09 2008 +0000
@@ -176,8 +176,12 @@
             r = r[:maxLen]
         return "<%s at %s%s>%s" % (self.__class__.__name__, hex(id(self)), self._frameName(), r)
 
+    def _doctemplateAttr(self,a):
+        return getattr(getattr(getattr(self,'canv',None),'_doctemplate',None),a,None)
+
     def _frameName(self):
         f = getattr(self,'_frame',None)
+        if not f: f = self._doctemplateAttr('frame')
         if f and f.id: return ' frame=%s' % f.id
         return ''
 
@@ -433,7 +437,7 @@
     pass
 
 class CondPageBreak(Spacer):
-    """Throw a page if not enough vertical space"""
+    """use up a frame if not enough vertical space effectively CondFrameBreak"""
     def __init__(self, height):
         self.height = height
 
@@ -442,8 +446,14 @@
 
     def wrap(self, availWidth, availHeight):
         if availHeight<self.height:
-            return (availWidth, availHeight)
-        return (0, 0)
+            f = self._doctemplateAttr('frame')
+            if not f: return availWidth, availHeight
+            from doctemplate import FrameBreak
+            f.add_generated_content(FrameBreak)
+        return 0, 0
+
+    def identity(self,maxLen=None):
+        return repr(self).replace(')',',frame=%s)'%self._frameName())
 
 def _listWrapOn(F,availWidth,canv,mergeSpace=1,obj=None,dims=None):
     '''return max width, required height for a list of flowables F'''
@@ -1104,3 +1114,56 @@
 
     def draw(self):
         self.canv.bookmarkHorizontal(self._name,0,0)
+
+class FrameSplitter(Flowable):
+    '''When encountered this flowable should either switch directly to nextTemplate
+    if remaining space in the current frame is less than gap+required or it should
+    temporarily modify the current template to have the frames from nextTemplate
+    that are listed in nextFrames and switch to the first of those frames. 
+    '''
+    _ZEROSIZE=1
+    def __init__(self,nextTemplate,nextFrames=[],gap=10,required=72):
+        self.nextTemplate=nextTemplate
+        self.nextFrames=nextFrames
+        self.gap=gap
+        self.required=required
+
+    def wrap(self,aW,aH):
+        frame = self._frame
+        from reportlab.platypus.doctemplate import NextPageTemplate,CurrentFrameFlowable,LayoutError
+        G=[NextPageTemplate(self.nextTemplate)]
+        if aH<self.gap+self.required-_FUZZ:
+            #we are going straight to the nextTemplate with no attempt to modify the frames
+            G.append(PageBreak())
+        else:
+            #we are going to modify the incoming templates
+            templates = self._doctemplateAttr('pageTemplates')
+            if templates is None:
+                raise LayoutError('%s called in non-doctemplate environment'%self.identity())
+            T=[t for t in templates if t.id==self.nextTemplate]
+            if not T:
+                raise LayoutError('%s.nextTemplate=%s not found' % (self.identity(),self.nextTemplate))
+            T=T[0]
+            F=[f for f in T.frames if f.id in self.nextFrames]
+            N=[f.id for f in F]
+            N=[f for f in self.nextFrames if f not in N]
+            if N:
+                raise LayoutError('%s frames=%r not found in pageTemplate(%s)\n%r has frames %r' % (self.identity(),N,T.id,T,[f.id for f in T.frames]))
+            T=self._doctemplateAttr('pageTemplate')
+            def unwrap(canv,doc,T=T,onPage=T.onPage,oldFrames=T.frames):
+                T.frames=oldFrames
+                T.onPage=onPage
+                onPage(canv,doc)
+            T.onPage=unwrap
+            h=aH-self.gap
+            for i,f in enumerate(F):
+                f=copy(f)
+                f.height=h
+                f._reset()
+                F[i]=f
+            T.frames=F
+            G.append(CurrentFrameFlowable(F[0].id))
+        frame.add_generated_content(*G)
+        return 0,0
+    def draw(self):
+        pass
--- a/reportlab/platypus/frames.py	Thu Aug 14 14:14:22 2008 +0000
+++ b/reportlab/platypus/frames.py	Mon Aug 18 14:42:09 2008 +0000
@@ -91,6 +91,22 @@
         else:
             self.__dict__[a] = v
 
+    def _saveGeom(self, **kwds):
+        if not self.__dict__.setdefault('_savedGeom',{}):
+            for ga in _geomAttr:
+                ga = '_'+ga
+                self.__dict__['_savedGeom'][ga] = self.__dict__[ga]
+        for k,v in kwds.iteritems():
+            setattr(self,k,v)
+
+    def _restoreGeom(self):
+        if self.__dict__.get('_savedGeom',None):
+            for ga in _geomAttr:
+                ga = '_'+ga
+                self.__dict__[ga] = self.__dict__[ga]['_savedGeom']
+                del self.__dict__['_savedGeom']
+            self._geom()
+
     def _geom(self):
         self._x2 = self._x1 + self._width
         self._y2 = self._y1 + self._height
@@ -101,6 +117,7 @@
         self._aH = self._y2 - self._y1p - self._topPadding
 
     def _reset(self):
+        self._restoreGeom()
         #drawing starts at top left
         self._x = self._x1 + self._leftPadding
         self._y = self._y2 - self._topPadding
@@ -122,48 +139,49 @@
         Raises a LayoutError if the object is too wide,
         or if it is too high for a totally empty frame,
         to avoid infinite loops"""
-        if getattr(flowable,'frameAction',None):
-            flowable.frameAction(self)
-            return 1
+        flowable._frame = self
+        flowable.canv = canv #so they can use stringWidth etc
+        try:
+            if getattr(flowable,'frameAction',None):
+                flowable.frameAction(self)
+                return 1
 
-        y = self._y
-        p = self._y1p
-        s = 0
-        aW = self._getAvailableWidth()
-        if not self._atTop:
-            s =flowable.getSpaceBefore()
-            if self._oASpace:
-                s = max(s-self._prevASpace,0)
-        h = y - p - s
-        if h>0:
-            flowable._frame = self
-            flowable.canv = canv #so they can use stringWidth etc
-            w, h = flowable.wrap(aW, h)
-            del flowable.canv, flowable._frame
-        else:
-            return 0
+            y = self._y
+            p = self._y1p
+            s = 0
+            aW = self._getAvailableWidth()
+            if not self._atTop:
+                s =flowable.getSpaceBefore()
+                if self._oASpace:
+                    s = max(s-self._prevASpace,0)
+            h = y - p - s
+            if h>0:
+                w, h = flowable.wrap(aW, h)
+            else:
+                return 0
 
-        h += s
-        y -= h
+            h += s
+            y -= h
 
-        if y < p-_FUZZ:
-            if not rl_config.allowTableBoundsErrors and ((h>self._aH or w>aW) and not trySplit):
-                from reportlab.platypus.doctemplate import LayoutError
-                raise LayoutError("Flowable %s (%sx%s points) too large for frame (%sx%s points)." % (
-                    flowable.__class__, w,h, aW,self._aH))
-            return 0
-        else:
-            #now we can draw it, and update the current point.
-            flowable._frame = self
-            flowable.drawOn(canv, self._x + self._leftExtraIndent, y, _sW=aW-w)
-            if self._debug: logger.debug('drew %s' % flowable.identity())
-            del flowable._frame
-            s = flowable.getSpaceAfter()
-            y -= s
-            if self._oASpace: self._prevASpace = s
-            if y!=self._y: self._atTop = 0
-            self._y = y
-            return 1
+            if y < p-_FUZZ:
+                if not rl_config.allowTableBoundsErrors and ((h>self._aH or w>aW) and not trySplit):
+                    from reportlab.platypus.doctemplate import LayoutError
+                    raise LayoutError("Flowable %s (%sx%s points) too large for frame (%sx%s points)." % (
+                        flowable.__class__, w,h, aW,self._aH))
+                return 0
+            else:
+                #now we can draw it, and update the current point.
+                flowable.drawOn(canv, self._x + self._leftExtraIndent, y, _sW=aW-w)
+                flowable.canv=canv
+                if self._debug: logger.debug('drew %s' % flowable.identity())
+                s = flowable.getSpaceAfter()
+                y -= s
+                if self._oASpace: self._prevASpace = s
+                if y!=self._y: self._atTop = 0
+                self._y = y
+                return 1
+        finally:
+            del flowable.canv, flowable._frame
 
     add = _add
 
@@ -220,3 +238,6 @@
             else:
                 #leave it in the list for later
                 break
+
+    def add_generated_content(self,*C):
+        self.__dict__.setdefault('_generated_content',[]).extend(C)