Articles Projects Tips Downloads Contacts About

Pagination in the JEditorPane/JTextPane. Part III (Headers and Footers)
By Stanislav Lapitsky

Advanced printing WYSIWYG feature supposes to have some page specific information Ė headers and footers. Many users would like to see such information. The article describes how to add JEditorPanes with styled content to printed page as header and footer. The article is based on described in the previous articles pagination features.

We assign header and footer as separate JEditorPaneís components stored in the PageableEditorKit, which are referenced in page layout and page drawing. There are kitís fileds assigned by two methods.

    public void setHeader(JEditorPane header) {
        this.header=header;
        header.getDocument().addDocumentListener(relayoutListener);
        isValidHF=false;
    }
    public void setFooter(JEditorPane footer) {
        this.footer=footer;
        footer.getDocument().addDocumentListener(relayoutListener);
        isValidHF=false;
    }

After header or footer is added to PageableEditorKit we have to recalculate their sizes. The above code sets flag about invalid header and footer sizes. When pagination is done the size is recalculated (once!) to avoid multiple time consuming calculations.

Header/footer sizes calculation.

To provide correct layout of content and leave enough space for header and footer we need their actual size. Both header and footer have the same width equals to page width without left and right insets. Height of header and footer mustnít be more than half of available page height so we define the limitation.

To obtain desired height we use JEditorPaneís approach. The same approach is used in JScrollPane. At first we specify some size to fix desired width. Then we ask JEditorPane for preferred size (we are interested in desired height). If the height is bigger than max available height we restrict the height.

    protected void calculateHFSizes() {
        int hfWidth=pageWidth-pageMargins.left-pageMargins.right-2*DRAW_PAGE_INSET;
        int maxHeight=(pageHeight-pageMargins.top-pageMargins.bottom-2*DRAW_PAGE_INSET)/2;

        if (header!=null) {
            header.setSize(hfWidth, pageHeight);
            int hHeight = Math.min(maxHeight, header.getPreferredSize().height);
            header.setSize(hfWidth, hHeight);
        }

        if (footer!=null) {
            footer.setSize(hfWidth, pageHeight);
            int fHeight = Math.min(maxHeight, footer.getPreferredSize().height);
            footer.setSize(hfWidth, fHeight);
        }
    }

    public void validateHF() {
        if (!isValidHF) {
            calculateHFSizes();

            isValidHF=true;
        }
    }

Then we adapt SectionView and PageableParagraphView code to shift content according to specified header and footer heights.

SectionView code

        protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
            super.layoutMajorAxis(targetSpan, axis, offsets, spans);
            //validate header and footer sizes if necessary
            validateHF();
            addHF();

            int n = offsets.length;
            pageNumber = 0;
            int headerHeight=header!=null ? header.getHeight() +HF_SHIFT:0;
            int footerHeight=footer!=null ? footer.getHeight() +HF_SHIFT:0;
            int totalOffset = headerHeight;
            for (int i = 0; i < n; i++) {
                offsets[i] = totalOffset;
                View v = getView(i);
                if (v instanceof MultiPageView) {
                    ( (MultiPageView) v).setBreakSpan(0);
                    ( (MultiPageView) v).setAdditionalSpace(0);
                }

                if ( (offsets[i] + spans[i]) > (pageNumber * pageHeight - DRAW_PAGE_INSET * 2 - 
                      pageMargins.top - pageMargins.bottom-footerHeight)) {
                    if ( (v instanceof MultiPageView) && (v.getViewCount() > 1)) {
                        MultiPageView multipageView = (MultiPageView) v;
                        int space = offsets[i] - (pageNumber - 1) * pageHeight;
                        int breakSpan = (pageNumber * pageHeight - DRAW_PAGE_INSET * 2 - 
                                         pageMargins.top - pageMargins.bottom-footerHeight) - offsets[i];
                        multipageView.setBreakSpan(breakSpan);
                        multipageView.setPageOffset(space);
                        multipageView.setStartPageNumber(pageNumber);
                        multipageView.setEndPageNumber(pageNumber);
                        int height = (int) getHeight();

                        int width = ( (BoxView) v).getWidth();
                        if (v instanceof PageableParagraphView) {
                            PageableParagraphView parView = (PageableParagraphView) v;
                            parView.layout(width, height);
                        }

                        pageNumber = multipageView.getEndPageNumber();
                        spans[i] += multipageView.getAdditionalSpace();
                    }
                    else {
                        offsets[i] = pageNumber * pageHeight+headerHeight;
                        pageNumber++;
                    }
                }
                totalOffset = (int) Math.min( (long) offsets[i] + (long) spans[i], Integer.MAX_VALUE);
            }
        }

PageableParagraphView code

        public void performMultiPageLayout(int targetSpan, int axis, int[] offsets, int[] spans) {
            if (breakSpan == 0)
                return;
            int space = breakSpan;

            additionalSpace = 0;
            endPageNumber = startPageNumber;
            int topInset = this.getTopInset();
            int offs = 0;
            int headerHeight=getHeaderHeight();
            int footerHeight=getFooterHeight();
            for (int i = 0; i < offsets.length; i++) {
                if (offs + spans[i] + topInset > space) {
                    int newOffset = endPageNumber * pageHeight;
                    int addSpace = newOffset - (startPageNumber - 1) * pageHeight - pageOffset - offs - 
                                   topInset-headerHeight;
                    additionalSpace += addSpace;
                    offs += addSpace;
                    for (int j = i; j < offsets.length; j++) {
                        offsets[j] += addSpace;
                    }
                    endPageNumber++;
                    space = (endPageNumber * pageHeight - 2 * DRAW_PAGE_INSET - pageMargins.top - pageMargins.bottom) - 
                            (startPageNumber - 1) * pageHeight - pageOffset-footerHeight;
                }
                offs += spans[i];
            }
        }

        protected int getHeaderHeight() {
            JTextComponent text = (JTextComponent)getContainer();
            if (text!=null && text instanceof JEditorPane && ((JEditorPane)text).getEditorKit() instanceof PageableEditorKit) {
                PageableEditorKit kit=(PageableEditorKit)((JEditorPane)text).getEditorKit();
                if (kit.getHeader()!=null) {
                    return kit.getHeader().getHeight()+PageableEditorKit.HF_SHIFT;
                }
            }
            return 0;
        }
        protected int getFooterHeight() {
            JTextComponent text = (JTextComponent)getContainer();
            if (text!=null && ((JEditorPane)text).getEditorKit() instanceof PageableEditorKit) {
                PageableEditorKit kit=(PageableEditorKit)((JEditorPane)text).getEditorKit();
                if (kit.getFooter()!=null) {
                    return kit.getFooter().getHeight()+PageableEditorKit.HF_SHIFT;
                }
            }
            return 0;
        }

Header footer painting.

The last step is painting of the header and footer. In the paint() method of SectionView we iterate through all pages and paint their frames. In the loop we just add calls paintHeader() and paintFooter() to paint appropriate components on pages.

        public void paint(Graphics g, Shape a) {
            Rectangle alloc = (a instanceof Rectangle) ? (Rectangle) a : a.getBounds();
            Shape baseClip = g.getClip().getBounds();
            int pageCount = getPageCount();
            Rectangle page = new Rectangle();
            page.x = alloc.x;
            page.y = alloc.y;
            page.height = pageHeight;
            page.width = pageWidth;
            String sC = Integer.toString(pageCount);
            for (int i = 0; i < pageCount; i++) {
                page.y = alloc.y + pageHeight * i;
                paintPageFrame(g, page, (Rectangle) baseClip);
                paintHeader(g, i, page);
                paintFooter(g, i, page);
                g.setColor(Color.blue);
                String sN = Integer.toString(i + 1);
                String pageStr = "Page: " + sN;
                pageStr += " of " + sC;
                g.drawString(pageStr,
                             page.x + page.width - 100,
                             page.y + page.height - 3);
            }
            super.paint(g, a);
            g.setColor(Color.gray);
            // Fills background of pages
            int currentWidth = (int) alloc.getWidth();
            int currentHeight = (int) alloc.getHeight();
            int x = page.x + DRAW_PAGE_INSET;
            int y = 0;
            int w = 0;
            int h = 0;
            if (pageWidth < currentWidth) {
                w = currentWidth;
                h = currentHeight;
                g.fillRect(page.x + page.width, alloc.y, w, h);
            }
            if (pageHeight * pageCount < currentHeight) {
                w = currentWidth;
                h = currentHeight;
                g.fillRect(page.x, alloc.y + page.height * pageCount, w, h);
            }
        }

        protected void paintHeader(Graphics g, int pageIndex, Rectangle page) {
            Graphics2D g2d=(Graphics2D)g;
            if (header!=null) {
                AffineTransform old=g2d.getTransform();
                g2d.translate(DRAW_PAGE_INSET+pageMargins.left,DRAW_PAGE_INSET+pageMargins.top+pageIndex*pageHeight);
                boolean isCaretVisible=header.getCaret().isVisible();
                boolean isCaretSelectionVisible=header.getCaret().isSelectionVisible();
                header.getCaret().setVisible(false);
                header.getCaret().setSelectionVisible(false);
                header.paint(g2d);
                header.getCaret().setVisible(isCaretVisible);
                header.getCaret().setSelectionVisible(isCaretSelectionVisible);
                g2d.setColor(Color.lightGray);
                g2d.draw(new Rectangle(-1,-1,header.getWidth()+1,header.getHeight()+1));
                g2d.setTransform(old);
            }
        }
        
        protected void paintFooter(Graphics g, int pageIndex, Rectangle page) {
            Graphics2D g2d=(Graphics2D)g;
            if (footer!=null) {
                AffineTransform old=g2d.getTransform();
                g2d.translate(DRAW_PAGE_INSET+pageMargins.left,(pageIndex+1)*pageHeight-DRAW_PAGE_INSET-
                              pageMargins.bottom-footer.getHeight());
                boolean isCaretVisible=footer.getCaret().isVisible();
                boolean isCaretSelectionVisible=footer.getCaret().isSelectionVisible();
                footer.getCaret().setVisible(false);
                footer.getCaret().setSelectionVisible(false);
                footer.paint(g2d);
                footer.getCaret().setVisible(isCaretVisible);
                footer.getCaret().setSelectionVisible(isCaretSelectionVisible);
                g2d.setColor(Color.lightGray);
                g2d.draw(new Rectangle(-1,-1,footer.getWidth()+1,footer.getHeight()+1));
                g2d.setTransform(old);
            }
        }

Direct editing of header and footer.

It is very comfortable for end user if there is a possibility to edit header and footer directly on editorís page. To let user edit WYSIWYG header and footer content we can add them to parent JEditorPane as usual child component and position them inside real header (or footer) rectangle on page.

When user clicks on header (or footer) rectangle in the main JEditorPane (with pagination) we have to calculate header component bounds and set them accordingly. I used a tricky way to detect lick on header (footer) based on viewToModel() method of root view. In fact every time user clicks on view the method is called to determine appropriate position in the document and set caret accordingly. In the viewToModel() method we check whether click occurred on header or footer by getClickedHFLocation() method call. The method returns location point of clicked header (footer). If user clicks outside headers and footers the method returns null and we call super.viewToModel() method.

        public Point getClickedHFLocation(float x, float y) {
            if (! (x >= DRAW_PAGE_INSET + pageMargins.left
                   && x <= pageWidth - DRAW_PAGE_INSET - pageMargins.right)) {
                return null;
            }
            int headerHeight=getHeader().getHeight();
            int footerHeight=getFooter().getHeight();
            int headerStartY=DRAW_PAGE_INSET + pageMargins.top;
            int footerStartY=pageHeight - DRAW_PAGE_INSET - pageMargins.bottom - footerHeight;
            int hfWidth=pageWidth-pageMargins.left-pageMargins.right-2*DRAW_PAGE_INSET;
            for (int i=0; i<getPageCount(); i++) {
                if (y<headerStartY) {
                    return null;
                }
                if (y<headerStartY+headerHeight) {
                    return new Point(DRAW_PAGE_INSET + pageMargins.left,headerStartY);
                }
                if (y>footerStartY && y<footerStartY+footerHeight) {
                    return new Point(DRAW_PAGE_INSET + pageMargins.left,footerStartY);
                }
                headerStartY+=pageHeight;
                footerStartY+=pageHeight;
            }
            return null;
        }


        public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
            Point location=getClickedHFLocation(x,y);
            if (location!=null) {
                if (location.y % pageHeight < pageHeight / 2) {
                    //header
                    header.setLocation(location);
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            header.requestFocus();
                        }
                    });
                }
                else {
                    //footer
                    footer.setLocation(location);
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            footer.requestFocus();
                        }
                    });
                }
                return -1;
            }
            else {
                return super.viewToModel(x, y, a, bias);
            }
        }

If a click is detected and location is obtained we determine whether header or footer was clicked and change location of appropriate component o fit the clicked rectangle.

Another important part of editing is recalculation of actual sizes and re-layout of the content according to new sizes to provide real time pagination updating process. When user types something we have to get size (height) of header (or footer) and invoke paginated layout of parent view. In case the new height of footer it doesnít equal to old height we have to reposition the component up if height is increased and down if decreased. When header and footer are added to kit we add a document listener. After each change Ė insert, remove or attribute change the relayout() method is called and content is re laid out according to new sizes.

    DocumentListener relayoutListener=new DocumentListener() {
        public void insertUpdate(DocumentEvent e) {
            relayout();
        }

        public void removeUpdate(DocumentEvent e) {
            relayout();
        }

        public void changedUpdate(DocumentEvent e) {
            relayout();
        }

        protected void relayout() {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    JEditorPane parent=null;
                    int lastFooterHeight=0;
                    if (footer !=null && footer.getParent()!=null && footer.getParent() instanceof JEditorPane) {
                        parent=(JEditorPane)footer.getParent();
                        lastFooterHeight=footer.getHeight();
                    }
                    else if (header !=null && header.getParent()!=null && header.getParent() instanceof JEditorPane) {
                        parent=(JEditorPane)header.getParent();
                    }
                    isValidHF = false;
                    validateHF();
                    if (footer!=null && lastFooterHeight!=footer.getHeight()) {
                        int shift=lastFooterHeight-footer.getHeight();
                        footer.setLocation(footer.getX(),footer.getY()+shift);
                    }
                    ( (SectionView) parent.getUI().getRootView(parent).getView(0)).layout(0, Short.MAX_VALUE);
                    parent.repaint();
                }
            });
        }
    };

After all the above code is implemented user can edit header and footer content just on page.

Appendix

Here is the full source code of the headers and footers example.

Back to Table of Content