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