1 /*
   2  * Copyright (c) 2012, 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 com.sun.javafx.scene.control.Properties;
  29 import javafx.application.Platform;
  30 import javafx.beans.InvalidationListener;
  31 import javafx.beans.Observable;
  32 import javafx.collections.FXCollections;
  33 import javafx.collections.ListChangeListener;
  34 import javafx.collections.MapChangeListener;
  35 import javafx.collections.ObservableList;
  36 import javafx.collections.ObservableMap;
  37 import javafx.geometry.Insets;
  38 import javafx.scene.AccessibleAttribute;
  39 import javafx.scene.Node;
  40 import javafx.scene.control.*;
  41 
  42 import javafx.scene.layout.Region;
  43 import javafx.scene.layout.StackPane;
  44 import javafx.util.Callback;
  45 
  46 import javafx.collections.WeakListChangeListener;
  47 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  48 
  49 import java.lang.ref.WeakReference;
  50 import java.util.List;
  51 import javafx.beans.WeakInvalidationListener;
  52 import javafx.beans.property.BooleanProperty;
  53 import javafx.beans.property.ObjectProperty;
  54 import javafx.geometry.HPos;
  55 import javafx.geometry.VPos;
  56 
  57 import java.security.AccessController;
  58 import java.security.PrivilegedAction;
  59 
  60 /**
  61  * TableViewSkinBase is the base skin class used by controls such as
  62  * {@link javafx.scene.control.TableView} and {@link javafx.scene.control.TreeTableView}
  63  * (the concrete classes are {@link TableViewSkin} and {@link TreeTableViewSkin},
  64  * respectively).
  65  *
  66  * @param <M> The type of the item stored in each row (for TableView, this is the type
  67  *           of the items list, and for TreeTableView, this is the type of the
  68  *           TreeItem).
  69  * @param <S> The type of the item, as represented by the selection model (for
  70  *           TableView, this is, again, the type of the items list, and for
  71  *           TreeTableView, this is TreeItem typed to the same type as M).
  72  * @param <C> The type of the virtualised control (e.g TableView, TreeTableView)
  73  * @param <I> The type of cell used by this virtualised control (e.g. TableRow, TreeTableRow)
  74  * @param <TC> The type of TableColumnBase used by this virtualised control (e.g. TableColumn, TreeTableColumn)
  75  *
  76  * @since 9
  77  * @see TableView
  78  * @see TreeTableView
  79  * @see TableViewSkin
  80  * @see TreeTableViewSkin
  81  */
  82 public abstract class TableViewSkinBase<M, S, C extends Control, I extends IndexedCell<M>, TC extends TableColumnBase<S,?>> extends VirtualContainerBase<C, I> {
  83 
  84     /***************************************************************************
  85      *                                                                         *
  86      * Static Fields                                                           *
  87      *                                                                         *
  88      **************************************************************************/
  89 
  90     private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987;
  91 
  92     private static final String EMPTY_TABLE_TEXT = ControlResources.getString("TableView.noContent");
  93     private static final String NO_COLUMNS_TEXT = ControlResources.getString("TableView.noColumns");
  94 
  95     // RT-34744 : IS_PANNABLE will be false unless
  96     // javafx.scene.control.skin.TableViewSkin.pannable
  97     // is set to true. This is done in order to make TableView functional
  98     // on embedded systems with touch screens which do not generate scroll
  99     // events for touch drag gestures.
 100     private static final boolean IS_PANNABLE =
 101             AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.TableViewSkin.pannable"));
 102 
 103 
 104 
 105     /***************************************************************************
 106      *                                                                         *
 107      * Internal Fields                                                         *
 108      *                                                                         *
 109      **************************************************************************/
 110 
 111     VirtualFlow<I> flow;
 112 
 113     private boolean contentWidthDirty = true;
 114 
 115     /**
 116      * This region is used to overlay atop the table when the user is performing
 117      * a column resize operation or a column reordering operation. It is a line
 118      * that runs the height of the table to indicate either the final width of
 119      * of the selected column, or the position the column will be 'dropped' into
 120      * when the reordering operation completes.
 121      */
 122     private Region columnReorderLine;
 123 
 124     /**
 125      * A region which is resized and positioned such that it perfectly matches
 126      * the dimensions of any TableColumn that is being reordered by the user.
 127      * This is useful, for example, as a semi-transparent overlay to give
 128      * feedback to the user as to which column is currently being moved.
 129      */
 130     private Region columnReorderOverlay;
 131 
 132     /**
 133      * The entire header region for all columns. This header region handles
 134      * column reordering and resizing. It also handles the positioning and
 135      * resizing of thte columnReorderLine and columnReorderOverlay.
 136      */
 137     private TableHeaderRow tableHeaderRow;
 138 
 139     private Callback<C, I> rowFactory;
 140 
 141     /**
 142      * Region placed over the top of the flow (and possibly the header row) if
 143      * there is no data and/or there are no columns specified.
 144      */
 145     private StackPane placeholderRegion;
 146     private Label placeholderLabel;
 147 
 148     private int visibleColCount;
 149 
 150     boolean needCellsRebuilt = true;
 151     boolean needCellsRecreated = true;
 152     boolean needCellsReconfigured = false;
 153     boolean forceCellRecreate = false;
 154 
 155     private int itemCount = -1;
 156 
 157 
 158 
 159     /***************************************************************************
 160      *                                                                         *
 161      * Listeners                                                               *
 162      *                                                                         *
 163      **************************************************************************/
 164 
 165     private MapChangeListener<Object, Object> propertiesMapListener = c -> {
 166         if (! c.wasAdded()) return;
 167         if (Properties.REFRESH.equals(c.getKey())) {
 168             refreshView();
 169             getSkinnable().getProperties().remove(Properties.REFRESH);
 170         } else if (Properties.RECREATE.equals(c.getKey())) {
 171             forceCellRecreate = true;
 172             refreshView();
 173             getSkinnable().getProperties().remove(Properties.RECREATE);
 174         }
 175     };
 176 
 177     private ListChangeListener<S> rowCountListener = c -> {
 178         while (c.next()) {
 179             if (c.wasReplaced()) {
 180                 // RT-28397: Support for when an item is replaced with itself (but
 181                 // updated internal values that should be shown visually).
 182 
 183                 // The ListViewSkin equivalent code here was updated to use the
 184                 // flow.setDirtyCell(int) API, but it was left alone here, otherwise
 185                 // our unit test for RT-36220 fails as we do not handle the case
 186                 // where the TableCell gets updated (only the TableRow does).
 187                 // Ideally we would use the dirtyCell API:
 188                 //
 189                 // for (int i = c.getFrom(); i < c.getTo(); i++) {
 190                 //     flow.setCellDirty(i);
 191                 // }
 192                 itemCount = 0;
 193                 break;
 194             } else if (c.getRemovedSize() == itemCount) {
 195                 // RT-22463: If the user clears out an items list then we
 196                 // should reset all cells (in particular their contained
 197                 // items) such that a subsequent addition to the list of
 198                 // an item which equals the old item (but is rendered
 199                 // differently) still displays as expected (i.e. with the
 200                 // updated display, not the old display).
 201                 itemCount = 0;
 202                 break;
 203             }
 204         }
 205 
 206         // fix for RT-37853
 207         if (getSkinnable() instanceof TableView) {
 208             ((TableView)getSkinnable()).edit(-1, null);
 209         }
 210 
 211         rowCountDirty = true;
 212         getSkinnable().requestLayout();
 213     };
 214 
 215     private ListChangeListener<TC> visibleLeafColumnsListener =
 216             c -> {
 217                 updateVisibleColumnCount();
 218                 while (c.next()) {
 219                     updateVisibleLeafColumnWidthListeners(c.getAddedSubList(), c.getRemoved());
 220                 }
 221             };
 222 
 223     private InvalidationListener widthListener = observable -> {
 224         // This forces the horizontal scrollbar to show when the column
 225         // resizing occurs. It is not ideal, but will work for now.
 226 
 227         // using 'needCellsReconfigured' here rather than 'needCellsRebuilt'
 228         // as otherwise performance suffers massively (RT-27831)
 229         needCellsReconfigured = true;
 230         if (getSkinnable() != null) {
 231             getSkinnable().requestLayout();
 232         }
 233     };
 234 
 235     private InvalidationListener itemsChangeListener;
 236 
 237     private WeakListChangeListener<S> weakRowCountListener =
 238             new WeakListChangeListener<>(rowCountListener);
 239     private WeakListChangeListener<TC> weakVisibleLeafColumnsListener =
 240             new WeakListChangeListener<>(visibleLeafColumnsListener);
 241     private WeakInvalidationListener weakWidthListener =
 242             new WeakInvalidationListener(widthListener);
 243     private WeakInvalidationListener weakItemsChangeListener;
 244 
 245 
 246 
 247     /***************************************************************************
 248      *                                                                         *
 249      * Constructors                                                            *
 250      *                                                                         *
 251      **************************************************************************/
 252 
 253     /**
 254      *
 255      * @param control
 256      */
 257     public TableViewSkinBase(final C control) {
 258         super(control);
 259         
 260         // init the VirtualFlow
 261         flow = getVirtualFlow();
 262         flow.setPannable(IS_PANNABLE);
 263 //        flow.setCellFactory(flow1 -> TableViewSkinBase.this.createCell());
 264         
 265         /*
 266          * Listening for scrolling along the X axis, but we need to be careful
 267          * to handle the situation appropriately when the hbar is invisible.
 268          */
 269         flow.getHbar().valueProperty().addListener(o -> horizontalScroll());
 270 
 271         // RT-37152
 272         flow.getHbar().setUnitIncrement(15);
 273         flow.getHbar().setBlockIncrement(TableColumnHeader.DEFAULT_COLUMN_WIDTH);
 274 
 275         columnReorderLine = new Region();
 276         columnReorderLine.getStyleClass().setAll("column-resize-line");
 277         columnReorderLine.setManaged(false);
 278         columnReorderLine.setVisible(false);
 279 
 280         columnReorderOverlay = new Region();
 281         columnReorderOverlay.getStyleClass().setAll("column-overlay");
 282         columnReorderOverlay.setVisible(false);
 283         columnReorderOverlay.setManaged(false);
 284 
 285         tableHeaderRow = createTableHeaderRow();
 286 //        tableHeaderRow.setColumnReorderLine(columnReorderLine);
 287         tableHeaderRow.setFocusTraversable(false);
 288 
 289         getChildren().addAll(tableHeaderRow, flow, columnReorderOverlay, columnReorderLine);
 290 
 291         updateVisibleColumnCount();
 292         updateVisibleLeafColumnWidthListeners(getVisibleLeafColumns(), FXCollections.<TC>emptyObservableList());
 293 
 294         tableHeaderRow.reorderingProperty().addListener(valueModel -> {
 295             getSkinnable().requestLayout();
 296         });
 297 
 298         getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
 299         
 300         updateTableItems(null, itemsProperty().get());
 301         itemsChangeListener = new InvalidationListener() {
 302             private WeakReference<ObservableList<S>> weakItemsRef = new WeakReference<>(itemsProperty().get());
 303 
 304             @Override public void invalidated(Observable observable) {
 305                 ObservableList<S> oldItems = weakItemsRef.get();
 306                 weakItemsRef = new WeakReference<>(itemsProperty().get());
 307                 updateTableItems(oldItems, itemsProperty().get());
 308             }
 309         };
 310         weakItemsChangeListener = new WeakInvalidationListener(itemsChangeListener);
 311         itemsProperty().addListener(weakItemsChangeListener);
 312 
 313         final ObservableMap<Object, Object> properties = control.getProperties();
 314         properties.remove(Properties.REFRESH);
 315         properties.remove(Properties.RECREATE);
 316         properties.addListener(propertiesMapListener);
 317         
 318         control.addEventHandler(ScrollToEvent.<TC>scrollToColumn(), event -> {
 319             scrollHorizontally(event.getScrollTarget());
 320         });
 321 
 322         // flow and flow.vbar width observer
 323         InvalidationListener widthObserver = valueModel -> {
 324             contentWidthDirty = true;
 325             getSkinnable().requestLayout();
 326         };
 327         flow.widthProperty().addListener(widthObserver);
 328         flow.getVbar().widthProperty().addListener(widthObserver);
 329 
 330         registerChangeListener(rowFactoryProperty(), e -> {
 331             Callback<C, I> oldFactory = rowFactory;
 332             rowFactory = rowFactoryProperty().get();
 333             if (oldFactory != rowFactory) {
 334                 needCellsRebuilt = true;
 335                 getSkinnable().requestLayout();
 336             }
 337         });
 338         registerChangeListener(placeholderProperty(), e -> updatePlaceholderRegionVisibility());
 339         registerChangeListener(flow.getVbar().visibleProperty(), e -> updateContentWidth());
 340     }
 341 
 342 
 343 
 344     /***************************************************************************
 345      *                                                                         *
 346      * Abstract Methods                                                        *
 347      *                                                                         *
 348      **************************************************************************/
 349 
 350     // returns the selection model of the control
 351     abstract TableSelectionModel<S> getSelectionModel();
 352 
 353     // returns the focus model of the control
 354     abstract TableFocusModel<S,TC> getFocusModel();
 355 
 356     // returns the currently focused cell in the focus model
 357     abstract TablePositionBase<? extends TC> getFocusedCell();
 358 
 359     // returns an ObservableList of the visible leaf columns of the control
 360     abstract ObservableList<? extends TC> getVisibleLeafColumns();
 361 
 362     // returns the index of a column in the visible leaf columns
 363     abstract int getVisibleLeafIndex(TC tc);
 364 
 365     // returns the leaf column at the given index
 366     abstract TC getVisibleLeafColumn(int col);
 367 
 368     // returns a list of the root columns
 369     abstract ObservableList<TC> getColumns();
 370 
 371     // returns the sort order of the control
 372     abstract ObservableList<TC> getSortOrder();
 373 
 374     // returns a property representing the list of items in the control
 375     abstract ObjectProperty<ObservableList<S>> itemsProperty();
 376 
 377     // returns a property representing the row factory in the control
 378     abstract ObjectProperty<Callback<C, I>> rowFactoryProperty();
 379 
 380     // returns the placeholder property for the control
 381     abstract ObjectProperty<Node> placeholderProperty();
 382 
 383     // returns the property used to represent whether the tableMenuButton should
 384     // be visible
 385     abstract BooleanProperty tableMenuButtonVisibleProperty();
 386 
 387     // returns a property representing the column resize properyt in the control
 388     abstract ObjectProperty<Callback<ResizeFeaturesBase, Boolean>> columnResizePolicyProperty();
 389 
 390     // Method to resize the given column by the given delta, returning a boolean
 391     // to indicate success or failure
 392     abstract boolean resizeColumn(TC tc, double delta);
 393 
 394     // Method to resize the column based on the content in that column, based on
 395     // the maxRows number of rows
 396     abstract void resizeColumnToFitContent(TC tc, int maxRows);
 397 
 398 
 399 
 400 
 401     /***************************************************************************
 402      *                                                                         *
 403      * Public API                                                              *
 404      *                                                                         *
 405      **************************************************************************/
 406 
 407     /** {@inheritDoc} */
 408     @Override public void dispose() {
 409         getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener);
 410         itemsProperty().removeListener(weakItemsChangeListener);
 411         getSkinnable().getProperties().removeListener(propertiesMapListener);
 412         updateTableItems(itemsProperty().get(), null);
 413 
 414         super.dispose();
 415     }
 416 
 417     /** {@inheritDoc} */
 418     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 419         return 400;
 420     }
 421 
 422     /** {@inheritDoc} */
 423     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 424         double prefHeight = computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset);
 425         
 426         List<? extends TC> cols = getVisibleLeafColumns();
 427         if (cols == null || cols.isEmpty()) {
 428             return prefHeight * GOLDEN_RATIO_MULTIPLIER;
 429         } 
 430 
 431         double pw = leftInset + rightInset;
 432         for (int i = 0, max = cols.size(); i < max; i++) {
 433             TC tc = cols.get(i);
 434             pw += Math.max(tc.getPrefWidth(), tc.getMinWidth());
 435         }
 436 //        return pw;
 437         return Math.max(pw, prefHeight * GOLDEN_RATIO_MULTIPLIER);
 438     }
 439     
 440     /** {@inheritDoc} */
 441     @Override protected void layoutChildren(final double x, double y,
 442             final double w, final double h) {
 443 
 444         C table = getSkinnable();
 445 
 446         // an unlikely scenario, but it does pop up in unit tests, so guarding
 447         // here to prevent test failures seems ok.
 448         if (table == null) {
 449             return;
 450         }
 451 
 452         super.layoutChildren(x, y, w, h);
 453         
 454         if (needCellsRecreated) {
 455             flow.recreateCells();
 456         } else if (needCellsRebuilt) {
 457             flow.rebuildCells();
 458         } else if (needCellsReconfigured) {
 459             flow.reconfigureCells();
 460         }
 461 
 462         needCellsRebuilt = false;
 463         needCellsRecreated = false;
 464         needCellsReconfigured = false;
 465 
 466         final double baselineOffset = table.getLayoutBounds().getHeight() / 2;
 467 
 468         // position the table header
 469         double tableHeaderRowHeight = tableHeaderRow.prefHeight(-1);
 470         layoutInArea(tableHeaderRow, x, y, w, tableHeaderRowHeight, baselineOffset, 
 471                 HPos.CENTER, VPos.CENTER);
 472         y += tableHeaderRowHeight;
 473 
 474         // let the virtual flow take up all remaining space
 475         // TODO this calculation is to ensure the bottom border is visible when
 476         // placed in a Pane. It is not ideal, but will suffice for now. See 
 477         // RT-14335 for more information.
 478         double flowHeight = Math.floor(h - tableHeaderRowHeight);
 479         if (getItemCount() == 0 || visibleColCount == 0) {
 480             // show message overlay instead of empty table
 481             layoutInArea(placeholderRegion, x, y,
 482                     w, flowHeight,
 483                     baselineOffset, HPos.CENTER, VPos.CENTER);
 484         } else {
 485             layoutInArea(flow, x, y,
 486                     w, flowHeight,
 487                     baselineOffset, HPos.CENTER, VPos.CENTER);
 488         }
 489         
 490         // painting the overlay over the column being reordered
 491         if (tableHeaderRow.getReorderingRegion() != null) {
 492             TableColumnHeader reorderingColumnHeader = tableHeaderRow.getReorderingRegion();
 493             TableColumnBase reorderingColumn = reorderingColumnHeader.getTableColumn();
 494             if (reorderingColumn != null) {
 495                 Node n = tableHeaderRow.getReorderingRegion();
 496                 
 497                 // determine where to draw the column header overlay, it's 
 498                 // either from the left-edge of the column, or 0, if the column
 499                 // is off the left-side of the TableView (i.e. horizontal 
 500                 // scrolling has occured).
 501                 double minX = tableHeaderRow.sceneToLocal(n.localToScene(n.getBoundsInLocal())).getMinX();
 502                 double overlayWidth = reorderingColumnHeader.getWidth();
 503                 if (minX < 0) {
 504                     overlayWidth += minX;
 505                 }
 506                 minX = minX < 0 ? 0 : minX;
 507                 
 508                 // prevent the overlay going out the right-hand side of the 
 509                 // TableView
 510                 if (minX + overlayWidth > w) {
 511                     overlayWidth = w - minX;
 512                     
 513                     if (flow.getVbar().isVisible()) {
 514                         overlayWidth -= flow.getVbar().getWidth() - 1;
 515                     }
 516                 }
 517                 
 518                 double contentAreaHeight = flowHeight;
 519                 if (flow.getHbar().isVisible()) {
 520                     contentAreaHeight -= flow.getHbar().getHeight();
 521                 }
 522                 
 523                 columnReorderOverlay.resize(overlayWidth, contentAreaHeight);
 524                 
 525                 columnReorderOverlay.setLayoutX(minX);
 526                 columnReorderOverlay.setLayoutY(tableHeaderRow.getHeight());
 527             }
 528             
 529             // paint the reorder line as well
 530             double cw = columnReorderLine.snappedLeftInset() + columnReorderLine.snappedRightInset();
 531             double lineHeight = h - (flow.getHbar().isVisible() ? flow.getHbar().getHeight() - 1 : 0);
 532             columnReorderLine.resizeRelocate(0, columnReorderLine.snappedTopInset(), cw, lineHeight);
 533         }
 534         
 535         columnReorderLine.setVisible(tableHeaderRow.isReordering());
 536         columnReorderOverlay.setVisible(tableHeaderRow.isReordering());
 537 
 538         checkContentWidthState();
 539     }
 540     
 541     
 542     
 543     /***************************************************************************
 544      *                                                                         *
 545      * Private implementation                                                  *
 546      *                                                                         *
 547      **************************************************************************/
 548 
 549     final TableHeaderRow getTableHeaderRow() {
 550         return tableHeaderRow;
 551     }
 552 
 553     private TableHeaderRow createTableHeaderRow() {
 554         return new TableHeaderRow(this);
 555     }
 556 
 557     @Override void updateRowCount() {
 558         updatePlaceholderRegionVisibility();
 559 
 560         int oldCount = itemCount;
 561         int newCount = getItemCount();
 562 
 563         itemCount = newCount;
 564 
 565         // if this is not called even when the count is the same, we get a
 566         // memory leak in VirtualFlow.sheet.children. This can probably be
 567         // optimised in the future when time permits.
 568         flow.setCellCount(newCount);
 569 
 570         if (forceCellRecreate) {
 571             needCellsRecreated = true;
 572             forceCellRecreate = false;
 573         } else if (newCount != oldCount) {
 574             // FIXME updateRowCount is called _a lot_. Perhaps we can make rebuildCells
 575             // smarter. Imagine if items has one million items added - do we really
 576             // need to rebuildCells a million times? Maybe this is better now that
 577             // we do rebuildCells instead of recreateCells.
 578             needCellsRebuilt = true;
 579         } else {
 580             needCellsReconfigured = true;
 581         }
 582     }
 583 
 584     private void checkContentWidthState() {
 585         // we test for item count here to resolve RT-14855, where the column
 586         // widths weren't being resized properly when in constrained layout mode
 587         // if there were no items.
 588         if (contentWidthDirty || getItemCount() == 0) {
 589             updateContentWidth();
 590             contentWidthDirty = false;
 591         }
 592     }
 593 
 594     void horizontalScroll() {
 595         tableHeaderRow.updateScrollX();
 596     }
 597 
 598     void onFocusPreviousCell() {
 599         TableFocusModel<S,TC> fm = getFocusModel();
 600         if (fm == null) return;
 601 
 602         flow.scrollTo(fm.getFocusedIndex());
 603     }
 604 
 605     void onFocusNextCell() {
 606         TableFocusModel<S,TC> fm = getFocusModel();
 607         if (fm == null) return;
 608 
 609         flow.scrollTo(fm.getFocusedIndex());
 610     }
 611 
 612     void onSelectPreviousCell() {
 613         SelectionModel<S> sm = getSelectionModel();
 614         if (sm == null) return;
 615 
 616         flow.scrollTo(sm.getSelectedIndex());
 617     }
 618 
 619     void onSelectNextCell() {
 620         SelectionModel<S> sm = getSelectionModel();
 621         if (sm == null) return;
 622 
 623         flow.scrollTo(sm.getSelectedIndex());
 624     }
 625 
 626     void onSelectLeftCell() {
 627         scrollHorizontally();
 628     }
 629 
 630     void onSelectRightCell() {
 631         scrollHorizontally();
 632     }
 633 
 634     void onMoveToFirstCell() {
 635         flow.scrollTo(0);
 636         flow.setPosition(0);
 637     }
 638 
 639     void onMoveToLastCell() {
 640         int endPos = getItemCount();
 641         flow.scrollTo(endPos);
 642         flow.setPosition(1);
 643     }
 644 
 645     private void updateTableItems(ObservableList<S> oldList, ObservableList<S> newList) {
 646         if (oldList != null) {
 647             oldList.removeListener(weakRowCountListener);
 648         }
 649 
 650         if (newList != null) {
 651             newList.addListener(weakRowCountListener);
 652         }
 653 
 654         rowCountDirty = true;
 655         getSkinnable().requestLayout();
 656     }
 657 
 658     Region getColumnReorderLine() {
 659         return columnReorderLine;
 660     }
 661 
 662     /**
 663      * Function used to scroll the container down by one 'page', although
 664      * if this is a horizontal container, then the scrolling will be to the right.
 665      */
 666     int onScrollPageDown(boolean isFocusDriven) {
 667         TableSelectionModel<S> sm = getSelectionModel();
 668         if (sm == null) return -1;
 669 
 670         final int itemCount = getItemCount();
 671 
 672         I lastVisibleCell = flow.getLastVisibleCellWithinViewPort();
 673         if (lastVisibleCell == null) return -1;
 674 
 675         int lastVisibleCellIndex = lastVisibleCell.getIndex();
 676 
 677         // we include this test here as the virtual flow will return cells that
 678         // exceed past the item count, so we need to clamp here (and further down
 679         // in this method also). See RT-19053 for more information.
 680         lastVisibleCellIndex = lastVisibleCellIndex >= itemCount ? itemCount - 1 : lastVisibleCellIndex;
 681 
 682         // isSelected represents focus OR selection
 683         boolean isSelected;
 684         if (isFocusDriven) {
 685             isSelected = lastVisibleCell.isFocused() || isCellFocused(lastVisibleCellIndex);
 686         } else {
 687             isSelected = lastVisibleCell.isSelected() || isCellSelected(lastVisibleCellIndex);
 688         }
 689 
 690         if (isSelected) {
 691             boolean isLeadIndex = isLeadIndex(isFocusDriven, lastVisibleCellIndex);
 692 
 693             if (isLeadIndex) {
 694                 // if the last visible cell is selected, we want to shift that cell up
 695                 // to be the top-most cell, or at least as far to the top as we can go.
 696                 flow.scrollToTop(lastVisibleCell);
 697 
 698                 I newLastVisibleCell = flow.getLastVisibleCellWithinViewPort();
 699                 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell;
 700             }
 701         }
 702 
 703         int newSelectionIndex = lastVisibleCell.getIndex();
 704         newSelectionIndex = newSelectionIndex >= itemCount ? itemCount - 1 : newSelectionIndex;
 705         flow.scrollTo(newSelectionIndex);
 706         return newSelectionIndex;
 707     }
 708 
 709     /**
 710      * Function used to scroll the container up by one 'page', although
 711      * if this is a horizontal container, then the scrolling will be to the left.
 712      */
 713     int onScrollPageUp(boolean isFocusDriven) {
 714         I firstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
 715         if (firstVisibleCell == null) return -1;
 716 
 717         int firstVisibleCellIndex = firstVisibleCell.getIndex();
 718 
 719         // isSelected represents focus OR selection
 720         boolean isSelected = false;
 721         if (isFocusDriven) {
 722             isSelected = firstVisibleCell.isFocused() || isCellFocused(firstVisibleCellIndex);
 723         } else {
 724             isSelected = firstVisibleCell.isSelected() || isCellSelected(firstVisibleCellIndex);
 725         }
 726 
 727         if (isSelected) {
 728             boolean isLeadIndex = isLeadIndex(isFocusDriven, firstVisibleCellIndex);
 729 
 730             if (isLeadIndex) {
 731                 // if the first visible cell is selected, we want to shift that cell down
 732                 // to be the bottom-most cell, or at least as far to the bottom as we can go.
 733                 flow.scrollToBottom(firstVisibleCell);
 734 
 735                 I newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
 736                 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell;
 737             }
 738         }
 739 
 740         int newSelectionIndex = firstVisibleCell.getIndex();
 741         flow.scrollTo(newSelectionIndex);
 742         return newSelectionIndex;
 743     }
 744 
 745     private boolean isLeadIndex(boolean isFocusDriven, int index) {
 746         final TableSelectionModel<S> sm = getSelectionModel();
 747         final FocusModel<S> fm = getFocusModel();
 748 
 749         return (isFocusDriven && fm.getFocusedIndex() == index)
 750                 || (! isFocusDriven && sm.getSelectedIndex() == index);
 751     }
 752 
 753     boolean isColumnPartiallyOrFullyVisible(TC col) {
 754         if (col == null || !col.isVisible()) return false;
 755 
 756         double scrollX = flow.getHbar().getValue();
 757 
 758         // work out where this column header is, and it's width (start -> end)
 759         double start = 0;
 760         final ObservableList<? extends TC> visibleLeafColumns = getVisibleLeafColumns();
 761         for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) {
 762             TableColumnBase<S,?> c = visibleLeafColumns.get(i);
 763             if (c.equals(col)) break;
 764             start += c.getWidth();
 765         }
 766         double end = start + col.getWidth();
 767 
 768         // determine the width of the table
 769         final Insets padding = getSkinnable().getPadding();
 770         double headerWidth = getSkinnable().getWidth() - padding.getLeft() + padding.getRight();
 771 
 772         return (start >= scrollX || end > scrollX) && (start < (headerWidth + scrollX) || end <= (headerWidth + scrollX));
 773     }
 774     
 775     /**
 776      * Keeps track of how many leaf columns are currently visible in this table.
 777      */
 778     private void updateVisibleColumnCount() {
 779         visibleColCount = getVisibleLeafColumns().size();
 780 
 781         updatePlaceholderRegionVisibility();
 782         needCellsRebuilt = true;
 783         getSkinnable().requestLayout();
 784     }
 785     
 786     private void updateVisibleLeafColumnWidthListeners(
 787             List<? extends TC> added, List<? extends TC> removed) {
 788         
 789         for (int i = 0, max = removed.size(); i < max; i++) {
 790             TC tc = removed.get(i);
 791             tc.widthProperty().removeListener(weakWidthListener);
 792         }
 793         for (int i = 0, max = added.size(); i < max; i++) {
 794             TC tc = added.get(i);
 795             tc.widthProperty().addListener(weakWidthListener);
 796         }
 797         needCellsRebuilt = true;
 798         getSkinnable().requestLayout();
 799     }
 800 
 801     final void updatePlaceholderRegionVisibility() {
 802         boolean visible = visibleColCount == 0 || getItemCount() == 0;
 803         
 804         if (visible) {
 805             if (placeholderRegion == null) {
 806                 placeholderRegion = new StackPane();
 807                 placeholderRegion.getStyleClass().setAll("placeholder");
 808                 getChildren().add(placeholderRegion);
 809             }
 810             
 811             Node placeholderNode = placeholderProperty().get();
 812 
 813             if (placeholderNode == null) {
 814                 if (placeholderLabel == null) {
 815                     placeholderLabel = new Label();
 816                 }
 817                 String s = visibleColCount == 0 ? NO_COLUMNS_TEXT : EMPTY_TABLE_TEXT;
 818                 placeholderLabel.setText(s);
 819 
 820                 placeholderRegion.getChildren().setAll(placeholderLabel);
 821             } else {
 822                 placeholderRegion.getChildren().setAll(placeholderNode);
 823             }
 824         }
 825 
 826         flow.setVisible(! visible);
 827         if (placeholderRegion != null) {
 828             placeholderRegion.setVisible(visible);
 829         }
 830     }
 831 
 832     /*
 833      * It's often important to know how much width is available for content
 834      * within the table, and this needs to exclude the width of any vertical
 835      * scrollbar.
 836      */
 837     private void updateContentWidth() {
 838         double contentWidth = flow.getWidth();
 839         
 840         if (flow.getVbar().isVisible()) {
 841             contentWidth -= flow.getVbar().getWidth();
 842         }
 843         
 844         if (contentWidth <= 0) {
 845             // Fix for RT-14855 when there is no content in the TableView.
 846             Control c = getSkinnable();
 847             contentWidth = c.getWidth() - (snappedLeftInset() + snappedRightInset());
 848         }
 849 
 850         contentWidth = Math.max(0.0, contentWidth);
 851 
 852         // FIXME this isn't perfect, but it prevents RT-14885, which results in
 853         // undesired horizontal scrollbars when in constrained resize mode
 854         getSkinnable().getProperties().put("TableView.contentWidth", Math.floor(contentWidth));
 855     }
 856 
 857     private void refreshView() {
 858         rowCountDirty = true;
 859         Control c = getSkinnable();
 860         if (c != null) {
 861             c.requestLayout();
 862         }
 863     }
 864 
 865     // Handles the horizontal scrolling when the selection mode is cell-based
 866     // and the newly selected cell belongs to a column which is not totally
 867     // visible.
 868     void scrollHorizontally() {
 869         TableFocusModel<S,TC> fm = getFocusModel();
 870         if (fm == null) return;
 871 
 872         TC col = getFocusedCell().getTableColumn();
 873         scrollHorizontally(col);
 874     }
 875 
 876     void scrollHorizontally(TC col) {
 877         if (col == null || !col.isVisible()) return;
 878         
 879         final Control control = getSkinnable();
 880 
 881         // RT-37060 - if we are trying to scroll to a column that has not
 882         // yet even been rendered, we must wait until the layout pass has
 883         // happened and then do the scroll. The laziest way to do this is to
 884         // queue up the task to run later, at which point we will have hopefully
 885         // fully run the column through layout and css.
 886         TableColumnHeader header = tableHeaderRow.getColumnHeaderFor(col);
 887         if (header == null || header.getWidth() <= 0) {
 888             Platform.runLater(() -> scrollHorizontally(col));
 889             return;
 890         }
 891 
 892         // work out where this column header is, and it's width (start -> end)
 893         double start = 0;
 894         for (TC c : getVisibleLeafColumns()) {
 895             if (c.equals(col)) break;
 896             start += c.getWidth();
 897         }
 898         double end = start + col.getWidth();
 899 
 900         // determine the visible width of the table
 901         double headerWidth = control.getWidth() - snappedLeftInset() - snappedRightInset();
 902 
 903         // determine by how much we need to translate the table to ensure that
 904         // the start position of this column lines up with the left edge of the
 905         // tableview, and also that the columns don't become detached from the
 906         // right edge of the table
 907         double pos = flow.getHbar().getValue();
 908         double max = flow.getHbar().getMax();
 909         double newPos;
 910         
 911         if (start < pos && start >= 0) {
 912             newPos = start;
 913         } else {
 914             double delta = start < 0 || end > headerWidth ? start - pos : 0;
 915             newPos = pos + delta > max ? max : pos + delta;
 916         }
 917 
 918         // FIXME we should add API in VirtualFlow so we don't end up going
 919         // direct to the hbar.
 920         // actually shift the flow - this will result in the header moving
 921         // as well
 922         flow.getHbar().setValue(newPos);
 923     }
 924 
 925     private boolean isCellSelected(int row) {
 926         TableSelectionModel<S> sm = getSelectionModel();
 927         if (sm == null) return false;
 928         if (! sm.isCellSelectionEnabled()) return false;
 929 
 930         int columnCount = getVisibleLeafColumns().size();
 931         for (int col = 0; col < columnCount; col++) {
 932             if (sm.isSelected(row, getVisibleLeafColumn(col))) {
 933                 return true;
 934             }
 935         }
 936 
 937         return false;
 938     }
 939     
 940     private boolean isCellFocused(int row) {
 941         TableFocusModel<S,TC> fm = getFocusModel();
 942         if (fm == null) return false;
 943 
 944         int columnCount = getVisibleLeafColumns().size();
 945         for (int col = 0; col < columnCount; col++) {
 946             if (fm.isFocused(row, getVisibleLeafColumn(col))) {
 947                 return true;
 948             }
 949         }
 950         
 951         return false;
 952     }
 953 
 954 
 955 
 956     /***************************************************************************
 957      *                                                                         *
 958      * A11y                                                                    *
 959      *                                                                         *
 960      **************************************************************************/
 961 
 962     /** {@inheritDoc} */
 963     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 964         switch (attribute) {
 965             case FOCUS_ITEM: {
 966                 TableFocusModel<S,?> fm = getFocusModel();
 967                 int focusedIndex = fm.getFocusedIndex();
 968                 if (focusedIndex == -1) {
 969                     if (placeholderRegion != null && placeholderRegion.isVisible()) {
 970                         return placeholderRegion.getChildren().get(0);
 971                     }
 972                     if (getItemCount() > 0) {
 973                         focusedIndex = 0;
 974                     } else {
 975                         return null;
 976                     }
 977                 }
 978                 return flow.getPrivateCell(focusedIndex);
 979             }
 980             case CELL_AT_ROW_COLUMN: {
 981                 int rowIndex = (Integer)parameters[0];
 982                 return flow.getPrivateCell(rowIndex);
 983             }
 984             case COLUMN_AT_INDEX: {
 985                 int index = (Integer)parameters[0];
 986                 TableColumnBase<S,?> column = getVisibleLeafColumn(index);
 987                 return getTableHeaderRow().getColumnHeaderFor(column);
 988             }
 989             case HEADER: {
 990                 /* Not sure how this is used by Accessibility, but without this VoiceOver will not
 991                  * look for column headers */
 992                 return getTableHeaderRow();
 993             }
 994             case VERTICAL_SCROLLBAR: return flow.getVbar();
 995             case HORIZONTAL_SCROLLBAR: return flow.getHbar();
 996             default: return super.queryAccessibleAttribute(attribute, parameters);
 997         }
 998     }
 999 
1000 }