Bidi support for ListBox Review at http://gwt-code-reviews.appspot.com/1137801 Review by: aharon@google.com git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9347 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/tools/api-checker/config/gwt21_22userApi.conf b/tools/api-checker/config/gwt21_22userApi.conf index e700609..a15c6b7 100644 --- a/tools/api-checker/config/gwt21_22userApi.conf +++ b/tools/api-checker/config/gwt21_22userApi.conf
@@ -273,3 +273,7 @@ com.google.gwt.user.cellview.client.Header::onBrowserEvent(Lcom/google/gwt/dom/client/Element;Lcom/google/gwt/dom/client/NativeEvent;) MISSING com.google.gwt.user.cellview.client.Header::render(Lcom/google/gwt/safehtml/shared/SafeHtmlBuilder;) MISSING +# These are supposedly ambiguous with other overloads on null values. +com.google.gwt.user.client.ui.ListBox::addItem(Ljava/lang/String;Ljava/lang/String;) OVERLOADED_METHOD_CALL +com.google.gwt.user.client.ui.ListBox::insertItem(Ljava/lang/String;Ljava/lang/String;I) OVERLOADED_METHOD_CALL +
diff --git a/user/src/com/google/gwt/user/client/ui/ListBox.java b/user/src/com/google/gwt/user/client/ui/ListBox.java index 4715f94..371e71c 100644 --- a/user/src/com/google/gwt/user/client/ui/ListBox.java +++ b/user/src/com/google/gwt/user/client/ui/ListBox.java
@@ -23,6 +23,13 @@ import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.HasChangeHandlers; import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.i18n.client.HasDirection.Direction; +import com.google.gwt.i18n.shared.BidiFormatter; +import com.google.gwt.i18n.shared.DirectionEstimator; +import com.google.gwt.i18n.shared.HasDirectionEstimator; +import com.google.gwt.i18n.shared.WordCountDirectionEstimator; + +import java.util.ArrayList; /** * A widget that presents a list of choices to the user, either as a list box or @@ -41,7 +48,14 @@ * <h3>Example</h3> * {@example com.google.gwt.examples.ListBoxExample} * </p> - * + * + * <p> + * <h3>Built-in Bidi Text Support</h3> + * This widget is capable of automatically adjusting its direction according to + * its content. This feature is controlled by {@link #setDirectionEstimator}, + * and is off by default. + * </p> + * * <h3>Use in UiBinder Templates</h3> * <p> * The items of a ListBox element are laid out in <g:item> elements. @@ -67,7 +81,10 @@ */ @SuppressWarnings("deprecation") public class ListBox extends FocusWidget implements SourcesChangeEvents, - HasChangeHandlers, HasName { + HasChangeHandlers, HasName, HasDirectionEstimator { + + public static final DirectionEstimator DEFAULT_DIRECTION_ESTIMATOR = + WordCountDirectionEstimator.get(); private static final int INSERT_AT_END = -1; @@ -94,6 +111,9 @@ return listBox; } + private DirectionEstimator estimator; + private ArrayList<String> itemTexts = new ArrayList<String>(); + /** * Creates an empty list box in single selection mode. */ @@ -149,6 +169,21 @@ } /** + * Adds an item to the list box, specifying its direction. This method has the + * same effect as + * + * <pre> + * addItem(item, dir, item) + * </pre> + * + * @param item the text of the item to be added + * @param dir the item's direction + */ + public void addItem(String item, Direction dir) { + insertItem(item, dir, INSERT_AT_END); + } + + /** * Adds an item to the list box, specifying an initial value for the item. * * @param item the text of the item to be added @@ -160,12 +195,29 @@ } /** + * Adds an item to the list box, specifying its direction and an initial value + * for the item. + * + * @param item the text of the item to be added + * @param dir the item's direction + * @param value the item's value, to be submitted if it is part of a + * {@link FormPanel}; cannot be <code>null</code> + */ + public void addItem(String item, Direction dir, String value) { + insertItem(item, dir, value, INSERT_AT_END); + } + + /** * Removes all items from the list box. */ public void clear() { getSelectElement().clear(); } + public DirectionEstimator getDirectionEstimator() { + return estimator; + } + /** * Gets the number of items present in the list box. * @@ -184,7 +236,7 @@ */ public String getItemText(int index) { checkIndex(index); - return getSelectElement().getOptions().getItem(index).getText(); + return itemTexts.get(index); } public String getName() { @@ -239,9 +291,28 @@ } /** + * Inserts an item into the list box, specifying its direction. Has the same + * effect as + * + * <pre> + * insertItem(item, dir, item, index) + * </pre> + * + * @param item the text of the item to be inserted + * @param dir the item's direction + * @param index the index at which to insert it + */ + public void insertItem(String item, Direction dir, int index) { + insertItem(item, dir, item, index); + } + + /** * Inserts an item into the list box, specifying an initial value for the - * item. If the index is less than zero, or greater than or equal to the - * length of the list, then the item will be appended to the end of the list. + * item. Has the same effect as + * + * <pre> + * insertItem(item, null, value, index) + * </pre> * * @param item the text of the item to be inserted * @param value the item's value, to be submitted if it is part of a @@ -249,12 +320,35 @@ * @param index the index at which to insert it */ public void insertItem(String item, String value, int index) { + insertItem(item, null, value, index); + } + + /** + * Inserts an item into the list box, specifying its direction and an initial + * value for the item. If the index is less than zero, or greater than or + * equal to the length of the list, then the item will be appended to the end + * of the list. + * + * @param item the text of the item to be inserted + * @param dir the item's direction. If {@code null}, the item is displayed in + * the widget's overall direction, or, if a direction estimator has + * been set, in the item's estimated direction. + * @param value the item's value, to be submitted if it is part of a + * {@link FormPanel}. + * @param index the index at which to insert it + */ + public void insertItem(String item, Direction dir, String value, int index) { SelectElement select = getSelectElement(); OptionElement option = Document.get().createOptionElement(); - option.setText(item); + option.setText(unicodeWrapIfNeeded(item, dir)); option.setValue(value); - if ((index == -1) || (index == select.getLength())) { + int itemCount = select.getLength(); + if (index < 0 || index > itemCount) { + index = itemCount; + } + itemTexts.add(index, item); + if (index == itemCount) { select.add(option, null); } else { OptionElement before = select.getOptions().getItem(index); @@ -294,13 +388,31 @@ /** * Removes the item at the specified index. - * + * * @param index the index of the item to be removed * @throws IndexOutOfBoundsException if the index is out of range */ public void removeItem(int index) { checkIndex(index); getSelectElement().remove(index); + itemTexts.remove(index); + } + + /** + * {@inheritDoc} + * See note at + * {@link #setDirectionEstimator(com.google.gwt.i18n.shared.DirectionEstimator)} + */ + public void setDirectionEstimator(boolean enabled) { + setDirectionEstimator(enabled ? DEFAULT_DIRECTION_ESTIMATOR : null); + } + + /** + * {@inheritDoc} + * Note: this does not affect the direction of already-existing content. + */ + public void setDirectionEstimator(DirectionEstimator directionEstimator) { + estimator = directionEstimator; } /** @@ -328,11 +440,25 @@ * @throws IndexOutOfBoundsException if the index is out of range */ public void setItemText(int index, String text) { + setItemText(index, text, null); + } + + /** + * Sets the text associated with the item at a given index. + * + * @param index the index of the item to be set + * @param text the item's new text + * @param dir the item's direction. + * @throws IndexOutOfBoundsException if the index is out of range + */ + public void setItemText(int index, String text, Direction dir) { checkIndex(index); if (text == null) { throw new NullPointerException("Cannot set an option to have null text"); } - getSelectElement().getOptions().getItem(index).setText(text); + getSelectElement().getOptions().getItem(index).setText(unicodeWrapIfNeeded( + text, dir)); + itemTexts.set(index, text); } /** @@ -424,4 +550,13 @@ private SelectElement getSelectElement() { return getElement().cast(); } + + private String unicodeWrapIfNeeded(String text, Direction dir) { + if (dir == null && estimator != null) { + dir = estimator.estimateDirection(text); + } + return dir == null ? text : + BidiFormatter.getInstanceForCurrentLocale().unicodeWrapWithKnownDir(dir, + text, false /* isHtml */, false /* dirReset */); + } }
diff --git a/user/test/com/google/gwt/user/client/ui/ListBoxTest.java b/user/test/com/google/gwt/user/client/ui/ListBoxTest.java index eb06f9e..0168129 100644 --- a/user/test/com/google/gwt/user/client/ui/ListBoxTest.java +++ b/user/test/com/google/gwt/user/client/ui/ListBoxTest.java
@@ -15,6 +15,9 @@ */ package com.google.gwt.user.client.ui; +import com.google.gwt.dom.client.SelectElement; +import com.google.gwt.i18n.client.HasDirection.Direction; +import com.google.gwt.i18n.shared.BidiFormatter; import com.google.gwt.junit.client.GWTTestCase; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DeferredCommand; @@ -24,6 +27,12 @@ */ public class ListBoxTest extends GWTTestCase { + private final String RTL_TEXT = "\u05e0 \u05e0\u05e0\u05e0\u05e0\u05e0" + + "\u05e0\u05e0\u05e0 \u05e0\u05e0\u05e0\u05e0\u05e0 \u05e0\u05e0\u05e0" + + "\u05e0\u05e0\u05e0 \u05e0\u05e0\u05e0 \u05e0\u05e0\u05e0"; + private final String LTR_TEXT = "The quick brown fox jumps over the" + + "lazy dog. The lazy dog seems quite amused."; + @Override public String getModuleName() { return "com.google.gwt.user.DebugTest"; @@ -52,9 +61,9 @@ delayTestFinish(5000); DeferredCommand.addCommand(new Command() { public void execute() { - UIObjectTest.assertDebugIdContents("myList-item0", "option0"); - UIObjectTest.assertDebugIdContents("myList-item1", "option1"); - UIObjectTest.assertDebugIdContents("myList-item2", "option2"); + UIObjectTest.assertDebugIdContents("myList-item0", "option0"); + UIObjectTest.assertDebugIdContents("myList-item1", "option1"); + UIObjectTest.assertDebugIdContents("myList-item2", "option2"); UIObjectTest.assertDebugIdContents("myList-item3", "option3"); finishTest(); } @@ -107,6 +116,46 @@ assertEquals("b", lb.getItemText(1)); assertEquals("c", lb.getItemText(2)); } + + // Insert items of different directions + { + // Explicit direction, no direction estimation + ListBox lb = new ListBox(); + lb.insertItem(RTL_TEXT, Direction.RTL, 0); + assertEquals(RTL_TEXT, lb.getItemText(0)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + RTL_TEXT, false /* isHtml */, false /* dirReset */), lb, 0); + lb.insertItem(LTR_TEXT, Direction.LTR, 0); + assertEquals(LTR_TEXT, lb.getItemText(0)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + LTR_TEXT, false /* isHtml */, false /* dirReset */), lb, 0); + lb.clear(); + + // Direction estimation + lb.setDirectionEstimator(true); + lb.addItem(RTL_TEXT); + assertEquals(RTL_TEXT, lb.getItemText(0)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + RTL_TEXT, false /* isHtml */, false /* dirReset */), lb, 0); + lb.addItem(LTR_TEXT); + assertEquals(LTR_TEXT, lb.getItemText(1)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + LTR_TEXT, false /* isHtml */, false /* dirReset */), lb, 1); + + // Explicit direction which is opposite to the estimated direction + lb.insertItem(RTL_TEXT, Direction.LTR, 0); + assertEquals(RTL_TEXT, lb.getItemText(0)); + assertOptionText( + BidiFormatter.getInstanceForCurrentLocale().unicodeWrapWithKnownDir( + Direction.LTR, RTL_TEXT, false /* isHtml */, false /* dirReset */), + lb, 0); + lb.insertItem(LTR_TEXT, Direction.RTL, 1); + assertEquals(LTR_TEXT, lb.getItemText(1)); + assertOptionText( + BidiFormatter.getInstanceForCurrentLocale().unicodeWrapWithKnownDir( + Direction.RTL, LTR_TEXT, false /* isHtml */, false /* dirReset */), + lb, 1); + } } public void testRemove() { @@ -214,6 +263,46 @@ assertEquals("bc", box.getItemText(1)); box.setItemText(0, ""); assertEquals("", box.getItemText(0)); + + // Text of different directions + { + ListBox lb = new ListBox(); + // Explicit direction, no direction estimation + lb.insertItem(RTL_TEXT, Direction.RTL, 0); + assertEquals(RTL_TEXT, lb.getItemText(0)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + RTL_TEXT, false /* isHtml */, false /* dirReset */), lb, 0); + lb.insertItem(LTR_TEXT, Direction.LTR, 0); + assertEquals(LTR_TEXT, lb.getItemText(0)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + LTR_TEXT, false /* isHtml */, false /* dirReset */), lb, 0); + + // Direction estimation + lb.setDirectionEstimator(true); + lb.setItemText(0, RTL_TEXT); + assertEquals(RTL_TEXT, lb.getItemText(0)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + RTL_TEXT, false /* isHtml */, false /* dirReset */), lb, 0); + lb.setItemText(0, LTR_TEXT); + assertEquals(LTR_TEXT, lb.getItemText(0)); + assertOptionText(BidiFormatter.getInstanceForCurrentLocale().unicodeWrap( + LTR_TEXT, false /* isHtml */, false /* dirReset */), lb, 0); + + // Explicit direction which is opposite to the estimated direction + lb.setItemText(0, LTR_TEXT, Direction.RTL); + assertEquals(LTR_TEXT, lb.getItemText(0)); + assertOptionText( + BidiFormatter.getInstanceForCurrentLocale().unicodeWrapWithKnownDir( + Direction.RTL, LTR_TEXT, false /* isHtml */, false /* dirReset */), + lb, 0); + lb.setItemText(0, RTL_TEXT, Direction.LTR); + assertEquals(RTL_TEXT, lb.getItemText(0)); + assertOptionText( + BidiFormatter.getInstanceForCurrentLocale().unicodeWrapWithKnownDir( + Direction.LTR, RTL_TEXT, false /* isHtml */, false /* dirReset */), + lb, 0); + } + try { box.setItemText(0, null); fail("Should have thrown Null Pointer"); @@ -262,4 +351,9 @@ assertEquals("item text", box.getItemText(1)); } } + + private void assertOptionText(String expected, ListBox listBox, int index) { + SelectElement select = listBox.getElement().cast(); + assertEquals(expected, select.getOptions().getItem(index).getText()); + } }