Search code examples
javaswingjtextareajtextpaneword-wrap

JTextPane line wrap behavior


Recently I have been working on a Java text editor project and I would like to use a JTextPane to replace the old JTextArea in order to implement syntax highlighting. However, a JTextPane lacks methods in JTextArea (such as append(), getLineStartOffset() etc) and I want to reimplement them in my class MyTextPane (a sub-class of JTextPane) but have run into troubles.

My current code (only a small self-contained part):

import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;

public class MyTextPane extends JTextPane
{
    public MyTextPane()
    {
        super();
    }

    public void append(String text)
    {
        try
        {
            Document doc = this.getDocument();
            doc.insertString(doc.getLength(),text,null);
        }
        catch (BadLocationException ex)
        {
            //must succeed
            throw new InternalError(ex.getMessage());
        }
    }

    public void insert(String text, int pos)
    {
        try
        {
            this.getStyledDocument().insertString(pos,text,null);
        }
        catch (BadLocationException ex)
        {
            throw new IllegalArgumentException(ex);
        }
    }

    public void replaceRange(String str, int start, int end)
    {
        try
        {
            Document doc = this.getDocument();
            doc.remove(start,end-start);
            doc.insertString(start,str,null);
        }
        catch (BadLocationException ex)
        {
            throw new IllegalArgumentException(ex);
        }
    }

    public void setLineWrap(boolean isLineWrap)
    {
        /*
         * implements later
         */
    }

    public boolean getLineWrap()
    {
        /*
         * implements later
         */
         return true;
    }

    public void setWrapStyleWord(boolean isWrapStyleWord)
    {
        /*
         * implements later
         */
    }

    public boolean getWrapStyleWord()
    {
        /*
         * implements later
         */
        return true;
    }

    public void setTabSize(int size)
    {
        /*
         * implements later
         */        
    }

    public int getTabSize()
    {
        /*
         * implements later
         */
        return 4;
    }

    public int getLineCount()
    {
        //follow JTextArea implementation
        Element root = this.getDocument().getDefaultRootElement();
        return root.getElementCount();
    }

    public int getLineStartOffset(int line) throws BadLocationException
    {
        //follow JTextArea implementation
        int count = this.getLineCount();
        Document doc = this.getDocument();
        if (line < 0)
        {
            throw new BadLocationException("Negative line", -1);
        }
        if (line >= count)
        {
            throw new BadLocationException("No such line", doc.getLength() + 1);
        }
        return doc.getDefaultRootElement().getElement(line).getStartOffset();
    }

    public int getLineEndOffset(int line) throws BadLocationException
    {
        //follow JTextArea implementation
        int count = this.getLineCount();
        Document doc = this.getDocument();
        if (line < 0)
        {
            throw new BadLocationException("Negative line", -1);
        }
        if (line >= count)
        {
            throw new BadLocationException("No such line", doc.getLength() + 1);
        }
        int end = doc.getDefaultRootElement().getElement(line).getEndOffset();
        return (line==count-1)?(end-1):end;
    }

    public int getLineOfOffset(int off) throws BadLocationException
    {
        //follow JTextArea implementation
        Document doc = this.getDocument();
        if (off < 0)
        {
            throw new BadLocationException("Can't translate offset to line", -1);
        }
        if (off > doc.getLength())
        {
            throw new BadLocationException("Can't translate offset to line", doc.getLength() + 1);
        }
        return doc.getDefaultRootElement().getElementIndex(off);
    }

    public static void main(String[] args)
    {
        final SimpleAttributeSet BOLD_SET = new SimpleAttributeSet();
        StyleConstants.setBold(BOLD_SET, true);
        StyleConstants.setForeground(BOLD_SET, new Color(0,0,125));
        SwingUtilities.invokeLater(new Runnable()
        {            
            @Override
            public void run()
            {
                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                MyTextPane textPane = new MyTextPane();
                frame.add(new JScrollPane(textPane), BorderLayout.CENTER);
                frame.setSize(200,200);
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}

As you can see, I have already added back some methods like append(). However, I can't think of any ways to control the line wrap policy.

The default behavior is quite strange: when there are one short word and one long word,

enter image description here

if I consecutively enter characters without a space,

enter image description here

it firstly appears like wrapping by words. However when I continue to enter characters,

enter image description here

it does not wrap at all.

Is there any elegant way to control the wrapping policy of a JTextPane? In other words, can a JTextPane wrap words like a JTextArea? I found so many duplicates (like this, this and this) but couldn't find a solution. Thanks in advance.


Solution

  • This is a discussion about the same problem: Word wrapping behavior in JTextPane since Java 7. The solution proposed by user StanislavL (who also appears to be very active on Stack Overflow: StanislavL) to support word wrapping works for me using Java 8. It uses a custom WrapEditorKit as the editor kit for the JTextPane (and the WrapEditorKit class in turn uses the WrapColumnFactory and WrapLabelView classes).

    Combining this with the NonWrappingTextPane example (from the book Core Swing: Advanced Programming by Kim Topley) makes it possible to switch line wrapping off:

    import java.awt.*;
    import javax.swing.*;
    
    public class WrapTestApp extends JFrame {
        public static void main(final String[] arguments) {
            new WrapTestApp();
        }
    
        public WrapTestApp() {
            setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
            setBounds(800, 400, 200, 200);
            getContentPane().setLayout(new BorderLayout());
            final CustomTextPane textPane = new CustomTextPane(true);
            final JScrollPane scrollPane = new JScrollPane(textPane);
            scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
            getContentPane().add(scrollPane, BorderLayout.CENTER);
            textPane.setText("ExampleOfTheWrapLongWordWithoutSpaces");
            setVisible(true);
        }
    }
    

    The CustomTextPane class:

    import javax.swing.*;
    import javax.swing.text.*;
    
    public class CustomTextPane extends JTextPane {
        private boolean lineWrap;
    
        public CustomTextPane(final boolean lineWrap) {
            this.lineWrap = lineWrap;
    
            if (lineWrap)
                setEditorKit(new WrapEditorKit());
        }
    
        @Override
        public boolean getScrollableTracksViewportWidth() {
            if (lineWrap)
                return super.getScrollableTracksViewportWidth();
            else
                return getParent() == null
                      || getUI().getPreferredSize(this).width <= getParent().getSize().width;
        }
    
        private class WrapEditorKit extends StyledEditorKit {
            private final ViewFactory defaultFactory = new WrapColumnFactory();
    
            @Override
            public ViewFactory getViewFactory() {
                return defaultFactory;
            }
        }
    
        private class WrapColumnFactory implements ViewFactory {
            @Override
            public View create(final Element element) {
                final String kind = element.getName();
                if (kind != null) {
                    switch (kind) {
                        case AbstractDocument.ContentElementName:
                            return new WrapLabelView(element);
                        case AbstractDocument.ParagraphElementName:
                            return new ParagraphView(element);
                        case AbstractDocument.SectionElementName:
                            return new BoxView(element, View.Y_AXIS);
                        case StyleConstants.ComponentElementName:
                            return new ComponentView(element);
                        case StyleConstants.IconElementName:
                            return new IconView(element);
                    }
                }
    
                // Default to text display.
                return new LabelView(element);
            }
        }
    
        private class WrapLabelView extends LabelView {
            public WrapLabelView(final Element element) {
                super(element);
            }
    
            @Override
            public float getMinimumSpan(final 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);
                }
            }
        }
    }