1 /*
   2  * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import java.util.ArrayList;
  29 import java.util.List;
  30 
  31 import com.sun.javafx.scene.control.Properties;
  32 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  33 import javafx.beans.InvalidationListener;
  34 import javafx.beans.WeakInvalidationListener;
  35 import javafx.collections.FXCollections;
  36 import javafx.collections.ListChangeListener;
  37 import javafx.collections.MapChangeListener;
  38 import javafx.collections.ObservableList;
  39 import javafx.collections.ObservableMap;
  40 import javafx.collections.WeakListChangeListener;
  41 import javafx.event.EventHandler;
  42 import javafx.geometry.Orientation;
  43 import javafx.scene.AccessibleAction;
  44 import javafx.scene.AccessibleAttribute;
  45 import javafx.scene.Node;
  46 import javafx.scene.control.Button;
  47 import javafx.scene.control.Control;
  48 import javafx.scene.control.FocusModel;
  49 import javafx.scene.control.IndexedCell;
  50 import javafx.scene.control.Label;
  51 import javafx.scene.control.ListCell;
  52 import javafx.scene.control.ListView;
  53 import javafx.scene.control.MultipleSelectionModel;
  54 import javafx.scene.control.SelectionModel;
  55 import com.sun.javafx.scene.control.behavior.ListViewBehavior;
  56 import javafx.scene.input.MouseEvent;
  57 import javafx.scene.layout.StackPane;
  58 
  59 import java.security.AccessController;
  60 import java.security.PrivilegedAction;
  61 
  62 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  63 
  64 /**
  65  * Default skin implementation for the {@link ListView} control.
  66  *
  67  * @see ListView
  68  * @since 9
  69  */
  70 public class ListViewSkin<T> extends VirtualContainerBase<ListView<T>, ListCell<T>> {
  71 
  72     /***************************************************************************
  73      *                                                                         *
  74      * Static Fields                                                           *
  75      *                                                                         *
  76      **************************************************************************/
  77 
  78     private static final String EMPTY_LIST_TEXT = ControlResources.getString("ListView.noContent");
  79 
  80     // RT-34744 : IS_PANNABLE will be false unless
  81     // javafx.scene.control.skin.ListViewSkin.pannable
  82     // is set to true. This is done in order to make ListView functional
  83     // on embedded systems with touch screens which do not generate scroll
  84     // events for touch drag gestures.
  85     private static final boolean IS_PANNABLE =
  86             AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.ListViewSkin.pannable"));
  87 
  88 
  89 
  90     /***************************************************************************
  91      *                                                                         *
  92      * Internal Fields                                                         *
  93      *                                                                         *
  94      **************************************************************************/
  95 
  96     private final VirtualFlow<ListCell<T>> flow;
  97 
  98     /**
  99      * Region placed over the top of the flow (and possibly the header row) if
 100      * there is no data.
 101      */
 102     // FIXME this should not be a StackPane
 103     private StackPane placeholderRegion;
 104     private Node placeholderNode;
 105 
 106     private ObservableList<T> listViewItems;
 107     private final InvalidationListener itemsChangeListener = observable -> updateListViewItems();
 108 
 109     private boolean needCellsRebuilt = true;
 110     private boolean needCellsReconfigured = false;
 111 
 112     private int itemCount = -1;
 113     private ListViewBehavior<T> behavior;
 114 
 115 
 116 
 117     /***************************************************************************
 118      *                                                                         *
 119      * Listeners                                                               *
 120      *                                                                         *
 121      **************************************************************************/
 122 
 123     private MapChangeListener<Object, Object> propertiesMapListener = c -> {
 124         if (! c.wasAdded()) return;
 125         if (Properties.RECREATE.equals(c.getKey())) {
 126             needCellsRebuilt = true;
 127             getSkinnable().requestLayout();
 128             getSkinnable().getProperties().remove(Properties.RECREATE);
 129         }
 130     };
 131 
 132     private final ListChangeListener<T> listViewItemsListener = new ListChangeListener<T>() {
 133         @Override public void onChanged(Change<? extends T> c) {
 134             while (c.next()) {
 135                 if (c.wasReplaced()) {
 136                     // RT-28397: Support for when an item is replaced with itself (but
 137                     // updated internal values that should be shown visually).
 138                     // This code was updated for RT-36714 to not update all cells,
 139                     // just those affected by the change
 140                     for (int i = c.getFrom(); i < c.getTo(); i++) {
 141                         flow.setCellDirty(i);
 142                     }
 143 
 144                     break;
 145                 } else if (c.getRemovedSize() == itemCount) {
 146                     // RT-22463: If the user clears out an items list then we
 147                     // should reset all cells (in particular their contained
 148                     // items) such that a subsequent addition to the list of
 149                     // an item which equals the old item (but is rendered
 150                     // differently) still displays as expected (i.e. with the
 151                     // updated display, not the old display).
 152                     itemCount = 0;
 153                     break;
 154                 }
 155             }
 156 
 157             // fix for RT-37853
 158             getSkinnable().edit(-1);
 159 
 160             rowCountDirty = true;
 161             getSkinnable().requestLayout();
 162         }
 163     };
 164 
 165     private final WeakListChangeListener<T> weakListViewItemsListener =
 166             new WeakListChangeListener<T>(listViewItemsListener);
 167 
 168 
 169 
 170     /***************************************************************************
 171      *                                                                         *
 172      * Constructors                                                            *
 173      *                                                                         *
 174      **************************************************************************/
 175 
 176     /**
 177      * Creates a new ListViewSkin instance, installing the necessary child
 178      * nodes into the Control {@link Control#getChildren() children} list, as
 179      * well as the necessary input mappings for handling key, mouse, etc events.
 180      *
 181      * @param control The control that this skin should be installed onto.
 182      */
 183     public ListViewSkin(final ListView<T> control) {
 184         super(control);
 185 
 186         // install default input map for the ListView control
 187         behavior = new ListViewBehavior<>(control);
 188 //        control.setInputMap(behavior.getInputMap());
 189 
 190         // init the behavior 'closures'
 191         behavior.setOnFocusPreviousRow(() -> onFocusPreviousCell());
 192         behavior.setOnFocusNextRow(() -> onFocusNextCell());
 193         behavior.setOnMoveToFirstCell(() -> onMoveToFirstCell());
 194         behavior.setOnMoveToLastCell(() -> onMoveToLastCell());
 195         behavior.setOnSelectPreviousRow(() -> onSelectPreviousCell());
 196         behavior.setOnSelectNextRow(() -> onSelectNextCell());
 197         behavior.setOnScrollPageDown(this::onScrollPageDown);
 198         behavior.setOnScrollPageUp(this::onScrollPageUp);
 199 
 200         updateListViewItems();
 201 
 202         // init the VirtualFlow
 203         flow = getVirtualFlow();
 204         flow.setId("virtual-flow");
 205         flow.setPannable(IS_PANNABLE);
 206         flow.setVertical(control.getOrientation() == Orientation.VERTICAL);
 207         flow.setCellFactory(flow -> createCell());
 208         flow.setFixedCellSize(control.getFixedCellSize());
 209         getChildren().add(flow);
 210         
 211         EventHandler<MouseEvent> ml = event -> {
 212             // RT-15127: cancel editing on scroll. This is a bit extreme
 213             // (we are cancelling editing on touching the scrollbars).
 214             // This can be improved at a later date.
 215             if (control.getEditingIndex() > -1) {
 216                 control.edit(-1);
 217             }
 218 
 219             // This ensures that the list maintains the focus, even when the vbar
 220             // and hbar controls inside the flow are clicked. Without this, the
 221             // focus border will not be shown when the user interacts with the
 222             // scrollbars, and more importantly, keyboard navigation won't be
 223             // available to the user.
 224             if (control.isFocusTraversable()) {
 225                 control.requestFocus();
 226             }
 227         };
 228         flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
 229         flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
 230         
 231         updateRowCount();
 232 
 233         control.itemsProperty().addListener(new WeakInvalidationListener(itemsChangeListener));
 234 
 235         final ObservableMap<Object, Object> properties = control.getProperties();
 236         properties.remove(Properties.RECREATE);
 237         properties.addListener(propertiesMapListener);
 238 
 239         // Register listeners
 240         registerChangeListener(control.itemsProperty(), o -> updateListViewItems());
 241         registerChangeListener(control.orientationProperty(), o ->
 242             flow.setVertical(control.getOrientation() == Orientation.VERTICAL)
 243         );
 244         registerChangeListener(control.cellFactoryProperty(), o -> flow.recreateCells());
 245         registerChangeListener(control.parentProperty(), o -> {
 246             if (control.getParent() != null && control.isVisible()) {
 247                 control.requestLayout();
 248             }
 249         });
 250         registerChangeListener(control.placeholderProperty(), o -> updatePlaceholderRegionVisibility());
 251         registerChangeListener(control.fixedCellSizeProperty(), o ->
 252             flow.setFixedCellSize(control.getFixedCellSize())
 253         );
 254     }
 255 
 256 
 257 
 258     /***************************************************************************
 259      *                                                                         *
 260      * Public API                                                              *
 261      *                                                                         *
 262      **************************************************************************/
 263 
 264     /** {@inheritDoc} */
 265     @Override public void dispose() {
 266         super.dispose();
 267 
 268         if (behavior != null) {
 269             behavior.dispose();
 270         }
 271     }
 272 
 273     /** {@inheritDoc} */
 274     @Override protected void layoutChildren(final double x, final double y,
 275                                             final double w, final double h) {
 276         super.layoutChildren(x, y, w, h);
 277 
 278         if (needCellsRebuilt) {
 279             flow.rebuildCells();
 280         } else if (needCellsReconfigured) {
 281             flow.reconfigureCells();
 282         }
 283 
 284         needCellsRebuilt = false;
 285         needCellsReconfigured = false;
 286 
 287         if (getItemCount() == 0) {
 288             // show message overlay instead of empty listview
 289             if (placeholderRegion != null) {
 290                 placeholderRegion.setVisible(w > 0 && h > 0);
 291                 placeholderRegion.resizeRelocate(x, y, w, h);
 292             }
 293         } else {
 294             flow.resizeRelocate(x, y, w, h);
 295         }
 296     }
 297 
 298     /** {@inheritDoc} */
 299     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 300         checkState();
 301 
 302         if (getItemCount() == 0) {
 303             if (placeholderRegion == null) {
 304                 updatePlaceholderRegionVisibility();
 305             }
 306             if (placeholderRegion != null) {
 307                 return placeholderRegion.prefWidth(height) + leftInset + rightInset;
 308             }
 309         }
 310 
 311         return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987;
 312     }
 313 
 314     /** {@inheritDoc} */
 315     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 316         return 400;
 317     }
 318 
 319     /** {@inheritDoc} */
 320     @Override int getItemCount() {
 321         return itemCount;
 322     }
 323 
 324     /** {@inheritDoc} */
 325     @Override void updateRowCount() {
 326         if (flow == null) return;
 327 
 328         int oldCount = itemCount;
 329         int newCount = listViewItems == null ? 0 : listViewItems.size();
 330 
 331         itemCount = newCount;
 332 
 333         flow.setCellCount(newCount);
 334 
 335         updatePlaceholderRegionVisibility();
 336         if (newCount != oldCount) {
 337             needCellsRebuilt = true;
 338         } else {
 339             needCellsReconfigured = true;
 340         }
 341     }
 342 
 343     /** {@inheritDoc} */
 344     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 345         switch (attribute) {
 346             case FOCUS_ITEM: {
 347                 FocusModel<?> fm = getSkinnable().getFocusModel();
 348                 int focusedIndex = fm.getFocusedIndex();
 349                 if (focusedIndex == -1) {
 350                     if (placeholderRegion != null && placeholderRegion.isVisible()) {
 351                         return placeholderRegion.getChildren().get(0);
 352                     }
 353                     if (getItemCount() > 0) {
 354                         focusedIndex = 0;
 355                     } else {
 356                         return null;
 357                     }
 358                 }
 359                 return flow.getPrivateCell(focusedIndex);
 360             }
 361             case ITEM_COUNT: return getItemCount();
 362             case ITEM_AT_INDEX: {
 363                 Integer rowIndex = (Integer)parameters[0];
 364                 if (rowIndex == null) return null;
 365                 if (0 <= rowIndex && rowIndex < getItemCount()) {
 366                     return flow.getPrivateCell(rowIndex);
 367                 }
 368                 return null;
 369             }
 370             case SELECTED_ITEMS: {
 371                 MultipleSelectionModel<T> sm = getSkinnable().getSelectionModel();
 372                 ObservableList<Integer> indices = sm.getSelectedIndices();
 373                 List<Node> selection = new ArrayList<>(indices.size());
 374                 for (int i : indices) {
 375                     ListCell<T> row = flow.getPrivateCell(i);
 376                     if (row != null) selection.add(row);
 377                 }
 378                 return FXCollections.observableArrayList(selection);
 379             }
 380             case VERTICAL_SCROLLBAR: return flow.getVbar();
 381             case HORIZONTAL_SCROLLBAR: return flow.getHbar();
 382             default: return super.queryAccessibleAttribute(attribute, parameters);
 383         }
 384     }
 385 
 386     /** {@inheritDoc} */
 387     @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
 388         switch (action) {
 389             case SHOW_ITEM: {
 390                 Node item = (Node)parameters[0];
 391                 if (item instanceof ListCell) {
 392                     @SuppressWarnings("unchecked")
 393                     ListCell<T> cell = (ListCell<T>)item;
 394                     flow.scrollTo(cell.getIndex());
 395                 }
 396                 break;
 397             }
 398             case SET_SELECTED_ITEMS: {
 399                 @SuppressWarnings("unchecked")
 400                 ObservableList<Node> items = (ObservableList<Node>)parameters[0];
 401                 if (items != null) {
 402                     MultipleSelectionModel<T> sm = getSkinnable().getSelectionModel();
 403                     if (sm != null) {
 404                         sm.clearSelection();
 405                         for (Node item : items) {
 406                             if (item instanceof ListCell) {
 407                                 @SuppressWarnings("unchecked")
 408                                 ListCell<T> cell = (ListCell<T>)item;
 409                                 sm.select(cell.getIndex());
 410                             }
 411                         }
 412                     }
 413                 }
 414                 break;
 415             }
 416             default: super.executeAccessibleAction(action, parameters);
 417         }
 418     }
 419 
 420 
 421 
 422     /***************************************************************************
 423      *                                                                         *
 424      * Private implementation                                                  *
 425      *                                                                         *
 426      **************************************************************************/
 427 
 428     /** {@inheritDoc} */
 429     private ListCell<T> createCell() {
 430         ListCell<T> cell;
 431         if (getSkinnable().getCellFactory() != null) {
 432             cell = getSkinnable().getCellFactory().call(getSkinnable());
 433         } else {
 434             cell = createDefaultCellImpl();
 435         }
 436 
 437         cell.updateListView(getSkinnable());
 438 
 439         return cell;
 440     }
 441 
 442     private void updateListViewItems() {
 443         if (listViewItems != null) {
 444             listViewItems.removeListener(weakListViewItemsListener);
 445         }
 446 
 447         this.listViewItems = getSkinnable().getItems();
 448 
 449         if (listViewItems != null) {
 450             listViewItems.addListener(weakListViewItemsListener);
 451         }
 452 
 453         rowCountDirty = true;
 454         getSkinnable().requestLayout();
 455     }
 456     
 457     private final void updatePlaceholderRegionVisibility() {
 458         boolean visible = getItemCount() == 0;
 459         
 460         if (visible) {
 461             placeholderNode = getSkinnable().getPlaceholder();
 462             if (placeholderNode == null && (EMPTY_LIST_TEXT != null && ! EMPTY_LIST_TEXT.isEmpty())) {
 463                 placeholderNode = new Label();
 464                 ((Label)placeholderNode).setText(EMPTY_LIST_TEXT);
 465             }
 466 
 467             if (placeholderNode != null) {
 468                 if (placeholderRegion == null) {
 469                     placeholderRegion = new StackPane();
 470                     placeholderRegion.getStyleClass().setAll("placeholder");
 471                     getChildren().add(placeholderRegion);
 472                 }
 473 
 474                 placeholderRegion.getChildren().setAll(placeholderNode);
 475             }
 476         }
 477 
 478         flow.setVisible(!visible);
 479         if (placeholderRegion != null) {
 480             placeholderRegion.setVisible(visible);
 481         }
 482     }
 483 
 484     private static <T> ListCell<T> createDefaultCellImpl() {
 485         return new ListCell<T>() {
 486             @Override public void updateItem(T item, boolean empty) {
 487                 super.updateItem(item, empty);
 488                 
 489                 if (empty) {
 490                     setText(null);
 491                     setGraphic(null);
 492                 } else if (item instanceof Node) {
 493                     setText(null);
 494                     Node currentNode = getGraphic();
 495                     Node newNode = (Node) item;
 496                     if (currentNode == null || ! currentNode.equals(newNode)) {
 497                         setGraphic(newNode);
 498                     }
 499                 } else {
 500                     /**
 501                      * This label is used if the item associated with this cell is to be
 502                      * represented as a String. While we will lazily instantiate it
 503                      * we never clear it, being more afraid of object churn than a minor
 504                      * "leak" (which will not become a "major" leak).
 505                      */
 506                     setText(item == null ? "null" : item.toString());
 507                     setGraphic(null);
 508                 }
 509             }
 510         };
 511     }
 512 
 513     private void onFocusPreviousCell() {
 514         FocusModel<T> fm = getSkinnable().getFocusModel();
 515         if (fm == null) return;
 516         flow.scrollTo(fm.getFocusedIndex());
 517     }
 518 
 519     private void onFocusNextCell() {
 520         FocusModel<T> fm = getSkinnable().getFocusModel();
 521         if (fm == null) return;
 522         flow.scrollTo(fm.getFocusedIndex());
 523     }
 524 
 525     private void onSelectPreviousCell() {
 526         SelectionModel<T> sm = getSkinnable().getSelectionModel();
 527         if (sm == null) return;
 528 
 529         int pos = sm.getSelectedIndex();
 530         flow.scrollTo(pos);
 531 
 532         // Fix for RT-11299
 533         IndexedCell<T> cell = flow.getFirstVisibleCell();
 534         if (cell == null || pos < cell.getIndex()) {
 535             flow.setPosition(pos / (double) getItemCount());
 536         }
 537     }
 538 
 539     private void onSelectNextCell() {
 540         SelectionModel<T> sm = getSkinnable().getSelectionModel();
 541         if (sm == null) return;
 542 
 543         int pos = sm.getSelectedIndex();
 544         flow.scrollTo(pos);
 545 
 546         // Fix for RT-11299
 547         ListCell<T> cell = flow.getLastVisibleCell();
 548         if (cell == null || cell.getIndex() < pos) {
 549             flow.setPosition(pos / (double) getItemCount());
 550         }
 551     }
 552 
 553     private void onMoveToFirstCell() {
 554         flow.scrollTo(0);
 555         flow.setPosition(0);
 556     }
 557 
 558     private void onMoveToLastCell() {
 559 //        SelectionModel sm = getSkinnable().getSelectionModel();
 560 //        if (sm == null) return;
 561 //
 562         int endPos = getItemCount() - 1;
 563 //        sm.select(endPos);
 564         flow.scrollTo(endPos);
 565         flow.setPosition(1);
 566     }
 567 
 568     /**
 569      * Function used to scroll the container down by one 'page', although
 570      * if this is a horizontal container, then the scrolling will be to the right.
 571      */
 572     private int onScrollPageDown(boolean isFocusDriven) {
 573         ListCell<T> lastVisibleCell = flow.getLastVisibleCellWithinViewPort();
 574         if (lastVisibleCell == null) return -1;
 575 
 576         final SelectionModel<T> sm = getSkinnable().getSelectionModel();
 577         final FocusModel<T> fm = getSkinnable().getFocusModel();
 578         if (sm == null || fm == null) return -1;
 579 
 580         int lastVisibleCellIndex = lastVisibleCell.getIndex();
 581 
 582 //        boolean isSelected = sm.isSelected(lastVisibleCellIndex) || fm.isFocused(lastVisibleCellIndex) || lastVisibleCellIndex == anchor;
 583         // isSelected represents focus OR selection
 584         boolean isSelected = false;
 585         if (isFocusDriven) {
 586             isSelected = lastVisibleCell.isFocused() || fm.isFocused(lastVisibleCellIndex);
 587         } else {
 588             isSelected = lastVisibleCell.isSelected() || sm.isSelected(lastVisibleCellIndex);
 589         }
 590 
 591         if (isSelected) {
 592             boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == lastVisibleCellIndex)
 593                                || (! isFocusDriven && sm.getSelectedIndex() == lastVisibleCellIndex);
 594 
 595             if (isLeadIndex) {
 596                 // if the last visible cell is selected, we want to shift that cell up
 597                 // to be the top-most cell, or at least as far to the top as we can go.
 598                 flow.scrollToTop(lastVisibleCell);
 599 
 600                 ListCell<T> newLastVisibleCell = flow.getLastVisibleCellWithinViewPort();
 601                 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell;
 602             }
 603         } else {
 604             // if the selection is not on the 'bottom' most cell, we firstly move
 605             // the selection down to that, without scrolling the contents, so
 606             // this is a no-op
 607         }
 608 
 609         int newSelectionIndex = lastVisibleCell.getIndex();
 610         flow.scrollTo(lastVisibleCell);
 611         return newSelectionIndex;
 612     }
 613 
 614     /**
 615      * Function used to scroll the container up by one 'page', although
 616      * if this is a horizontal container, then the scrolling will be to the left.
 617      */
 618     private int onScrollPageUp(boolean isFocusDriven) {
 619         ListCell<T> firstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
 620         if (firstVisibleCell == null) return -1;
 621 
 622         final SelectionModel<T> sm = getSkinnable().getSelectionModel();
 623         final FocusModel<T> fm = getSkinnable().getFocusModel();
 624         if (sm == null || fm == null) return -1;
 625 
 626         int firstVisibleCellIndex = firstVisibleCell.getIndex();
 627 
 628         // isSelected represents focus OR selection
 629         boolean isSelected = false;
 630         if (isFocusDriven) {
 631             isSelected = firstVisibleCell.isFocused() || fm.isFocused(firstVisibleCellIndex);
 632         } else {
 633             isSelected = firstVisibleCell.isSelected() || sm.isSelected(firstVisibleCellIndex);
 634         }
 635 
 636         if (isSelected) {
 637             boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == firstVisibleCellIndex)
 638                                || (! isFocusDriven && sm.getSelectedIndex() == firstVisibleCellIndex);
 639 
 640             if (isLeadIndex) {
 641                 // if the first visible cell is selected, we want to shift that cell down
 642                 // to be the bottom-most cell, or at least as far to the bottom as we can go.
 643                 flow.scrollToBottom(firstVisibleCell);
 644 
 645                 ListCell<T> newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
 646                 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell;
 647             }
 648         } else {
 649             // if the selection is not on the 'top' most cell, we firstly move
 650             // the selection up to that, without scrolling the contents, so
 651             // this is a no-op
 652         }
 653 
 654         int newSelectionIndex = firstVisibleCell.getIndex();
 655         flow.scrollTo(firstVisibleCell);
 656         return newSelectionIndex;
 657     }
 658 }