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