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