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