1 /*
   2  * Copyright (c) 2010, 2016, 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