Wrap long words in JTextPane (Java 7)
Asked Answered
L

5

17

In all versions of Java up to 6, the default behaviour of a JTextPane put inside a JScrollPane was: wrap lines at word boundaries if possible. If not, then wrap them anyway.

In JDK 7, the default behaviour seems to be: wrap lines at word boundaries if possible. If not, just expand the width of the JTextPane (never wrap long words).

It is easy to reproduce this, here is a SSCCE:


public class WrappingTest extends JFrame
{

    public static void main ( String[] args )
    {
        new WrappingTest(); 
    }

    public WrappingTest ()
    {
        setSize(200,200);
        getContentPane().setLayout(new BorderLayout());
        JTextPane jtp = new JTextPane();
        JScrollPane jsp = new JScrollPane(jtp);
        jsp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        getContentPane().add(jsp,BorderLayout.CENTER);
        setVisible(true);
    }

}

Just run it in JDK 6 and in JDK 7, write some small words, and write a long word, and you will see the difference.

My question is simple... the new default behaviour in JDK 7 totally messes a program of mine (they should be more careful at Oracle with changing this kind of defaults... they seem unimportant but when you're using a JTextPane to display data that usually contains very long strings of letters, they're not so unimportant - in fact I'm going to file a bug report, but I'd like to have a workaround while/if they don't resolve it). Any way to go back to the previous behaviour?

Note that I have checked the answer to the related question How is word-wrapping implemented in JTextPane, and how do I make it wrap a string without spaces? but it doesn't answer this question - it provides a way of making the JTextPane wrap without any regard at all for whitespace, but for me the desired behaviour is split lines at whitespace if possible, and elsewhere if not possible (as in previous Java versions).

Lamarlamarck answered 29/12, 2011 at 10:33 Comment(2)
Does using invokeLater() help?Babul
I have exactly the same problem. Related: forums.oracle.com/forums/thread.jspa?threadID=2374090 (no answers...) The poster there already created a bug report, but it was closed as "not a bug", without a word of explanation...Eyelash
S
14

For me the fix works (tested under 1.7.0_09)

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

public class WrapTestApp extends JFrame {

    public static void main ( String[] args ) {
        new WrapTestApp();
    }

    public WrapTestApp () {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(200,200);
        getContentPane().setLayout(new BorderLayout());
        JTextPane jtp = new JTextPane();
        jtp.setEditorKit(new WrapEditorKit());
        JScrollPane jsp = new JScrollPane(jtp);
        jsp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        getContentPane().add(jsp, BorderLayout.CENTER);
        jtp.setText("ExampleOfTheWrapLongWordWithoutSpaces");
        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 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);
            }
        }

    }
}
Sphygmic answered 14/11, 2012 at 9:8 Comment(4)
In fact all we need is to let getMinimumSpan() of LabelView return 0 for X_AXIS. ViewFactory is a way to replace default LabelViewSphygmic
aaach i can see and get it, thank you, quite correctly to works for jtp.setComponentOrientation(RTL); tooPlowman
How are we suppose to solve the casting problem in the line jtp.setEditorKit(new WrapEditorKit());? I get a javax.swing.text.DefaultStyledDocument cannot be cast to javax.swing.text.html.HTMLDocument in that line.Semblance
You use another editor kit. Guess HTMLEditorKit. Do the same for the kit.Sphygmic
E
3

Good catch from @dk89, but alas the given workarounds don't work: JDK 7 apparently still doesn't offer a wait to set a custom BreakIterator on a JTextComponent; not even on a GlyphView, where the generation of the BreakIterator is private. And if we insert the string char by char, it still doesn't work: I suppose the consecutive runs of text with identical style (AttributeSet) are collapsed together.

I have spent two days trying to do a custom EditorKit, as advised elsewhere, but it doesn't work well (with JDK 1.7.0_4 at least) as the text.

I tried the solution given at How to word wrap text stored in JTextPanes which are cells in a JList and a variant found at http://www.experts-exchange.com/Programming/Languages/Java/Q_20393892.html

But I found out that the breakView is no longer called when the JTextPane is smaller than the longest word in the sentence. So it doesn't work at all when there is only one (long) word. That's our case, as we display user-provided, identifier-like strings, often without spaces, in rather small spaces.

I finally found a simple solution, derived from the suggestion in the bug report: indeed, insert the string char by char, but alternate styles! Thus, we have as many segments as we have chars, and the string is wrapped at char bounds. Until the next "bug fix"?

Code snippets:

private JTextPane tp;
private SimpleAttributeSet sas = new SimpleAttributeSet();

tp= new JTextPane();
sas.addAttribute( "A", "C" ); // Arbitrary attribute names and value, not used actually

    // Set the global attributes (italics, etc.)
    tp.setParagraphAttributes(styleParagraphAttributes, true);

    Document doc = tp.getDocument();
    try
    {
        doc.remove(0, doc.getLength()); // Clear
        for (int i = 0; i < textToDisplay.length(); i++)
        {
            doc.insertString(doc.getLength(), textToDisplay.substring(i, i+1),
                    // Change attribute every other char
                    i % 2 == 0 ? null : sas);
        }
    }
    catch (BadLocationException ble)
    {
        log.warn("Cannot happen...", ble);
    }

As stated in the bug, they should have provided an easy way (some property perhaps, or some injectable stuff) to revert to the old behavior.

Eyelash answered 27/6, 2012 at 14:51 Comment(0)
T
1

Take a look at this bug:

https://bugs.java.com/bugdatabase/view_bug?bug_id=6539700

Teresa answered 29/12, 2011 at 15:19 Comment(2)
Thanks, it's remarkably similar... but unfortunately it doesn't seem to be the same bug (it has to do with attribute sets, and was fixed long ago) :/Lamarlamarck
Well, I think it is related, although the behavior in 1.6 wasn't changed. Look at the comment: "It should be noted that the requested default splitting behavior (breaking GlyphView at arbitrary place when no valid breakpoint is found by BreakIterator) is horribly wrong and should not be restored in any JDK release, either future or past." Looks like we need to do more work now...Eyelash
A
1

Hi I've had the same problem but found a work-around:

just create an extended class of JTextPane e.g.

        MyWrapJTextPane extends JTextPane

and overwrite the following method - it works ;-)

        public boolean getScrollableTracksViewportWidth() {
            return true;
        }
Architrave answered 2/1, 2012 at 18:35 Comment(1)
Sorry: no doesn't work! this solves only the problem with long lines (containing white-spaces) - they are wrapped now correctly - but long words still didn't get wrapped :-(Architrave
P
1

Overriding getMinimumSpan and return 0 for X_AXIS makes text breaking by whitespace. If you want a character based breaking, try this:


    internal class WhitespaceBasedWrapLabelView(elem: Element?) : LabelView(elem) {
        override fun getMinimumSpan(axis: Int): Float {
            return when (axis) {
                X_AXIS -> 0f
                Y_AXIS -> super.getMinimumSpan(axis)
                else -> throw IllegalArgumentException("Invalid axis: $axis")
            }
        }
    }
    
    internal class CharacterBasedWrapLabelView(elem: Element?) : LabelView(elem) {
        override fun getMinimumSpan(axis: Int): Float {
            return when (axis) {
                X_AXIS -> 0f
                Y_AXIS -> super.getMinimumSpan(axis)
                else -> throw IllegalArgumentException("Invalid axis: $axis")
            }
        }
    
        override fun breakView(axis: Int, p0: Int, pos: Float, len: Float): View {
            if (axis == X_AXIS) {
                checkPainter()
                val p1 = glyphPainter.getBoundedPosition(this, p0, pos, len)
                // bounded region.
                if (p0 == startOffset && p1 == endOffset) {
                    return this
                }
                val v = createFragment(p0, p1) as GlyphView
                // v.x = pos
                getTabbedSpan(pos, tabExpander)
                return v
            }
            return super.breakView(axis, p0, pos, len)
        }
    
        override fun getBreakWeight(axis: Int, pos: Float, len: Float): Int {
            if (axis == X_AXIS) {
                checkPainter()
                val p0 = startOffset
                val p1 = glyphPainter.getBoundedPosition(this, p0, pos, len)
                return if (p1 == p0) BadBreakWeight else GoodBreakWeight
            }
            return super.getBreakWeight(axis, pos, len)
        }
    }

Pandurate answered 9/6, 2023 at 6:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.