Strange text wrapping with styled text in JTextPane with Java 7
Asked Answered
I

3

16

I have two different editors using JTextPane with strange bugs in Java 7 that did not occur with the previous JVM versions. It happens with long lines containing styled text or components.

Here is an example demonstrating this bug. In this example, the default style is applied for all the text each time a character is inserted. I tested it with the JDK 1.7.0_04.

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

public class BugWrapJava7 extends JFrame {

    JTextPane jtp;
    StyledDocument doc;

    public BugWrapJava7() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        jtp = new JTextPane();
        add(jtp, BorderLayout.CENTER);
        jtp.setText("\ntype some text in the above empty line and check the wrapping behavior");
        doc = jtp.getStyledDocument();
        doc.addDocumentListener(new DocumentListener() {
            public void insertUpdate(DocumentEvent e) {
                insert();
            }
            public void removeUpdate(DocumentEvent e) {
            }
            public void changedUpdate(DocumentEvent e) {
            }
        });
        setSize(200, 200);
        setVisible(true);
    }
    public void insert() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                Style defaultStyle = jtp.getStyle(StyleContext.DEFAULT_STYLE);
                doc.setCharacterAttributes(0, doc.getLength(), defaultStyle, false);
            }
        });
    }
    public static void main(String[] args) {
        new BugWrapJava7();
    }
}

My question is : is there something wrong in my code, or is it indeed a new bug introduced in Java 7 ? And if it is a new JVM bug, is there a workaround ?

It might be related to question 8666727, but the problem here lies in the wrong wrapping rather than the appearance of a scrollbar.

Inadvertent answered 12/6, 2012 at 15:50 Comment(3)
you are right, I saw that and can comparing difference +1, no idea about changes in Java7 (leaving my upgrade in Java 1.7.0_15 and more)Map
It probably won't help, but call pack() immediately before setSize(200, 200); (I don't have Java 7 available to test it).Barrault
@Andrew Thompson I added all good Swing rulles, without any changes, please see in my post hereMap
M
17

for futures readers, bug is still present in JDK 1.7.0_04.,

comparing Java7 and with stable Java6,

enter image description here<------ Java7 v.s. Java6 --->enter image description here

enter image description here<------ Java7 v.s. Java6 --->enter image description here

enter image description here<------ Java7 v.s. Java6 --->enter image description here

enter image description here <------ Java7 v.s. Java6 ---> enter image description here

from code

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

public class BugWrapJava7 {

    private JFrame frame = new JFrame();
    private JTextPane jtp;
    private StyledDocument doc;

    public BugWrapJava7() {
        jtp = new JTextPane();
        jtp.setText("\ntype some text in the above empty line and check the wrapping behavior");
        doc = jtp.getStyledDocument();
        doc.addDocumentListener(new DocumentListener() {

            public void insertUpdate(DocumentEvent e) {
                insert();
            }

            public void removeUpdate(DocumentEvent e) {
                insert();
            }

            public void changedUpdate(DocumentEvent e) {
                insert();
            }

            public void insert() {
                SwingUtilities.invokeLater(new Runnable() {

                    public void run() {
                        Style defaultStyle = jtp.getStyle(StyleContext.DEFAULT_STYLE);
                        doc.setCharacterAttributes(0, doc.getLength(), defaultStyle, false);
                    }
                });
            }
        });
        JScrollPane scroll = new JScrollPane(jtp);
        scroll.setPreferredSize(new Dimension(200, 200));
        frame.add(scroll);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                BugWrapJava7 bugWrapJava7 = new BugWrapJava7();
            }
        });
    }
}
Map answered 12/6, 2012 at 17:44 Comment(6)
<------ Java7 v.s. Java6 ---> That is going the extra mile. +1Barrault
@Andrew Thompson hehehe this is same mode, because I'm not good in Html, and not able to create gap between two imagesMap
That edit is great. But my bad, I should have been more clear. I meant. Wow! Not just tests on two different JRE versions to show the regression, but screen-shots thrown in for free! :)Barrault
psssst, there are another differencies, but I promised myself that I'll be patient and waiting for Java7 next 2-3 yearsMap
Thanks for the screenshots, they will be very helpful if the bug gets fixed... I will mark your answer as the best one, but I still wonder if a fix is underway : the bug report you link to (which I had already noticed: it is in the first answer to the question I linked to) does not say a bug is present in 1.7.0_04, and it is even marked as fixed for 7(b70), which I assume is before 1.7.0_04. I am still afraid the crazy behavior visible in my example might be shrug off as "not a bug" by JDK developers...Inadvertent
@Inadvertent as you can see in Bug trace, the same issue is present in Java5, Java6 and Java7 too, no idea when and how will be ..., please to try to test what's happensMap
K
8

Investigated this. The reason is breakSpots caching. Looks like LabelView stores them and don't recalculate offsets on previos text edit. If I reset them manually the bug does not happen.

A workaround (very dirty because of private breakSpots fields) is following

import java.awt.Dimension;
import java.lang.reflect.Field;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

public class BugWrapJava7 {

    private JFrame frame = new JFrame();
    private JTextPane jtp;
    private StyledDocument doc;

    public BugWrapJava7() {
        jtp = new JTextPane();
        jtp.setEditorKit(new MyStyledEditorKit());
        jtp.setText("\ntype some text in the above empty line and check the wrapping behavior");
        doc = jtp.getStyledDocument();
        doc.addDocumentListener(new DocumentListener() {

            public void insertUpdate(DocumentEvent e) {
                insert();
            }

            public void removeUpdate(DocumentEvent e) {
                insert();
            }

            public void changedUpdate(DocumentEvent e) {
                insert();
            }

            public void insert() {
                SwingUtilities.invokeLater(new Runnable() {

                    public void run() {
                        Style defaultStyle = jtp.getStyle(StyleContext.DEFAULT_STYLE);
                        doc.setCharacterAttributes(0, doc.getLength(), defaultStyle, false);
                    }
                });
            }
        });
        JScrollPane scroll = new JScrollPane(jtp);
        scroll.setPreferredSize(new Dimension(200, 200));
        frame.add(scroll);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                BugWrapJava7 bugWrapJava7 = new BugWrapJava7();
            }
        });
    }
}

class MyStyledEditorKit extends StyledEditorKit {
    private MyFactory factory;

    public ViewFactory getViewFactory() {
        if (factory == null) {
            factory = new MyFactory();
        }
        return factory;
    }
}

class MyFactory implements ViewFactory {
    public View create(Element elem) {
        String kind = elem.getName();
        if (kind != null) {
            if (kind.equals(AbstractDocument.ContentElementName)) {
                return new MyLabelView(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 MyLabelView extends LabelView {
    public MyLabelView(Element elem) {
        super(elem);
    }
    public View breakView(int axis, int p0, float pos, float len) {
        if (axis == View.X_AXIS) {
            resetBreakSpots();
        }
        return super.breakView(axis, p0, pos, len);
    }

    private void resetBreakSpots() {
        try {
            // HACK the breakSpots private fields
            Field f=GlyphView.class.getDeclaredField("breakSpots");
            f.setAccessible(true);
            f.set(this, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

Less hack without the reflection. Based on usual reset of breakSpots on model change.

class MyLabelView extends LabelView {

    boolean isResetBreakSpots=false;

    public MyLabelView(Element elem) {
        super(elem);
    }
    public View breakView(int axis, int p0, float pos, float len) {
        if (axis == View.X_AXIS) {
            resetBreakSpots();
        }
        return super.breakView(axis, p0, pos, len);
    }

    private void resetBreakSpots() {
        isResetBreakSpots=true;
        removeUpdate(null, null, null);
        isResetBreakSpots=false;

//        try {
//            Field f=GlyphView.class.getDeclaredField("breakSpots");
//            f.setAccessible(true);
//            f.set(this, null);
//        } catch (Exception e) {
//            e.printStackTrace();
//        }
    }

    public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) {
        super.removeUpdate(e, a, f);
    }

    public void preferenceChanged(View child, boolean width, boolean height) {
        if (!isResetBreakSpots) {
            super.preferenceChanged(child, width, height);
        }
    }
}

UPDATE: This one fixes TextSamplerDemo as well. I reset all spots for all labels views.

class MyStyledEditorKit extends StyledEditorKit {
    private MyFactory factory;

    public ViewFactory getViewFactory() {
        if (factory == null) {
            factory = new MyFactory();
        }
        return factory;
    }
}

class MyFactory implements ViewFactory {
    public View create(Element elem) {
        String kind = elem.getName();
        if (kind != null) {
            if (kind.equals(AbstractDocument.ContentElementName)) {
                return new MyLabelView(elem);
            } else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                return new MyParagraphView(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 MyParagraphView extends ParagraphView {

    public MyParagraphView(Element elem) {
        super(elem);
    }
public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) {
    super.removeUpdate(e, a, f);
    resetBreakSpots();
}
public void insertUpdate(DocumentEvent e, Shape a, ViewFactory f) {
    super.insertUpdate(e, a, f);
    resetBreakSpots();
}

private void resetBreakSpots() {
    for (int i=0; i<layoutPool.getViewCount(); i++) {
        View v=layoutPool.getView(i);
        if (v instanceof MyLabelView) {
            ((MyLabelView)v).resetBreakSpots();
        }
    }
}

}

class MyLabelView extends LabelView {

    boolean isResetBreakSpots=false;

    public MyLabelView(Element elem) {
        super(elem);
    }
    public View breakView(int axis, int p0, float pos, float len) {
        if (axis == View.X_AXIS) {
            resetBreakSpots();
        }
        return super.breakView(axis, p0, pos, len);
    }

    public void resetBreakSpots() {
        isResetBreakSpots=true;
        removeUpdate(null, null, null);
        isResetBreakSpots=false;
   }

    public void removeUpdate(DocumentEvent e, Shape a, ViewFactory f) {
        super.removeUpdate(e, a, f);
    }

    public void preferenceChanged(View child, boolean width, boolean height) {
        if (!isResetBreakSpots) {
            super.preferenceChanged(child, width, height);
        }
    }
}
Katakana answered 9/1, 2013 at 8:2 Comment(10)
similair hack / workaround by (@StanislavL) for Java7 & JTextAreaMap
Thank you for this workaround. It works well for the test. It also improves the behavior in an editor using JComponent and styles in the textpane, although there are still weird things happening (such as line breaks happening sometimes for no reason). Unfortunately, this workaround does not work at all with Java applets.Inadvertent
@Inadvertent please provide SSCCE for the remaining issues and I will try to figure out what's wrong.Katakana
TextSamplerDemo exhibits the Java7 bug. Just download it, apply your patch, and try to add or remove characters at the beginning of the editable pane. With the patch, words are not cut, but unnecessary breaks happen sometimes in the following text.Inadvertent
this link lets you download the images needed for TextSamplerDemo.Inadvertent
Also, I checked that your new version without reflexion works with applets. And it does not seem to slow things down. This is good.Inadvertent
@Inadvertent improved fix is added. For me it works fine with the TextSamplerDemoKatakana
It seems to be working, although you should also reset the breakspots in MyParagraphView.removeUpdate, otherwise weird things happen when removing characters. But surely you overloaded this method for a reason ? I also understand now why your workaround does not affect performance: you prevent the call to preferenceChanged, which I know is a performance killer in the current JDK implementation (it revalidates the whole textpane and all the components inside !).Inadvertent
@Inadvertent Right. The same code should be called on removeUpdate. I just forgot to copy/paste the same in hurry. Changed.Katakana
Seems like this issue still exists in Java 8, the answer appears to solve it. Thanks.Mesoglea
F
0

My HTMLDocument was wrapping-by-letter. I installed StanislavL's above solution. No joy, still wrapping-by-letter inside <td> elements. So I fixed a bug in StanislavL's code. Joy; wrapping-by-word everywhere. Then I discovered that my program wasn't using the StanislavL code! It seems that my tinkering fixed the same bug in my own code.

The problem is that there are two different kinds of Element (in javax.swing.text and javax.swing.text.html.parser) and both have a tag type name with the string value "content". Method Element.getName confuses this by returning a String instead of an Object.

So where the code says ".equals()"

    String kind = elem.getName();
    if (kind != null) {
        if (kind.equals(AbstractDocument.ContentElementName)) {
    ...

it should say "=="

    Object absDocName = attrs.getAttribute(AbstractDocument.ElementNameAttribute)
    if (absDocName != null){
        if (kind == AbstractDocument.ContentElementName) {
    ...
Fico answered 3/9, 2019 at 18:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.