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 private boolean updateEditingIndex = true; 112 113 /** 114 * Listens to the selection model on the ListView. Whenever the selection model 115 * is changed (updated), the selected property on the ListCell is updated accordingly. 116 */ 117 private final ListChangeListener<Integer> selectedListener = c -> { 118 updateSelection(); 119 }; 120 121 /** 122 * Listens to the selectionModel property on the ListView. Whenever the entire model is changed, 123 * we have to unhook the weakSelectedListener and update the selection. 124 */ 125 private final ChangeListener<MultipleSelectionModel<T>> selectionModelPropertyListener = new ChangeListener<MultipleSelectionModel<T>>() { 126 @Override 127 public void changed( 128 ObservableValue<? extends MultipleSelectionModel<T>> observable, 129 MultipleSelectionModel<T> oldValue, 130 MultipleSelectionModel<T> newValue) { 131 132 if (oldValue != null) { 133 oldValue.getSelectedIndices().removeListener(weakSelectedListener); 134 } 135 136 if (newValue != null) { 137 newValue.getSelectedIndices().addListener(weakSelectedListener); 138 } 139 140 updateSelection(); 141 } 142 143 }; 144 145 /** 146 * Listens to the items on the ListView. Whenever the items are changed in such a way that 147 * it impacts the index of this ListCell, then we must update the item. 148 */ 149 private final ListChangeListener<T> itemsListener = c -> { 150 boolean doUpdate = false; 151 while (c.next()) { 152 // RT-35395: We only update the item in this cell if the current cell 153 // index is within the range of the change and certain changes to the 154 // list have occurred. 155 final int currentIndex = getIndex(); 156 final ListView<T> lv = getListView(); 157 final List<T> items = lv == null ? null : lv.getItems(); 158 final int itemCount = items == null ? 0 : items.size(); 159 160 final boolean indexAfterChangeFromIndex = currentIndex >= c.getFrom(); 161 final boolean indexBeforeChangeToIndex = currentIndex < c.getTo() || currentIndex == itemCount; 162 final boolean indexInRange = indexAfterChangeFromIndex && indexBeforeChangeToIndex; 163 164 doUpdate = indexInRange || (indexAfterChangeFromIndex && !c.wasReplaced() && (c.wasRemoved() || c.wasAdded())); 165 } 166 167 if (doUpdate) { 168 updateItem(-1); 169 } 170 }; 171 172 /** 173 * Listens to the items property on the ListView. Whenever the entire list is changed, 174 * we have to unhook the weakItemsListener and update the item. 175 */ 176 private final InvalidationListener itemsPropertyListener = new InvalidationListener() { 177 private WeakReference<ObservableList<T>> weakItemsRef = new WeakReference<>(null); 178 179 @Override public void invalidated(Observable observable) { 180 ObservableList<T> oldItems = weakItemsRef.get(); 181 if (oldItems != null) { 182 oldItems.removeListener(weakItemsListener); 183 } 184 185 ListView<T> listView = getListView(); 186 ObservableList<T> items = listView == null ? null : listView.getItems(); 187 weakItemsRef = new WeakReference<>(items); 188 189 if (items != null) { 190 items.addListener(weakItemsListener); 191 } 192 updateItem(-1); 193 } 194 }; 195 196 /** 197 * Listens to the focus model on the ListView. Whenever the focus model changes, 198 * the focused property on the ListCell is updated 199 */ 200 private final InvalidationListener focusedListener = value -> { 201 updateFocus(); 202 }; 203 204 /** 205 * Listens to the focusModel property on the ListView. Whenever the entire model is changed, 206 * we have to unhook the weakFocusedListener and update the focus. 207 */ 208 private final ChangeListener<FocusModel<T>> focusModelPropertyListener = new ChangeListener<FocusModel<T>>() { 209 @Override public void changed(ObservableValue<? extends FocusModel<T>> observable, 210 FocusModel<T> oldValue, 211 FocusModel<T> newValue) { 212 if (oldValue != null) { 213 oldValue.focusedIndexProperty().removeListener(weakFocusedListener); 214 } 215 if (newValue != null) { 216 newValue.focusedIndexProperty().addListener(weakFocusedListener); 217 } 218 updateFocus(); 219 } 220 }; 221 222 223 private final WeakInvalidationListener weakEditingListener = new WeakInvalidationListener(editingListener); 224 private final WeakListChangeListener<Integer> weakSelectedListener = new WeakListChangeListener<Integer>(selectedListener); 225 private final WeakChangeListener<MultipleSelectionModel<T>> weakSelectionModelPropertyListener = new WeakChangeListener<MultipleSelectionModel<T>>(selectionModelPropertyListener); 226 private final WeakListChangeListener<T> weakItemsListener = new WeakListChangeListener<T>(itemsListener); 227 private final WeakInvalidationListener weakItemsPropertyListener = new WeakInvalidationListener(itemsPropertyListener); 228 private final WeakInvalidationListener weakFocusedListener = new WeakInvalidationListener(focusedListener); 229 private final WeakChangeListener<FocusModel<T>> weakFocusModelPropertyListener = new WeakChangeListener<FocusModel<T>>(focusModelPropertyListener); 230 231 /*************************************************************************** 232 * * 233 * Properties * 234 * * 235 **************************************************************************/ 236 237 /** 238 * The ListView associated with this Cell. 239 */ 240 private ReadOnlyObjectWrapper<ListView<T>> listView = new ReadOnlyObjectWrapper<ListView<T>>(this, "listView") { 241 /** 242 * A weak reference to the ListView itself, such that whenever the ... 243 */ 244 private WeakReference<ListView<T>> weakListViewRef = new WeakReference<ListView<T>>(null); 245 246 @Override protected void invalidated() { 247 // Get the current and old list view references 248 final ListView<T> currentListView = get(); 249 final ListView<T> oldListView = weakListViewRef.get(); 250 251 // If the currentListView is the same as the oldListView, then 252 // there is nothing to be done. 253 if (currentListView == oldListView) return; 254 255 // If the old list view is not null, then we must unhook all its listeners 256 if (oldListView != null) { 257 // If the old selection model isn't null, unhook it 258 final MultipleSelectionModel<T> sm = oldListView.getSelectionModel(); 259 if (sm != null) { 260 sm.getSelectedIndices().removeListener(weakSelectedListener); 261 } 262 263 // If the old focus model isn't null, unhook it 264 final FocusModel<T> fm = oldListView.getFocusModel(); 265 if (fm != null) { 266 fm.focusedIndexProperty().removeListener(weakFocusedListener); 267 } 268 269 // If the old items isn't null, unhook the listener 270 final ObservableList<T> items = oldListView.getItems(); 271 if (items != null) { 272 items.removeListener(weakItemsListener); 273 } 274 275 // Remove the listeners of the properties on ListView 276 oldListView.editingIndexProperty().removeListener(weakEditingListener); 277 oldListView.itemsProperty().removeListener(weakItemsPropertyListener); 278 oldListView.focusModelProperty().removeListener(weakFocusModelPropertyListener); 279 oldListView.selectionModelProperty().removeListener(weakSelectionModelPropertyListener); 280 } 281 282 if (currentListView != null) { 283 final MultipleSelectionModel<T> sm = currentListView.getSelectionModel(); 284 if (sm != null) { 285 sm.getSelectedIndices().addListener(weakSelectedListener); 286 } 287 288 final FocusModel<T> fm = currentListView.getFocusModel(); 289 if (fm != null) { 290 fm.focusedIndexProperty().addListener(weakFocusedListener); 291 } 292 293 final ObservableList<T> items = currentListView.getItems(); 294 if (items != null) { 295 items.addListener(weakItemsListener); 296 } 297 298 currentListView.editingIndexProperty().addListener(weakEditingListener); 299 currentListView.itemsProperty().addListener(weakItemsPropertyListener); 300 currentListView.focusModelProperty().addListener(weakFocusModelPropertyListener); 301 currentListView.selectionModelProperty().addListener(weakSelectionModelPropertyListener); 302 303 weakListViewRef = new WeakReference<ListView<T>>(currentListView); 304 } 305 306 updateItem(-1); 307 updateSelection(); 308 updateFocus(); 309 requestLayout(); 310 } 311 }; 312 private void setListView(ListView<T> value) { listView.set(value); } 313 public final ListView<T> getListView() { return listView.get(); } 314 public final ReadOnlyObjectProperty<ListView<T>> listViewProperty() { return listView.getReadOnlyProperty(); } 315 316 317 318 /*************************************************************************** 319 * * 320 * Public API * 321 * * 322 **************************************************************************/ 323 324 /** {@inheritDoc} */ 325 @Override void indexChanged(int oldIndex, int newIndex) { 326 super.indexChanged(oldIndex, newIndex); 327 328 if (isEditing() && newIndex == oldIndex) { 329 // no-op 330 // Fix for RT-31165 - if we (needlessly) update the index whilst the 331 // cell is being edited it will no longer be in an editing state. 332 // This means that in certain (common) circumstances that it will 333 // appear that a cell is uneditable as, despite being clicked, it 334 // will not change to the editing state as a layout of VirtualFlow 335 // is immediately invoked, which forces all cells to be updated. 336 } else { 337 updateItem(oldIndex); 338 updateSelection(); 339 updateFocus(); 340 } 341 } 342 343 /** {@inheritDoc} */ 344 @Override protected Skin<?> createDefaultSkin() { 345 return new ListCellSkin<T>(this); 346 } 347 348 349 /*************************************************************************** 350 * * 351 * Editing API * 352 * * 353 **************************************************************************/ 354 355 /** {@inheritDoc} */ 356 @Override public void startEdit() { 357 final ListView<T> list = getListView(); 358 if (!isEditable() || (list != null && ! list.isEditable())) { 359 return; 360 } 361 362 // it makes sense to get the cell into its editing state before firing 363 // the event to the ListView below, so that's what we're doing here 364 // by calling super.startEdit(). 365 super.startEdit(); 366 367 // Inform the ListView of the edit starting. 368 if (list != null) { 369 list.fireEvent(new ListView.EditEvent<T>(list, 370 ListView.<T>editStartEvent(), 371 null, 372 list.getEditingIndex())); 373 list.edit(getIndex()); 374 list.requestFocus(); 375 } 376 } 377 378 /** {@inheritDoc} */ 379 @Override public void commitEdit(T newValue) { 380 if (! isEditing()) return; 381 ListView<T> list = getListView(); 382 383 if (list != null) { 384 // Inform the ListView of the edit being ready to be committed. 385 list.fireEvent(new ListView.EditEvent<T>(list, 386 ListView.<T>editCommitEvent(), 387 newValue, 388 list.getEditingIndex())); 389 } 390 391 // inform parent classes of the commit, so that they can switch us 392 // out of the editing state. 393 // This MUST come before the updateItem call below, otherwise it will 394 // call cancelEdit(), resulting in both commit and cancel events being 395 // fired (as identified in RT-29650) 396 super.commitEdit(newValue); 397 398 // update the item within this cell, so that it represents the new value 399 updateItem(newValue, false); 400 401 if (list != null) { 402 // reset the editing index on the ListView. This must come after the 403 // event is fired so that the developer on the other side can consult 404 // the ListView editingIndex property (if they choose to do that 405 // rather than just grab the int from the event). 406 list.edit(-1); 407 408 // request focus back onto the list, only if the current focus 409 // owner has the list as a parent (otherwise the user might have 410 // clicked out of the list entirely and given focus to something else. 411 // It would be rude of us to request it back again. 412 ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(list); 413 } 414 } 415 416 /** {@inheritDoc} */ 417 @Override public void cancelEdit() { 418 if (! isEditing()) return; 419 420 // Inform the ListView of the edit being cancelled. 421 ListView<T> list = getListView(); 422 423 super.cancelEdit(); 424 425 if (list != null) { 426 int editingIndex = list.getEditingIndex(); 427 428 // reset the editing index on the ListView 429 if (updateEditingIndex) 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 editingIndex)); 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 // If my index is not the one being edited then I need to cancel 551 // the edit. The tricky thing here is that as part of this call 552 // I cannot end up calling list.edit(-1) the way that the standard 553 // cancelEdit method would do. Yet, I need to call cancelEdit 554 // so that subclasses which override cancelEdit can execute. So, 555 // I have to use a kind of hacky flag workaround. 556 updateEditingIndex = false; 557 cancelEdit(); 558 updateEditingIndex = true; 559 } 560 } 561 } 562 563 564 565 /*************************************************************************** 566 * * 567 * Stylesheet Handling * 568 * * 569 **************************************************************************/ 570 571 private static final String DEFAULT_STYLE_CLASS = "list-cell"; 572 573 574 575 /*************************************************************************** 576 * * 577 * Accessibility handling * 578 * * 579 **************************************************************************/ 580 581 @Override 582 public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 583 switch (attribute) { 584 case INDEX: return getIndex(); 585 case SELECTED: return isSelected(); 586 default: return super.queryAccessibleAttribute(attribute, parameters); 587 } 588 } 589 590 @Override 591 public void executeAccessibleAction(AccessibleAction action, Object... parameters) { 592 switch (action) { 593 case REQUEST_FOCUS: { 594 ListView<T> listView = getListView(); 595 if (listView != null) { 596 FocusModel<T> fm = listView.getFocusModel(); 597 if (fm != null) { 598 fm.focus(getIndex()); 599 } 600 } 601 break; 602 } 603 default: super.executeAccessibleAction(action, parameters); 604 } 605 } 606 } 607