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}