1 /*
   2  * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control;
  27 
  28 import javafx.beans.property.ObjectProperty;
  29 import javafx.beans.property.ObjectPropertyBase;
  30 import javafx.beans.property.SimpleObjectProperty;
  31 import javafx.beans.value.ChangeListener;
  32 import javafx.collections.FXCollections;
  33 import javafx.collections.ListChangeListener;
  34 import javafx.collections.ObservableList;
  35 import javafx.beans.property.ReadOnlyBooleanProperty;
  36 import javafx.beans.property.ReadOnlyBooleanWrapper;
  37 import javafx.event.ActionEvent;
  38 import javafx.event.Event;
  39 import javafx.event.EventHandler;
  40 import javafx.event.EventType;
  41 import javafx.scene.AccessibleAction;
  42 import javafx.scene.AccessibleAttribute;
  43 import javafx.scene.AccessibleRole;
  44 import javafx.util.StringConverter;
  45 import javafx.css.PseudoClass;
  46 
  47 import com.sun.javafx.scene.control.skin.ChoiceBoxSkin;
  48 
  49 import javafx.beans.DefaultProperty;
  50 
  51 /**
  52  * The ChoiceBox is used for presenting the user with a relatively small set of
  53  * predefined choices from which they may choose. The ChoiceBox, when "showing",
  54  * will display to the user these choices and allow them to pick exactly one
  55  * choice. When not showing, the current choice is displayed.
  56  * <p>
  57  * By default, the ChoiceBox has no item selected unless otherwise specified. 
  58  * Although the ChoiceBox will only allow a user to select from the predefined
  59  * list, it is possible for the developer to specify the selected item to be
  60  * something other than what is available in the predefined list. This is
  61  * required for several important use cases.
  62  * <p>
  63  * It means configuration of the ChoiceBox is order independent. You
  64  * may either specify the items and then the selected item, or you may
  65  * specify the selected item and then the items. Either way will function
  66  * correctly.
  67  * <p>
  68  * ChoiceBox item selection is handled by 
  69  * {@link javafx.scene.control.SelectionModel SelectionModel}
  70  * As with ListView and ComboBox, it is possible to modify the 
  71  * {@link javafx.scene.control.SelectionModel SelectionModel} that is used, 
  72  * although this is likely to be rarely changed. ChoiceBox supports only a 
  73  * single selection model, hence the default used is a {@link SingleSelectionModel}.
  74  *
  75  * <pre>
  76  * import javafx.scene.control.ChoiceBox;
  77  *
  78  * ChoiceBox cb = new ChoiceBox();
  79  * cb.getItems().addAll("item1", "item2", "item3");
  80  * </pre>
  81  * @since JavaFX 2.0
  82  */
  83 @DefaultProperty("items")
  84 public class ChoiceBox<T> extends Control {
  85 
  86     /***************************************************************************
  87      *                                                                         *
  88      * Static properties and methods                                           *
  89      *                                                                         *
  90      **************************************************************************/
  91 
  92     /**
  93      * Called prior to the ChoiceBox showing its popup after the user
  94      * has clicked or otherwise interacted with the ChoiceBox.
  95      * @since JavaFX 8u60
  96      */
  97     public static final EventType<Event> ON_SHOWING =
  98             new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_SHOWING");
  99 
 100     /**
 101      * Called after the ChoiceBox has shown its popup.
 102      * @since JavaFX 8u60
 103      */
 104     public static final EventType<Event> ON_SHOWN =
 105             new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_SHOWN");
 106 
 107     /**
 108      * Called when the ChoiceBox popup <b>will</b> be hidden.
 109      * @since JavaFX 8u60
 110      */
 111     public static final EventType<Event> ON_HIDING =
 112             new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_HIDING");
 113 
 114     /**
 115      * Called when the ChoiceBox popup has been hidden.
 116      * @since JavaFX 8u60
 117      */
 118     public static final EventType<Event> ON_HIDDEN =
 119             new EventType<Event>(Event.ANY, "CHOICE_BOX_ON_HIDDEN");
 120 
 121 
 122 
 123     /***************************************************************************
 124      *                                                                         *
 125      * Constructors                                                            *
 126      *                                                                         *
 127      **************************************************************************/
 128 
 129     /**
 130      * Create a new ChoiceBox which has an empty list of items.
 131      */
 132     public ChoiceBox() {
 133         this(FXCollections.<T>observableArrayList());
 134     }
 135 
 136     /**
 137      * Create a new ChoiceBox with the given set of items. Since it is observable,
 138      * the content of this list may change over time and the ChoiceBox will
 139      * be updated accordingly.
 140      * @param items
 141      */
 142     public ChoiceBox(ObservableList<T> items) {
 143         getStyleClass().setAll("choice-box");
 144         setAccessibleRole(AccessibleRole.COMBO_BOX);
 145         setItems(items);
 146         setSelectionModel(new ChoiceBoxSelectionModel<T>(this));
 147         
 148         // listen to the value property, if the value is
 149         // set to something that exists in the items list, update the
 150         // selection model to indicate that this is the selected item
 151         valueProperty().addListener((ov, t, t1) -> {
 152             if (getItems() == null) return;
 153             int index = getItems().indexOf(t1);
 154             if (index > -1) {
 155                 getSelectionModel().select(index);
 156             }
 157         });
 158     }
 159 
 160     /***************************************************************************
 161      *                                                                         *
 162      * Properties                                                              *
 163      *                                                                         *
 164      **************************************************************************/
 165 
 166     /**
 167      * The selection model for the ChoiceBox. Only a single choice can be made,
 168      * hence, the ChoiceBox supports only a SingleSelectionModel. Generally, the
 169      * main interaction with the selection model is to explicitly set which item
 170      * in the items list should be selected, or to listen to changes in the
 171      * selection to know which item has been chosen.
 172      */
 173     private ObjectProperty<SingleSelectionModel<T>> selectionModel = 
 174             new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") {
 175          private SelectionModel<T> oldSM = null;
 176         @Override protected void invalidated() {
 177             if (oldSM != null) {
 178                 oldSM.selectedItemProperty().removeListener(selectedItemListener);
 179             }
 180             SelectionModel<T> sm = get();
 181             oldSM = sm;
 182             if (sm != null) {
 183                 sm.selectedItemProperty().addListener(selectedItemListener);
 184             }
 185         }                
 186     };
 187     
 188     private ChangeListener<T> selectedItemListener = (ov, t, t1) -> {
 189         if (! valueProperty().isBound()) {
 190             setValue(t1);
 191         }
 192     };
 193 
 194     
 195     public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); }
 196     public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); }
 197     public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; }
 198 
 199 
 200     /**
 201      * Indicates whether the drop down is displaying the list of choices to the
 202      * user. This is a readonly property which should be manipulated by means of
 203      * the #show and #hide methods.
 204      */
 205     private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper() {
 206         @Override protected void invalidated() {
 207             pseudoClassStateChanged(SHOWING_PSEUDOCLASS_STATE, get());
 208             notifyAccessibleAttributeChanged(AccessibleAttribute.EXPANDED);
 209         }
 210 
 211         @Override
 212         public Object getBean() {
 213             return ChoiceBox.this;
 214         }
 215 
 216         @Override
 217         public String getName() {
 218             return "showing";
 219         }
 220     };
 221     public final boolean isShowing() { return showing.get(); }
 222     public final ReadOnlyBooleanProperty showingProperty() { return showing.getReadOnlyProperty(); }
 223     private void setShowing(boolean value) {
 224         // these events will not fire if the showing property is bound
 225         Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWING) :
 226                 new Event(ComboBoxBase.ON_HIDING));
 227         showing.set(value);
 228         Event.fireEvent(this, value ? new Event(ComboBoxBase.ON_SHOWN) :
 229                 new Event(ComboBoxBase.ON_HIDDEN));
 230     }
 231 
 232     /**
 233      * The items to display in the choice box. The selected item (as indicated in the
 234      * selection model) must always be one of these items.
 235      */
 236     private ObjectProperty<ObservableList<T>> items = new ObjectPropertyBase<ObservableList<T>>() {
 237         ObservableList<T> old;
 238         @Override protected void invalidated() {
 239             final ObservableList<T> newItems = get();
 240             if (old != newItems) {
 241                 // Add and remove listeners
 242                 if (old != null) old.removeListener(itemsListener);
 243                 if (newItems != null) newItems.addListener(itemsListener);
 244                 // Clear the selection model
 245                 final SingleSelectionModel<T> sm = getSelectionModel();
 246                 if (sm != null) {
 247                     if (newItems != null && newItems.isEmpty()) {
 248                         // RT-29433 - clear selection.
 249                         sm.clearSelection();
 250                     } else if (sm.getSelectedIndex() == -1 && sm.getSelectedItem() != null) {
 251                         int newIndex = getItems().indexOf(sm.getSelectedItem());
 252                         if (newIndex != -1) {
 253                             sm.setSelectedIndex(newIndex);
 254                         }
 255                     } else sm.clearSelection();
 256                 }
 257 //                if (sm != null) sm.setSelectedIndex(-1);
 258                 // Save off the old items
 259                 old = newItems;
 260             }
 261         }
 262 
 263         @Override
 264         public Object getBean() {
 265             return ChoiceBox.this;
 266         }
 267 
 268         @Override
 269         public String getName() {
 270             return "items";
 271         }
 272     };
 273     public final void setItems(ObservableList<T> value) { items.set(value); }
 274     public final ObservableList<T> getItems() { return items.get(); }
 275     public final ObjectProperty<ObservableList<T>> itemsProperty() { return items; }
 276 
 277     private final ListChangeListener<T> itemsListener = c -> {
 278         final SingleSelectionModel<T> sm = getSelectionModel();
 279         if (sm!= null) {
 280             if (getItems() == null || getItems().isEmpty()) {
 281                 sm.clearSelection();
 282             } else {
 283                 int newIndex = getItems().indexOf(sm.getSelectedItem());
 284                 sm.setSelectedIndex(newIndex);
 285             }
 286         }
 287         if (sm != null) {
 288 
 289             // Look for the selected item as having been removed. If it has been,
 290             // then we need to clear the selection in the selection model.
 291             final T selectedItem = sm.getSelectedItem();
 292             while (c.next()) {
 293                 if (selectedItem != null && c.getRemoved().contains(selectedItem)) {
 294                     sm.clearSelection();
 295                     break;
 296                     }
 297             }
 298         }
 299     };
 300     
 301     /**
 302      * Allows a way to specify how to represent objects in the items list. When
 303      * a StringConverter is set, the object toString method is not called and 
 304      * instead its toString(object T) is called, passing the objects in the items list. 
 305      * This is useful when using domain objects in a ChoiceBox as this property 
 306      * allows for customization of the representation. Also, any of the pre-built
 307      * Converters available in the {@link javafx.util.converter} package can be set. 
 308      * @since JavaFX 2.1
 309      */
 310     public ObjectProperty<StringConverter<T>> converterProperty() { return converter; }
 311     private ObjectProperty<StringConverter<T>> converter = 
 312             new SimpleObjectProperty<StringConverter<T>>(this, "converter", null);
 313     public final void setConverter(StringConverter<T> value) { converterProperty().set(value); }
 314     public final StringConverter<T> getConverter() {return converterProperty().get(); }
 315     
 316     /**
 317      * The value of this ChoiceBox is defined as the selected item in the ChoiceBox
 318      * selection model. The valueProperty is synchronized with the selectedItem. 
 319      * This property allows for bi-directional binding of external properties to the 
 320      * ChoiceBox and updates the selection model accordingly. 
 321      * @since JavaFX 2.1
 322      */
 323     public ObjectProperty<T> valueProperty() { return value; }
 324     private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value") {
 325         @Override protected void invalidated() {
 326             super.invalidated();
 327             fireEvent(new ActionEvent());
 328             // Update selection
 329             final SingleSelectionModel<T> sm = getSelectionModel();
 330             if (sm != null) {
 331                 sm.select(super.getValue());
 332             }
 333             notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
 334         }
 335     };
 336     public final void setValue(T value) { valueProperty().set(value); }
 337     public final T getValue() { return valueProperty().get(); }
 338 
 339 
 340     // --- On Action
 341     /**
 342      * The ChoiceBox action, which is invoked whenever the ChoiceBox
 343      * {@link #valueProperty() value} property is changed. This
 344      * may be due to the value property being programmatically changed or when the
 345      * user selects an item in a popup menu.
 346      *
 347      * @since JavaFX 8u60
 348      */
 349     public final ObjectProperty<EventHandler<ActionEvent>> onActionProperty() { return onAction; }
 350     public final void setOnAction(EventHandler<ActionEvent> value) { onActionProperty().set(value); }
 351     public final EventHandler<ActionEvent> getOnAction() { return onActionProperty().get(); }
 352     private ObjectProperty<EventHandler<ActionEvent>> onAction = new ObjectPropertyBase<EventHandler<ActionEvent>>() {
 353         @Override protected void invalidated() {
 354             setEventHandler(ActionEvent.ACTION, get());
 355         }
 356 
 357         @Override
 358         public Object getBean() {
 359             return ChoiceBox.this;
 360         }
 361 
 362         @Override
 363         public String getName() {
 364             return "onAction";
 365         }
 366     };
 367 
 368 
 369     // --- On Showing
 370     /**
 371      * Called just prior to the {@code ChoiceBox} popup being shown.
 372      * @since JavaFX 8u60
 373      */
 374     public final ObjectProperty<EventHandler<Event>> onShowingProperty() { return onShowing; }
 375     public final void setOnShowing(EventHandler<Event> value) { onShowingProperty().set(value); }
 376     public final EventHandler<Event> getOnShowing() { return onShowingProperty().get(); }
 377     private ObjectProperty<EventHandler<Event>> onShowing = new ObjectPropertyBase<EventHandler<Event>>() {
 378         @Override protected void invalidated() {
 379             setEventHandler(ON_SHOWING, get());
 380         }
 381 
 382         @Override public Object getBean() {
 383             return ChoiceBox.this;
 384         }
 385 
 386         @Override public String getName() {
 387             return "onShowing";
 388         }
 389     };
 390 
 391 
 392     // -- On Shown
 393     /**
 394      * Called just after the {@link ChoiceBox} popup is shown.
 395      * @since JavaFX 8u60
 396      */
 397     public final ObjectProperty<EventHandler<Event>> onShownProperty() { return onShown; }
 398     public final void setOnShown(EventHandler<Event> value) { onShownProperty().set(value); }
 399     public final EventHandler<Event> getOnShown() { return onShownProperty().get(); }
 400     private ObjectProperty<EventHandler<Event>> onShown = new ObjectPropertyBase<EventHandler<Event>>() {
 401         @Override protected void invalidated() {
 402             setEventHandler(ON_SHOWN, get());
 403         }
 404 
 405         @Override public Object getBean() {
 406             return ChoiceBox.this;
 407         }
 408 
 409         @Override public String getName() {
 410             return "onShown";
 411         }
 412     };
 413 
 414 
 415     // --- On Hiding
 416     /**
 417      * Called just prior to the {@link ChoiceBox} popup being hidden.
 418      * @since JavaFX 8u60
 419      */
 420     public final ObjectProperty<EventHandler<Event>> onHidingProperty() { return onHiding; }
 421     public final void setOnHiding(EventHandler<Event> value) { onHidingProperty().set(value); }
 422     public final EventHandler<Event> getOnHiding() { return onHidingProperty().get(); }
 423     private ObjectProperty<EventHandler<Event>> onHiding = new ObjectPropertyBase<EventHandler<Event>>() {
 424         @Override protected void invalidated() {
 425             setEventHandler(ON_HIDING, get());
 426         }
 427 
 428         @Override public Object getBean() {
 429             return ChoiceBox.this;
 430         }
 431 
 432         @Override public String getName() {
 433             return "onHiding";
 434         }
 435     };
 436 
 437 
 438     // --- On Hidden
 439     /**
 440      * Called just after the {@link ChoiceBox} popup has been hidden.
 441      * @since JavaFX 8u60
 442      */
 443     public final ObjectProperty<EventHandler<Event>> onHiddenProperty() { return onHidden; }
 444     public final void setOnHidden(EventHandler<Event> value) { onHiddenProperty().set(value); }
 445     public final EventHandler<Event> getOnHidden() { return onHiddenProperty().get(); }
 446     private ObjectProperty<EventHandler<Event>> onHidden = new ObjectPropertyBase<EventHandler<Event>>() {
 447         @Override protected void invalidated() {
 448             setEventHandler(ON_HIDDEN, get());
 449         }
 450 
 451         @Override public Object getBean() {
 452             return ChoiceBox.this;
 453         }
 454 
 455         @Override public String getName() {
 456             return "onHidden";
 457         }
 458     };
 459 
 460     /***************************************************************************
 461      *                                                                         *
 462      * Methods                                                                 *
 463      *                                                                         *
 464      **************************************************************************/
 465 
 466     /**
 467      * Opens the list of choices.
 468      */
 469     public void show() {
 470         if (!isDisabled()) setShowing(true);
 471     }
 472 
 473     /**
 474      * Closes the list of choices.
 475      */
 476     public void hide() {
 477         setShowing(false);
 478     }
 479 
 480     /** {@inheritDoc} */
 481     @Override protected Skin<?> createDefaultSkin() {
 482         return new ChoiceBoxSkin<T>(this);
 483     }
 484 
 485     /***************************************************************************
 486      *                                                                         *
 487      * Stylesheet Handling                                                     *
 488      *                                                                         *
 489      **************************************************************************/
 490 
 491     private static final PseudoClass SHOWING_PSEUDOCLASS_STATE =
 492             PseudoClass.getPseudoClass("showing");
 493 
 494     // package for testing
 495     static class ChoiceBoxSelectionModel<T> extends SingleSelectionModel<T> {
 496         private final ChoiceBox<T> choiceBox;
 497 
 498         public ChoiceBoxSelectionModel(final ChoiceBox<T> cb) {
 499             if (cb == null) {
 500                 throw new NullPointerException("ChoiceBox can not be null");
 501             }
 502             this.choiceBox = cb;
 503        
 504             /*
 505              * The following two listeners are used in conjunction with
 506              * SelectionModel.select(T obj) to allow for a developer to select
 507              * an item that is not actually in the data model. When this occurs,
 508              * we actively try to find an index that matches this object, going
 509              * so far as to actually watch for all changes to the items list,
 510              * rechecking each time.
 511              */
 512 
 513             // watching for changes to the items list content
 514             final ListChangeListener<T> itemsContentObserver = c -> {
 515                 if (choiceBox.getItems() == null || choiceBox.getItems().isEmpty()) {
 516                     setSelectedIndex(-1);
 517                 } else if (getSelectedIndex() == -1 && getSelectedItem() != null) {
 518                     int newIndex = choiceBox.getItems().indexOf(getSelectedItem());
 519                     if (newIndex != -1) {
 520                         setSelectedIndex(newIndex);
 521                     }
 522                 }
 523             };
 524             if (this.choiceBox.getItems() != null) {
 525                 this.choiceBox.getItems().addListener(itemsContentObserver);
 526             }
 527 
 528             // watching for changes to the items list
 529             ChangeListener<ObservableList<T>> itemsObserver = (valueModel, oldList, newList) -> {
 530                 if (oldList != null) {
 531                     oldList.removeListener(itemsContentObserver);
 532                 }
 533                 if (newList != null) {
 534                     newList.addListener(itemsContentObserver);
 535                 }
 536                 setSelectedIndex(-1);
 537                 if (getSelectedItem() != null) {
 538                     int newIndex = choiceBox.getItems().indexOf(getSelectedItem());
 539                     if (newIndex != -1) {
 540                         setSelectedIndex(newIndex);
 541                     }
 542                 }
 543             };
 544             this.choiceBox.itemsProperty().addListener(itemsObserver);
 545         }
 546 
 547         // API Implementation
 548         @Override protected T getModelItem(int index) {
 549             final ObservableList<T> items = choiceBox.getItems();
 550             if (items == null) return null;
 551             if (index < 0 || index >= items.size()) return null;
 552             return items.get(index);
 553         }
 554 
 555         @Override protected int getItemCount() {
 556             final ObservableList<T> items = choiceBox.getItems();
 557             return items == null ? 0 : items.size();
 558         }
 559 
 560         /**
 561          * Selects the given row. Since the SingleSelectionModel can only support having
 562          * a single row selected at a time, this also causes any previously selected
 563          * row to be unselected.
 564          * This method is overridden here so that we can move past a Separator
 565          * in a ChoiceBox and select the next valid menuitem.
 566          */
 567         @Override public void select(int index) {
 568             // this does not sound right, we should let the superclass handle it.
 569             final T value = getModelItem(index);
 570             if (value instanceof Separator) {
 571                 select(++index);
 572             } else {
 573                 super.select(index);
 574             }
 575             
 576             if (choiceBox.isShowing()) {
 577                 choiceBox.hide();
 578             }
 579         }
 580     }
 581 
 582     /***************************************************************************
 583      *                                                                         *
 584      * Accessibility handling                                                  *
 585      *                                                                         *
 586      **************************************************************************/
 587 
 588     @Override
 589     public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 590         switch(attribute) {
 591             case TEXT:
 592                 String accText = getAccessibleText();
 593                 if (accText != null && !accText.isEmpty()) return accText;
 594 
 595                 //let the skin first.
 596                 Object title = super.queryAccessibleAttribute(attribute, parameters);
 597                 if (title != null) return title;
 598                 StringConverter<T> converter = getConverter();
 599                 if (converter == null) {
 600                     return getValue() != null ? getValue().toString() : "";
 601                 }
 602                 return converter.toString(getValue());
 603             case EXPANDED: return isShowing();
 604             default: return super.queryAccessibleAttribute(attribute, parameters);
 605         }
 606     }
 607 
 608     @Override
 609     public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 610         switch (action) {
 611             case COLLAPSE: hide(); break;
 612             case EXPAND: show(); break;
 613             default: super.executeAccessibleAction(action); break;
 614         }
 615     }
 616 
 617 }