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.lang.ref.WeakReference; 29 import java.util.List; 30 31 import javafx.beans.InvalidationListener; 32 import javafx.beans.Observable; 33 import javafx.beans.WeakInvalidationListener; 34 import javafx.beans.property.ReadOnlyObjectProperty; 35 import javafx.beans.property.ReadOnlyObjectWrapper; 36 import javafx.beans.value.ChangeListener; 37 import javafx.beans.value.ObservableValue; 38 import javafx.beans.value.WeakChangeListener; 39 import javafx.collections.ListChangeListener; 40 import javafx.collections.ObservableList; 41 import javafx.collections.WeakListChangeListener; 42 import javafx.scene.AccessibleAction; 43 import javafx.scene.AccessibleAttribute; 44 import javafx.scene.AccessibleRole; 45 46 import javafx.scene.control.skin.ListCellSkin; 47 48 /** 49 * <p>The {@link Cell} type used within {@link ListView} instances. In addition 50 * to the API defined on Cell and {@link IndexedCell}, the ListCell is more 51 * tightly bound to a ListView, allowing for better support of editing events, 52 * etc. 53 * 54 * <p>A ListView maintains selection, indicating which cell(s) have been selected, 55 * and focus, indicating the current focus owner for any given ListView. For each 56 * property, each ListCell has a boolean reflecting whether this specific cell is 57 * selected or focused. To achieve this, each ListCell has a reference back to 58 * the ListView that it is being used within. Each ListCell belongs to one and 59 * only one ListView. 60 * 61 * <p>Note that in the case of virtualized controls like ListView, when a cell 62 * has focus this is not in the same sense as application focus. When a ListCell 63 * has focus it simply represents the fact that the cell will receive keyboard 64 * events in the situation that the owning ListView actually contains focus. Of 65 * course, in the case where a cell has a Node set in the 66 * {@link #graphicProperty() graphic} property, it is completely legal for this 67 * Node to request, and acquire focus as would normally be expected. 68 * 69 * @param <T> The type of the item contained within the ListCell. 70 * @since JavaFX 2.0 71 */ 72 // TODO add code examples 73 public class ListCell<T> extends IndexedCell<T> { 74 75 /*************************************************************************** 76 * * 77 * Constructors * 78 * * 79 **************************************************************************/ 80 81 /** 82 * Creates a default ListCell with the default style class of 'list-cell'. 83 */ 84 public ListCell() { 85 getStyleClass().addAll(DEFAULT_STYLE_CLASS); 86 setAccessibleRole(AccessibleRole.LIST_ITEM); 87 } 88 89 90 /*************************************************************************** 91 * * 92 * Listeners * 93 * We have to listen to a number of properties on the ListView itself * 94 * as well as attach listeners to a couple different ObservableLists. * 95 * We have to be sure to unhook these listeners whenever the reference * 96 * to the ListView changes, or whenever one of the ObservableList * 97 * references changes (such as setting the selectionModel, focusModel, * 98 * or items). * 99 * * 100 **************************************************************************/ 101 102 /** 103 * Listens to the editing index on the ListView. It is possible for the developer 104 * to call the ListView#edit(int) method and cause a specific cell to start 105 * editing. In such a case, we need to be notified so we can call startEdit 106 * on our side. 107 */ 108 private final InvalidationListener editingListener = value -> { 109 updateEditing(); 110 }; 111 112 /** 113 * Listens to the selection model on the ListView. Whenever the selection model 114 * is changed (updated), the selected property on the ListCell is updated accordingly. 115 */ 116 private final ListChangeListener<Integer> selectedListener = c -> { 117 updateSelection(); 118 }; 119 120 /** 121 * Listens to the selectionModel property on the ListView. Whenever the entire model is changed, 122 * we have to unhook the weakSelectedListener and update the selection. 123 */ 124 private final ChangeListener<MultipleSelectionModel<T>> selectionModelPropertyListener = new ChangeListener<MultipleSelectionModel<T>>() { 125 @Override 126 public void changed( 127 ObservableValue<? extends MultipleSelectionModel<T>> observable, 128 MultipleSelectionModel<T> oldValue, 129 MultipleSelectionModel<T> newValue) { 130 131 if (oldValue != null) { 132 oldValue.getSelectedIndices().removeListener(weakSelectedListener); 133 } 134 135 if (newValue != null) { 136 newValue.getSelectedIndices().addListener(weakSelectedListener); 137 } 138 139 updateSelection(); 140 } 141 142 }; 143 144 /** 145 * Listens to the items on the ListView. Whenever the items are changed in such a way that 146 * it impacts the index of this ListCell, then we must update the item. 147 */ 148 private final ListChangeListener<T> itemsListener = c -> { 149 boolean doUpdate = false; 150 while (c.next()) { 151 // RT-35395: We only update the item in this cell if the current cell 152 // index is within the range of the change and certain changes to the 153 // list have occurred. 154 final int currentIndex = getIndex(); 155 final ListView<T> lv = getListView(); 156 final List<T> items = lv == null ? null : lv.getItems(); 157 final int itemCount = items == null ? 0 : items.size(); 158 159 final boolean indexAfterChangeFromIndex = currentIndex >= c.getFrom(); 160 final boolean indexBeforeChangeToIndex = currentIndex < c.getTo() || currentIndex == itemCount; 161 final boolean indexInRange = indexAfterChangeFromIndex && indexBeforeChangeToIndex; 162 163 doUpdate = indexInRange || (indexAfterChangeFromIndex && !c.wasReplaced() && (c.wasRemoved() || c.wasAdded())); 164 } 165 166 if (doUpdate) { 167 updateItem(-1); 168 } 169 }; 170 171 /** 172 * Listens to the items property on the ListView. Whenever the entire list is changed, 173 * we have to unhook the weakItemsListener and update the item. 174 */ 175 private final InvalidationListener itemsPropertyListener = new InvalidationListener() { 176 private WeakReference<ObservableList<T>> weakItemsRef = new WeakReference<>(null); 177 178 @Override public void invalidated(Observable observable) { 179 ObservableList<T> oldItems = weakItemsRef.get(); 180 if (oldItems != null) { 181 oldItems.removeListener(weakItemsListener); 182 } 183 184 ListView<T> listView = getListView(); 185 ObservableList<T> items = listView == null ? null : listView.getItems(); 186 weakItemsRef = new WeakReference<>(items); 187 188 if (items != null) { 189 items.addListener(weakItemsListener); 190 } 191 updateItem(-1); 192 } 193 }; 194 195 /** 196 * Listens to the focus model on the ListView. Whenever the focus model changes, 197 * the focused property on the ListCell is updated 198 */ 199 private final InvalidationListener focusedListener = value -> { 200 updateFocus(); 201 }; 202 203 /** 204 * Listens to the focusModel property on the ListView. Whenever the entire model is changed, 205 * we have to unhook the weakFocusedListener and update the focus. 206 */ 207 private final ChangeListener<FocusModel<T>> focusModelPropertyListener = new ChangeListener<FocusModel<T>>() { 208 @Override public void changed(ObservableValue<? extends FocusModel<T>> observable, 209 FocusModel<T> oldValue, 210 FocusModel<T> newValue) { 211 if (oldValue != null) { 212 oldValue.focusedIndexProperty().removeListener(weakFocusedListener); 213 } 214 if (newValue != null) { 215 newValue.focusedIndexProperty().addListener(weakFocusedListener); 216 } 217 updateFocus(); 218 } 219 }; 220 221 222 private final WeakInvalidationListener weakEditingListener = new WeakInvalidationListener(editingListener); 223 private final WeakListChangeListener<Integer> weakSelectedListener = new WeakListChangeListener<Integer>(selectedListener); 224 private final WeakChangeListener<MultipleSelectionModel<T>> weakSelectionModelPropertyListener = new WeakChangeListener<MultipleSelectionModel<T>>(selectionModelPropertyListener); 225 private final WeakListChangeListener<T> weakItemsListener = new WeakListChangeListener<T>(itemsListener); 226 private final WeakInvalidationListener weakItemsPropertyListener = new WeakInvalidationListener(itemsPropertyListener); 227 private final WeakInvalidationListener weakFocusedListener = new WeakInvalidationListener(focusedListener); 228 private final WeakChangeListener<FocusModel<T>> weakFocusModelPropertyListener = new WeakChangeListener<FocusModel<T>>(focusModelPropertyListener); 229 230 231 232 /*************************************************************************** 233 * * 234 * Properties * 235 * * 236 **************************************************************************/ 237 238 /** 239 * The ListView associated with this Cell. 240 */ 241 private ReadOnlyObjectWrapper<ListView<T>> listView = new ReadOnlyObjectWrapper<ListView<T>>(this, "listView") { 242 /** 243 * A weak reference to the ListView itself, such that whenever the ... 244 */ 245 private WeakReference<ListView<T>> weakListViewRef = new WeakReference<ListView<T>>(null); 246 247 @Override protected void invalidated() { 248 // Get the current and old list view references 249 final ListView<T> currentListView = get(); 250 final ListView<T> oldListView = weakListViewRef.get(); 251 252 // If the currentListView is the same as the oldListView, then 253 // there is nothing to be done. 254 if (currentListView == oldListView) return; 255 256 // If the old list view is not null, then we must unhook all its listeners 257 if (oldListView != null) { 258 // If the old selection model isn't null, unhook it 259 final MultipleSelectionModel<T> sm = oldListView.getSelectionModel(); 260 if (sm != null) { 261 sm.getSelectedIndices().removeListener(weakSelectedListener); 262 } 263 264 // If the old focus model isn't null, unhook it 265 final FocusModel<T> fm = oldListView.getFocusModel(); 266 if (fm != null) { 267 fm.focusedIndexProperty().removeListener(weakFocusedListener); 268 } 269 270 // If the old items isn't null, unhook the listener 271 final ObservableList<T> items = oldListView.getItems(); 272 if (items != null) { 273 items.removeListener(weakItemsListener); 274 } 275 276 // Remove the listeners of the properties on ListView 277 oldListView.editingIndexProperty().removeListener(weakEditingListener); 278 oldListView.itemsProperty().removeListener(weakItemsPropertyListener); 279 oldListView.focusModelProperty().removeListener(weakFocusModelPropertyListener); 280 oldListView.selectionModelProperty().removeListener(weakSelectionModelPropertyListener); 281 } 282 283 if (currentListView != null) { 284 final MultipleSelectionModel<T> sm = currentListView.getSelectionModel(); 285 if (sm != null) { 286 sm.getSelectedIndices().addListener(weakSelectedListener); 287 } 288 289 final FocusModel<T> fm = currentListView.getFocusModel(); 290 if (fm != null) { 291 fm.focusedIndexProperty().addListener(weakFocusedListener); 292 } 293 294 final ObservableList<T> items = currentListView.getItems(); 295 if (items != null) { 296 items.addListener(weakItemsListener); 297 } 298 299 currentListView.editingIndexProperty().addListener(weakEditingListener); 300 currentListView.itemsProperty().addListener(weakItemsPropertyListener); 301 currentListView.focusModelProperty().addListener(weakFocusModelPropertyListener); 302 currentListView.selectionModelProperty().addListener(weakSelectionModelPropertyListener); 303 304 weakListViewRef = new WeakReference<ListView<T>>(currentListView); 305 } 306 307 updateItem(-1); 308 updateSelection(); 309 updateFocus(); 310 requestLayout(); 311 } 312 }; 313 private void setListView(ListView<T> value) { listView.set(value); } 314 public final ListView<T> getListView() { return listView.get(); } 315 public final ReadOnlyObjectProperty<ListView<T>> listViewProperty() { return listView.getReadOnlyProperty(); } 316 317 318 319 /*************************************************************************** 320 * * 321 * Public API * 322 * * 323 **************************************************************************/ 324 325 /** {@inheritDoc} */ 326 @Override void indexChanged(int oldIndex, int newIndex) { 327 super.indexChanged(oldIndex, newIndex); 328 329 if (isEditing()) { 330 // no-op 331 // Fix for RT-31165 - if we (needlessly) update the index whilst the 332 // cell is being edited it will no longer be in an editing state. 333 // This means that in certain (common) circumstances that it will 334 // appear that a cell is uneditable as, despite being clicked, it 335 // will not change to the editing state as a layout of VirtualFlow 336 // is immediately invoked, which forces all cells to be updated. 337 } else { 338 updateItem(oldIndex); 339 } 340 341 updateSelection(); 342 updateFocus(); 343 } 344 345 /** {@inheritDoc} */ 346 @Override protected Skin<?> createDefaultSkin() { 347 return new ListCellSkin<T>(this); 348 } 349 350 351 /*************************************************************************** 352 * * 353 * Editing API * 354 * * 355 **************************************************************************/ 356 357 /** {@inheritDoc} */ 358 @Override public void startEdit() { 359 final ListView<T> list = getListView(); 360 if (!isEditable() || (list != null && ! list.isEditable())) { 361 return; 362 } 363 364 // it makes sense to get the cell into its editing state before firing 365 // the event to the ListView below, so that's what we're doing here 366 // by calling super.startEdit(). 367 super.startEdit(); 368 369 // Inform the ListView of the edit starting. 370 if (list != null) { 371 list.fireEvent(new ListView.EditEvent<T>(list, 372 ListView.<T>editStartEvent(), 373 null, 374 getIndex())); 375 list.edit(getIndex()); 376 list.requestFocus(); 377 } 378 } 379 380 /** {@inheritDoc} */ 381 @Override public void commitEdit(T newValue) { 382 if (! isEditing()) return; 383 ListView<T> list = getListView(); 384 385 // inform parent classes of the commit, so that they can switch us 386 // out of the editing state. 387 // This MUST come before the updateItem call below, otherwise it will 388 // call cancelEdit(), resulting in both commit and cancel events being 389 // fired (as identified in RT-29650) 390 super.commitEdit(newValue); 391 392 if (list != null) { 393 // Inform the ListView of the edit being ready to be committed. 394 list.fireEvent(new ListView.EditEvent<T>(list, 395 ListView.editCommitEvent(), 396 newValue, 397 getIndex())); 398 } 399 400 // update the item within this cell, so that it represents the new value 401 updateItem(newValue, false); 402 403 if (list != null) { 404 // reset the editing index on the ListView. This must come after the 405 // event is fired so that the developer on the other side can consult 406 // the ListView editingIndex property (if they choose to do that 407 // rather than just grab the int from the event). 408 list.edit(-1); 409 410 // request focus back onto the list, only if the current focus 411 // owner has the list as a parent (otherwise the user might have 412 // clicked out of the list entirely and given focus to something else. 413 // It would be rude of us to request it back again. 414 ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(list); 415 } 416 } 417 418 /** {@inheritDoc} */ 419 @Override public void cancelEdit() { 420 if (! isEditing()) return; 421 422 // Inform the ListView of the edit being cancelled. 423 ListView<T> list = getListView(); 424 425 super.cancelEdit(); 426 427 if (list != null) { 428 // reset the editing index on the ListView 429 list.edit(-1); 430 431 // request focus back onto the list, only if the current focus 432 // owner has the list as a parent (otherwise the user might have 433 // clicked out of the list entirely and given focus to something else. 434 // It would be rude of us to request it back again. 435 ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(list); 436 437 list.fireEvent(new ListView.EditEvent<T>(list, 438 ListView.<T>editCancelEvent(), 439 null, 440 getIndex())); 441 } 442 } 443 444 445 /* ************************************************************************* 446 * * 447 * Private implementation * 448 * * 449 **************************************************************************/ 450 451 private boolean firstRun = true; 452 private void updateItem(int oldIndex) { 453 final ListView<T> lv = getListView(); 454 final List<T> items = lv == null ? null : lv.getItems(); 455 final int index = getIndex(); 456 final int itemCount = items == null ? -1 : items.size(); 457 458 // Compute whether the index for this cell is for a real item 459 boolean valid = items != null && index >=0 && index < itemCount; 460 461 final T oldValue = getItem(); 462 final boolean isEmpty = isEmpty(); 463 464 // Cause the cell to update itself 465 outer: if (valid) { 466 final T newValue = items.get(index); 467 468 // RT-35864 - if the index didn't change, then avoid calling updateItem 469 // unless the item has changed. 470 if (oldIndex == index) { 471 if (!isItemChanged(oldValue, newValue)) { 472 // RT-37054: we break out of the if/else code here and 473 // proceed with the code following this, so that we may 474 // still update references, listeners, etc as required. 475 break outer; 476 } 477 } 478 updateItem(newValue, false); 479 } else { 480 // RT-30484 We need to allow a first run to be special-cased to allow 481 // for the updateItem method to be called at least once to allow for 482 // the correct visual state to be set up. In particular, in RT-30484 483 // refer to Ensemble8PopUpTree.png - in this case the arrows are being 484 // shown as the new cells are instantiated with the arrows in the 485 // children list, and are only hidden in updateItem. 486 if ((!isEmpty && oldValue != null) || firstRun) { 487 updateItem(null, true); 488 firstRun = false; 489 } 490 } 491 } 492 493 /** 494 * Updates the ListView associated with this Cell. 495 * 496 * Note: This function is intended to be used by experts, primarily 497 * by those implementing new Skins. It is not common 498 * for developers or designers to access this function directly. 499 * @param listView the ListView associated with this cell 500 */ 501 public final void updateListView(ListView<T> listView) { 502 setListView(listView); 503 } 504 505 private void updateSelection() { 506 if (isEmpty()) return; 507 int index = getIndex(); 508 ListView<T> listView = getListView(); 509 if (index == -1 || listView == null) return; 510 511 SelectionModel<T> sm = listView.getSelectionModel(); 512 if (sm == null) { 513 updateSelected(false); 514 return; 515 } 516 517 boolean isSelected = sm.isSelected(index); 518 if (isSelected() == isSelected) return; 519 520 updateSelected(isSelected); 521 } 522 523 private void updateFocus() { 524 int index = getIndex(); 525 ListView<T> listView = getListView(); 526 if (index == -1 || listView == null) return; 527 528 FocusModel<T> fm = listView.getFocusModel(); 529 if (fm == null) { 530 setFocused(false); 531 return; 532 } 533 534 setFocused(fm.isFocused(index)); 535 } 536 537 private void updateEditing() { 538 final int index = getIndex(); 539 final ListView<T> list = getListView(); 540 final int editIndex = list == null ? -1 : list.getEditingIndex(); 541 final boolean editing = isEditing(); 542 543 // Check that the list is specified, and my index is not -1 544 if (index != -1 && list != null) { 545 // If my index is the index being edited and I'm not currently in 546 // the edit mode, then I need to enter the edit mode 547 if (index == editIndex && !editing) { 548 startEdit(); 549 } else if (index != editIndex && editing) { 550 attemptEditCommit(); 551 } 552 } 553 } 554 555 556 557 /*************************************************************************** 558 * * 559 * Stylesheet Handling * 560 * * 561 **************************************************************************/ 562 563 private static final String DEFAULT_STYLE_CLASS = "list-cell"; 564 565 566 567 /*************************************************************************** 568 * * 569 * Accessibility handling * 570 * * 571 **************************************************************************/ 572 573 @Override 574 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 575 switch (attribute) { 576 case INDEX: return getIndex(); 577 case SELECTED: return isSelected(); 578 default: return super.queryAccessibleAttribute(attribute, parameters); 579 } 580 } 581 582 @Override 583 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 584 switch (action) { 585 case REQUEST_FOCUS: { 586 ListView<T> listView = getListView(); 587 if (listView != null) { 588 FocusModel<T> fm = listView.getFocusModel(); 589 if (fm != null) { 590 fm.focus(getIndex()); 591 } 592 } 593 break; 594 } 595 default: super.executeAccessibleAction(action, parameters); 596 } 597 } 598 } 599