Articles Projects Tips Downloads Contacts About

Multicolumn text in the JEditorPane/JTextPane.
By Stanislav Lapitsky

Newspaper like style of text flow supposes fixed width of each column and moving text to the top of next column after previous is filled. An example implementation in the article provides such text flow. When first column is overflowed the JEditorPane doesn’t increase height but adds one more column and continues text there.
Adding new columns increases preferred width instead. So when text is bigger than available space JEditorPane will be increased horizontally and horizontal scroll bar will appear keeping height the same.

Basement of implementation.

The following pictures illustrate how the layout is done and how paragraphs get their location and bounds. The layout is performed in 3 main steps.

  1. Layout with fixed width (column width) to adjust all paragraphs’ rows.
  2. Detecting paragraph’s bounds and first rows’ location. If paragraph fills more than one column bounds are sum off columns’ bounds filled by the paragraph.
  3. Placing paragraph to appropriate locations.

Picture 1. Shows original layout.

Picture 2. Moves original layout to one column layout (fixed content width).

Picture 3. Paragraphs’ shapes.

Picture 4. Result layout.

Layout is done and text flows through 3 columns.

Implementation.

Now let’s see the code to achieve the behavior described above. Overriding the layout method and pass fixed column width to super method like this

    protected void layout(int width, int height) {
        columnHeight=height;
        super.layout(columnWidth,height);
    }

provides one big column of text.

Next two methods layoutMajorAxis and layoutMinorAxis provide spans and offsets of all children (paragraphs) so we override them and fill paragraphs sizes there by calling the performMultiColumnLayout. Starting from first paragraph we lay out paragraph’s rows measuring remained available height. As soon as next row’s height is bigger then the rest we add one more column to the paragraph and the new row starts from top of new column. Paragraph may fill more than two columns so we iterate the same approach increasing paragraph’s column. After paragraph’s content is layed out we know how many columns it fills and the rest available height in last column. The two values we pass to next paragraph layout method.

    protected void layoutMajorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
        super.layoutMajorAxis(targetSpan,axis,offsets,spans);
        majorTargetSpan=targetSpan;
        majorOffsets=offsets;
        majorSpans=spans;
        performMultiColumnLayout();
        for (int i=0; i<offsets.length; i++) {
            spans[i]=columnHeight;
            offsets[i]=0;
        }
    }

    protected void layoutMinorAxis(int targetSpan, int axis, int[] offsets, int[] spans) {
        super.layoutMinorAxis(targetSpan,axis,offsets,spans);
        minorTargetSpan=targetSpan;
        minorOffsets=offsets;
        minorSpans=spans;
        performMultiColumnLayout();
        for (int i=0; i<offsets.length; i++) {
            View v=getView(i);
            if (v instanceof MultiColumnParagraphView) {
                MultiColumnParagraphView par=(MultiColumnParagraphView)v;
                spans[i] = par.columnCount*columnWidth;
                offsets[i]=par.columnNumber*columnWidth;
            }
        }
    }

    protected void performMultiColumnLayout() {
        if (majorOffsets==null || minorOffsets==null || minorOffsets.length!=majorOffsets.length) {
            return;
        }
        int childCount=majorOffsets.length;
        int verticalStartOffset=0;
        int columnNumber=0;
        starts=new Point[childCount];
        for (int i=0; i<childCount; i++) {
            View v=getView(i);
            starts[i]=new Point();
            if (v instanceof MultiColumnParagraphView) {
                MultiColumnParagraphView par=(MultiColumnParagraphView)v;
                par.verticalStartOffset=verticalStartOffset;
                par.columnWidth=columnWidth;
                par.columnHeight=columnHeight;
                par.columnNumber=columnNumber;
                par.performMultiColumnLayout();
                starts[i].y=verticalStartOffset;
                starts[i].x=columnNumber*columnWidth;
                verticalStartOffset=columnHeight-par.restHeight;
                columnNumber+=par.columnCount-1;
            }
        }
        columnCount = columnNumber + 1;
    }

To provide correct measuring we override root view’s methods

    public float getPreferredSpan(int axis) {
        if (axis==View.Y_AXIS) {
            return columnHeight;
        }
        else {
            return columnWidth*columnCount;
        }
    }
    public float getMinimumSpan(int axis) {
        if (axis==View.Y_AXIS) {
            return columnHeight;
        }
        else {
            return columnWidth*columnCount;
        }
    }
    public float getMaximumSpan(int axis) {
        if (axis==View.Y_AXIS) {
            return columnHeight;
        }
        else {
            return columnWidth*columnCount;
        }
    }

Then we correct viewToModel method to provide correct mouse click detecting and positioning of caret after user clicks on view.

    public int viewToModel(float x, float y, Shape a, Position.Bias[] bias) {
        //define child container
        if (starts!=null) {
            for (int i=starts.length-1; i>0; i--) {
                if ((starts[i].x<x && starts[i].y<y)
                    || (starts[i].x+columnWidth<x)
                    ){
                    return getView(i).viewToModel(x,y,a,bias);
                }
            }
        }
        return getView(0).viewToModel(x,y,a,bias);
    }

The same approach is used in each paragraph view. Additionally getChildAllocation, getViewIndexAtPoint, getViewAtPoint are corrected to provide proper view rendering and caret navigation.

    protected int getViewIndexAtPoint(int x, int y, Rectangle alloc) {
        if (starts!=null) {
            for (int i=starts.length-1; i>0; i--) {
                if ((starts[i].x<x && starts[i].y<y)
                    || (starts[i].x+columnWidth<x)){
                    return i;
                }
            }
        }
        return 0;
    }
    protected View getViewAtPoint(int x, int y, Rectangle alloc) {
        if (starts!=null) {
            for (int i=starts.length-1; i>0; i--) {
                if ((starts[i].x<x && starts[i].y<y)
                    || (starts[i].x+columnWidth<x)){
                    return getView(i);
                }
            }
        }
        return getView(0);
    }
    public Shape getChildAllocation(int index, Shape a) {
        Rectangle r=super.getChildAllocation(index,a).getBounds();
        r.x=starts[index].x+3;
        r.y=starts[index].y+3;
        return r;
    }

The StyledEditorKit was extended to provide ViewFactory with replaced root view and paragraph view.

class MultiColumnEditorKit extends StyledEditorKit {
    ViewFactory defaultFactory=new MultiColumnFactory();
    public ViewFactory getViewFactory() {
        return defaultFactory;
    }
}

class MultiColumnFactory 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 MultiColumnParagraphView(elem);
            } else if (kind.equals(AbstractDocument.SectionElementName)) {
                return new MultiColumnView(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);
    }
}

After that assign the kit to JEditorPane and all typed text will be layed out in multiple columns. If previous column is full a new one is created and text appears there.

Appendix

Here is the full source code of the multicolumn text example.

Back to Table of Content