This discussion is archived
1 2 Previous Next 18 Replies Latest reply: Dec 19, 2011 8:42 AM by 903219 RSS

Bidi Text in JTextPane and JTextArea changes line height

903219 Newbie
Currently Being Moderated
You can use the following simple code to demonstrate issue. When you run it, copy and paste an Arabic character from http://www.alanwood.net/unicode/arabic.html into the display area. This creates a bidi condition in the document. The caret changes to one with a flag at top to indicate LTR or RTL property of the character. The line height of the display is changed as well, to one of lesser height. I am able to detect the bidi state. I cannot get any metric to indicate what the line height change was. I've exhausted every possible line height measure I could find. The height reported by all methods does not change from the non-bidi state to the bidi state even though the line height clearly is reduced. Anyone else encounter this? Any proven means to measure the difference?

import javax.swing.JFrame;
import javax.swing.JTextPane;

public class HelloSwing {

public static void main(String[] args) {
JFrame frame = new JFrame("HelloWorldSwing");
String OSname = new String();
OSname = System.getProperty("os.name");
JTextPane tp = new JTextPane();
frame.getContentPane().add(tp);
tp.setText(OSname);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setSize(400, 200);
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
}
  • 1. Re: Bidi Text in JTextPane and JTextArea changes line height
    StanislavL Pro
    Currently Being Moderated
    GlyphPainter is responsible for this. The methods
         public abstract float getHeight(GlyphView v);
         public abstract float getAscent(GlyphView v);
         public abstract float getDescent(GlyphView v);
    Here are my attempts to use custom GlyphPainter.
    http://java-sl.com/Scale_In_JEditorPane_GlyphPainter.html
    http://java-sl.com/gp_effects.html

    Use the code to measure height change.
  • 2. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    Mr. Lapitsky, thank you for sharing this information with me. The GlyphPaint class that you identified is new to me and not one that I've yet tried. I will need a couple days for testing and I will post back my results. Thanks again for your time.
  • 3. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    Mr. Lapitsky, I have studied your extensions for font rendering. I believe that the font metric you use is based on the getStringBounds() method of the Font class. I modified the simple demonstration program I posted to better illustrate the particular issue with line height that seems to be based only on the bidi property. This simple program included below has three line height metric computation methods, measureMetric1(), measureMetric2(), and measureMetric3() that is based on your recommended getStringBounds(). None of them provide correct line height data when the bidi condition is true. It is interesting to note that getStringBounds() height metric actually changes when bidi becomes true. Using measureMetric3() you can paste an Arabic or Hungarian character into the JTestPane to create the bidi state, and observe a weird height as a result of it. If you delete the RTL character you pasted the height metric actually returns to a more reasonable value. You can repeat this over and over and the height value will promptly change to a wrong value each time bidi becomes true. Perhaps there is a deeper issue in the font rendering mechanics of Java that is causing this. Your very interesting extensions to font rendering do not attempt to display mixed bidi characters. Perhaps this would break you rendering extensions.


    import java.awt.Container;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.Graphics2D;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import java.awt.Insets;
    import java.awt.event.KeyEvent;
    import java.awt.event.KeyListener;
    import java.awt.font.FontRenderContext;
    import java.awt.font.LineMetrics;
    import java.awt.geom.Rectangle2D;

    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JTextPane;
    import javax.swing.text.Style;

    public class HelloSwing implements KeyListener {
    JLabel jl;
    JTextPane tp;
    JFrame frame;

    /**
    * measureMetric() gets called by HelloSwing constructor and each time a key
    * is released (as with ctrl-v). This will measure the line height using the
    * measure method enabled and display the float value in the JLabel at the top
    * of the display.<br>
    *
    * Note1: measureMetric1() always returns the same integer value (as float) regardless of bidi.<br>
    * Note2: measureMetric2() does return a float value however it remains the same regardless of bidi.<br>
    * Note3: measureMetric3() when bidi is true the return value changes however it is clearly wrong.<br>
    *
    */
    public void measureMetric()
    {
    float fHeight;

    //fHeight = measureMetric1(); //This reports the line height as 16 points and does not change when bidi is true.
    //fHeight = measureMetric2(); //This reports the line height as 15.09375 and does not change when bidi is true.
    fHeight = measureMetric3(); //This reports the line height as 15.09375 and changes to 3.0292969 when bidi is true.
    jl.setText("" + fHeight);
    }

         public static void main(String[] args) {
    //Make Display
    HelloSwing helloSwing = new HelloSwing();
         }
         
         public HelloSwing()
         {
    //Make Widgets
    jl = new JLabel();
    tp = new JTextPane();
    frame = new JFrame("HelloWorldSwing");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    makeDisplay();
    measureMetric();
         }
         

         /*
         * Several line height metric measures...
         * measureMetric1() is not recommended and is based on an integer value.
         * measureMetric2() is recommended and is based on a float value.
         * measureMetric3() is based on getStringBounds() and is based on a double value.
         */
         
         public float measureMetric1()
         {
    int iFontHeight = tp.getFontMetrics(tp.getFont()).getHeight();
    return (float)iFontHeight;
         }
         public float measureMetric2()
         {
         Font f = tp.getFont(); //get default font
         String s = tp.getText(); //point at text content.
         Graphics2D g2 = (Graphics2D)tp.getGraphics(); //cast g2
         float fHeight = 0f;
         float fAscent = 0f;
         float fDescent = 0f;
         if (g2 != null)
         {
         LineMetrics lm = f.getLineMetrics(s, g2.getFontRenderContext());
         fHeight = lm.getHeight();
         fAscent = lm.getAscent();
         fDescent = lm.getDescent();
         }
         return fHeight;
         }
         public float measureMetric3()
         {
    String s = tp.getText(); //point at text content.
         Graphics2D g2 = (Graphics2D)tp.getGraphics(); //cast g2
         FontRenderContext frc = g2.getFontRenderContext();
         Rectangle2D bounds = g2.getFont().getStringBounds(s, frc);
    float fHeight = (float)bounds.getHeight();
         return fHeight;
         }

    public void keyPressed(KeyEvent e) {}
    public void keyReleased(KeyEvent e) {measureMetric();}
    public void keyTyped(KeyEvent e) {}


    public void makeDisplay()
    {
    /*Add widgets to this panel with the attendant gridbag constraints.
    * GRID LAYOUT
    * -----------------------------------
    * (0,0)
    * | A | JLabel
    * -----------------------------------
    * (0,1)
    * | B | JTextPane
    * -----------------------------------
    *
    * A = Display the line height
    * B = Text area for random text
    */
    frame.setLayout(new GridBagLayout());
    GridBagConstraints g = new GridBagConstraints();

    //Configure widgets
    Dimension d = new Dimension(100, 10);
    jl.setPreferredSize(d);
    jl.setMaximumSize(d);
    jl.setMinimumSize(d);
    jl.setText("13");

    d = new Dimension(370, 130);
    tp.setPreferredSize(d);
    tp.setMaximumSize(d);
    tp.setMinimumSize(d);
    tp.setText("Test line of Roman non-bidi characters.");

    //Add widgets
    Container c = frame.getContentPane();
    //JLabel
    g.gridx = 0;
    g.gridy = 0;
    g.gridwidth = 1;
    g.weightx = 0.0; //No expansion in horizontal
    g.weighty = 0.0; //No expansion in vertical
    g.fill = GridBagConstraints.NONE; //No expansion on resize
    g.anchor = GridBagConstraints.NORTHWEST;
    g.insets = new Insets(0,0,10,0); //Top, Left, Bottom, Right
    c.add(jl, g);

    g.gridx = 0;
    g.gridy = 1;
    g.gridwidth = 1;
    g.weightx = 0.0; //No expansion in horizontal
    g.weighty = 0.0; //No expansion in vertical
    g.fill = GridBagConstraints.NONE; //No expansion on resize
    g.anchor = GridBagConstraints.WEST;
    g.insets = new Insets(0,0,0,0); //Top, Left, Bottom, Right
    c.add(tp,g);
    tp.addKeyListener(this);

    frame.pack();
    frame.setSize(400, 200);
    frame.setLocationByPlatform(true);
    frame.setVisible(true);
    }

    }
  • 4. Re: Bidi Text in JTextPane and JTextArea changes line height
    StanislavL Pro
    Currently Being Moderated
    May be it's better to use modelToView() method passing the caret position before and after the change? It returns Rectangle representing the caret vertical line.

    Or alternatively you cn start from RootView of your JTextPane and go down till the leaf view (normally it's GlyphView extension) achieved. Then you can use getPreferredSpan(View.Y_AXIS);

    You can try this http://java-sl.com/JEditorPaneStructureTool.html
    In the views structure section you cn see full tree of views and you can see clicked view bounds in the JEditorPane.
  • 5. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    Thank you for the additional recommendations. With respect to your two new ideas, I tested the first one in two different ways. Both tests give the same result. The height is reported as 16 points when strictly Roman characters and becomes 15 when the bidi state is true. It makes sense that the height is the same as you pointed out that the modelToView rectangle represents the caret's vertical line.

    public float measureMetric4()
    {
    //The Caret is derived from Rectangle.
    float fHeight = ((Rectangle)tp.getCaret()).height;
    return fHeight;
    }
    public float measureMetric5()
    {
    Rectangle textRect = new Rectangle();
    try {
    textRect = tp.modelToView(1);
    } catch (BadLocationException e) {
    }
    float fHeight = (float)textRect.height;
    return fHeight;
    }

    I may be able to use the caret height, however this approach "feels" structurally wrong. I don't mean to oversimplify the issue of matching font points to a specific display's pixel resolution, etc. But my preference is that the JTextPane Class be capable of reporting a line height that can be communicated to other containers. It is an important metric of the text area and should be correct. I am trying to resolve this issue because something in the Java Swing system is changing the line height whenever the bidi state becomes true. I am puzzled at why it is necessary to alter the line height in order to support the bidi condition. I'm even more puzzled at "what" is doing this. I've also seen that this altered line height scales strange with scaled font sizes. It is very difficult to manage this behavior.

    I have a question regarding the caret height. Does the caret line "always" span the full height of the line, regardless of font scale?

    I will try to test your last recommendation to use the RootView of the JTextPane and locate the lowest leaf view and then use the getPreferredSpan(View.Y_AXIS) on it. Time is not my friend in this issue. I will post back my finding when I complete that. Thanks again for your help.
  • 6. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    Mr. Lapitsky, I tried to understand your last recommendation that I start from the RootView of the JTextPane and then go down to the leaf view. I am unable to get past simply obtaining a root view using "View rootView = tp.getUI().getRootView(tp);" where tp is my JTestPane object.
    I'm stumped at that point. I studied your getAllocation() method at http://java-sl.com/JEditorPaneStructureTool.html but it is unclear to me what your starting view is. The code appears to walk "UP" the tree, not down. I would like to better understand your recommendation but I need to first know what the method parameter v in "getAllocation(View v, JEditorPane edit)" is. Is it the rootView that I obtained from the UI?
  • 7. Re: Bidi Text in JTextPane and JTextArea changes line height
    StanislavL Pro
    Currently Being Moderated
    By default GlyphPainter1 i used. As soon as bidi chars are inserted the painter is replaced with GlyphPainter2.

    The last one based on TextLayout rather than font metrics.
    From GlyphPainter2 source
        public float getHeight(GlyphView v) {
         return layout.getAscent() + layout.getDescent() + layout.getLeading();
        }
    See also TextLayoutStrategy
       void sync(FlowView fv) {
            View lv = getLogicalView(fv);
            text.setView(lv);
    
            Container container = fv.getContainer();
            FontRenderContext frc = sun.swing.SwingUtilities2.
                                        getFontRenderContext(container);
            BreakIterator iter;
            Container c = fv.getContainer();
            if (c != null) {
                iter = BreakIterator.getLineInstance(c.getLocale());
            } else {
                iter = BreakIterator.getLineInstance();
            }
    
            measurer = new LineBreakMeasurer(text, iter, frc);
    
            // If the children of the FlowView's logical view are GlyphViews, they 
            // need to have their painters updated.
            int n = lv.getViewCount();
            for( int i=0; i<n; i++ ) {
                View child = lv.getView(i);
                if( child instanceof GlyphView ) {
                    int p0 = child.getStartOffset();
                    int p1 = child.getEndOffset();
                    measurer.setPosition(text.toIteratorIndex(p0));
                    TextLayout layout 
                        = measurer.nextLayout( Float.MAX_VALUE,
                                               text.toIteratorIndex(p1), false );
                    ((GlyphView)child).setGlyphPainter(new GlyphPainter2(layout));
                }
            }
    
            // Reset measurer.
            measurer.setPosition(text.getBeginIndex());
    
        }
    After you got the rootView you can get it's children. The you need the child which contains the desired offser (caret position). E.g. 0 for start position. IN that case you have to go to the first child always. GO dowwn till the leaf view is achieved (view with children count=0). It should be GlyphView. COuld be also e.g. ImageView or ComponentView but if you pass offset of real text element the leaf is GlyphView. The you can get the view's GlypnPainter and ask for the height.

    BTW: Why do you need the difference?
  • 8. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    I've made some progress with your final recommendation, but it will take a bit more time to complete. I'll post it back as soon as I complete coding it and testing. I did some research today on the glyph management for font rendering. Interesting stuff.

    I don't really need the difference, I need the correct line height. I have a scrollable region that contains a JList and JTextPane side by side. For each line in the text area there is a corresponding line number in the JList. I use the text area's line height to set the cell height in the JList. Note that the JList cell height is strictly integer. The application appeared to be working fine until I pasted a RTL Arabic character into the JTextArea field and encountered the i18n BIDI STATE line height change Java feature. So I changed from JTextArea to JTextPane so that I can "shim" the line height using StyleConstants.setSpaceAbove() and StyleConstants.setSpaceBelow(). However testing with various font sizes demonstrated that I could not get the correct line height from the text area. In order to assure that the two widgets align correctly I need a truthful line height to determine the correct shim sizes to restore the alignment.
  • 9. Re: Bidi Text in JTextPane and JTextArea changes line height
    StanislavL Pro
    Currently Being Moderated
    I think you can override the LabelView and provide desired height there. JUst replace default ViewFactory with your own where the LabelView is replaced.
  • 10. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    To update you, I performed some testing last night. There are two child descents from the RootView, a view and element descent. The view is obviously the one you recommended, but I thought I would clarify that for anyone who reads this post later. Following your advice I used the following code to first get the root view from the JTextPane and then traverse down the view tree until the child views are zero.

         View rootView = tp.getUI().getRootView(tp);
         View child = rootView.getView(0);
         while (child.getViewCount()>0)
         {
         System.out.println(child.getClass().toString());
         child = child.getView(0);
         }
         if (child instanceof javax.swing.text.LabelView)
         {
         LabelView lv = (LabelView)child;
         System.out.println(lv.getGlyphPainter().getClass().toString());
         }

    The resulting tree varies... because I am not familiar with this I thought it was strange. When the application constructor runs it builds the display and then calls the measureMetric() method which in turn now calls the measureMetric6() method that contains the code presented just above. When the constructor runs the result is:
         class javax.swing.text.BoxView

    When the measureMetric6() method runs afterward (press the CTRL key to create the keyRelesed() event that calls the measureMetric() method) the result is:
         class javax.swing.text.BoxView
         class javax.swing.text.ParagraphView
         class javax.swing.text.ParagraphView$Row
         class javax.swing.text.GlyphPainter1 <========= Which is what you predicted.

    The GlyphPainter1 (as you pointed out) changes to GlyphPainter2 when the i18n character is pasted into the JTextPane.

    But the question I now have is how would I get the GlyphPainter object from the JTextPane when the constructor first creates the JTextPane with the string "Test line of Roman non-bidi characters."? It appears from this experiment that the views are not yet available when the constructor is run. I am again out of time so I'll complete this experiment and post back the final result. Your advise has been very appreciated. Thank you.
  • 11. Re: Bidi Text in JTextPane and JTextArea changes line height
    StanislavL Pro
    Currently Being Moderated
    GlyphPainter isn't set on creation but on first paint. When the paint is called views must be layed out. To provide correct layout all the views must be measured about their preferred spans. That's moment when sync() is called which checks which GlyphPainter should be used for the view and calls the painter to get all the required sizes (height, width etc.)

    http://java-sl.com/tip_colored_strikethrough.html
    That's example how to add the custom LabelView. In the view you can use cast your Document to StyledDocument and use method
    public Font getFont(AttributeSet attr)
    Just pass attributes of the view's Element. Then based on the font return always fixed height of the font rather than asking GlyphPainter.
  • 12. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    I apologize but I am slightly confused. Is your recommendation that I replace LabelView with my own or do you still think that the getPreferredSpan(View.Y_AXIS) has merit? Regarding that, I completed my experiments with getPreferredSpan(View.Y_AXIS):

    float fHeight = 0;
    //use the RootView of the JTextPane and locate the lowest leaf view and then use the getPreferredSpan(View.Y_AXIS) on i
    View rootView = tp.getUI().getRootView(tp);
    View child = rootView.getView(0);
    while (child.getViewCount()>0)
    {
    System.out.println(child.getClass().toString());
    child = child.getView(0);
    }
    if (child instanceof javax.swing.text.LabelView)
    {
    LabelView lv = (LabelView)child;
    GlyphPainter gp = lv.getGlyphPainter();
    fHeight = lv.getPreferredSpan(View.Y_AXIS);
    }
    return fHeight;

    I have two problems with this approach.
    1. The LabelView is not available except unless the content of the display is repainted. I was not able to force the JTextPane to paint from the constructor using any of the means I am aware of. I tried using the window open event as well. So I can't get the initial line height to set the cell height of the JList.
    2. Second, and more important, when it does provide access to the GlyphPainter it returns the height values of 16.0 point for Roman only and 15.09375 point for i18n. The i18n value is strange because it is the exact same value obtained by the measureMetric2() method on ALL font conditions. The measureMetric2() method uses Font.getLineMetrics(). So I strongly suspect that this height is not correct.
  • 13. Re: Bidi Text in JTextPane and JTextArea changes line height
    StanislavL Pro
    Currently Being Moderated
    1. The LabelView is created after setText() call but GlyphPainter is initialized after the first paint.
    But you don't need the painter itself. LabelView exists and you can get its modelElement and get font as i suggested above

    2. The size is defined like this. I see no easy way to "correct" it.
  • 14. Re: Bidi Text in JTextPane and JTextArea changes line height
    903219 Newbie
    Currently Being Moderated
    Mr. Lapitsky, Thank you for you kind assistance with my inquiry. I appreciate that you have taken the time to discuss possible solutions and consider my results. I especially want to thank you for your generosity and hard work to share your knowledge with other Java programmers on your outstanding web site http://java-sl.com.

    I agree with your final answer that there is no easy way to correct my application. I am amazed by the programmers of the Eclipse IDE. If you paste i18n characters into their line numbered display there is no impact. Evidently there is an answer to my problem, I just lack the time and knowledge to grasp it.

    Thanks again for your time.
1 2 Previous Next

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points