1 /* 2 * Copyright (c) 2010, 2016, 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 java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.HashMap; 31 import java.util.List; 32 33 import com.sun.javafx.scene.control.Properties; 34 import com.sun.javafx.scene.control.SelectedItemsReadOnlyObservableList; 35 import com.sun.javafx.scene.control.behavior.ListCellBehavior; 36 import javafx.beans.InvalidationListener; 37 import javafx.beans.Observable; 38 import javafx.beans.WeakInvalidationListener; 39 import javafx.beans.property.BooleanProperty; 40 import javafx.beans.property.DoubleProperty; 41 import javafx.beans.property.ObjectProperty; 42 import javafx.beans.property.ObjectPropertyBase; 43 import javafx.beans.property.ReadOnlyIntegerProperty; 44 import javafx.beans.property.ReadOnlyIntegerWrapper; 45 import javafx.beans.property.SimpleBooleanProperty; 46 import javafx.beans.property.SimpleObjectProperty; 47 import javafx.beans.value.WritableValue; 48 import javafx.collections.FXCollections; 49 import javafx.collections.ListChangeListener; 50 import javafx.collections.ListChangeListener.Change; 51 import javafx.collections.MapChangeListener; 52 import javafx.collections.ObservableList; 53 import javafx.css.StyleableDoubleProperty; 54 import javafx.event.Event; 55 import javafx.event.EventHandler; 56 import javafx.event.EventType; 57 import javafx.geometry.Orientation; 58 import javafx.scene.layout.Region; 59 import javafx.util.Callback; 60 import javafx.css.StyleableObjectProperty; 61 import javafx.css.CssMetaData; 62 63 import javafx.css.converter.EnumConverter; 64 65 import javafx.collections.WeakListChangeListener; 66 67 import javafx.css.converter.SizeConverter; 68 import javafx.scene.control.skin.ListViewSkin; 69 70 import java.lang.ref.WeakReference; 71 72 import javafx.css.PseudoClass; 73 import javafx.beans.DefaultProperty; 74 import javafx.css.Styleable; 75 import javafx.css.StyleableProperty; 76 import javafx.scene.AccessibleAttribute; 77 import javafx.scene.AccessibleRole; 78 import javafx.scene.Node; 79 import javafx.util.Pair; 80 81 /** 82 * A ListView displays a horizontal or vertical list of items from which the 83 * user may select, or with which the user may interact. A ListView is able to 84 * have its generic type set to represent the type of data in the backing model. 85 * Doing this has the benefit of making various methods in the ListView, as well 86 * as the supporting classes (mentioned below), type-safe. In addition, making 87 * use of the generic type supports substantially simplified development of applications 88 * making use of ListView, as all modern IDEs are able to auto-complete far 89 * more successfully with the additional type information. 90 * 91 * <h3>Populating a ListView</h3> 92 * <p>A simple example of how to create and populate a ListView of names (Strings) 93 * is shown here: 94 * 95 * <pre> 96 * {@code 97 * ObservableList<String> names = FXCollections.observableArrayList( 98 * "Julia", "Ian", "Sue", "Matthew", "Hannah", "Stephan", "Denise"); 99 * ListView<String> listView = new ListView<String>(names);}</pre> 100 * 101 * <p>The elements of the ListView are contained within the 102 * {@link #itemsProperty() items} {@link ObservableList}. This 103 * ObservableList is automatically observed by the ListView, such that any 104 * changes that occur inside the ObservableList will be automatically shown in 105 * the ListView itself. If passing the <code>ObservableList</code> in to the 106 * ListView constructor is not feasible, the recommended approach for setting 107 * the items is to simply call: 108 * 109 * <pre> 110 * {@code 111 * ObservableList<T> content = ... 112 * listView.setItems(content);}</pre> 113 * 114 * The end result of this is, as noted above, that the ListView will automatically 115 * refresh the view to represent the items in the list. 116 * 117 * <p>Another approach, whilst accepted by the ListView, <b>is not the 118 * recommended approach</b>: 119 * 120 * <pre> 121 * {@code 122 * List<T> content = ... 123 * getItems().setAll(content);}</pre> 124 * 125 * The issue with the approach shown above is that the content list is being 126 * copied into the items list - meaning that subsequent changes to the content 127 * list are not observed, and will not be reflected visually within the ListView. 128 * 129 * <h3>ListView Selection / Focus APIs</h3> 130 * <p>To track selection and focus, it is necessary to become familiar with the 131 * {@link SelectionModel} and {@link FocusModel} classes. A ListView has at most 132 * one instance of each of these classes, available from 133 * {@link #selectionModelProperty() selectionModel} and 134 * {@link #focusModelProperty() focusModel} properties respectively. 135 * Whilst it is possible to use this API to set a new selection model, in 136 * most circumstances this is not necessary - the default selection and focus 137 * models should work in most circumstances. 138 * 139 * <p>The default {@link SelectionModel} used when instantiating a ListView is 140 * an implementation of the {@link MultipleSelectionModel} abstract class. 141 * However, as noted in the API documentation for 142 * the {@link MultipleSelectionModel#selectionModeProperty() selectionMode} 143 * property, the default value is {@link SelectionMode#SINGLE}. To enable 144 * multiple selection in a default ListView instance, it is therefore necessary 145 * to do the following: 146 * 147 * <pre> 148 * {@code 149 * listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}</pre> 150 * 151 * <h3>Customizing ListView Visuals</h3> 152 * <p>The visuals of the ListView can be entirely customized by replacing the 153 * default {@link #cellFactoryProperty() cell factory}. A cell factory is used to 154 * generate {@link ListCell} instances, which are used to represent an item in the 155 * ListView. See the {@link Cell} class documentation for a more complete 156 * description of how to write custom Cells. 157 * 158 * <h3>Editing</h3> 159 * <p>This control supports inline editing of values, and this section attempts to 160 * give an overview of the available APIs and how you should use them.</p> 161 * 162 * <p>Firstly, cell editing most commonly requires a different user interface 163 * than when a cell is not being edited. This is the responsibility of the 164 * {@link Cell} implementation being used. For ListView, this is the responsibility 165 * of the {@link #cellFactoryProperty() cell factory}. It is your choice whether the cell is 166 * permanently in an editing state (e.g. this is common for {@link CheckBox} cells), 167 * or to switch to a different UI when editing begins (e.g. when a double-click 168 * is received on a cell).</p> 169 * 170 * <p>To know when editing has been requested on a cell, 171 * simply override the {@link javafx.scene.control.Cell#startEdit()} method, and 172 * update the cell {@link javafx.scene.control.Cell#textProperty() text} and 173 * {@link javafx.scene.control.Cell#graphicProperty() graphic} properties as 174 * appropriate (e.g. set the text to null and set the graphic to be a 175 * {@link TextField}). Additionally, you should also override 176 * {@link Cell#cancelEdit()} to reset the UI back to its original visual state 177 * when the editing concludes. In both cases it is important that you also 178 * ensure that you call the super method to have the cell perform all duties it 179 * must do to enter or exit its editing mode.</p> 180 * 181 * <p>Once your cell is in an editing state, the next thing you are most probably 182 * interested in is how to commit or cancel the editing that is taking place. This is your 183 * responsibility as the cell factory provider. Your cell implementation will know 184 * when the editing is over, based on the user input (e.g. when the user presses 185 * the Enter or ESC keys on their keyboard). When this happens, it is your 186 * responsibility to call {@link Cell#commitEdit(Object)} or 187 * {@link Cell#cancelEdit()}, as appropriate.</p> 188 * 189 * <p>When you call {@link Cell#commitEdit(Object)} an event is fired to the 190 * ListView, which you can observe by adding an {@link EventHandler} via 191 * {@link ListView#setOnEditCommit(javafx.event.EventHandler)}. Similarly, 192 * you can also observe edit events for 193 * {@link ListView#setOnEditStart(javafx.event.EventHandler) edit start} 194 * and {@link ListView#setOnEditCancel(javafx.event.EventHandler) edit cancel}.</p> 195 * 196 * <p>By default the ListView edit commit handler is non-null, with a default 197 * handler that attempts to overwrite the property value for the 198 * item in the currently-being-edited row. It is able to do this as the 199 * {@link Cell#commitEdit(Object)} method is passed in the new value, and this 200 * is passed along to the edit commit handler via the 201 * {@link EditEvent} that is fired. It is simply a matter of calling 202 * {@link EditEvent#getNewValue()} to retrieve this value. 203 * 204 * <p>It is very important to note that if you call 205 * {@link ListView#setOnEditCommit(javafx.event.EventHandler)} with your own 206 * {@link EventHandler}, then you will be removing the default handler. Unless 207 * you then handle the writeback to the property (or the relevant data source), 208 * nothing will happen. You can work around this by using the 209 * {@link ListView#addEventHandler(javafx.event.EventType, javafx.event.EventHandler)} 210 * method to add a {@link ListView#EDIT_COMMIT_EVENT} {@link EventType} with 211 * your desired {@link EventHandler} as the second argument. Using this method, 212 * you will not replace the default implementation, but you will be notified when 213 * an edit commit has occurred.</p> 214 * 215 * <p>Hopefully this summary answers some of the commonly asked questions. 216 * Fortunately, JavaFX ships with a number of pre-built cell factories that 217 * handle all the editing requirements on your behalf. You can find these 218 * pre-built cell factories in the javafx.scene.control.cell package.</p> 219 * 220 * @see ListCell 221 * @see MultipleSelectionModel 222 * @see FocusModel 223 * @param <T> This type is used to represent the type of the objects stored in 224 * the ListViews {@link #itemsProperty() items} ObservableList. It is 225 * also used in the {@link #selectionModelProperty() selection model} 226 * and {@link #focusModelProperty() focus model}. 227 * @since JavaFX 2.0 228 */ 229 // TODO add code examples 230 @DefaultProperty("items") 231 public class ListView<T> extends Control { 232 233 /*************************************************************************** 234 * * 235 * Static properties and methods * 236 * * 237 **************************************************************************/ 238 239 /** 240 * An EventType that indicates some edit event has occurred. It is the parent 241 * type of all other edit events: {@link #EDIT_START_EVENT}, 242 * {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT}. 243 */ 244 @SuppressWarnings("unchecked") 245 public static <T> EventType<ListView.EditEvent<T>> editAnyEvent() { 246 return (EventType<ListView.EditEvent<T>>) EDIT_ANY_EVENT; 247 } 248 private static final EventType<?> EDIT_ANY_EVENT = 249 new EventType<>(Event.ANY, "LIST_VIEW_EDIT"); 250 251 /** 252 * An EventType used to indicate that an edit event has started within the 253 * ListView upon which the event was fired. 254 */ 255 @SuppressWarnings("unchecked") 256 public static <T> EventType<ListView.EditEvent<T>> editStartEvent() { 257 return (EventType<ListView.EditEvent<T>>) EDIT_START_EVENT; 258 } 259 private static final EventType<?> EDIT_START_EVENT = 260 new EventType<>(editAnyEvent(), "EDIT_START"); 261 262 /** 263 * An EventType used to indicate that an edit event has just been canceled 264 * within the ListView upon which the event was fired. 265 */ 266 @SuppressWarnings("unchecked") 267 public static <T> EventType<ListView.EditEvent<T>> editCancelEvent() { 268 return (EventType<ListView.EditEvent<T>>) EDIT_CANCEL_EVENT; 269 } 270 private static final EventType<?> EDIT_CANCEL_EVENT = 271 new EventType<>(editAnyEvent(), "EDIT_CANCEL"); 272 273 /** 274 * An EventType used to indicate that an edit event has been committed 275 * within the ListView upon which the event was fired. 276 */ 277 @SuppressWarnings("unchecked") 278 public static <T> EventType<ListView.EditEvent<T>> editCommitEvent() { 279 return (EventType<ListView.EditEvent<T>>) EDIT_COMMIT_EVENT; 280 } 281 private static final EventType<?> EDIT_COMMIT_EVENT = 282 new EventType<>(editAnyEvent(), "EDIT_COMMIT"); 283 284 285 286 /*************************************************************************** 287 * * 288 * Fields * 289 * * 290 **************************************************************************/ 291 292 // by default we always select the first row in the ListView, and when the 293 // items list changes, we also reselect the first row. In some cases, such as 294 // for the ComboBox, this is not desirable, so it can be disabled here. 295 private boolean selectFirstRowByDefault = true; 296 297 298 299 /*************************************************************************** 300 * * 301 * Constructors * 302 * * 303 **************************************************************************/ 304 305 /** 306 * Creates a default ListView which will display contents stacked vertically. 307 * As no {@link ObservableList} is provided in this constructor, an empty 308 * ObservableList is created, meaning that it is legal to directly call 309 * {@link #getItems()} if so desired. However, as noted elsewhere, this 310 * is not the recommended approach 311 * (instead call {@link #setItems(javafx.collections.ObservableList)}). 312 * 313 * <p>Refer to the {@link ListView} class documentation for details on the 314 * default state of other properties. 315 */ 316 public ListView() { 317 this(FXCollections.<T>observableArrayList()); 318 } 319 320 /** 321 * Creates a default ListView which will stack the contents retrieved from the 322 * provided {@link ObservableList} vertically. 323 * 324 * <p>Attempts to add a listener to the {@link ObservableList}, such that all 325 * subsequent changes inside the list will be shown to the user. 326 * 327 * <p>Refer to the {@link ListView} class documentation for details on the 328 * default state of other properties. 329 */ 330 public ListView(ObservableList<T> items) { 331 getStyleClass().setAll(DEFAULT_STYLE_CLASS); 332 setAccessibleRole(AccessibleRole.LIST_VIEW); 333 334 setItems(items); 335 336 // Install default.... 337 // ...selection model 338 setSelectionModel(new ListView.ListViewBitSetSelectionModel<T>(this)); 339 340 // ...focus model 341 setFocusModel(new ListView.ListViewFocusModel<T>(this)); 342 343 // ...edit commit handler 344 setOnEditCommit(DEFAULT_EDIT_COMMIT_HANDLER); 345 346 // Fix for RT-36651, which was introduced by RT-35679 (above) and resolved 347 // by having special-case code to remove the listener when requested. 348 // This is done by ComboBoxListViewSkin, so that selection is not done 349 // when a ComboBox is shown. 350 getProperties().addListener((MapChangeListener<Object, Object>) change -> { 351 if (change.wasAdded() && "selectFirstRowByDefault".equals(change.getKey())) { 352 Boolean _selectFirstRowByDefault = (Boolean) change.getValueAdded(); 353 if (_selectFirstRowByDefault == null) return; 354 selectFirstRowByDefault = _selectFirstRowByDefault; 355 } 356 }); 357 } 358 359 360 361 /*************************************************************************** 362 * * 363 * Callbacks and Events * 364 * * 365 **************************************************************************/ 366 367 private EventHandler<ListView.EditEvent<T>> DEFAULT_EDIT_COMMIT_HANDLER = t -> { 368 int index = t.getIndex(); 369 List<T> list = getItems(); 370 if (index < 0 || index >= list.size()) return; 371 list.set(index, t.getNewValue()); 372 }; 373 374 375 376 /*************************************************************************** 377 * * 378 * Properties * 379 * * 380 **************************************************************************/ 381 382 // --- Items 383 private ObjectProperty<ObservableList<T>> items; 384 385 /** 386 * Sets the underlying data model for the ListView. Note that it has a generic 387 * type that must match the type of the ListView itself. 388 */ 389 public final void setItems(ObservableList<T> value) { 390 itemsProperty().set(value); 391 } 392 393 /** 394 * Returns an {@link ObservableList} that contains the items currently being 395 * shown to the user. This may be null if 396 * {@link #setItems(javafx.collections.ObservableList)} has previously been 397 * called, however, by default it is an empty ObservableList. 398 * 399 * @return An ObservableList containing the items to be shown to the user, or 400 * null if the items have previously been set to null. 401 */ 402 public final ObservableList<T> getItems() { 403 return items == null ? null : items.get(); 404 } 405 406 /** 407 * The underlying data model for the ListView. Note that it has a generic 408 * type that must match the type of the ListView itself. 409 */ 410 public final ObjectProperty<ObservableList<T>> itemsProperty() { 411 if (items == null) { 412 items = new SimpleObjectProperty<>(this, "items"); 413 } 414 return items; 415 } 416 417 418 // --- Placeholder Node 419 private ObjectProperty<Node> placeholder; 420 /** 421 * This Node is shown to the user when the listview has no content to show. 422 * This may be the case because the table model has no data in the first 423 * place or that a filter has been applied to the list model, resulting 424 * in there being nothing to show the user.. 425 * @since JavaFX 8.0 426 */ 427 public final ObjectProperty<Node> placeholderProperty() { 428 if (placeholder == null) { 429 placeholder = new SimpleObjectProperty<Node>(this, "placeholder"); 430 } 431 return placeholder; 432 } 433 public final void setPlaceholder(Node value) { 434 placeholderProperty().set(value); 435 } 436 public final Node getPlaceholder() { 437 return placeholder == null ? null : placeholder.get(); 438 } 439 440 441 // --- Selection Model 442 private ObjectProperty<MultipleSelectionModel<T>> selectionModel = new SimpleObjectProperty<MultipleSelectionModel<T>>(this, "selectionModel"); 443 444 /** 445 * Sets the {@link MultipleSelectionModel} to be used in the ListView. 446 * Despite a ListView requiring a <b>Multiple</b>SelectionModel, it is possible 447 * to configure it to only allow single selection (see 448 * {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)} 449 * for more information). 450 */ 451 public final void setSelectionModel(MultipleSelectionModel<T> value) { 452 selectionModelProperty().set(value); 453 } 454 455 /** 456 * Returns the currently installed selection model. 457 */ 458 public final MultipleSelectionModel<T> getSelectionModel() { 459 return selectionModel == null ? null : selectionModel.get(); 460 } 461 462 /** 463 * The SelectionModel provides the API through which it is possible 464 * to select single or multiple items within a ListView, as well as inspect 465 * which items have been selected by the user. Note that it has a generic 466 * type that must match the type of the ListView itself. 467 */ 468 public final ObjectProperty<MultipleSelectionModel<T>> selectionModelProperty() { 469 return selectionModel; 470 } 471 472 473 // --- Focus Model 474 private ObjectProperty<FocusModel<T>> focusModel; 475 476 /** 477 * Sets the {@link FocusModel} to be used in the ListView. 478 */ 479 public final void setFocusModel(FocusModel<T> value) { 480 focusModelProperty().set(value); 481 } 482 483 /** 484 * Returns the currently installed {@link FocusModel}. 485 */ 486 public final FocusModel<T> getFocusModel() { 487 return focusModel == null ? null : focusModel.get(); 488 } 489 490 /** 491 * The FocusModel provides the API through which it is possible 492 * to both get and set the focus on a single item within a ListView. Note 493 * that it has a generic type that must match the type of the ListView itself. 494 */ 495 public final ObjectProperty<FocusModel<T>> focusModelProperty() { 496 if (focusModel == null) { 497 focusModel = new SimpleObjectProperty<FocusModel<T>>(this, "focusModel"); 498 } 499 return focusModel; 500 } 501 502 503 // --- Orientation 504 private ObjectProperty<Orientation> orientation; 505 506 /** 507 * Sets the orientation of the ListView, which dictates whether 508 * it scrolls vertically or horizontally. 509 */ 510 public final void setOrientation(Orientation value) { 511 orientationProperty().set(value); 512 }; 513 514 /** 515 * Returns the current orientation of the ListView, which dictates whether 516 * it scrolls vertically or horizontally. 517 */ 518 public final Orientation getOrientation() { 519 return orientation == null ? Orientation.VERTICAL : orientation.get(); 520 } 521 522 /** 523 * The orientation of the {@code ListView} - this can either be horizontal 524 * or vertical. 525 */ 526 public final ObjectProperty<Orientation> orientationProperty() { 527 if (orientation == null) { 528 orientation = new StyleableObjectProperty<Orientation>(Orientation.VERTICAL) { 529 @Override public void invalidated() { 530 final boolean active = (get() == Orientation.VERTICAL); 531 pseudoClassStateChanged(PSEUDO_CLASS_VERTICAL, active); 532 pseudoClassStateChanged(PSEUDO_CLASS_HORIZONTAL, !active); 533 } 534 535 @Override 536 public CssMetaData<ListView<?>,Orientation> getCssMetaData() { 537 return ListView.StyleableProperties.ORIENTATION; 538 } 539 540 @Override 541 public Object getBean() { 542 return ListView.this; 543 } 544 545 @Override 546 public String getName() { 547 return "orientation"; 548 } 549 }; 550 } 551 return orientation; 552 } 553 554 555 556 557 // --- Cell Factory 558 private ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactory; 559 560 /** 561 * Sets a new cell factory to use in the ListView. This forces all old 562 * {@link ListCell}'s to be thrown away, and new ListCell's created with 563 * the new cell factory. 564 */ 565 public final void setCellFactory(Callback<ListView<T>, ListCell<T>> value) { 566 cellFactoryProperty().set(value); 567 } 568 569 /** 570 * Returns the current cell factory. 571 */ 572 public final Callback<ListView<T>, ListCell<T>> getCellFactory() { 573 return cellFactory == null ? null : cellFactory.get(); 574 } 575 576 /** 577 * <p>Setting a custom cell factory has the effect of deferring all cell 578 * creation, allowing for total customization of the cell. Internally, the 579 * ListView is responsible for reusing ListCells - all that is necessary 580 * is for the custom cell factory to return from this function a ListCell 581 * which might be usable for representing any item in the ListView. 582 * 583 * <p>Refer to the {@link Cell} class documentation for more detail. 584 */ 585 public final ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryProperty() { 586 if (cellFactory == null) { 587 cellFactory = new SimpleObjectProperty<Callback<ListView<T>, ListCell<T>>>(this, "cellFactory"); 588 } 589 return cellFactory; 590 } 591 592 593 // --- Fixed cell size 594 private DoubleProperty fixedCellSize; 595 596 /** 597 * Sets the new fixed cell size for this control. Any value greater than 598 * zero will enable fixed cell size mode, whereas a zero or negative value 599 * (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size 600 * mode. 601 * 602 * @param value The new fixed cell size value, or a value less than or equal 603 * to zero (or Region.USE_COMPUTED_SIZE) to disable. 604 * @since JavaFX 8.0 605 */ 606 public final void setFixedCellSize(double value) { 607 fixedCellSizeProperty().set(value); 608 } 609 610 /** 611 * Returns the fixed cell size value. A value less than or equal to zero is 612 * used to represent that fixed cell size mode is disabled, and a value 613 * greater than zero represents the size of all cells in this control. 614 * 615 * @return A double representing the fixed cell size of this control, or a 616 * value less than or equal to zero if fixed cell size mode is disabled. 617 * @since JavaFX 8.0 618 */ 619 public final double getFixedCellSize() { 620 return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get(); 621 } 622 /** 623 * Specifies whether this control has cells that are a fixed height (of the 624 * specified value). If this value is less than or equal to zero, 625 * then all cells are individually sized and positioned. This is a slow 626 * operation. Therefore, when performance matters and developers are not 627 * dependent on variable cell sizes it is a good idea to set the fixed cell 628 * size value. Generally cells are around 24px, so setting a fixed cell size 629 * of 24 is likely to result in very little difference in visuals, but a 630 * improvement to performance. 631 * 632 * <p>To set this property via CSS, use the -fx-fixed-cell-size property. 633 * This should not be confused with the -fx-cell-size property. The difference 634 * between these two CSS properties is that -fx-cell-size will size all 635 * cells to the specified size, but it will not enforce that this is the 636 * only size (thus allowing for variable cell sizes, and preventing the 637 * performance gains from being possible). Therefore, when performance matters 638 * use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are 639 * specified in CSS, -fx-fixed-cell-size takes precedence.</p> 640 * 641 * @since JavaFX 8.0 642 */ 643 public final DoubleProperty fixedCellSizeProperty() { 644 if (fixedCellSize == null) { 645 fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) { 646 @Override public CssMetaData<ListView<?>,Number> getCssMetaData() { 647 return StyleableProperties.FIXED_CELL_SIZE; 648 } 649 650 @Override public Object getBean() { 651 return ListView.this; 652 } 653 654 @Override public String getName() { 655 return "fixedCellSize"; 656 } 657 }; 658 } 659 return fixedCellSize; 660 } 661 662 663 // --- Editable 664 private BooleanProperty editable; 665 public final void setEditable(boolean value) { 666 editableProperty().set(value); 667 } 668 public final boolean isEditable() { 669 return editable == null ? false : editable.get(); 670 } 671 /** 672 * Specifies whether this ListView is editable - only if the ListView and 673 * the ListCells within it are both editable will a ListCell be able to go 674 * into their editing state. 675 */ 676 public final BooleanProperty editableProperty() { 677 if (editable == null) { 678 editable = new SimpleBooleanProperty(this, "editable", false); 679 } 680 return editable; 681 } 682 683 684 // --- Editing Index 685 private ReadOnlyIntegerWrapper editingIndex; 686 687 private void setEditingIndex(int value) { 688 editingIndexPropertyImpl().set(value); 689 } 690 691 /** 692 * Returns the index of the item currently being edited in the ListView, 693 * or -1 if no item is being edited. 694 */ 695 public final int getEditingIndex() { 696 return editingIndex == null ? -1 : editingIndex.get(); 697 } 698 699 /** 700 * <p>A property used to represent the index of the item currently being edited 701 * in the ListView, if editing is taking place, or -1 if no item is being edited. 702 * 703 * <p>It is not possible to set the editing index, instead it is required that 704 * you call {@link #edit(int)}. 705 */ 706 public final ReadOnlyIntegerProperty editingIndexProperty() { 707 return editingIndexPropertyImpl().getReadOnlyProperty(); 708 } 709 710 private ReadOnlyIntegerWrapper editingIndexPropertyImpl() { 711 if (editingIndex == null) { 712 editingIndex = new ReadOnlyIntegerWrapper(this, "editingIndex", -1); 713 } 714 return editingIndex; 715 } 716 717 718 // --- On Edit Start 719 private ObjectProperty<EventHandler<ListView.EditEvent<T>>> onEditStart; 720 721 /** 722 * Sets the {@link EventHandler} that will be called when the user begins 723 * an edit. 724 * 725 * <p>This is a convenience method - the same result can be 726 * achieved by calling 727 * <code>addEventHandler(ListView.EDIT_START_EVENT, eventHandler)</code>. 728 */ 729 public final void setOnEditStart(EventHandler<ListView.EditEvent<T>> value) { 730 onEditStartProperty().set(value); 731 } 732 733 /** 734 * Returns the {@link EventHandler} that will be called when the user begins 735 * an edit. 736 */ 737 public final EventHandler<ListView.EditEvent<T>> getOnEditStart() { 738 return onEditStart == null ? null : onEditStart.get(); 739 } 740 741 /** 742 * This event handler will be fired when the user successfully initiates 743 * editing. 744 */ 745 public final ObjectProperty<EventHandler<ListView.EditEvent<T>>> onEditStartProperty() { 746 if (onEditStart == null) { 747 onEditStart = new ObjectPropertyBase<EventHandler<ListView.EditEvent<T>>>() { 748 @Override protected void invalidated() { 749 setEventHandler(ListView.<T>editStartEvent(), get()); 750 } 751 752 @Override 753 public Object getBean() { 754 return ListView.this; 755 } 756 757 @Override 758 public String getName() { 759 return "onEditStart"; 760 } 761 }; 762 } 763 return onEditStart; 764 } 765 766 767 // --- On Edit Commit 768 private ObjectProperty<EventHandler<ListView.EditEvent<T>>> onEditCommit; 769 770 /** 771 * Sets the {@link EventHandler} that will be called when the user has 772 * completed their editing. This is called as part of the 773 * {@link ListCell#commitEdit(java.lang.Object)} method. 774 * 775 * <p>This is a convenience method - the same result can be 776 * achieved by calling 777 * <code>addEventHandler(ListView.EDIT_START_EVENT, eventHandler)</code>. 778 */ 779 public final void setOnEditCommit(EventHandler<ListView.EditEvent<T>> value) { 780 onEditCommitProperty().set(value); 781 } 782 783 /** 784 * Returns the {@link EventHandler} that will be called when the user commits 785 * an edit. 786 */ 787 public final EventHandler<ListView.EditEvent<T>> getOnEditCommit() { 788 return onEditCommit == null ? null : onEditCommit.get(); 789 } 790 791 /** 792 * <p>This property is used when the user performs an action that should 793 * result in their editing input being persisted.</p> 794 * 795 * <p>The EventHandler in this property should not be called directly - 796 * instead call {@link ListCell#commitEdit(java.lang.Object)} from within 797 * your custom ListCell. This will handle firing this event, updating the 798 * view, and switching out of the editing state.</p> 799 */ 800 public final ObjectProperty<EventHandler<ListView.EditEvent<T>>> onEditCommitProperty() { 801 if (onEditCommit == null) { 802 onEditCommit = new ObjectPropertyBase<EventHandler<ListView.EditEvent<T>>>() { 803 @Override protected void invalidated() { 804 setEventHandler(ListView.<T>editCommitEvent(), get()); 805 } 806 807 @Override 808 public Object getBean() { 809 return ListView.this; 810 } 811 812 @Override 813 public String getName() { 814 return "onEditCommit"; 815 } 816 }; 817 } 818 return onEditCommit; 819 } 820 821 822 // --- On Edit Cancel 823 private ObjectProperty<EventHandler<ListView.EditEvent<T>>> onEditCancel; 824 825 /** 826 * Sets the {@link EventHandler} that will be called when the user cancels 827 * an edit. 828 */ 829 public final void setOnEditCancel(EventHandler<ListView.EditEvent<T>> value) { 830 onEditCancelProperty().set(value); 831 } 832 833 /** 834 * Returns the {@link EventHandler} that will be called when the user cancels 835 * an edit. 836 */ 837 public final EventHandler<ListView.EditEvent<T>> getOnEditCancel() { 838 return onEditCancel == null ? null : onEditCancel.get(); 839 } 840 841 /** 842 * This event handler will be fired when the user cancels editing a cell. 843 */ 844 public final ObjectProperty<EventHandler<ListView.EditEvent<T>>> onEditCancelProperty() { 845 if (onEditCancel == null) { 846 onEditCancel = new ObjectPropertyBase<EventHandler<ListView.EditEvent<T>>>() { 847 @Override protected void invalidated() { 848 setEventHandler(ListView.<T>editCancelEvent(), get()); 849 } 850 851 @Override 852 public Object getBean() { 853 return ListView.this; 854 } 855 856 @Override 857 public String getName() { 858 return "onEditCancel"; 859 } 860 }; 861 } 862 return onEditCancel; 863 } 864 865 866 867 868 /*************************************************************************** 869 * * 870 * Public API * 871 * * 872 **************************************************************************/ 873 874 /** 875 * Instructs the ListView to begin editing the item in the given index, if 876 * the ListView is {@link #editableProperty() editable}. Once 877 * this method is called, if the current {@link #cellFactoryProperty()} is 878 * set up to support editing, the Cell will switch its visual state to enable 879 * for user input to take place. 880 * 881 * @param itemIndex The index of the item in the ListView that should be 882 * edited. 883 */ 884 public void edit(int itemIndex) { 885 if (!isEditable()) return; 886 setEditingIndex(itemIndex); 887 } 888 889 /** 890 * Scrolls the ListView such that the item in the given index is visible to 891 * the end user. 892 * 893 * @param index The index that should be made visible to the user, assuming 894 * of course that it is greater than, or equal to 0, and less than the 895 * size of the items list contained within the given ListView. 896 */ 897 public void scrollTo(int index) { 898 ControlUtils.scrollToIndex(this, index); 899 } 900 901 /** 902 * Scrolls the TableView so that the given object is visible within the viewport. 903 * @param object The object that should be visible to the user. 904 * @since JavaFX 8.0 905 */ 906 public void scrollTo(T object) { 907 if( getItems() != null ) { 908 int idx = getItems().indexOf(object); 909 if( idx >= 0 ) { 910 ControlUtils.scrollToIndex(this, idx); 911 } 912 } 913 } 914 915 /** 916 * Called when there's a request to scroll an index into view using {@link #scrollTo(int)} 917 * or {@link #scrollTo(Object)} 918 * @since JavaFX 8.0 919 */ 920 private ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollTo; 921 922 public void setOnScrollTo(EventHandler<ScrollToEvent<Integer>> value) { 923 onScrollToProperty().set(value); 924 } 925 926 public EventHandler<ScrollToEvent<Integer>> getOnScrollTo() { 927 if( onScrollTo != null ) { 928 return onScrollTo.get(); 929 } 930 return null; 931 } 932 933 public ObjectProperty<EventHandler<ScrollToEvent<Integer>>> onScrollToProperty() { 934 if( onScrollTo == null ) { 935 onScrollTo = new ObjectPropertyBase<EventHandler<ScrollToEvent<Integer>>>() { 936 @Override protected void invalidated() { 937 setEventHandler(ScrollToEvent.scrollToTopIndex(), get()); 938 } 939 940 @Override public Object getBean() { 941 return ListView.this; 942 } 943 944 @Override public String getName() { 945 return "onScrollTo"; 946 } 947 }; 948 } 949 return onScrollTo; 950 } 951 952 /** {@inheritDoc} */ 953 @Override protected Skin<?> createDefaultSkin() { 954 return new ListViewSkin<T>(this); 955 } 956 957 /** 958 * Calling {@code refresh()} forces the ListView control to recreate and 959 * repopulate the cells necessary to populate the visual bounds of the control. 960 * In other words, this forces the ListView to update what it is showing to 961 * the user. This is useful in cases where the underlying data source has 962 * changed in a way that is not observed by the ListView itself. 963 * 964 * @since JavaFX 8u60 965 */ 966 public void refresh() { 967 getProperties().put(Properties.RECREATE, Boolean.TRUE); 968 } 969 970 971 972 /*************************************************************************** 973 * * 974 * Private Implementation * 975 * * 976 **************************************************************************/ 977 978 979 980 /*************************************************************************** 981 * * 982 * Stylesheet Handling * 983 * * 984 **************************************************************************/ 985 986 private static final String DEFAULT_STYLE_CLASS = "list-view"; 987 988 private static class StyleableProperties { 989 private static final CssMetaData<ListView<?>,Orientation> ORIENTATION = 990 new CssMetaData<ListView<?>,Orientation>("-fx-orientation", 991 new EnumConverter<Orientation>(Orientation.class), 992 Orientation.VERTICAL) { 993 994 @Override 995 public Orientation getInitialValue(ListView<?> node) { 996 // A vertical ListView should remain vertical 997 return node.getOrientation(); 998 } 999 1000 @Override 1001 public boolean isSettable(ListView<?> n) { 1002 return n.orientation == null || !n.orientation.isBound(); 1003 } 1004 1005 @SuppressWarnings("unchecked") // orientationProperty() is a StyleableProperty<Orientation> 1006 @Override 1007 public StyleableProperty<Orientation> getStyleableProperty(ListView<?> n) { 1008 return (StyleableProperty<Orientation>)n.orientationProperty(); 1009 } 1010 }; 1011 1012 private static final CssMetaData<ListView<?>,Number> FIXED_CELL_SIZE = 1013 new CssMetaData<ListView<?>,Number>("-fx-fixed-cell-size", 1014 SizeConverter.getInstance(), 1015 Region.USE_COMPUTED_SIZE) { 1016 1017 @Override public Double getInitialValue(ListView<?> node) { 1018 return node.getFixedCellSize(); 1019 } 1020 1021 @Override public boolean isSettable(ListView<?> n) { 1022 return n.fixedCellSize == null || !n.fixedCellSize.isBound(); 1023 } 1024 1025 @Override public StyleableProperty<Number> getStyleableProperty(ListView<?> n) { 1026 return (StyleableProperty<Number>)(WritableValue<Number>)n.fixedCellSizeProperty(); 1027 } 1028 }; 1029 1030 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES; 1031 static { 1032 final List<CssMetaData<? extends Styleable, ?>> styleables = 1033 new ArrayList<CssMetaData<? extends Styleable, ?>>(Control.getClassCssMetaData()); 1034 styleables.add(ORIENTATION); 1035 styleables.add(FIXED_CELL_SIZE); 1036 STYLEABLES = Collections.unmodifiableList(styleables); 1037 } 1038 } 1039 1040 /** 1041 * @return The CssMetaData associated with this class, which may include the 1042 * CssMetaData of its superclasses. 1043 * @since JavaFX 8.0 1044 */ 1045 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { 1046 return StyleableProperties.STYLEABLES; 1047 } 1048 1049 /** 1050 * {@inheritDoc} 1051 * @since JavaFX 8.0 1052 */ 1053 @Override 1054 public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() { 1055 return getClassCssMetaData(); 1056 } 1057 1058 private static final PseudoClass PSEUDO_CLASS_VERTICAL = 1059 PseudoClass.getPseudoClass("vertical"); 1060 private static final PseudoClass PSEUDO_CLASS_HORIZONTAL = 1061 PseudoClass.getPseudoClass("horizontal"); 1062 1063 1064 1065 /*************************************************************************** 1066 * * 1067 * Accessibility handling * 1068 * * 1069 **************************************************************************/ 1070 1071 @Override 1072 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 1073 switch (attribute) { 1074 case MULTIPLE_SELECTION: { 1075 MultipleSelectionModel<T> sm = getSelectionModel(); 1076 return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE; 1077 } 1078 default: return super.queryAccessibleAttribute(attribute, parameters); 1079 } 1080 } 1081 1082 1083 /*************************************************************************** 1084 * * 1085 * Support Interfaces * 1086 * * 1087 **************************************************************************/ 1088 1089 1090 1091 /*************************************************************************** 1092 * * 1093 * Support Classes * 1094 * * 1095 **************************************************************************/ 1096 1097 /** 1098 * An {@link Event} subclass used specifically in ListView for representing 1099 * edit-related events. It provides additional API to easily access the 1100 * index that the edit event took place on, as well as the input provided 1101 * by the end user. 1102 * 1103 * @param <T> The type of the input, which is the same type as the ListView 1104 * itself. 1105 * @since JavaFX 2.0 1106 */ 1107 public static class EditEvent<T> extends Event { 1108 private final T newValue; 1109 private final int editIndex; 1110 private final ListView<T> source; 1111 1112 private static final long serialVersionUID = 20130724L; 1113 1114 /** 1115 * Common supertype for all edit event types. 1116 * @since JavaFX 8.0 1117 */ 1118 public static final EventType<?> ANY = EDIT_ANY_EVENT; 1119 1120 /** 1121 * Creates a new EditEvent instance to represent an edit event. This 1122 * event is used for {@link #EDIT_START_EVENT}, 1123 * {@link #EDIT_COMMIT_EVENT} and {@link #EDIT_CANCEL_EVENT} types. 1124 */ 1125 public EditEvent(ListView<T> source, 1126 EventType<? extends ListView.EditEvent<T>> eventType, 1127 T newValue, 1128 int editIndex) { 1129 super(source, Event.NULL_SOURCE_TARGET, eventType); 1130 this.source = source; 1131 this.editIndex = editIndex; 1132 this.newValue = newValue; 1133 } 1134 1135 /** 1136 * Returns the ListView upon which the edit took place. 1137 */ 1138 @Override public ListView<T> getSource() { 1139 return source; 1140 } 1141 1142 /** 1143 * Returns the index in which the edit took place. 1144 */ 1145 public int getIndex() { 1146 return editIndex; 1147 } 1148 1149 /** 1150 * Returns the value of the new input provided by the end user. 1151 */ 1152 public T getNewValue() { 1153 return newValue; 1154 } 1155 1156 /** 1157 * Returns a string representation of this {@code EditEvent} object. 1158 * @return a string representation of this {@code EditEvent} object. 1159 */ 1160 @Override public String toString() { 1161 return "ListViewEditEvent [ newValue: " + getNewValue() + ", ListView: " + getSource() + " ]"; 1162 } 1163 } 1164 1165 1166 1167 // package for testing 1168 static class ListViewBitSetSelectionModel<T> extends MultipleSelectionModelBase<T> { 1169 1170 /*********************************************************************** 1171 * * 1172 * Constructors * 1173 * * 1174 **********************************************************************/ 1175 1176 public ListViewBitSetSelectionModel(final ListView<T> listView) { 1177 if (listView == null) { 1178 throw new IllegalArgumentException("ListView can not be null"); 1179 } 1180 1181 this.listView = listView; 1182 1183 ((SelectedItemsReadOnlyObservableList)getSelectedItems()).setItemsList(listView.getItems()); 1184 1185 1186 /* 1187 * The following two listeners are used in conjunction with 1188 * SelectionModel.select(T obj) to allow for a developer to select 1189 * an item that is not actually in the data model. When this occurs, 1190 * we actively try to find an index that matches this object, going 1191 * so far as to actually watch for all changes to the items list, 1192 * rechecking each time. 1193 */ 1194 itemsObserver = new InvalidationListener() { 1195 private WeakReference<ObservableList<T>> weakItemsRef = new WeakReference<>(listView.getItems()); 1196 1197 @Override public void invalidated(Observable observable) { 1198 ObservableList<T> oldItems = weakItemsRef.get(); 1199 weakItemsRef = new WeakReference<>(listView.getItems()); 1200 ((SelectedItemsReadOnlyObservableList)getSelectedItems()).setItemsList(listView.getItems()); 1201 updateItemsObserver(oldItems, listView.getItems()); 1202 } 1203 }; 1204 1205 this.listView.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); 1206 if (listView.getItems() != null) { 1207 this.listView.getItems().addListener(weakItemsContentObserver); 1208 } 1209 1210 updateItemCount(); 1211 1212 updateDefaultSelection(); 1213 } 1214 1215 // watching for changes to the items list content 1216 private final ListChangeListener<T> itemsContentObserver = new ListChangeListener<T>() { 1217 @Override public void onChanged(Change<? extends T> c) { 1218 updateItemCount(); 1219 1220 boolean doSelectionUpdate = true; 1221 1222 while (c.next()) { 1223 final T selectedItem = getSelectedItem(); 1224 final int selectedIndex = getSelectedIndex(); 1225 1226 if (listView.getItems() == null || listView.getItems().isEmpty()) { 1227 selectedItemChange = c; 1228 clearSelection(); 1229 selectedItemChange = null; 1230 } else if (selectedIndex == -1 && selectedItem != null) { 1231 int newIndex = listView.getItems().indexOf(selectedItem); 1232 if (newIndex != -1) { 1233 setSelectedIndex(newIndex); 1234 doSelectionUpdate = false; 1235 } 1236 } else if (c.wasRemoved() && 1237 c.getRemovedSize() == 1 && 1238 ! c.wasAdded() && 1239 selectedItem != null && 1240 selectedItem.equals(c.getRemoved().get(0))) { 1241 // Bug fix for RT-28637 1242 if (getSelectedIndex() < getItemCount()) { 1243 final int previousRow = selectedIndex == 0 ? 0 : selectedIndex - 1; 1244 T newSelectedItem = getModelItem(previousRow); 1245 if (! selectedItem.equals(newSelectedItem)) { 1246 startAtomic(); 1247 clearSelection(selectedIndex); 1248 stopAtomic(); 1249 select(newSelectedItem); 1250 } 1251 } 1252 } 1253 } 1254 1255 if (doSelectionUpdate) { 1256 updateSelection(c); 1257 } 1258 } 1259 }; 1260 1261 // watching for changes to the items list 1262 private final InvalidationListener itemsObserver; 1263 1264 private WeakListChangeListener<T> weakItemsContentObserver = 1265 new WeakListChangeListener<>(itemsContentObserver); 1266 1267 1268 1269 1270 /*********************************************************************** 1271 * * 1272 * Internal properties * 1273 * * 1274 **********************************************************************/ 1275 1276 private final ListView<T> listView; 1277 1278 private int itemCount = 0; 1279 1280 private int previousModelSize = 0; 1281 1282 // Listen to changes in the listview items list, such that when it 1283 // changes we can update the selected indices bitset to refer to the 1284 // new indices. 1285 // At present this is basically a left/right shift operation, which 1286 // seems to work ok. 1287 private void updateSelection(Change<? extends T> c) { 1288 // // debugging output 1289 // System.out.println(listView.getId()); 1290 // if (c.wasAdded()) { 1291 // System.out.println("\tAdded size: " + c.getAddedSize() + ", Added sublist: " + c.getAddedSubList()); 1292 // } 1293 // if (c.wasRemoved()) { 1294 // System.out.println("\tRemoved size: " + c.getRemovedSize() + ", Removed sublist: " + c.getRemoved()); 1295 // } 1296 // if (c.wasReplaced()) { 1297 // System.out.println("\tWas replaced"); 1298 // } 1299 // if (c.wasPermutated()) { 1300 // System.out.println("\tWas permutated"); 1301 // } 1302 c.reset(); 1303 1304 List<Pair<Integer, Integer>> shifts = new ArrayList<>(); 1305 while (c.next()) { 1306 if (c.wasReplaced()) { 1307 if (c.getList().isEmpty()) { 1308 // the entire items list was emptied - clear selection 1309 clearSelection(); 1310 } else { 1311 int index = getSelectedIndex(); 1312 1313 if (previousModelSize == c.getRemovedSize()) { 1314 // all items were removed from the model 1315 clearSelection(); 1316 } else if (index < getItemCount() && index >= 0) { 1317 // Fix for RT-18969: the list had setAll called on it 1318 // Use of makeAtomic is a fix for RT-20945 1319 startAtomic(); 1320 clearSelection(index); 1321 stopAtomic(); 1322 select(index); 1323 } else { 1324 // Fix for RT-22079 1325 clearSelection(); 1326 } 1327 } 1328 } else if (c.wasAdded() || c.wasRemoved()) { 1329 int shift = c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize(); 1330 shifts.add(new Pair<>(c.getFrom(), shift)); 1331 } else if (c.wasPermutated()) { 1332 1333 // General approach: 1334 // -- detected a sort has happened 1335 // -- Create a permutation lookup map (1) 1336 // -- dump all the selected indices into a list (2) 1337 // -- clear the selected items / indexes (3) 1338 // -- create a list containing the new indices (4) 1339 // -- for each previously-selected index (5) 1340 // -- if index is in the permutation lookup map 1341 // -- add the new index to the new indices list 1342 // -- Perform batch selection (6) 1343 1344 // (1) 1345 int length = c.getTo() - c.getFrom(); 1346 HashMap<Integer, Integer> pMap = new HashMap<Integer, Integer>(length); 1347 for (int i = c.getFrom(); i < c.getTo(); i++) { 1348 pMap.put(i, c.getPermutation(i)); 1349 } 1350 1351 // (2) 1352 List<Integer> selectedIndices = new ArrayList<Integer>(getSelectedIndices()); 1353 1354 1355 // (3) 1356 clearSelection(); 1357 1358 // (4) 1359 List<Integer> newIndices = new ArrayList<Integer>(getSelectedIndices().size()); 1360 1361 // (5) 1362 for (int i = 0; i < selectedIndices.size(); i++) { 1363 int oldIndex = selectedIndices.get(i); 1364 1365 if (pMap.containsKey(oldIndex)) { 1366 Integer newIndex = pMap.get(oldIndex); 1367 newIndices.add(newIndex); 1368 } 1369 } 1370 1371 // (6) 1372 if (!newIndices.isEmpty()) { 1373 if (newIndices.size() == 1) { 1374 select(newIndices.get(0)); 1375 } else { 1376 int[] ints = new int[newIndices.size() - 1]; 1377 for (int i = 0; i < newIndices.size() - 1; i++) { 1378 ints[i] = newIndices.get(i + 1); 1379 } 1380 selectIndices(newIndices.get(0), ints); 1381 } 1382 } 1383 } 1384 } 1385 1386 if (!shifts.isEmpty()) { 1387 shiftSelection(shifts, null); 1388 } 1389 1390 previousModelSize = getItemCount(); 1391 } 1392 1393 1394 1395 /*********************************************************************** 1396 * * 1397 * Public selection API * 1398 * * 1399 **********************************************************************/ 1400 1401 /** {@inheritDoc} */ 1402 @Override public void selectAll() { 1403 // when a selectAll happens, the anchor should not change, so we store it 1404 // before, and restore it afterwards 1405 final int anchor = ListCellBehavior.getAnchor(listView, -1); 1406 super.selectAll(); 1407 ListCellBehavior.setAnchor(listView, anchor, false); 1408 } 1409 1410 /** {@inheritDoc} */ 1411 @Override public void clearAndSelect(int row) { 1412 ListCellBehavior.setAnchor(listView, row, false); 1413 super.clearAndSelect(row); 1414 } 1415 1416 /** {@inheritDoc} */ 1417 @Override protected void focus(int row) { 1418 if (listView.getFocusModel() == null) return; 1419 listView.getFocusModel().focus(row); 1420 1421 listView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); 1422 } 1423 1424 /** {@inheritDoc} */ 1425 @Override protected int getFocusedIndex() { 1426 if (listView.getFocusModel() == null) return -1; 1427 return listView.getFocusModel().getFocusedIndex(); 1428 } 1429 1430 @Override protected int getItemCount() { 1431 return itemCount; 1432 } 1433 1434 @Override protected T getModelItem(int index) { 1435 List<T> items = listView.getItems(); 1436 if (items == null) return null; 1437 if (index < 0 || index >= itemCount) return null; 1438 1439 return items.get(index); 1440 } 1441 1442 1443 1444 /*********************************************************************** 1445 * * 1446 * Private implementation * 1447 * * 1448 **********************************************************************/ 1449 1450 private void updateItemCount() { 1451 if (listView == null) { 1452 itemCount = -1; 1453 } else { 1454 List<T> items = listView.getItems(); 1455 itemCount = items == null ? -1 : items.size(); 1456 } 1457 } 1458 1459 private void updateItemsObserver(ObservableList<T> oldList, ObservableList<T> newList) { 1460 // update listeners 1461 if (oldList != null) { 1462 oldList.removeListener(weakItemsContentObserver); 1463 } 1464 if (newList != null) { 1465 newList.addListener(weakItemsContentObserver); 1466 } 1467 1468 updateItemCount(); 1469 updateDefaultSelection(); 1470 } 1471 1472 private void updateDefaultSelection() { 1473 // when the items list totally changes, we should clear out 1474 // the selection and focus 1475 int newSelectionIndex = -1; 1476 int newFocusIndex = -1; 1477 if (listView.getItems() != null) { 1478 T selectedItem = getSelectedItem(); 1479 if (selectedItem != null) { 1480 newSelectionIndex = listView.getItems().indexOf(selectedItem); 1481 newFocusIndex = newSelectionIndex; 1482 } 1483 1484 // we put focus onto the first item, if there is at least 1485 // one item in the list 1486 if (listView.selectFirstRowByDefault && newFocusIndex == -1) { 1487 newFocusIndex = listView.getItems().size() > 0 ? 0 : -1; 1488 } 1489 } 1490 1491 clearSelection(); 1492 select(newSelectionIndex); 1493 // focus(newFocusIndex); 1494 } 1495 } 1496 1497 1498 1499 // package for testing 1500 static class ListViewFocusModel<T> extends FocusModel<T> { 1501 1502 private final ListView<T> listView; 1503 private int itemCount = 0; 1504 1505 public ListViewFocusModel(final ListView<T> listView) { 1506 if (listView == null) { 1507 throw new IllegalArgumentException("ListView can not be null"); 1508 } 1509 1510 this.listView = listView; 1511 1512 itemsObserver = new InvalidationListener() { 1513 private WeakReference<ObservableList<T>> weakItemsRef = new WeakReference<>(listView.getItems()); 1514 1515 @Override public void invalidated(Observable observable) { 1516 ObservableList<T> oldItems = weakItemsRef.get(); 1517 weakItemsRef = new WeakReference<>(listView.getItems()); 1518 updateItemsObserver(oldItems, listView.getItems()); 1519 } 1520 }; 1521 this.listView.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); 1522 if (listView.getItems() != null) { 1523 this.listView.getItems().addListener(weakItemsContentListener); 1524 } 1525 1526 updateItemCount(); 1527 updateDefaultFocus(); 1528 1529 focusedIndexProperty().addListener(o -> { 1530 listView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); 1531 }); 1532 } 1533 1534 1535 private void updateItemsObserver(ObservableList<T> oldList, ObservableList<T> newList) { 1536 // the listview items list has changed, we need to observe 1537 // the new list, and remove any observer we had from the old list 1538 if (oldList != null) oldList.removeListener(weakItemsContentListener); 1539 if (newList != null) newList.addListener(weakItemsContentListener); 1540 1541 updateItemCount(); 1542 updateDefaultFocus(); 1543 } 1544 1545 private final InvalidationListener itemsObserver; 1546 1547 // Listen to changes in the listview items list, such that when it 1548 // changes we can update the focused index to refer to the new indices. 1549 private final ListChangeListener<T> itemsContentListener = c -> { 1550 updateItemCount(); 1551 1552 while (c.next()) { 1553 // looking at the first change 1554 int from = c.getFrom(); 1555 1556 if (c.wasReplaced() || c.getAddedSize() == getItemCount()) { 1557 updateDefaultFocus(); 1558 return; 1559 } 1560 1561 if (getFocusedIndex() == -1 || from > getFocusedIndex()) { 1562 return; 1563 } 1564 1565 c.reset(); 1566 boolean added = false; 1567 boolean removed = false; 1568 int addedSize = 0; 1569 int removedSize = 0; 1570 while (c.next()) { 1571 added |= c.wasAdded(); 1572 removed |= c.wasRemoved(); 1573 addedSize += c.getAddedSize(); 1574 removedSize += c.getRemovedSize(); 1575 } 1576 1577 if (added && !removed) { 1578 focus(Math.min(getItemCount() - 1, getFocusedIndex() + addedSize)); 1579 } else if (!added && removed) { 1580 focus(Math.max(0, getFocusedIndex() - removedSize)); 1581 } 1582 } 1583 }; 1584 1585 private WeakListChangeListener<T> weakItemsContentListener 1586 = new WeakListChangeListener<T>(itemsContentListener); 1587 1588 @Override protected int getItemCount() { 1589 return itemCount; 1590 } 1591 1592 @Override protected T getModelItem(int index) { 1593 if (isEmpty()) return null; 1594 if (index < 0 || index >= itemCount) return null; 1595 1596 return listView.getItems().get(index); 1597 } 1598 1599 private boolean isEmpty() { 1600 return itemCount == -1; 1601 } 1602 1603 private void updateItemCount() { 1604 if (listView == null) { 1605 itemCount = -1; 1606 } else { 1607 List<T> items = listView.getItems(); 1608 itemCount = items == null ? -1 : items.size(); 1609 } 1610 } 1611 1612 private void updateDefaultFocus() { 1613 // when the items list totally changes, we should clear out 1614 // the focus 1615 int newValueIndex = -1; 1616 if (listView.getItems() != null) { 1617 T focusedItem = getFocusedItem(); 1618 if (focusedItem != null) { 1619 newValueIndex = listView.getItems().indexOf(focusedItem); 1620 } 1621 1622 // we put focus onto the first item, if there is at least 1623 // one item in the list 1624 if (newValueIndex == -1) { 1625 newValueIndex = listView.getItems().size() > 0 ? 0 : -1; 1626 } 1627 } 1628 1629 focus(newValueIndex); 1630 } 1631 } 1632 }