Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.control;
027
028import javafx.beans.property.ObjectProperty;
029import javafx.beans.property.ObjectPropertyBase;
030import javafx.beans.property.SimpleObjectProperty;
031import javafx.beans.value.ChangeListener;
032import javafx.beans.value.ObservableValue;
033import javafx.collections.FXCollections;
034import javafx.collections.ListChangeListener;
035import javafx.collections.ObservableList;
036import javafx.beans.property.ReadOnlyBooleanProperty;
037import javafx.beans.property.ReadOnlyBooleanWrapper;
038import javafx.event.ActionEvent;
039import javafx.util.StringConverter;
040
041import javafx.css.PseudoClass;
042import com.sun.javafx.scene.control.skin.ChoiceBoxSkin;
043import javafx.beans.DefaultProperty;
044
045/**
046 * The ChoiceBox is used for presenting the user with a relatively small set of
047 * predefined choices from which they may choose. The ChoiceBox, when "showing",
048 * will display to the user these choices and allow them to pick exactly one
049 * choice. When not showing, the current choice is displayed.
050 * <p>
051 * The ChoiceBox can be configured either to support <code>null</code> as a
052 * valid choice, or to prohibit it. In the case that it is prohibited, there
053 * will always be some item that is selected, as long as there is at least
054 * one item defined. By default, no item is selected unless
055 * otherwise specified. In the case that <code>null</code> is acceptable,
056 * a default entry may be inserted into the list of choices at the top,
057 * with a name similar to "None" and localized for different Locales.
058 * <p>
059 * Although the ChoiceBox will only allow a user to select from the predefined
060 * list, it is possible for the developer to specify the selected item to be
061 * something other than what is available in the predefined list. This is
062 * required for several important use cases.
063 * <p>
064 * It means configuration of the ChoiceBox is order independent. You
065 * may either specify the items and then the selected item, or you may
066 * specify the selected item and then the items. Either way will function
067 * correctly.
068 * <p>
069 * ChoiceBox item selection is handled by 
070 * {@link javafx.scene.control.SelectionModel SelectionModel}
071 * As with ListView and ComboBox, it is possible to modify the 
072 * {@link javafx.scene.control.SelectionModel SelectionModel} that is used, 
073 * although this is likely to be rarely changed. ChoiceBox supports only a 
074 * single selection model, hence the default used is a {@link SingleSelectionModel}.
075 *
076 * <pre>
077 * import javafx.scene.control.ChoiceBox;
078 *
079 * ChoiceBox cb = new ChoiceBox();
080 * cb.getItems().addAll("item1", "item2", "item3");
081 * </pre>
082 */
083@DefaultProperty("items")
084public class ChoiceBox<T> extends Control {
085    /***************************************************************************
086     *                                                                         *
087     * Constructors                                                            *
088     *                                                                         *
089     **************************************************************************/
090
091    /**
092     * Create a new ChoiceBox which has an empty list of items.
093     */
094    public ChoiceBox() {
095        this(FXCollections.<T>observableArrayList());
096    }
097
098    /**
099     * Create a new ChoiceBox with the given set of items. Since it is observable,
100     * the content of this list may change over time and the ChoiceBox will
101     * be updated accordingly.
102     * @param items
103     */
104    public ChoiceBox(ObservableList<T> items) {
105        getStyleClass().setAll("choice-box");
106        setItems(items);
107        setSelectionModel(new ChoiceBoxSelectionModel<T>(this));
108        
109        // listen to the value property, if the value is
110        // set to something that exists in the items list, update the
111        // selection model to indicate that this is the selected item
112        valueProperty().addListener(new ChangeListener<T>() {
113            @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) {
114                if (getItems() == null) return;
115                int index = getItems().indexOf(t1);
116                if (index > -1) {
117                    getSelectionModel().select(index);
118                }
119            }
120        });
121    }
122
123    /***************************************************************************
124     *                                                                         *
125     * Properties                                                              *
126     *                                                                         *
127     **************************************************************************/
128
129    /**
130     * The selection model for the ChoiceBox. Only a single choice can be made,
131     * hence, the ChoiceBox supports only a SingleSelectionModel. Generally, the
132     * main interaction with the selection model is to explicitly set which item
133     * in the items list should be selected, or to listen to changes in the
134     * selection to know which item has been chosen.
135     */
136    private ObjectProperty<SingleSelectionModel<T>> selectionModel = 
137            new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") {
138         private SelectionModel<T> oldSM = null;
139        @Override protected void invalidated() {
140            if (oldSM != null) {
141                oldSM.selectedItemProperty().removeListener(selectedItemListener);
142            }
143            SelectionModel<T> sm = get();
144            oldSM = sm;
145            if (sm != null) {
146                sm.selectedItemProperty().addListener(selectedItemListener);
147            }
148        }                
149    };
150    
151    private ChangeListener<T> selectedItemListener = new ChangeListener<T>() {
152        @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) {
153            if (! valueProperty().isBound()) {
154                setValue(t1);
155            }
156        }
157    };
158
159    
160    public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); }
161    public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); }
162    public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; }
163
164
165    /**
166     * Indicates whether the drop down is displaying the list of choices to the
167     * user. This is a readonly property which should be manipulated by means of
168     * the #show and #hide methods.
169     */
170    private ReadOnlyBooleanWrapper showing = new ReadOnlyBooleanWrapper() {
171        @Override protected void invalidated() {
172            pseudoClassStateChanged(SHOWING_PSEUDOCLASS_STATE, get());
173        }
174
175        @Override
176        public Object getBean() {
177            return ChoiceBox.this;
178        }
179
180        @Override
181        public String getName() {
182            return "showing";
183        }
184    };
185    public final boolean isShowing() { return showing.get(); }
186    public final ReadOnlyBooleanProperty showingProperty() { return showing.getReadOnlyProperty(); }
187
188    /**
189     * The items to display in the choice box. The selected item (as indicated in the
190     * selection model) must always be one of these items.
191     */
192    private ObjectProperty<ObservableList<T>> items = new ObjectPropertyBase<ObservableList<T>>() {
193        ObservableList<T> old;
194        @Override protected void invalidated() {
195            final ObservableList<T> newItems = get();
196            if (old != newItems) {
197                // Add and remove listeners
198                if (old != null) old.removeListener(itemsListener);
199                if (newItems != null) newItems.addListener(itemsListener);
200                // Clear the selection model
201                final SingleSelectionModel<T> sm = getSelectionModel();
202                if (sm != null) {
203                    if (newItems != null && newItems.isEmpty()) {
204                        sm.setSelectedIndex(-1);
205                    } else if (sm.getSelectedIndex() == -1 && sm.getSelectedItem() != null) {
206                        int newIndex = getItems().indexOf(sm.getSelectedItem());
207                        if (newIndex != -1) {
208                            sm.setSelectedIndex(newIndex);
209                        }
210                    } else sm.clearSelection();
211                }
212//                if (sm != null) sm.setSelectedIndex(-1);
213                // Save off the old items
214                old = newItems;
215            }
216        }
217
218        @Override
219        public Object getBean() {
220            return ChoiceBox.this;
221        }
222
223        @Override
224        public String getName() {
225            return "items";
226        }
227    };
228    public final void setItems(ObservableList<T> value) { items.set(value); }
229    public final ObservableList<T> getItems() { return items.get(); }
230    public final ObjectProperty<ObservableList<T>> itemsProperty() { return items; }
231
232    private final ListChangeListener<T> itemsListener = new ListChangeListener<T>() {
233        @Override public void onChanged(Change<? extends T> c) {
234            final SingleSelectionModel<T> sm = getSelectionModel();
235            if (sm!= null) {
236                if (getItems() == null || getItems().isEmpty()) {
237                    sm.clearSelection();
238                } else {
239                    int newIndex = getItems().indexOf(sm.getSelectedItem());
240                    sm.setSelectedIndex(newIndex);
241                }
242            }
243            if (sm != null) {
244                
245                // Look for the selected item as having been removed. If it has been,
246                // then we need to clear the selection in the selection model.
247                final T selectedItem = sm.getSelectedItem();
248                while (c.next()) {
249                    if (selectedItem != null && c.getRemoved().contains(selectedItem)) {
250                        sm.clearSelection();
251                        break;
252                        }
253                }
254            }
255        }
256    };
257    
258    /**
259     * Allows a way to specify how to represent objects in the items list. When
260     * a StringConverter is set, the object toString method is not called and 
261     * instead its toString(object T) is called, passing the objects in the items list. 
262     * This is useful when using domain objects in a ChoiceBox as this property 
263     * allows for customization of the representation. Also, any of the pre-built
264     * Converters available in the {@link javafx.util.converter} package can be set. 
265     */
266    public ObjectProperty<StringConverter<T>> converterProperty() { return converter; }
267    private ObjectProperty<StringConverter<T>> converter = 
268            new SimpleObjectProperty<StringConverter<T>>(this, "converter", null);
269    public final void setConverter(StringConverter<T> value) { converterProperty().set(value); }
270    public final StringConverter<T> getConverter() {return converterProperty().get(); }
271    
272    /**
273     * The value of this ChoiceBox is defined as the selected item in the ChoiceBox
274     * selection model. The valueProperty is synchronized with the selectedItem. 
275     * This property allows for bi-directional binding of external properties to the 
276     * ChoiceBox and updates the selection model accordingly. 
277     */
278    public ObjectProperty<T> valueProperty() { return value; }
279    private ObjectProperty<T> value = new SimpleObjectProperty<T>(this, "value") {
280        @Override protected void invalidated() {
281            super.invalidated();
282            fireEvent(new ActionEvent());
283            // Update selection
284            final SingleSelectionModel<T> sm = getSelectionModel();
285            if (sm != null) {
286                sm.select(super.getValue());
287            }
288        }
289    };
290    public final void setValue(T value) { valueProperty().set(value); }
291    public final T getValue() { return valueProperty().get(); }
292
293    /***************************************************************************
294     *                                                                         *
295     * Methods                                                                 *
296     *                                                                         *
297     **************************************************************************/
298
299    /**
300     * Opens the list of choices.
301     */
302    public void show() {
303        if (!isDisabled()) showing.set(true);
304    }
305
306    /**
307     * Closes the list of choices.
308     */
309    public void hide() {
310        showing.set(false);
311    }
312
313    /** {@inheritDoc} */
314    @Override protected Skin<?> createDefaultSkin() {
315        return new ChoiceBoxSkin<T>(this);
316    }
317
318    /***************************************************************************
319     *                                                                         *
320     * Stylesheet Handling                                                     *
321     *                                                                         *
322     **************************************************************************/
323
324    private static final PseudoClass SHOWING_PSEUDOCLASS_STATE =
325            PseudoClass.getPseudoClass("showing");
326
327    // package for testing
328    static class ChoiceBoxSelectionModel<T> extends SingleSelectionModel<T> {
329        private final ChoiceBox<T> choiceBox;
330
331        public ChoiceBoxSelectionModel(final ChoiceBox<T> cb) {
332            if (cb == null) {
333                throw new NullPointerException("ChoiceBox can not be null");
334            }
335            this.choiceBox = cb;
336       
337            /*
338             * The following two listeners are used in conjunction with
339             * SelectionModel.select(T obj) to allow for a developer to select
340             * an item that is not actually in the data model. When this occurs,
341             * we actively try to find an index that matches this object, going
342             * so far as to actually watch for all changes to the items list,
343             * rechecking each time.
344             */
345
346            // watching for changes to the items list content
347            final ListChangeListener<T> itemsContentObserver = new ListChangeListener<T>() {
348                @Override public void onChanged(Change<? extends T> c) {
349                    if (choiceBox.getItems() == null || choiceBox.getItems().isEmpty()) {
350                        setSelectedIndex(-1);
351                    } else if (getSelectedIndex() == -1 && getSelectedItem() != null) {
352                        int newIndex = choiceBox.getItems().indexOf(getSelectedItem());
353                        if (newIndex != -1) {
354                            setSelectedIndex(newIndex);
355                        }
356                    }
357                }
358            };
359            if (this.choiceBox.getItems() != null) {
360                this.choiceBox.getItems().addListener(itemsContentObserver);
361            }
362
363            // watching for changes to the items list
364            ChangeListener<ObservableList<T>> itemsObserver = new ChangeListener<ObservableList<T>>() {
365                @Override
366                public void changed(ObservableValue<? extends ObservableList<T>> valueModel, ObservableList<T> oldList, ObservableList<T> newList) {
367                    if (oldList != null) {
368                        oldList.removeListener(itemsContentObserver);
369                    }
370                    if (newList != null) {
371                        newList.addListener(itemsContentObserver);
372                    }
373                    setSelectedIndex(-1);
374                    if (getSelectedItem() != null) {
375                        int newIndex = choiceBox.getItems().indexOf(getSelectedItem());
376                        if (newIndex != -1) {
377                            setSelectedIndex(newIndex);
378                        }
379                    }
380                }
381            };
382            this.choiceBox.itemsProperty().addListener(itemsObserver);
383        }
384
385        // API Implementation
386        @Override protected T getModelItem(int index) {
387            final ObservableList<T> items = choiceBox.getItems();
388            if (items == null) return null;
389            if (index < 0 || index >= items.size()) return null;
390            return items.get(index);
391        }
392
393        @Override protected int getItemCount() {
394            final ObservableList<T> items = choiceBox.getItems();
395            return items == null ? 0 : items.size();
396        }
397
398        /**
399         * Selects the given row. Since the SingleSelectionModel can only support having
400         * a single row selected at a time, this also causes any previously selected
401         * row to be unselected.
402         * This method is overridden here so that we can move past a Separator
403         * in a ChoiceBox and select the next valid menuitem.
404         */
405        @Override public void select(int index) {
406            // this does not sound right, we should let the superclass handle it.
407            final T value = getModelItem(index);
408            if (value instanceof Separator) {
409                select(++index);
410            } else {
411                super.select(index);
412            }
413            
414            if (choiceBox.isShowing()) {
415                choiceBox.hide();
416            }
417        }
418    }
419}