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.collections.WeakListChangeListener;
029import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
030import javafx.beans.InvalidationListener;
031import javafx.beans.Observable;
032import javafx.beans.property.*;
033import javafx.beans.value.ChangeListener;
034import javafx.beans.value.ObservableValue;
035import javafx.beans.value.WeakChangeListener;
036import javafx.collections.FXCollections;
037import javafx.collections.ListChangeListener;
038import javafx.collections.ObservableList;
039import javafx.scene.Node;
040import javafx.util.Callback;
041import javafx.util.StringConverter;
042
043/**
044 * An implementation of the {@link ComboBoxBase} abstract class for the most common
045 * form of ComboBox, where a popup list is shown to users providing them with
046 * a choice that they may select from. For more information around the general
047 * concepts and API of ComboBox, refer to the {@link ComboBoxBase} class 
048 * documentation.
049 * 
050 * <p>On top of ComboBoxBase, the ComboBox class introduces additional API. Most
051 * importantly, it adds an {@link #itemsProperty() items} property that works in
052 * much the same way as the ListView {@link ListView#itemsProperty() items}
053 * property. In other words, it is the content of the items list that is displayed
054 * to users when they click on the ComboBox button.
055 * 
056 * <p>By default, when the popup list is showing, the maximum number of rows
057 * visible is 10, but this can be changed by modifying the 
058 * {@link #visibleRowCountProperty() visibleRowCount} property. If the number of
059 * items in the ComboBox is less than the value of <code>visibleRowCount</code>,
060 * then the items size will be used instead so that the popup list is not
061 * exceedingly long.
062 * 
063 * <p>As with ListView, it is possible to modify the 
064 * {@link javafx.scene.control.SelectionModel selection model} that is used, 
065 * although this is likely to be rarely changed. This is because the ComboBox
066 * enforces the need for a {@link javafx.scene.control.SingleSelectionModel} 
067 * instance, and it is not likely that there is much need for alternate 
068 * implementations. Nonetheless, the option is there should use cases be found 
069 * for switching the selection model.
070 * 
071 * <p>As the ComboBox internally renders content with a ListView, API exists in
072 * the ComboBox class to allow for a custom cell factory to be set. For more
073 * information on cell factories, refer to the {@link Cell} and {@link ListCell}
074 * classes. It is important to note that if a cell factory is set on a ComboBox,
075 * cells will only be used in the ListView that shows when the ComboBox is 
076 * clicked. If you also want to customize the rendering of the 'button' area
077 * of the ComboBox, you can set a custom {@link ListCell} instance in the 
078 * {@link #buttonCellProperty() button cell} property. One way of doing this
079 * is with the following code (note the use of {@code setButtonCell}:
080 * 
081 * <pre>
082 * {@code
083 * Callback<ListView<String>, ListCell<String>> cellFactory = ...;
084 * ComboBox comboBox = new ComboBox();
085 * comboBox.setItems(items);
086 * comboBox.setButtonCell(cellFactory.call(null));
087 * comboBox.setCellFactory(cellFactory);}</pre>
088 * 
089 * <p>Because a ComboBox can be {@link #editableProperty() editable}, and the
090 * default means of allowing user input is via a {@link TextField}, a 
091 * {@link #converterProperty() string converter} property is provided to allow
092 * for developers to specify how to translate a users string into an object of
093 * type T, such that the {@link #valueProperty() value} property may contain it.
094 * By default the converter simply returns the String input as the user typed it,
095 * which therefore assumes that the type of the editable ComboBox is String. If 
096 * a different type is specified and the ComboBox is to be editable, it is 
097 * necessary to specify a custom {@link StringConverter}.
098 * 
099 * <h3>A warning about inserting Nodes into the ComboBox items list</h3>
100 * ComboBox allows for the items list to contain elements of any type, including 
101 * {@link Node} instances. Putting nodes into 
102 * the items list is <strong>strongly not recommended</strong>. This is because 
103 * the default {@link #cellFactoryProperty() cell factory} simply inserts Node 
104 * items directly into the cell, including in the ComboBox 'button' area too. 
105 * Because the scenegraph only allows for Nodes to be in one place at a time, 
106 * this means that when an item is selected it becomes removed from the ComboBox
107 * list, and becomes visible in the button area. When selection changes the 
108 * previously selected item returns to the list and the new selection is removed.
109 * 
110 * <p>The recommended approach, rather than inserting Node instances into the 
111 * items list, is to put the relevant information into the ComboBox, and then
112 * provide a custom {@link #cellFactoryProperty() cell factory}. For example,
113 * rather than use the following code:
114 * 
115 * <pre>
116 * {@code
117 * ComboBox<Rectangle> cmb = new ComboBox<Rectangle>();
118 * cmb.getItems().addAll(
119 *     new Rectangle(10, 10, Color.RED), 
120 *     new Rectangle(10, 10, Color.GREEN), 
121 *     new Rectangle(10, 10, Color.BLUE));}</pre>
122 * 
123 * <p>You should do the following:</p>
124 * 
125 * <pre><code>
126 * ComboBox&lt;Color&gt; cmb = new ComboBox&lt;Color&gt;();
127 * cmb.getItems().addAll(
128 *     Color.RED,
129 *     Color.GREEN,
130 *     Color.BLUE);
131 *
132 * cmb.setCellFactory(new Callback&lt;ListView&lt;Color&gt;, ListCell&lt;Color&gt;&gt;() {
133 *     &#064;Override public ListCell&lt;Color&gt; call(ListView&lt;Color&gt; p) {
134 *         return new ListCell&lt;Color&gt;() {
135 *             private final Rectangle rectangle;
136 *             { 
137 *                 setContentDisplay(ContentDisplay.GRAPHIC_ONLY); 
138 *                 rectangle = new Rectangle(10, 10);
139 *             }
140 *             
141 *             &#064;Override protected void updateItem(Color item, boolean empty) {
142 *                 super.updateItem(item, empty);
143 *                 
144 *                 if (item == null || empty) {
145 *                     setGraphic(null);
146 *                 } else {
147 *                     rectangle.setFill(item);
148 *                     setGraphic(rectangle);
149 *                 }
150 *            }
151 *       };
152 *   }
153 *});</code></pre>
154 * 
155 * <p>Admittedly the above approach is far more verbose, but it offers the 
156 * required functionality without encountering the scenegraph constraints.
157 * 
158 * @see ComboBoxBase
159 * @see Cell
160 * @see ListCell
161 * @see StringConverter
162 */
163public class ComboBox<T> extends ComboBoxBase<T> {
164    
165    /***************************************************************************
166     *                                                                         *
167     * Static properties and methods                                           *
168     *                                                                         *
169     **************************************************************************/
170    
171    private static <T> StringConverter<T> defaultStringConverter() {
172        return new StringConverter<T>() {
173            @Override public String toString(T t) {
174                return t == null ? null : t.toString();
175            }
176
177            @Override public T fromString(String string) {
178                return (T) string;
179            }
180        };
181    }
182    
183    
184    
185    /***************************************************************************
186     *                                                                         *
187     * Constructors                                                            *
188     *                                                                         *
189     **************************************************************************/
190
191    /**
192     * Creates a default ComboBox instance with an empty 
193     * {@link #itemsProperty() items} list and default 
194     * {@link #selectionModelProperty() selection model}.
195     */
196    public ComboBox() {
197        this(FXCollections.<T>observableArrayList());
198    }
199    
200    /**
201     * Creates a default ComboBox instance with the provided items list and
202     * a default {@link #selectionModelProperty() selection model}.
203     */
204    public ComboBox(ObservableList<T> items) {
205        getStyleClass().add(DEFAULT_STYLE_CLASS);
206        setItems(items);
207        setSelectionModel(new ComboBoxSelectionModel<T>(this));
208        
209        // listen to the value property input by the user, and if the value is
210        // set to something that exists in the items list, we should update the
211        // selection model to indicate that this is the selected item
212        valueProperty().addListener(new ChangeListener<T>() {
213            @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) {
214                if (getItems() == null) return;
215                
216                SelectionModel<T> sm = getSelectionModel();
217                int index = getItems().indexOf(t1);
218                
219                if (index == -1) {
220                    sm.setSelectedItem(t1);
221                } else {
222                    // we must compare the value here with the currently selected
223                    // item. If they are different, we overwrite the selection
224                    // properties to reflect the new value.
225                    // We do this as there can be circumstances where there are
226                    // multiple instances of a value in the ComboBox items list,
227                    // and if we don't check here we may change the selection
228                    // mistakenly because the indexOf above will return the first
229                    // instance always, and selection may be on the second or 
230                    // later instances. This is RT-19227.
231                    T selectedItem = sm.getSelectedItem();
232                    if (selectedItem == null || ! selectedItem.equals(getValue())) {
233                        sm.clearAndSelect(index);
234                    }
235                }
236            }
237        });
238        
239        editableProperty().addListener(new InvalidationListener() {
240            @Override public void invalidated(Observable o) {
241                // when editable changes, we reset the selection / value states
242                getSelectionModel().clearSelection();
243            }
244        });
245    }
246    
247 
248    
249    /***************************************************************************
250     *                                                                         *
251     * Properties                                                              *
252     *                                                                         *
253     **************************************************************************/
254    
255    // --- items
256    /**
257     * The list of items to show within the ComboBox popup.
258     */
259    private ObjectProperty<ObservableList<T>> items = new SimpleObjectProperty<ObservableList<T>>(this, "items") {
260        @Override protected void invalidated() {
261            // FIXME temporary fix for RT-15793. This will need to be
262            // properly fixed when time permits
263            if (getSelectionModel() instanceof ComboBoxSelectionModel) {
264                ((ComboBoxSelectionModel<T>)getSelectionModel()).updateItemsObserver(null, getItems());
265            }
266            if (getSkin() instanceof ComboBoxListViewSkin) {
267                ComboBoxListViewSkin<?> skin = (ComboBoxListViewSkin<?>) getSkin();
268                skin.updateListViewItems();
269            }
270        }
271    };
272    public final void setItems(ObservableList<T> value) { itemsProperty().set(value); }
273    public final ObservableList<T> getItems() {return items.get(); }
274    public ObjectProperty<ObservableList<T>> itemsProperty() { return items; }
275    
276    
277    // --- string converter
278    /**
279     * Converts the user-typed input (when the ComboBox is 
280     * {@link #editableProperty() editable}) to an object of type T, such that 
281     * the input may be retrieved via the  {@link #valueProperty() value} property.
282     */
283    public ObjectProperty<StringConverter<T>> converterProperty() { return converter; }
284    private ObjectProperty<StringConverter<T>> converter = 
285            new SimpleObjectProperty<StringConverter<T>>(this, "converter", ComboBox.<T>defaultStringConverter());
286    public final void setConverter(StringConverter<T> value) { converterProperty().set(value); }
287    public final StringConverter<T> getConverter() {return converterProperty().get(); }
288    
289    
290    // --- cell factory
291    /**
292     * Providing a custom cell factory allows for complete customization of the
293     * rendering of items in the ComboBox. Refer to the {@link Cell} javadoc
294     * for more information on cell factories.
295     */
296    private ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory = 
297            new SimpleObjectProperty<Callback<ListView<T>, ListCell<T>>>(this, "cellFactory");
298    public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value) { cellFactoryProperty().set(value); }
299    public final Callback<ListView<T>, ListCell<T>> getCellFactory() {return cellFactoryProperty().get(); }
300    public ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryProperty() { return cellFactory; }
301    
302    
303    // --- button cell
304    /**
305     * The button cell is used to render what is shown in the ComboBox 'button'
306     * area. If a cell is set here, it does not change the rendering of the
307     * ComboBox popup list - that rendering is controlled via the 
308     * {@link #cellFactoryProperty() cell factory} API.
309     * @since 2.2
310     */
311    public ObjectProperty<ListCell<T>> buttonCellProperty() { return buttonCell; }
312    private ObjectProperty<ListCell<T>> buttonCell = 
313            new SimpleObjectProperty<ListCell<T>>(this, "buttonCell");
314    public final void setButtonCell(ListCell<T> value) { buttonCellProperty().set(value); }
315    public final ListCell<T> getButtonCell() {return buttonCellProperty().get(); }
316    
317    
318    // --- Selection Model
319    /**
320     * The selection model for the ComboBox. A ComboBox only supports
321     * single selection.
322     */
323    private ObjectProperty<SingleSelectionModel<T>> selectionModel = new SimpleObjectProperty<SingleSelectionModel<T>>(this, "selectionModel") {
324        private SingleSelectionModel<T> oldSM = null;
325        @Override protected void invalidated() {
326            if (oldSM != null) {
327                oldSM.selectedItemProperty().removeListener(selectedItemListener);
328            }
329            SingleSelectionModel<T> sm = get();
330            oldSM = sm;
331            if (sm != null) {
332                sm.selectedItemProperty().addListener(selectedItemListener);
333            }
334        }                
335    };
336    public final void setSelectionModel(SingleSelectionModel<T> value) { selectionModel.set(value); }
337    public final SingleSelectionModel<T> getSelectionModel() { return selectionModel.get(); }
338    public final ObjectProperty<SingleSelectionModel<T>> selectionModelProperty() { return selectionModel; }
339    
340    
341    // --- Visible Row Count
342    /**
343     * The maximum number of rows to be visible in the ComboBox popup when it is
344     * showing. By default this value is 10, but this can be changed to increase
345     * or decrease the height of the popup.
346     */
347    private IntegerProperty visibleRowCount
348            = new SimpleIntegerProperty(this, "visibleRowCount", 10);
349    public final void setVisibleRowCount(int value) { visibleRowCount.set(value); }
350    public final int getVisibleRowCount() { return visibleRowCount.get(); }
351    public final IntegerProperty visibleRowCountProperty() { return visibleRowCount; }
352    
353    
354    // --- Editor
355    private TextField textField;
356    /**
357     * The editor for the ComboBox. The editor is null if the ComboBox is not
358     * {@link #editableProperty() editable}.
359     * @since 2.2
360     */
361    private ReadOnlyObjectWrapper<TextField> editor;
362    public final TextField getEditor() { 
363        return editorProperty().get(); 
364    }
365    public final ReadOnlyObjectProperty<TextField> editorProperty() { 
366        if (editor == null) {
367            editor = new ReadOnlyObjectWrapper<TextField>(this, "editor");
368            textField = new TextField();
369            editor.set(textField);
370        }
371        return editor.getReadOnlyProperty(); 
372    }
373
374    
375    // --- Placeholder Node
376    private ObjectProperty<Node> placeholder;
377    /**
378     * This Node is shown to the user when the ComboBox has no content to show.
379     * The placeholder node is shown in the ComboBox popup area
380     * when the items list is null or empty. This is different than the 
381     * {@link #emptyTextProperty() emptyText} property, which is shown in the
382     * ComboBox Button / TextField area when there is no user-input value.
383     */
384    public final ObjectProperty<Node> placeholderProperty() {
385        if (placeholder == null) {
386            placeholder = new SimpleObjectProperty<Node>(this, "placeholder");
387        }
388        return placeholder;
389    }
390    public final void setPlaceholder(Node value) {
391        placeholderProperty().set(value);
392    }
393    public final Node getPlaceholder() {
394        return placeholder == null ? null : placeholder.get();
395    }
396    
397    
398    
399    /***************************************************************************
400     *                                                                         *
401     * Methods                                                                 *
402     *                                                                         *
403     **************************************************************************/
404
405    /** {@inheritDoc} */
406    @Override protected Skin<?> createDefaultSkin() {
407        return new ComboBoxListViewSkin<T>(this);
408    }
409    
410    
411    
412    /***************************************************************************
413     *                                                                         *
414     * Callbacks and Events                                                    *
415     *                                                                         *
416     **************************************************************************/    
417    
418    // Listen to changes in the selectedItem property of the SelectionModel.
419    // When it changes, set the selectedItem in the value property.
420    private ChangeListener<T> selectedItemListener = new ChangeListener<T>() {
421        @Override public void changed(ObservableValue<? extends T> ov, T t, T t1) {
422            if (wasSetAllCalled && t1 == null) {
423                // no-op: fix for RT-22572 where the developer was completely
424                // replacing all items in the ComboBox, and expecting the 
425                // selection (and ComboBox.value) to remain set. If this isn't
426                // here, we would updateValue(null). 
427                // Additional fix for RT-22937: adding the '&& t1 == null'. 
428                // Without this, there would be circumstances where the user 
429                // selecting a new value from the ComboBox would end up in here,
430                // when we really should go into the updateValue(t1) call below.
431                // We should only ever go into this clause if t1 is null.
432                wasSetAllCalled = false;
433            } else {
434                updateValue(t1);
435            }
436        }
437    };
438
439
440
441    /***************************************************************************
442     *                                                                         *
443     * Private methods                                                         *
444     *                                                                         *
445     **************************************************************************/        
446
447    private void updateValue(T newValue) {
448        if (! valueProperty().isBound()) {
449            setValue(newValue);
450        }
451    }
452     
453
454    
455    
456    /***************************************************************************
457     *                                                                         *
458     * Stylesheet Handling                                                     *
459     *                                                                         *
460     **************************************************************************/
461
462    private static final String DEFAULT_STYLE_CLASS = "combo-box";
463    
464    private boolean wasSetAllCalled = false;
465    private int previousItemCount = -1;
466    
467    // package for testing
468    static class ComboBoxSelectionModel<T> extends SingleSelectionModel<T> {
469        private final ComboBox<T> comboBox;
470
471        public ComboBoxSelectionModel(final ComboBox<T> cb) {
472            if (cb == null) {
473                throw new NullPointerException("ComboBox can not be null");
474            }
475            this.comboBox = cb;
476            
477            selectedIndexProperty().addListener(new InvalidationListener() {
478                @Override public void invalidated(Observable valueModel) {
479                    // we used to lazily retrieve the selected item, but now we just
480                    // do it when the selection changes.
481                    setSelectedItem(getModelItem(getSelectedIndex()));
482                }
483            });
484
485            /*
486             * The following two listeners are used in conjunction with
487             * SelectionModel.select(T obj) to allow for a developer to select
488             * an item that is not actually in the data model. When this occurs,
489             * we actively try to find an index that matches this object, going
490             * so far as to actually watch for all changes to the items list,
491             * rechecking each time.
492             */
493
494            this.comboBox.itemsProperty().addListener(weakItemsObserver);
495            if (comboBox.getItems() != null) {
496                this.comboBox.getItems().addListener(weakItemsContentObserver);
497            }
498        }
499        
500        // watching for changes to the items list content
501        private final ListChangeListener<T> itemsContentObserver = new ListChangeListener<T>() {
502            @Override public void onChanged(Change<? extends T> c) {
503                if (comboBox.getItems() == null || comboBox.getItems().isEmpty()) {
504                    setSelectedIndex(-1);
505                } else if (getSelectedIndex() == -1 && getSelectedItem() != null) {
506                    int newIndex = comboBox.getItems().indexOf(getSelectedItem());
507                    if (newIndex != -1) {
508                        setSelectedIndex(newIndex);
509                    }
510                }
511                
512                while (c.next()) {
513                    comboBox.wasSetAllCalled = comboBox.previousItemCount == c.getRemovedSize();
514                    
515                    
516                    if (c.getFrom() <= getSelectedIndex() && getSelectedIndex()!= -1 && (c.wasAdded() || c.wasRemoved())) {
517                        int shift = c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize();
518                        clearAndSelect(getSelectedIndex() + shift);
519                    }
520                }
521                
522                comboBox.previousItemCount = getItemCount();
523            }
524        };
525        
526        // watching for changes to the items list
527        private final ChangeListener<ObservableList<T>> itemsObserver = new ChangeListener<ObservableList<T>>() {
528            @Override
529            public void changed(ObservableValue<? extends ObservableList<T>> valueModel, 
530                ObservableList<T> oldList, ObservableList<T> newList) {
531                    updateItemsObserver(oldList, newList);
532            }
533        };
534        
535        private WeakListChangeListener<T> weakItemsContentObserver =
536                new WeakListChangeListener<T>(itemsContentObserver);
537        
538        private WeakChangeListener<ObservableList<T>> weakItemsObserver = 
539                new WeakChangeListener<ObservableList<T>>(itemsObserver);
540        
541        private void updateItemsObserver(ObservableList<T> oldList, ObservableList<T> newList) {
542            // update listeners
543            if (oldList != null) {
544                oldList.removeListener(weakItemsContentObserver);
545            }
546            if (newList != null) {
547                newList.addListener(weakItemsContentObserver);
548            }
549
550            // when the items list totally changes, we should clear out
551            // the selection and focus
552            setSelectedIndex(-1);
553        }
554
555        // API Implementation
556        @Override protected T getModelItem(int index) {
557            final ObservableList<T> items = comboBox.getItems();
558            if (items == null) return null;
559            if (index < 0 || index >= items.size()) return null;
560            return items.get(index);
561        }
562
563        @Override protected int getItemCount() {
564            final ObservableList<T> items = comboBox.getItems();
565            return items == null ? 0 : items.size();
566        }
567    }
568}