Articles Projects Tips Downloads Contacts About

Pagination in the JEditorPane/JTextPane
By Stanislav Lapitsky

This article is supposed to be the first from the series of articles about implementing multi-page layout and building advanced WYSIWYG text editors. It will explain how to split paragraph’s rows between pages and paint page decorations around. If you don't need real WYSIWYG editor but just would like to print or print preview existing JEditorPane's content e.g. HTML loaded in JEditorPane you can try another way to print. See the article or download the JEditorPanePrinter.jar.

Constants and terms definition.

Draw page inset – width of space between page’s border and JEditorPane’s border.

Page margins – margins between page’s border and content.

MultiPageView’s additional space – the space which should be added to view’s height when view is separated between pages. In other words when view is separated between pages some view’s children (rows) are moved down. This shift is additional space.

MultiPageView’s start page number – page number where view begins.

MultiPageView’s end page number – page number where view ends. If view is very long it can take several pages and difference between end page number and start page number show how many pages the view takes.

MultiPageView’s break span – space in the paragraph when it should be split. Children (rows) which end before the break span remain on the previous page other are moved on the next page.

MultiPageView’s page offset – offset of view on the start page i.e. space between page top edge and view.

There are images below which help to explain what should be achieved.
Before pagination is done:
Layout before pagination

After pagination is done:
Layout after pagination

Introduction.

There are different kind of multi-page views in WYSIWYG editor e.g. paragraphs, tables etc. This article illustrates how it works for paragraphs but a MultiPageView interface was defined so it may be easy extended for all required kind of views. The interface is supposed to be used with classes which extend BoxView class and uses performMultiPageLayout() method because as extension of layoutMajorAxis() method.

Code of MultiPageView interface.

The implementation consists of three main steps overriding two views Section (root) view (usual BoxView) which lays out children – paragraphs (ParagraphView). The example is based on the StyledEditorKit from SUN so we start form the EditorKit extension. We replace ViewFactory to return PageableParagraphView rather than ParagraphView.

    class PageableViewFactory implements ViewFactory {
        public View create(Element elem) {
            String kind = elem.getName();
            if (kind != null) {
                if (kind.equals(AbstractDocument.ContentElementName)) {
                    return new LabelView(elem);
                } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                    return new PageableParagraphView(elem);
                } else if (kind.equals(AbstractDocument.SectionElementName)) {
                    return new SectionView(elem, View.Y_AXIS);
                } else if (kind.equals(StyleConstants.ComponentElementName)) {
                    return new ComponentView(elem);
                } else if (kind.equals(StyleConstants.IconElementName)) {
                    return new IconView(elem);
                }
            }
            // default to text display
            return new LabelView(elem);
        }
    }

Multi-page ParagraphView.

The new PageableParagraphView implements defined above MultiPageView interface. We define all properties and setters/getters methods of interface. The most important part is performMultiPageLayout() implementation. When paragraph’s children should be laid out two methods layoutMinorAxis() and layoutMajorAxis(0 are called. For the paragraph major axis is Y axis because children should be placed vertically. We call super method of ancestor to evaluate children’s offsets and spans and after that call performMultiPageLayout() to move some rows to the next page.

We go through all rows and check whether current row intersects page end. When such row is found this row and all the rows below are shifted down, 1 added to the end page number and additional space is increased. The space between the end of last row on the previous page and the start of the first row on the next page is added to the additional space. At the same time we change break span and page offset according to new page parameters and the loop continues. It is necessary because paragraph might be bigger than one page and should be split more than once. The main goal of performMultiPageLayout() method is shifting down rows and evaluation of additional space.

        protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
            super.layoutMajorAxis(targetSpan, axis, offsets, spans);
            performMultiPageLayout(targetSpan, axis, offsets, spans);
        }

        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();
            for (int i = 0; i < offsets.length; i++) {
                if (offsets[i] + spans[i] + topInset > space) {
                    int newOffset = endPageNumber * pageHeight;
                    int addSpace = newOffset - (startPageNumber - 1) * pageHeight - pageOffset - offsets[i];
                    additionalSpace += addSpace;
                    for (int j = i; j < offsets.length; j++) {
                        offsets[j] += addSpace;
                    }
                    endPageNumber++;
                    space = (endPageNumber * pageHeight) - (startPageNumber - 1) * pageHeight - pageOffset;
                }
            }
        }

Section view.

The SectionView’s code shrinks paragraph to fit allocated pages’ space and paints page decorations such as page borders, shadow, page numbers etc. So the implementation consists of two parts: children layout and painting.

The SectionView extends the same BoxView as paragraph and the major axis is Y axis as well. Thus to shrink paragraphs we override layoutMajorAxis() method. We allow ancestor BoxView’s method to do main job by calling super layoutMajorAxis() and after that go through them to correct their offsets and spans. We check whether current child view intersects page bottom edge. As soon as we get such child view we try to split it between pages. If the view isn’t instance of MultiPageView and can’t be split we just move it on the next page. In other cases we start following procedure.

  1. Determine break span and page offset.
  2. Set all parameters to prepare multi-page layout.
  3. Call layout() method that in turn calls layoutMajorAxis() and performMultipageLayout() methods.
  4. Extracting calculated parameters end page number and additional space.
  5. Increase the paragraph’s span by the additional space and reset current page number.
        protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
            super.layoutMajorAxis(targetSpan, axis, offsets, spans);
            int totalOffset = 0;
            int n = offsets.length;
            pageNumber = 1;
            for (int i = 0; i < n; i++) {
                offsets[i] = totalOffset;
                View v = getView(i);

                if ((offsets[i] + spans[i]) > (pageNumber * pageHeight - DRAW_PAGE_INSET * 2 - 
                     pageMargins.bottom-pageMargins.top)) {
                    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) - offsets[i];
                        multipageView.setBreakSpan(breakSpan);
                        multipageView.setPageOffset(space);
                        multipageView.setStartPageNumber(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;
                        pageNumber++;
                    }
                }
                totalOffset = (int) Math.min((long) offsets[i] + (long) spans[i], Integer.MAX_VALUE);
            }
        }

After the loop is ended we rearrange paragraphs and rows inside the paragraphs. pageNumber field contains number of the last page i.e. page count. After that the preferred, minimum and maximum size of our root view are fixed. The width equals page width and the height equals page height multiplies page count. Let’s change getXXXSpan() methods accordingly:

        protected void layout(int width, int height) {
            width = pageWidth - 2 * DRAW_PAGE_INSET - pageMargins.left - pageMargins.right;
            this.setInsets((short) (DRAW_PAGE_INSET + pageMargins.top), 
                           (short) (startX + DRAW_PAGE_INSET + pageMargins.left), 
                           (short) (DRAW_PAGE_INSET + pageMargins.bottom), 
                           (short) (DRAW_PAGE_INSET + pageMargins.right));
            super.layout(width, height);
        }

        public float getMaximumSpan(int axis) {
            return getPreferredSpan(axis);
        }

        public float getMinimumSpan(int axis) {
            return getPreferredSpan(axis);
        }

        public float getPreferredSpan(int axis) {
            float span = 0;
            if (axis == View.X_AXIS) {
                span = pageWidth;
            } else {
                span = pageHeight * getPageCount();
            }
            return span;
        }

Now JEditorPane’s WYSIWYG content is arranged as required and we have to paint it with all page decorations. We call super paint() to reflect text and create a loop for painting frame and sign of each page. For the loop we use page count calculated in the layoutMajorAxis(). paintPageFrame() paints borders and shadow around each page and drawString() paints the number of current page. In some cases available width and height of JEditorPane might be bigger than view’s width and height. For these cases we fill extra space to the left and to the bottom of view with background color.

        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.y = alloc.y;
            page.height = pageHeight;
            page.width = pageWidth;
            startX = page.x = (alloc.width - page.width) >> 1;
            for (int i = 0; i < pageCount; i++) {
                page.y = alloc.y + pageHeight * i;
                paintPageFrame(g, page, (Rectangle) baseClip, alloc, i);
            }
            super.paint(g, a);
            g.setColor(Color.lightGray);
            // 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(0,alloc.y, startX, h);
                g.fillRect(page.x + page.width, alloc.y, ((alloc.width - page.width) >> 1), h);
            }
        }

        public void paintPageFrame(Graphics g, Shape page, Rectangle container, Rectangle screen, int pageIndex) {
            Rectangle alloc = (page instanceof Rectangle) ? (Rectangle) page : page.getBounds();
            if (container.intersection(alloc).height <= 0)
                return;
            Color oldColor = g.getColor();

            //borders
            g.setColor(Color.lightGray);
            g.fillRect(alloc.x, alloc.y, alloc.width, DRAW_PAGE_INSET);
            g.fillRect(alloc.x, alloc.y, DRAW_PAGE_INSET, alloc.height);
            g.fillRect(alloc.x, alloc.y + alloc.height - DRAW_PAGE_INSET, alloc.width, DRAW_PAGE_INSET);
            g.fillRect(alloc.x + alloc.width - DRAW_PAGE_INSET, alloc.y, DRAW_PAGE_INSET, alloc.height);

            //frame
            g.setColor(Color.black);
            g.drawLine(alloc.x + DRAW_PAGE_INSET, alloc.y + DRAW_PAGE_INSET, alloc.x + alloc.width - DRAW_PAGE_INSET, 
                       alloc.y + DRAW_PAGE_INSET);
            g.drawLine(alloc.x + DRAW_PAGE_INSET, alloc.y + DRAW_PAGE_INSET, alloc.x + DRAW_PAGE_INSET, 
                       alloc.y + alloc.height - DRAW_PAGE_INSET);
            g.drawLine(alloc.x + DRAW_PAGE_INSET, alloc.y + alloc.height - DRAW_PAGE_INSET, 
                       alloc.x + alloc.width - DRAW_PAGE_INSET, alloc.y + alloc.height - DRAW_PAGE_INSET);
            g.drawLine(alloc.x + alloc.width - DRAW_PAGE_INSET, alloc.y + DRAW_PAGE_INSET, 
                       alloc.x + alloc.width - DRAW_PAGE_INSET, alloc.y + alloc.height - DRAW_PAGE_INSET);

            //shadow
            g.fillRect(alloc.x + alloc.width - DRAW_PAGE_INSET, alloc.y + DRAW_PAGE_INSET + 4, 4, 
                       alloc.height - 2 * DRAW_PAGE_INSET);
            g.fillRect(alloc.x + DRAW_PAGE_INSET + 4, alloc.y + alloc.height - DRAW_PAGE_INSET, 
                       alloc.width - 2 * DRAW_PAGE_INSET, 4);

            if(parentPane.isDrawBorder()){
                g.setColor(Color.black);
                g.drawRect(alloc.x + pageMargins.left-1,alloc.y + pageMargins.top-1,alloc.width - 
                           pageMargins.left- pageMargins.right + 1,alloc.height- pageMargins.top- pageMargins.bottom+1);
            }

            if(parentPane.getHeader() != null){
                g.drawString(parentPane.getHeader(),alloc.x + pageMargins.left,(int)(alloc.y + 72 * 0.6));
            }

            if(parentPane.isPrintDate()){
                Date date = new java.util.Date();
                g.drawString(date.toString(),alloc.x + pageMargins.left,(int)(alloc.y + alloc.height - 72 * 0.3));
            }
        }

Now the PageableEditorKit is ready and we assign the kit to the JEditorPane component.

Appendix

Here is the full source code of the pagination example.

Back to Table of Content