Word wrapping with mixed LTR and RTL text not correct

XMLWordPrintable

    • Type: Bug
    • Resolution: Won't Fix
    • Priority: P4
    • None
    • Affects Version/s: 7u71
    • Component/s: client-libs

      FULL PRODUCT VERSION :
      java version "1.7.0_71"
      Java(TM) SE Runtime Environment (build 1.7.0_71-b14)
      Java HotSpot(TM) Client VM (build 24.71-b01, mixed mode, sharing)

      ADDITIONAL OS VERSION INFORMATION :
      Microsoft Windows [Version 6.1.7601]

      A DESCRIPTION OF THE PROBLEM :
      The problem involves word-wrapped JLabel components, when the text contains mixed right-to-left and left-to-right characters.

      Generally, word-wrapping a label breaks the text into multiple lines, depending on the width of the label component. For a given text string, the wider the label component, the more words fit on each line, and the fewer wrapped lines are required. It must never happen for any given text string that as the label becomes wider, more lines are required (or equivalently, as a label becomes narrower, fewer lines are required). Such a bug can cause display anomalies, for example an infinite repaint loop caused by a boundary where a vertical scroll bar appears, then disappears, then appears again, without end.

      This bug occurs only when the text string contains mixed RTL and LTR characters, for example a combination of English and Hebrew, or a combination of Hebrew and numeric digits. We don't see this problem when the text string contains just Hebrew characters, or just LTR characters.

      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      The sample program is a Swing application that takes a text string entered into a text field, sets the text into a JLabel, and applies wrapping at different pixel widths to the JLabel's HTML View. For each width setting, the preferred vertical height is measured. The width is varied monotonically between 200 pixels and 10 pixels. The program detects discontinuities where, as the width decreases by one pixel, the height required to wrap the text within that width decreases, instead of remaining the same or increasing. All the discontinuities found are displayed in a table with two columns. The first column displays the width/height pair with lower width, and the second column displays the width/height pair with higher width. Some sample text strings are provided for testing, or the user can enter a text string of their choosing to analyze for discontinuities. To reproduce the problem, select or type a sample string that contains a mix of Hebrew and English characters, or a mix of Hebrew characters and numeric digits.

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      As the width is decreased pixel by pixel, from 200 down to 10 pixels, the preferred height of the view should either remain the same or increase.
      ACTUAL -
      For mixed text, at some points, decreasing the width by one pixel causes the preferred height to decrease.

      REPRODUCIBILITY :
      This bug can be reproduced often.

      ---------- BEGIN SOURCE ----------
      import java.awt.BorderLayout;
      import java.awt.GridBagConstraints;
      import java.awt.GridBagLayout;
      import java.awt.Insets;
      import java.awt.event.ActionEvent;
      import java.awt.event.ActionListener;
      import java.util.Vector;

      import javax.swing.ButtonGroup;
      import javax.swing.JButton;
      import javax.swing.JFrame;
      import javax.swing.JLabel;
      import javax.swing.JPanel;
      import javax.swing.JRadioButton;
      import javax.swing.JScrollPane;
      import javax.swing.JTable;
      import javax.swing.JTextField;
      import javax.swing.plaf.basic.BasicHTML;
      import javax.swing.table.DefaultTableModel;
      import javax.swing.text.View;

      public class RTLWrap {

      private final String hebrewEnglishText = "English text and \u05E9 \u05E0\u05D5\u05DE\u05D1\u05D9 \u05DD\u05DB \u05D9\u05E7\u05E0\u05E8\u05E7' \u05D0\u05E7\u05E1\u05D0 \u05E9\u05D3 '\u05E7\u05DA\u05DA";
      private final String hebrewDigitsText = "\u05D9\u05E7\u05E8\u05E7 \u05DF\u05D3 \u05D3\u05DD\u05E6\u05E7 \u05DC\u05DF\u05DE\u05D2 \u05DD\u05DB 2902 029 007685 \u05D0\u05D9\u05E9\u05D0 \u05DF \u05E9\u05E6 \u05D0\u05D8\u05E4\u05DF\u05DE\u05E2";
      private final String hebrewText = "\u05D9\u05E7\u05E8\u05E7 \u05DF\u05D3 \u05D3\u05DD\u05E6\u05E7 \u05DC\u05DF\u05DE\u05D2 \u05DD\u05DB '\u05E7\u05DF\u05E8\u05D2 \u05D9\u05E7\u05E0\u05E8\u05E7' \u05D0\u05E7\u05E1\u05D0 \u05D0\u05D9\u05E9\u05D0 \u05DF \u05E9\u05E6 \u05D0\u05D8\u05E4\u05DF\u05DE\u05E2";
      private final String englishText = "This is some English text without any Hebrew text at all";

      GridBagConstraints c = new GridBagConstraints();
      final JTextField txtFld = new JTextField("");
      final ActionListener radioActionListener = new btnActionListener();

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

      public RTLWrap() {
      final JFrame guiFrame = new JFrame();
      // make sure the program exits when the
      // frame closes
      guiFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      guiFrame.setTitle("Right To Left Wrapping Problem");
      guiFrame.setSize(500, 400);
      // This will center the JFrame in the middle
      // of the screen
      guiFrame.setLocationRelativeTo(null);

      // This is the hidden label we will use to test the wrapping
      final JLabel wrappedLabel = new JLabel();

      JPanel textPanel = new JPanel(new GridBagLayout());
      JLabel txtLbl = new JLabel("I18N Text to wrap:");
      c.gridy = 0;
      c.insets = new Insets(10,0,0,0);
      textPanel.add(txtLbl,c);
      c.gridy++;
      c.weightx = 1.0F;
      c.fill = GridBagConstraints.HORIZONTAL;
      c.insets = new Insets(0,10,0,10);
      c.gridwidth = 2;
      textPanel.add(txtFld,c);

      JRadioButton btnType = new JRadioButton("Type My Own"); btnType.setActionCommand("Type"); btnType.setSelected(true); btnType.addActionListener(radioActionListener);
      JRadioButton btnEnglish = new JRadioButton("Pure English"); btnEnglish.setActionCommand("English"); btnEnglish.addActionListener(radioActionListener);
      JRadioButton btnHebrew = new JRadioButton("Pure Hebrew"); btnHebrew.setActionCommand("Hebrew"); btnHebrew.addActionListener(radioActionListener);
      JRadioButton btnEngHeb = new JRadioButton("English and Hebrew Mixed"); btnEngHeb.setActionCommand("EngHeb"); btnEngHeb.addActionListener(radioActionListener);
      JRadioButton btnHebDig = new JRadioButton("Hebrew and Digits Mixed"); btnHebDig.setActionCommand("HebDig"); btnHebDig.addActionListener(radioActionListener);
      ButtonGroup btnGroup = new ButtonGroup();
      btnGroup.add(btnType); btnGroup.add(btnEnglish); btnGroup.add(btnHebrew); btnGroup.add(btnEngHeb); btnGroup.add(btnHebDig);

      c.gridy++;
      c.weightx = 0.0F;
      c.fill = GridBagConstraints.NONE;
      c.insets = new Insets(0,0,0,0);
      textPanel.add(btnType,c);
      c.gridy++;
      c.gridwidth = 1;
      c.insets = new Insets(0,30,0,10);
      c.anchor = GridBagConstraints.WEST;
      textPanel.add(btnEnglish,c); textPanel.add(btnHebrew,c);
      c.gridy++;
      textPanel.add(btnEngHeb,c); textPanel.add(btnHebDig,c);
      c.anchor = GridBagConstraints.CENTER;

      JPanel discontinuityPanel = new JPanel(new GridBagLayout());
      JLabel discontinuitiesLbl = new JLabel("Discontinuities Discovered:");
      Vector<String> columnNames = new Vector<String>();
      columnNames.add("W1 / W2"); columnNames.add("H1 / H2");
      Vector<Vector<String>> rowData = new Vector<Vector<String>>();
      Vector<String> row = new Vector<String>();
      row.add("None"); row.add("None");
      rowData.add(row);
      final JTable jtable = new JTable(rowData, columnNames);
      JScrollPane scrollPane = new JScrollPane(jtable);
      jtable.setFillsViewportHeight(true);

      c = new GridBagConstraints();
      c.gridy = 0;
      c.insets = new Insets(30,0,10,0);
      discontinuityPanel.add(discontinuitiesLbl,c);
      c.gridy++;
      c.insets = new Insets(0,0,0,0);
      c.weighty = 5.0F;
      c.fill = GridBagConstraints.BOTH;
      discontinuityPanel.add(scrollPane,c);

      JPanel btnPanel = new JPanel(new GridBagLayout());
      JButton discoverBtn = new JButton("Discover Wrapping Discontinuities");
      c = new GridBagConstraints();
      c.gridy = 0;
      c.fill = GridBagConstraints.NONE;
      c.insets = new Insets(30,0,10,0);
      btnPanel.add(discoverBtn,c);

      discoverBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent event) {
      wrappedLabel.setText("<html>"+txtFld.getText()+"</html>");
      int lastHeight = 0;
      boolean discontinuityFound = false;
      DefaultTableModel model = (DefaultTableModel) jtable.getModel();
      model.setRowCount(0);
      jtable.paintImmediately(0,0,jtable.getWidth(),jtable.getHeight());

      for ( int width = 200; width >=10; width-- )
      {
      View view = (View) wrappedLabel.getClientProperty(BasicHTML.propertyKey);
      view.setSize(width, 99999);
      view.replace(0, wrappedLabel.getText().length(), null);
      int height = (int) view.getPreferredSpan(View.Y_AXIS);

      if ( height < lastHeight )
      {
      // found a discontinuity
      discontinuityFound = true;
      // insert a row in the table
      Vector<String> newRow = new Vector<String>();
      newRow.add(width+" / "+(width+1)); newRow.add(height + " / " + lastHeight);
      model.insertRow(0, newRow);
      }
      lastHeight = height;
      }
      if ( !discontinuityFound )
      {
      // Add a NONE row
      Vector<String> newRow = new Vector<String>();
      newRow.add("None"); newRow.add("None");
      model.addRow(newRow);
      }
      }
      });


      // The JFrame uses the BorderLayout layout manager.
      // Put the two JPanels and JButton in different areas.
      guiFrame.add(textPanel, BorderLayout.NORTH);
      guiFrame.add(discontinuityPanel, BorderLayout.CENTER);
      guiFrame.add(btnPanel, BorderLayout.SOUTH);
      // make sure the JFrame is visible
      guiFrame.setVisible(true);
      }

      class btnActionListener implements ActionListener
      {
      @Override
      public void actionPerformed(ActionEvent e) {
      String selection = e.getActionCommand();
      if ( "English".equals(selection) )
      {
      txtFld.setText(englishText);
      }
      else if ( "Hebrew".equals(selection) )
      {
      txtFld.setText(hebrewText);
      }
      else if ( "EngHeb".equals(selection) )
      {
      txtFld.setText(hebrewEnglishText);
      }
      else if ( "HebDig".equals(selection) )
      {
      txtFld.setText(hebrewDigitsText);
      }
      else if ( "Type".equals(selection) )
      {
      txtFld.setText("");
      }
       
      }
      }

      }

      ---------- END SOURCE ----------

      CUSTOMER SUBMITTED WORKAROUND :
      No workaround.

            Assignee:
            Unassigned
            Reporter:
            Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated:
              Resolved: