Search code examples
javaswingjscrollpanejtextpane

How to: JTextPane auto wrapping NOT on word boundary such as "space", but on letter boundary


I put a JTextPane into a JScrollPane. As I tried, the JTextPane will auto-wrap a long line if it exceed the width of the display area. And the auto wrapping is based on word boundary, such as a space character.

My content contains a lot of space. And I want to display it literally. So I need auto-wrapping, but I want it happen ONLY at the maximum width of display area, NOT on word boundary.

How?

What I have tried:

  • Replace all the space with '\0', so literally, my content is a single big word.

ADD 1

Below is my failed attempt after reading StanislavL's solution. I am not blaming his solution since my scenario is not exactly the same as his.

StanislavL's solution requires that a row contains at least 2 LabelViews. According to him, this is imposed by Swing's implementation of layout() method where forced break works only if row view has more than one child (see: http://java-sl.com/wrap.html). So StanislavL deliberately assigned a special attribute to the \r character which ensure a separate LabelView. And use the \r as a landmark of wrapping. But in my scenario I cannot insert any characters to my content.

My idea is simple, just provide a customized implementation of ViewFactory for the StyledEditorKit since the ViewFactory interface determines the break weight and how a break should happen:

this.jTextPane.setEditorKit(new StyledEditorKit(){
    @Override 
       public ViewFactory getViewFactory(){
           return new LetterWrappingStyledViewFactory(maxCharWidth);
        } 
    }); 

Below is my implementation of the interface ViewFactory:

public class LetterWrappingStyledViewFactory implements ViewFactory {


    public int maxCharWidth = -1; // this is the max width where I want wrap to happen.

    public LetterWrappingStyledViewFactory(int maxCharWidth) {
        this.maxCharWidth = maxCharWidth;
    }

    public View create(Element elem) {
        String kind = elem.getName();
        if (kind != null) {
            if (kind.equals(AbstractDocument.ContentElementName)) {
                return new LabelView(elem) {
                    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 > maxCharWidth)
                                return View.ForcedBreakWeight;
                            else
                                return View.BadBreakWeight;
                        }
                        return super.getBreakWeight(axis, pos, len);
                    }

                    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);
                            if (p0 == getStartOffset() && p1 <= maxCharWidth) {
                                return this;
                            }
                            return createFragment(p0, maxCharWidth);
                        }
                        return this;
                    }
                };
            } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                return new ParagraphView(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);
    }
}

Solution

  • This example works with StyledEditorKit (tested with java 7)

    import javax.swing.*;
    import javax.swing.text.*;
    
    
    public class WrapApp extends JFrame {
        JEditorPane edit=new JEditorPane();
        public WrapApp() {
            super("Wrap in the mid");
            setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            edit.setEditorKit(new WrapEditorKit());
            edit.setText("111 222 333333333333333333333333333333333333333333333");
    
            getContentPane().add(new JScrollPane(edit));
            setSize(200,200);
            setLocationRelativeTo(null);
        }
    
        public static void main(String[] args) {
            WrapApp m = new WrapApp();
            m.setVisible(true);
        }
    
    
    }
    
    class WrapEditorKit extends StyledEditorKit {
        ViewFactory defaultFactory=new WrapColumnFactory();
        public ViewFactory getViewFactory() {
            return defaultFactory;
        }
    
    }
    
    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 ParagraphView(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);
        }
    }
    
    class WrapLabelView extends LabelView {
        public WrapLabelView(Element elem) {
            super(elem);
        }
    
        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;
                }
                return View.GoodBreakWeight;
            }
            return super.getBreakWeight(axis, pos, len);
        }
        public float getMinimumSpan(int axis) {
            switch (axis) {
                case View.X_AXIS:
                    return 0;
                case View.Y_AXIS:
                    return super.getMinimumSpan(axis);
                default:
                    throw new IllegalArgumentException("Invalid axis: " + axis);
            }
        }
    
        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);
                GlyphView v = (GlyphView) createFragment(p0, p1);
                return v;
            }
            return super.breakView(axis, p0, pos, len);
        }
    }
    

    UPDATE:

    About the GlyphPainter. In fact we should find a position where we can break GlyphView (this past in getBoundedPosition()). p0 means char offset in the Document (GlyphView represents Character element or fragment of the Element)) SO imagine a big string of text. In the Document it's just one Element because it has all the same attributes. But we have to create multiple LabelView because the text fragment is too big to fit the available width. SO for the first label the p0 is 0. Then for the next one is an offset where we break our initial huge view. Then another offset if we break it once more. Then 2 parameters just representing x shift and widht of container (parent view for example).

    SO we have a big text and should find where to break. Current way is easier because we don't care about priorities (spaces vs another chars).