Articles Projects Tips Downloads Contacts About

"Forced line wrap" and "No wrap" in the JEditorPane/JTextPane
By Stanislav Lapitsky

Unlike JTextArea wrapping related methods setLineWrap() and setWrapStyleWord() JEditorPane class doesn’t provide such features. The wrapping is defined by EditorKit implementation. By default paragraphs wrap lines by word. Often programmer has to manually disable the wrapping or add a forced wrap. Forced wrap means paragraph remains the same but text is wrapped in the mid of row. The same behavior provide MS Word when user presses SHIFT+ENTER key. Another example is HTML tag <BR> which breaks line but doesn’t start a new paragraph.

The article shows how to achieve this in JEditorPane. The solution is based on StyledEditorKit extension but can be implemented in any other kit.

"No wrap" implementation.

Normally line wrap happens when row content’s width is bigger than available space. The available space is defined as width of paragraph’s container. The container sets size of paragraph and performs layout. The first step is overriding layout and passing max width. This way we set the available width to be much bigger that necessary. So the paragraph will have one row and no need to wrap. We override ParagraphView class with own implementation of layout().

    public void layout(int width, int height) {
        super.layout(Short.MAX_VALUE, height);
    }

Then we correct minimum paragraph’s span calculation to provide correct feedback to container and let container get real span. This fixes some issues when JEditorPane is added into JScrollPane.

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

The result is a paragraph with no wrap. If we need to switch warp on/off we can define a flag and call super.layout() or custom layout depending on what we need – wrap/no wrap.

Line breaking algorithm.

To understand how the wrapping works let’s describe line breaking algorithm of paragraph.

LabelView class represents separate fragment of text with the same attributes. ParagraphView layout LabelViews in rows breaking labels if necessary. First step of layout is creation of single very long line (pool of LabelViews) which will be continuously broken to fit available paragraph space.

Paragraph creates a row and adds to the row the first LabelView from the pool and measure current row. If the width of row after adding the label is less than available paragraph width the next label is added till the row width exceeds the available width. As soon as the width is exceeded paragraph starts to search where the row can be broken. It means paragraph need an offset where we can break view. All the labels in the row are asked for break weight. A quote from javadoc: “<break weight > - Determines how attractive a break opportunity in this view is. This can be used for determining which view is the most attractive to call breakView method on in the process of formatting. The higher the weight, the more attractive the break. A value equal to or lower than View.BadBreakWeight should not be considered for a break. A value greater than or equal to View.ForcedBreakWeight should be broken.

There are four base break weights the LabelView can return:
View.BadBreakWeight – means no break possible.
View.GoodBreakWeight – can break if there is no better opportunity.
View.ExcellentBreakWeight – it’s preferred to break the view.
View.ForcedBreakWeight – the view must be broken.

In most cases LabelViews returns View.ExcellentBreakWeight if there is a space chars ‘ ‘, ‘\t’ etc. (Word wrap supposes to break views by spaces) and View.GoodBreakWeight if there is no space but only letters. To explain this imagine following. You type a very long string of characters and ask for break weight. If there is a space we would prefer to break by space and start a new word on a new row. If there is no space we can break in any possible place to fit our row in available paragraph width.

Forced wrap implementation.

We define a special character ‘\r’ in model. The character means we want to break row after the char. The char is inserted with a special attribute to provide a separate view for the char. This is done because of swing’s implementation of layout where forced break works only if row view has more than one child. (See FlowView.FlowStrategy inner class line #477 in swing’s source code). The attribute can be used with the ‘\r’ only and must be removed from input Attributes of the kit we use.

We also associate line break insertion with SHIFT+ENTER key combination. The code looks like this:

    protected void insertLineBreak() {
        try {
            int offs = edit.getCaretPosition();
            Document doc = edit.getDocument();
            SimpleAttributeSet attrs;
            if (doc instanceof StyledDocument) {
                attrs = new SimpleAttributeSet(((StyledDocument)doc).getCharacterElement(offs).getAttributes());
            }
            else {
                attrs = new SimpleAttributeSet();
            }
            attrs.addAttribute(LINE_BREAK_ATTRIBUTE_NAME,Boolean.TRUE);
            doc.insertString(offs, "\r", attrs);
            edit.setCaretPosition(offs+1);
        }
        catch (BadLocationException ex) {
            //should never happens
            ex.printStackTrace();
        }
    }
    protected void initKeyMap() {
        Keymap kMap=edit.getKeymap();
        Action a=new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                insertLineBreak();
            }
        };
        kMap.addActionForKeyStroke(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,KeyEvent.SHIFT_MASK),a);
    }

Then we extend LabelView and provide proper break weight calculation.

    public int getBreakWeight(int axis, float pos, float len) {
        if (axis == View.X_AXIS) {
            checkPainter();
            int p0 = getStartOffset();
            int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, len);
            if (p1 == p0) {
                // can't even fit a single character
                return View.BadBreakWeight;
            }
            try {
                //if the view contains line break char return forced break
                if (getDocument().getText(p0, p1 - p0).indexOf("\r") >= 0) {
                    return View.ForcedBreakWeight;
                }
            }
            catch (BadLocationException ex) {
                //should never happen
            }
        }
        return super.getBreakWeight(axis, pos, len);
    }

and change code to break the label by the ‘\r’ char. Like this:

    public View breakView(int axis, int p0, float pos, float len) {
        if (axis == View.X_AXIS) {
            checkPainter();
            int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, len);
            try {
                //if the view contains line break char break the view
                int index = getDocument().getText(p0, p1 - p0).indexOf("\r");
                if (index >= 0) {
                    GlyphView v = (GlyphView) createFragment(p0, p0 + index + 1);
                    return v;
                }
            }
            catch (BadLocationException ex) {
                //should never happen
            }
        }
        return super.breakView(axis, p0, pos, len);
    }

The final step is extending kit. All we need it to replace SUN’s views with own implementation and remove the custom attribute from input attributes.

class WrapEditorKit extends StyledEditorKit {
    ViewFactory defaultFactory=new WrapColumnFactory();
    public ViewFactory getViewFactory() {
        return defaultFactory;
    }

    public MutableAttributeSet getInputAttributes() {
        MutableAttributeSet mAttrs=super.getInputAttributes();
        mAttrs.removeAttribute(WrapApp.LINE_BREAK_ATTRIBUTE_NAME);
        return mAttrs;
    }
}

class WrapColumnFactory implements ViewFactory {
    public View create(Element elem) {
        String kind = elem.getName();
        if (kind != null) {
            if (kind.equals(AbstractDocument.ContentElementName)) {
                return new WrapLabelView(elem);
            } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                return new NoWrapParagraphView(elem);
            } else if (kind.equals(AbstractDocument.SectionElementName)) {
                return new BoxView(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);
    }

Appendix

Here is the full source code of the wrap example.

Back to Table of Content