changeset 255:0ae14bbf91a1

fix RT-18656 Add support for StringConverter to ChoiceBox & RT-18475 Support bi-directional binding for the value of a ChoiceBox.
author Paru Somashekar <paru.somashekar@oracle.com>
date Wed, 11 Jan 2012 23:52:24 -0800
parents 4e697a25d882
children 5771e7157e47
files javafx-ui-controls/src/com/sun/javafx/scene/control/behavior/ChoiceBoxBehavior.java javafx-ui-controls/src/com/sun/javafx/scene/control/skin/ChoiceBoxSkin.java javafx-ui-controls/src/javafx/scene/control/ChoiceBox.java javafx-ui-controls/test/javafx/scene/control/ChoiceBoxTest.java javafx-ui-controls/test/javafx/scene/control/ComboBoxTest.java
diffstat 5 files changed, 246 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- a/javafx-ui-controls/src/com/sun/javafx/scene/control/behavior/ChoiceBoxBehavior.java	Wed Jan 11 22:04:43 2012 -0500
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/behavior/ChoiceBoxBehavior.java	Wed Jan 11 23:52:24 2012 -0800
@@ -45,7 +45,7 @@
  *
  * @profile common
  */
-public class ChoiceBoxBehavior extends BehaviorBase<ChoiceBox> {
+public class ChoiceBoxBehavior<T> extends BehaviorBase<ChoiceBox<T>> {
     /**
      * The key bindings for the ChoiceBox. It seems this should really be the
      * same as with the ButtonBehavior super class, but it doesn't handle ENTER
--- a/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/ChoiceBoxSkin.java	Wed Jan 11 22:04:43 2012 -0500
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/ChoiceBoxSkin.java	Wed Jan 11 23:52:24 2012 -0800
@@ -25,7 +25,7 @@
 
 package com.sun.javafx.scene.control.skin;
 
-import com.sun.javafx.scene.control.WeakListChangeListener;
+import javafx.util.StringConverter;
 import javafx.beans.InvalidationListener;
 import javafx.beans.Observable;
 import javafx.beans.value.ChangeListener;
@@ -51,13 +51,13 @@
 import javafx.scene.text.Text;
 
 import com.sun.javafx.scene.control.behavior.ChoiceBoxBehavior;
-
+import com.sun.javafx.scene.control.WeakListChangeListener;
 
 
 /**
  * ChoiceBoxSkin - default implementation
  */
-public class ChoiceBoxSkin extends SkinBase<ChoiceBox, ChoiceBoxBehavior> {
+    public class ChoiceBoxSkin<T> extends SkinBase<ChoiceBox<T>, ChoiceBoxBehavior<T>> {
 
     public ChoiceBoxSkin(ChoiceBox control) {
         super(control, new ChoiceBoxBehavior(control));
@@ -253,7 +253,9 @@
         } else if (o instanceof SeparatorMenuItem) {
             popupItem = (SeparatorMenuItem) o;
         } else {
-            final RadioMenuItem item = new RadioMenuItem(o == null ? "" : o.toString());
+            StringConverter c = getSkinnable().getConverter();
+            String displayString = (c == null) ? ((o == null) ? "" : o.toString()) :  c.toString(o);
+            final RadioMenuItem item = new RadioMenuItem(displayString);
             item.setId("choice-box-menu-item");
             item.setToggleGroup(toggleGroup);
             final int index = i;
--- a/javafx-ui-controls/src/javafx/scene/control/ChoiceBox.java	Wed Jan 11 22:04:43 2012 -0500
+++ b/javafx-ui-controls/src/javafx/scene/control/ChoiceBox.java	Wed Jan 11 23:52:24 2012 -0800
@@ -34,9 +34,12 @@
 import javafx.collections.FXCollections;
 import javafx.collections.ListChangeListener;
 import javafx.collections.ObservableList;
-import com.sun.javafx.css.StyleManager;
 import javafx.beans.property.ReadOnlyBooleanProperty;
 import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.event.ActionEvent;
+import javafx.util.StringConverter;
+
+import com.sun.javafx.css.StyleManager;
 
 /**
  * The ChoiceBox is used for presenting the user with a relatively small set of
@@ -94,6 +97,19 @@
         getStyleClass().setAll("choice-box");
         setItems(items);
         setSelectionModel(new ChoiceBoxSelectionModel<T>(this));
+        
+        // listen to the value property, if the value is
+        // set to something that exists in the items list, update the
+        // selection model to indicate that this is the selected item
+        valueProperty().addListener(new ChangeListener<T>() {
+            @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) {
+                if (getItems() == null) return;
+                int index = getItems().indexOf(t1);
+                if (index > -1) {
+                    getSelectionModel().select(index);
+                }
+            }
+        });
     }
 
     /***************************************************************************
@@ -109,7 +125,30 @@
      * in the items list should be selected, or to listen to changes in the
      * selection to know which item has been chosen.
      */
-    private ObjectProperty<SingleSelectionModel<T>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel");
+    private ObjectProperty<SingleSelectionModel<T>> selectionModel = 
+            new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") {
+         private SelectionModel<T> oldSM = null;
+        @Override protected void invalidated() {
+            if (oldSM != null) {
+                oldSM.selectedItemProperty().removeListener(selectedItemListener);
+            }
+            SelectionModel sm = get();
+            oldSM = sm;
+            if (sm != null) {
+                sm.selectedItemProperty().addListener(selectedItemListener);
+            }
+        }                
+    };
+    
+    private ChangeListener<T> selectedItemListener = new ChangeListener<T>() {
+        @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) {
+            if (! valueProperty().isBound()) {
+                setValue(t1);
+            }
+        }
+    };
+
+    
     public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); }
     public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); }
     public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; }
@@ -185,7 +224,16 @@
     private final ListChangeListener<T> itemsListener = new ListChangeListener<T>() {
         @Override public void onChanged(Change<? extends T> c) {
             final SingleSelectionModel<T> sm = getSelectionModel();
+            if (sm!= null) {
+                if (getItems() == null || getItems().isEmpty()) {
+                    sm.clearSelection();
+                } else {
+                    int newIndex = getItems().indexOf(sm.getSelectedItem());
+                    sm.setSelectedIndex(newIndex);
+                }
+            }
             if (sm != null) {
+                
                 // Look for the selected item as having been removed. If it has been,
                 // then we need to clear the selection in the selection model.
                 final T selectedItem = sm.getSelectedItem();
@@ -193,11 +241,46 @@
                     if (selectedItem != null && c.getRemoved().contains(selectedItem)) {
                         sm.clearSelection();
                         break;
-                    }
+                        }
                 }
             }
         }
     };
+    
+    /**
+     * Allows a way to specify how to represent objects in the items list. When
+     * a StringConverter is set, the object toString method is not called and 
+     * instead its toString(object T) is called, passing the objects in the items list. 
+     * This is useful when using domain objects in a ChoiceBox as this property 
+     * allows for customization of the representation. Also, any of the pre-built
+     * Converters available in the {@link javafx.util.converter} package can be set. 
+     */
+    public ObjectProperty<StringConverter<T>> converterProperty() { return converter; }
+    private ObjectProperty<StringConverter<T>> converter = 
+            new SimpleObjectProperty<StringConverter<T>>(this, "converter", null);
+    public final void setConverter(StringConverter<T> value) { converterProperty().set(value); }
+    public final StringConverter<T> getConverter() {return converterProperty().get(); }
+    
+    /**
+     * The value of this ChoiceBox is defined as the selected item in the ChoiceBox
+     * selection model. The valueProperty is synchronized with the selectedItem. 
+     * This property allows for bi-directional binding of external properties to the 
+     * ChoiceBox and updates the selection model accordingly. 
+     */
+    public ObjectProperty<T> valueProperty() { return value; }
+    private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value") {
+        @Override protected void invalidated() {
+            super.invalidated();
+            fireEvent(new ActionEvent());
+            // Update selection
+            final SingleSelectionModel<T> sm = getSelectionModel();
+            if (sm != null) {
+                sm.select(getValue());
+            }
+        }
+    };
+    public final void setValue(T value) { valueProperty().set(value); }
+    public final T getValue() { return valueProperty().get(); }
 
     /***************************************************************************
      *                                                                         *
@@ -318,7 +401,7 @@
          */
         @Override public void select(int index) {
             final int rowCount = getItemCount();
-            if (rowCount == 0 || index < 0 || index >= rowCount) return;
+            // this does not sound right, we should let the superclass handle it.
             final T value = getModelItem(index);
             if (value instanceof Separator) {
                 select(++index);
--- a/javafx-ui-controls/test/javafx/scene/control/ChoiceBoxTest.java	Wed Jan 11 22:04:43 2012 -0500
+++ b/javafx-ui-controls/test/javafx/scene/control/ChoiceBoxTest.java	Wed Jan 11 23:52:24 2012 -0800
@@ -14,8 +14,11 @@
 import static org.junit.Assert.assertTrue;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
 import javafx.collections.FXCollections;
 import javafx.collections.ObservableList;
+import javafx.util.StringConverter;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -51,6 +54,9 @@
     @Test public void noArgConstructor_selectedIndexIsNegativeOne() {
         assertEquals(-1, box.getSelectionModel().getSelectedIndex());
     }
+    @Test public void noArgConstructor_converterIsNotNull() {
+        assertNull(box.getConverter());
+    }
     
     @Test public void singleArgConstructorSetsTheStyleClass() {
         final ChoiceBox<String> b2 = new ChoiceBox<String>(FXCollections.observableArrayList("Hi"));
@@ -83,6 +89,11 @@
         assertEquals(-1, b2.getSelectionModel().getSelectedIndex());
     }
     
+    @Test public void singleArgConstructor_converterIsNotNull() {
+        final ChoiceBox<String> b2 = new ChoiceBox<String>(FXCollections.observableArrayList("Hi"));
+        assertNull(b2.getConverter());
+    }
+    
     /*********************************************************************
      * Tests for selection model                                         *
      ********************************************************************/
@@ -195,6 +206,140 @@
         assertEquals(null, box.getSelectionModel().getSelectedItem());
     }
     
+    @Test public void ensureSelectionModelUpdatesValueProperty_withSelectIndex() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().select(0);
+        assertEquals("Apple", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelUpdatesValueProperty_withSelectItem() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().select("Apple");
+        assertEquals("Apple", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelUpdatesValueProperty_withSelectPrevious() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().select(2);
+        box.getSelectionModel().selectPrevious();
+        assertEquals("Orange", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelUpdatesValueProperty_withSelectNext() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().select("Orange");
+        box.getSelectionModel().selectNext();
+        assertEquals("Banana", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelUpdatesValueProperty_withSelectFirst() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().selectFirst();
+        assertEquals("Apple", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelUpdatesValueProperty_withSelectLast() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().selectLast();
+        assertEquals("Banana", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelClearsValueProperty() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().select(0);
+        assertEquals("Apple", box.getValue());
+        
+        box.getSelectionModel().clearSelection();
+        assertNull(box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelClearsValuePropertyWhenNegativeOneSelected() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getValue());
+        box.getSelectionModel().select(0);
+        assertEquals("Apple", box.getValue());
+        
+        box.getSelectionModel().select(-1);
+        assertEquals(null, box.getValue());
+    }
+    
+    @Test public void ensureValueIsCorrectWhenItemsIsAddedToWithExistingSelection() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        box.getSelectionModel().select(1);
+        box.getItems().add(0, "pineapple");
+        assertEquals(2, box.getSelectionModel().getSelectedIndex());
+        assertEquals("Orange", box.getSelectionModel().getSelectedItem());
+        assertEquals("Orange", box.getValue());
+    }
+    
+    @Test public void ensureValueIsCorrectWhenItemsAreRemovedWithExistingSelection() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        box.getSelectionModel().select(1);
+        
+        box.getItems().remove("Apple");
+        
+        assertEquals(0, box.getSelectionModel().getSelectedIndex());
+        assertEquals("Orange", box.getSelectionModel().getSelectedItem());
+        assertEquals("Orange", box.getValue());
+    }
+    
+    @Test public void ensureValueIsUpdatedByCorrectSelectionModelWhenSelectionModelIsChanged() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        SelectionModel sm1 = box.getSelectionModel();
+        sm1.select(1);
+        assertEquals("Orange", box.getValue());
+        SingleSelectionModel sm2 = new ChoiceBox.ChoiceBoxSelectionModel(box);
+        box.setSelectionModel(sm2);
+        
+        sm1.select(2);  // value should not change as we are using old SM
+        assertEquals("Orange", box.getValue());
+        
+        sm2.select(0);  // value should change, as we are using new SM
+        assertEquals("Apple", box.getValue());
+    }
+    
+    @Test public void ensureValueDoesNotChangeWhenBoundAndNoExceptions() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        
+        StringProperty sp = new SimpleStringProperty("empty");
+        box.valueProperty().bind(sp);
+        
+        box.getSelectionModel().select(1);
+        assertEquals("empty", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelUpdatesWhenValueChanges() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        assertNull(box.getSelectionModel().getSelectedItem());
+        box.setValue("Orange");
+        assertEquals("Orange", box.getSelectionModel().getSelectedItem());
+    }
+    
+    @Test public void ensureValueEqualsSelectedItemWhenNotInItemsList() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        box.getSelectionModel().setSelectedItem("pineapple");
+        assertEquals("pineapple", box.getSelectionModel().getSelectedItem());
+        assertEquals("pineapple", box.getValue());
+    }
+    
+    @Test public void ensureSelectionModelUpdatesWhenValueChangesToNull() {
+        box.getItems().addAll("Apple", "Orange", "Banana");
+        box.setValue("pineapple");
+        assertEquals("pineapple", box.getSelectionModel().getSelectedItem());
+        assertEquals("pineapple", box.getValue());
+        box.setValue(null);
+        assertEquals(null, box.getSelectionModel().getSelectedItem());
+        assertEquals(-1, box.getSelectionModel().getSelectedIndex());
+        assertEquals(null, box.getValue());
+    }
+    
     /*********************************************************************
      * Tests for showing property                                        *
      ********************************************************************/
--- a/javafx-ui-controls/test/javafx/scene/control/ComboBoxTest.java	Wed Jan 11 22:04:43 2012 -0500
+++ b/javafx-ui-controls/test/javafx/scene/control/ComboBoxTest.java	Wed Jan 11 23:52:24 2012 -0800
@@ -450,6 +450,13 @@
         assertEquals(null, comboBox.getValue());
     }
     
+    @Test public void ensureValueEqualsSelectedItemWhenNotInItemsList() {
+        comboBox.getItems().addAll("Apple", "Orange", "Banana");
+        comboBox.getSelectionModel().setSelectedItem("pineapple");
+        assertEquals("pineapple", comboBox.getSelectionModel().getSelectedItem());
+        assertEquals("pineapple", comboBox.getValue());
+    }
+    
     /*********************************************************************
      * Tests for default values                                         *
      ********************************************************************/