1 /*
   2  * Copyright (c) 2012, 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.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     // RT-34744 : IS_PANNABLE will be false unless
  93     // javafx.scene.control.skin.TableViewSkin.pannable
  94     // is set to true. This is done in order to make TableView functional
  95     // on embedded systems with touch screens which do not generate scroll
  96     // events for touch drag gestures.
  97     private static final boolean IS_PANNABLE =
  98             AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.TableViewSkin.pannable"));
  99 
 100 
 101 
 102     /***************************************************************************
 103      *                                                                         *
 104      * Internal Fields                                                         *
 105      *                                                                         *
 106      **************************************************************************/
 107 
 108     // JDK-8090129: These constants should not be static, because the
 109     // Locale may change between instances.
 110     private final String EMPTY_TABLE_TEXT = ControlResources.getString("TableView.noContent");
 111     private final String NO_COLUMNS_TEXT = ControlResources.getString("TableView.noColumns");
 112 
 113     VirtualFlow<I> flow;
 114 
 115     private boolean contentWidthDirty = true;
 116 
 117     /**
 118      * This region is used to overlay atop the table when the user is performing
 119      * a column resize operation or a column reordering operation. It is a line
 120      * that runs the height of the table to indicate either the final width of
 121      * of the selected column, or the position the column will be 'dropped' into
 122      * when the reordering operation completes.
 123      */
 124     private Region columnReorderLine;
 125 
 126     /**
 127      * A region which is resized and positioned such that it perfectly matches
 128      * the dimensions of any TableColumn that is being reordered by the user.
 129      * This is useful, for example, as a semi-transparent overlay to give
 130      * feedback to the user as to which column is currently being moved.
 131      */
 132     private Region columnReorderOverlay;
 133 
 134     /**
 135      * The entire header region for all columns. This header region handles
 136      * column reordering and resizing. It also handles the positioning and
 137      * resizing of thte columnReorderLine and columnReorderOverlay.
 138      */
 139     private TableHeaderRow tableHeaderRow;
 140 
 141     private Callback<C, I> rowFactory;
 142 
 143     /**
 144      * Region placed over the top of the flow (and possibly the header row) if
 145      * there is no data and/or there are no columns specified.
 146      */
 147     private StackPane placeholderRegion;
 148     private Label placeholderLabel;
 149 
 150     private int visibleColCount;
 151 
 152     boolean needCellsRecreated = true;
 153     boolean needCellsReconfigured = 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             needCellsRecreated = 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         markItemCountDirty();
 212         getSkinnable().requestLayout();
 213     };
 214 
 215     private ListChangeListener<TC> visibleLeafColumnsListener = c -> {
 216         updateVisibleColumnCount();
 217         while (c.next()) {
 218             updateVisibleLeafColumnWidthListeners(c.getAddedSubList(), c.getRemoved());
 219         }
 220     };
 221 
 222     private InvalidationListener widthListener = observable -> {
 223         // This forces the horizontal scrollbar to show when the column
 224         // resizing occurs. It is not ideal, but will work for now.
 225 
 226         // using 'needCellsReconfigured' here rather than 'needCellsRebuilt'
 227         // as otherwise performance suffers massively (RT-27831)
 228         needCellsReconfigured = true;
 229         if (getSkinnable() != null) {
 230             getSkinnable().requestLayout();
 231         }
 232     };
 233 
 234     private InvalidationListener itemsChangeListener;
 235 
 236     private WeakListChangeListener<S> weakRowCountListener =
 237             new WeakListChangeListener<>(rowCountListener);
 238     private WeakListChangeListener<TC> weakVisibleLeafColumnsListener =
 239             new WeakListChangeListener<>(visibleLeafColumnsListener);
 240     private WeakInvalidationListener weakWidthListener =
 241             new WeakInvalidationListener(widthListener);
 242     private WeakInvalidationListener weakItemsChangeListener;
 243 
 244 
 245 
 246     /***************************************************************************
 247      *                                                                         *
 248      * Constructors                                                            *
 249      *                                                                         *
 250      **************************************************************************/
 251 
 252     /**
 253      *
 254      * @param control
 255      */
 256     public TableViewSkinBase(final C control) {
 257         super(control);
 258 
 259         // init the VirtualFlow
 260         flow = getVirtualFlow();
 261         flow.setPannable(IS_PANNABLE);
 262 //        flow.setCellFactory(flow1 -> TableViewSkinBase.this.createCell());
 263 
 264         /*
 265          * Listening for scrolling along the X axis, but we need to be careful
 266          * to handle the situation appropriately when the hbar is invisible.
 267          */
 268         flow.getHbar().valueProperty().addListener(o -> horizontalScroll());
 269 
 270         // RT-37152
 271         flow.getHbar().setUnitIncrement(15);
 272         flow.getHbar().setBlockIncrement(TableColumnHeader.DEFAULT_COLUMN_WIDTH);
 273 
 274         columnReorderLine = new Region();
 275         columnReorderLine.getStyleClass().setAll("column-resize-line");
 276         columnReorderLine.setManaged(false);
 277         columnReorderLine.setVisible(false);
 278 
 279         columnReorderOverlay = new Region();
 280         columnReorderOverlay.getStyleClass().setAll("column-overlay");
 281         columnReorderOverlay.setVisible(false);
 282         columnReorderOverlay.setManaged(false);
 283 
 284         tableHeaderRow = createTableHeaderRow();
 285 //        tableHeaderRow.setColumnReorderLine(columnReorderLine);
 286         tableHeaderRow.setFocusTraversable(false);
 287 
 288         getChildren().addAll(tableHeaderRow, flow, columnReorderOverlay, columnReorderLine);
 289 
 290         updateVisibleColumnCount();
 291         updateVisibleLeafColumnWidthListeners(getVisibleLeafColumns(), FXCollections.<TC>emptyObservableList());
 292 
 293         tableHeaderRow.reorderingProperty().addListener(valueModel -> {
 294             getSkinnable().requestLayout();
 295         });
 296 
 297         getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
 298 
 299         final ObjectProperty<ObservableList<S>> itemsProperty = TableSkinUtils.itemsProperty(this);
 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         final ObjectProperty<Callback<C, I>> rowFactoryProperty = TableSkinUtils.rowFactoryProperty(this);
 331         registerChangeListener(rowFactoryProperty, e -> {
 332             Callback<C, I> oldFactory = rowFactory;
 333             rowFactory = rowFactoryProperty.get();
 334             if (oldFactory != rowFactory) {
 335                 requestRebuildCells();
 336             }
 337         });
 338         registerChangeListener(TableSkinUtils.placeholderProperty(this), e -> updatePlaceholderRegionVisibility());
 339         registerChangeListener(flow.getVbar().visibleProperty(), e -> updateContentWidth());
 340     }
 341 
 342 
 343 
 344     /***************************************************************************
 345      *                                                                         *
 346      * Abstract Methods                                                        *
 347      *                                                                         *
 348      **************************************************************************/
 349 
 350 
 351 
 352 
 353 
 354     /***************************************************************************
 355      *                                                                         *
 356      * Public API                                                              *
 357      *                                                                         *
 358      **************************************************************************/
 359 
 360     /** {@inheritDoc} */
 361     @Override public void dispose() {
 362         final ObjectProperty<ObservableList<S>> itemsProperty = TableSkinUtils.itemsProperty(this);
 363 
 364         getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener);
 365         itemsProperty.removeListener(weakItemsChangeListener);
 366         getSkinnable().getProperties().removeListener(propertiesMapListener);
 367         updateTableItems(itemsProperty.get(), null);
 368 
 369         super.dispose();
 370     }
 371 
 372     /** {@inheritDoc} */
 373     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 374         return 400;
 375     }
 376 
 377     /** {@inheritDoc} */
 378     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 379         double prefHeight = computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset);
 380 
 381         List<? extends TC> cols = getVisibleLeafColumns();
 382         if (cols == null || cols.isEmpty()) {
 383             return prefHeight * GOLDEN_RATIO_MULTIPLIER;
 384         }
 385 
 386         double pw = leftInset + rightInset;
 387         for (int i = 0, max = cols.size(); i < max; i++) {
 388             TC tc = cols.get(i);
 389             pw += Math.max(tc.getPrefWidth(), tc.getMinWidth());
 390         }
 391 //        return pw;
 392         return Math.max(pw, prefHeight * GOLDEN_RATIO_MULTIPLIER);
 393     }
 394 
 395     /** {@inheritDoc} */
 396     @Override protected void layoutChildren(final double x, double y,
 397             final double w, final double h) {
 398 
 399         C table = getSkinnable();
 400 
 401         // an unlikely scenario, but it does pop up in unit tests, so guarding
 402         // here to prevent test failures seems ok.
 403         if (table == null) {
 404             return;
 405         }
 406 
 407         super.layoutChildren(x, y, w, h);
 408 
 409         if (needCellsRecreated) {
 410             flow.recreateCells();
 411         } else if (needCellsReconfigured) {
 412             flow.reconfigureCells();
 413         }
 414 
 415         needCellsRecreated = false;
 416         needCellsReconfigured = false;
 417 
 418         final double baselineOffset = table.getLayoutBounds().getHeight() / 2;
 419 
 420         // position the table header
 421         double tableHeaderRowHeight = tableHeaderRow.prefHeight(-1);
 422         layoutInArea(tableHeaderRow, x, y, w, tableHeaderRowHeight, baselineOffset,
 423                 HPos.CENTER, VPos.CENTER);
 424         y += tableHeaderRowHeight;
 425 
 426         // let the virtual flow take up all remaining space
 427         // TODO this calculation is to ensure the bottom border is visible when
 428         // placed in a Pane. It is not ideal, but will suffice for now. See
 429         // RT-14335 for more information.
 430         double flowHeight = Math.floor(h - tableHeaderRowHeight);
 431         if (getItemCount() == 0 || visibleColCount == 0) {
 432             // show message overlay instead of empty table
 433             layoutInArea(placeholderRegion, x, y,
 434                     w, flowHeight,
 435                     baselineOffset, HPos.CENTER, VPos.CENTER);
 436         } else {
 437             layoutInArea(flow, x, y,
 438                     w, flowHeight,
 439                     baselineOffset, HPos.CENTER, VPos.CENTER);
 440         }
 441 
 442         // painting the overlay over the column being reordered
 443         if (tableHeaderRow.getReorderingRegion() != null) {
 444             TableColumnHeader reorderingColumnHeader = tableHeaderRow.getReorderingRegion();
 445             TableColumnBase reorderingColumn = reorderingColumnHeader.getTableColumn();
 446             if (reorderingColumn != null) {
 447                 Node n = tableHeaderRow.getReorderingRegion();
 448 
 449                 // determine where to draw the column header overlay, it's
 450                 // either from the left-edge of the column, or 0, if the column
 451                 // is off the left-side of the TableView (i.e. horizontal
 452                 // scrolling has occured).
 453                 double minX = tableHeaderRow.sceneToLocal(n.localToScene(n.getBoundsInLocal())).getMinX();
 454                 double overlayWidth = reorderingColumnHeader.getWidth();
 455                 if (minX < 0) {
 456                     overlayWidth += minX;
 457                 }
 458                 minX = minX < 0 ? 0 : minX;
 459 
 460                 // prevent the overlay going out the right-hand side of the
 461                 // TableView
 462                 if (minX + overlayWidth > w) {
 463                     overlayWidth = w - minX;
 464 
 465                     if (flow.getVbar().isVisible()) {
 466                         overlayWidth -= flow.getVbar().getWidth() - 1;
 467                     }
 468                 }
 469 
 470                 double contentAreaHeight = flowHeight;
 471                 if (flow.getHbar().isVisible()) {
 472                     contentAreaHeight -= flow.getHbar().getHeight();
 473                 }
 474 
 475                 columnReorderOverlay.resize(overlayWidth, contentAreaHeight);
 476 
 477                 columnReorderOverlay.setLayoutX(minX);
 478                 columnReorderOverlay.setLayoutY(tableHeaderRow.getHeight());
 479             }
 480 
 481             // paint the reorder line as well
 482             double cw = columnReorderLine.snappedLeftInset() + columnReorderLine.snappedRightInset();
 483             double lineHeight = h - (flow.getHbar().isVisible() ? flow.getHbar().getHeight() - 1 : 0);
 484             columnReorderLine.resizeRelocate(0, columnReorderLine.snappedTopInset(), cw, lineHeight);
 485         }
 486 
 487         columnReorderLine.setVisible(tableHeaderRow.isReordering());
 488         columnReorderOverlay.setVisible(tableHeaderRow.isReordering());
 489 
 490         checkContentWidthState();
 491     }
 492 
 493     /**
 494      * Creates a new TableHeaderRow instance. By default this method should not be overridden, but in some
 495      * circumstances it makes sense (e.g. testing, or when extreme customization is desired).
 496      *
 497      * @return A new TableHeaderRow instance.
 498      */
 499     protected TableHeaderRow createTableHeaderRow() {
 500         return new TableHeaderRow(this);
 501     }
 502 
 503 
 504 
 505     /***************************************************************************
 506      *                                                                         *
 507      * Private implementation                                                  *
 508      *                                                                         *
 509      **************************************************************************/
 510 
 511     final TableHeaderRow getTableHeaderRow() {
 512         return tableHeaderRow;
 513     }
 514 
 515     private TableSelectionModel<S> getSelectionModel() {
 516         return TableSkinUtils.getSelectionModel(this);
 517     }
 518 
 519     private TableFocusModel<M,?> getFocusModel() {
 520         return TableSkinUtils.getFocusModel(this);
 521     }
 522 
 523     // returns the currently focused cell in the focus model
 524     private TablePositionBase<? extends TC> getFocusedCell() {
 525         return TableSkinUtils.getFocusedCell(this);
 526     }
 527 
 528     // returns an ObservableList of the visible leaf columns of the control
 529     private ObservableList<? extends TC> getVisibleLeafColumns() {
 530         return TableSkinUtils.getVisibleLeafColumns(this);
 531     }
 532 
 533     /** {@inheritDoc} */
 534     @Override protected void updateItemCount() {
 535         updatePlaceholderRegionVisibility();
 536 
 537         int oldCount = itemCount;
 538         int newCount = getItemCount();
 539 
 540         itemCount = newCount;
 541 
 542         if (itemCount == 0) {
 543             flow.getHbar().setValue(0.0);
 544         }
 545 
 546         // if this is not called even when the count is the same, we get a
 547         // memory leak in VirtualFlow.sheet.children. This can probably be
 548         // optimised in the future when time permits.
 549         flow.setCellCount(newCount);
 550 
 551         if (newCount != oldCount) {
 552             // FIXME updateItemCount is called _a lot_. Perhaps we can make rebuildCells
 553             // smarter. Imagine if items has one million items added - do we really
 554             // need to rebuildCells a million times? Maybe this is better now that
 555             // we do rebuildCells instead of recreateCells.
 556             requestRebuildCells();
 557         } else {
 558             needCellsReconfigured = true;
 559         }
 560     }
 561 
 562     private void checkContentWidthState() {
 563         // we test for item count here to resolve RT-14855, where the column
 564         // widths weren't being resized properly when in constrained layout mode
 565         // if there were no items.
 566         if (contentWidthDirty || getItemCount() == 0) {
 567             updateContentWidth();
 568             contentWidthDirty = false;
 569         }
 570     }
 571 
 572     void horizontalScroll() {
 573         tableHeaderRow.updateScrollX();
 574     }
 575 
 576     void onFocusPreviousCell() {
 577         TableFocusModel<M,?> fm = getFocusModel();
 578         if (fm == null) return;
 579 
 580         flow.scrollTo(fm.getFocusedIndex());
 581     }
 582 
 583     void onFocusNextCell() {
 584         TableFocusModel<M,?> fm = getFocusModel();
 585         if (fm == null) return;
 586 
 587         flow.scrollTo(fm.getFocusedIndex());
 588     }
 589 
 590     void onSelectPreviousCell() {
 591         SelectionModel<S> sm = getSelectionModel();
 592         if (sm == null) return;
 593 
 594         flow.scrollTo(sm.getSelectedIndex());
 595     }
 596 
 597     void onSelectNextCell() {
 598         SelectionModel<S> sm = getSelectionModel();
 599         if (sm == null) return;
 600 
 601         flow.scrollTo(sm.getSelectedIndex());
 602     }
 603 
 604     void onSelectLeftCell() {
 605         scrollHorizontally();
 606     }
 607 
 608     void onSelectRightCell() {
 609         scrollHorizontally();
 610     }
 611 
 612     void onMoveToFirstCell() {
 613         flow.scrollTo(0);
 614         flow.setPosition(0);
 615     }
 616 
 617     void onMoveToLastCell() {
 618         int endPos = getItemCount();
 619         flow.scrollTo(endPos);
 620         flow.setPosition(1);
 621     }
 622 
 623     private void updateTableItems(ObservableList<S> oldList, ObservableList<S> newList) {
 624         if (oldList != null) {
 625             oldList.removeListener(weakRowCountListener);
 626         }
 627 
 628         if (newList != null) {
 629             newList.addListener(weakRowCountListener);
 630         }
 631 
 632         markItemCountDirty();
 633         getSkinnable().requestLayout();
 634     }
 635 
 636     Region getColumnReorderLine() {
 637         return columnReorderLine;
 638     }
 639 
 640     /**
 641      * Function used to scroll the container down by one 'page', although
 642      * if this is a horizontal container, then the scrolling will be to the right.
 643      */
 644     int onScrollPageDown(boolean isFocusDriven) {
 645         TableSelectionModel<S> sm = getSelectionModel();
 646         if (sm == null) return -1;
 647 
 648         final int itemCount = getItemCount();
 649 
 650         I lastVisibleCell = flow.getLastVisibleCellWithinViewPort();
 651         if (lastVisibleCell == null) return -1;
 652 
 653         int lastVisibleCellIndex = lastVisibleCell.getIndex();
 654 
 655         // we include this test here as the virtual flow will return cells that
 656         // exceed past the item count, so we need to clamp here (and further down
 657         // in this method also). See RT-19053 for more information.
 658         lastVisibleCellIndex = lastVisibleCellIndex >= itemCount ? itemCount - 1 : lastVisibleCellIndex;
 659 
 660         // isSelected represents focus OR selection
 661         boolean isSelected;
 662         if (isFocusDriven) {
 663             isSelected = lastVisibleCell.isFocused() || isCellFocused(lastVisibleCellIndex);
 664         } else {
 665             isSelected = lastVisibleCell.isSelected() || isCellSelected(lastVisibleCellIndex);
 666         }
 667 
 668         if (isSelected) {
 669             boolean isLeadIndex = isLeadIndex(isFocusDriven, lastVisibleCellIndex);
 670 
 671             if (isLeadIndex) {
 672                 // if the last visible cell is selected, we want to shift that cell up
 673                 // to be the top-most cell, or at least as far to the top as we can go.
 674                 flow.scrollToTop(lastVisibleCell);
 675 
 676                 I newLastVisibleCell = flow.getLastVisibleCellWithinViewPort();
 677                 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell;
 678             }
 679         }
 680 
 681         int newSelectionIndex = lastVisibleCell.getIndex();
 682         newSelectionIndex = newSelectionIndex >= itemCount ? itemCount - 1 : newSelectionIndex;
 683         flow.scrollTo(newSelectionIndex);
 684         return newSelectionIndex;
 685     }
 686 
 687     /**
 688      * Function used to scroll the container up by one 'page', although
 689      * if this is a horizontal container, then the scrolling will be to the left.
 690      */
 691     int onScrollPageUp(boolean isFocusDriven) {
 692         I firstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
 693         if (firstVisibleCell == null) return -1;
 694 
 695         int firstVisibleCellIndex = firstVisibleCell.getIndex();
 696 
 697         // isSelected represents focus OR selection
 698         boolean isSelected = false;
 699         if (isFocusDriven) {
 700             isSelected = firstVisibleCell.isFocused() || isCellFocused(firstVisibleCellIndex);
 701         } else {
 702             isSelected = firstVisibleCell.isSelected() || isCellSelected(firstVisibleCellIndex);
 703         }
 704 
 705         if (isSelected) {
 706             boolean isLeadIndex = isLeadIndex(isFocusDriven, firstVisibleCellIndex);
 707 
 708             if (isLeadIndex) {
 709                 // if the first visible cell is selected, we want to shift that cell down
 710                 // to be the bottom-most cell, or at least as far to the bottom as we can go.
 711                 flow.scrollToBottom(firstVisibleCell);
 712 
 713                 I newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort();
 714                 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell;
 715             }
 716         }
 717 
 718         int newSelectionIndex = firstVisibleCell.getIndex();
 719         flow.scrollTo(newSelectionIndex);
 720         return newSelectionIndex;
 721     }
 722 
 723     private boolean isLeadIndex(boolean isFocusDriven, int index) {
 724         final TableSelectionModel<S> sm = getSelectionModel();
 725         final FocusModel<M> fm = getFocusModel();
 726 
 727         return (isFocusDriven && fm.getFocusedIndex() == index)
 728                 || (! isFocusDriven && sm.getSelectedIndex() == index);
 729     }
 730 
 731     /**
 732      * Keeps track of how many leaf columns are currently visible in this table.
 733      */
 734     private void updateVisibleColumnCount() {
 735         visibleColCount = getVisibleLeafColumns().size();
 736 
 737         updatePlaceholderRegionVisibility();
 738         requestRebuildCells();
 739     }
 740 
 741     private void updateVisibleLeafColumnWidthListeners(
 742             List<? extends TC> added, List<? extends TC> removed) {
 743 
 744         for (int i = 0, max = removed.size(); i < max; i++) {
 745             TC tc = removed.get(i);
 746             tc.widthProperty().removeListener(weakWidthListener);
 747         }
 748         for (int i = 0, max = added.size(); i < max; i++) {
 749             TC tc = added.get(i);
 750             tc.widthProperty().addListener(weakWidthListener);
 751         }
 752         requestRebuildCells();
 753     }
 754 
 755     final void updatePlaceholderRegionVisibility() {
 756         boolean visible = visibleColCount == 0 || getItemCount() == 0;
 757 
 758         if (visible) {
 759             if (placeholderRegion == null) {
 760                 placeholderRegion = new StackPane();
 761                 placeholderRegion.getStyleClass().setAll("placeholder");
 762                 getChildren().add(placeholderRegion);
 763             }
 764 
 765             Node placeholderNode = TableSkinUtils.placeholderProperty(this).get();
 766 
 767             if (placeholderNode == null) {
 768                 if (placeholderLabel == null) {
 769                     placeholderLabel = new Label();
 770                 }
 771                 String s = visibleColCount == 0 ? NO_COLUMNS_TEXT : EMPTY_TABLE_TEXT;
 772                 placeholderLabel.setText(s);
 773 
 774                 placeholderRegion.getChildren().setAll(placeholderLabel);
 775             } else {
 776                 placeholderRegion.getChildren().setAll(placeholderNode);
 777             }
 778         }
 779 
 780         flow.setVisible(! visible);
 781         if (placeholderRegion != null) {
 782             placeholderRegion.setVisible(visible);
 783         }
 784     }
 785 
 786     /*
 787      * It's often important to know how much width is available for content
 788      * within the table, and this needs to exclude the width of any vertical
 789      * scrollbar.
 790      */
 791     private void updateContentWidth() {
 792         double contentWidth = flow.getWidth();
 793 
 794         if (flow.getVbar().isVisible()) {
 795             contentWidth -= flow.getVbar().getWidth();
 796         }
 797 
 798         if (contentWidth <= 0) {
 799             // Fix for RT-14855 when there is no content in the TableView.
 800             Control c = getSkinnable();
 801             contentWidth = c.getWidth() - (snappedLeftInset() + snappedRightInset());
 802         }
 803 
 804         contentWidth = Math.max(0.0, contentWidth);
 805 
 806         // FIXME this isn't perfect, but it prevents RT-14885, which results in
 807         // undesired horizontal scrollbars when in constrained resize mode
 808         getSkinnable().getProperties().put("TableView.contentWidth", Math.floor(contentWidth));
 809     }
 810 
 811     private void refreshView() {
 812         markItemCountDirty();
 813         Control c = getSkinnable();
 814         if (c != null) {
 815             c.requestLayout();
 816         }
 817     }
 818 
 819     // Handles the horizontal scrolling when the selection mode is cell-based
 820     // and the newly selected cell belongs to a column which is not totally
 821     // visible.
 822     void scrollHorizontally() {
 823         TableFocusModel<M,?> fm = getFocusModel();
 824         if (fm == null) return;
 825 
 826         TC col = getFocusedCell().getTableColumn();
 827         scrollHorizontally(col);
 828     }
 829 
 830     void scrollHorizontally(TC col) {
 831         if (col == null || !col.isVisible()) return;
 832 
 833         final Control control = getSkinnable();
 834 
 835         // RT-37060 - if we are trying to scroll to a column that has not
 836         // yet even been rendered, we must wait until the layout pass has
 837         // happened and then do the scroll. The laziest way to do this is to
 838         // queue up the task to run later, at which point we will have hopefully
 839         // fully run the column through layout and css.
 840         TableColumnHeader header = tableHeaderRow.getColumnHeaderFor(col);
 841         if (header == null || header.getWidth() <= 0) {
 842             Platform.runLater(() -> scrollHorizontally(col));
 843             return;
 844         }
 845 
 846         // work out where this column header is, and it's width (start -> end)
 847         double start = 0;
 848         for (TC c : getVisibleLeafColumns()) {
 849             if (c.equals(col)) break;
 850             start += c.getWidth();
 851         }
 852         double end = start + col.getWidth();
 853 
 854         // determine the visible width of the table
 855         double headerWidth = control.getWidth() - snappedLeftInset() - snappedRightInset();
 856 
 857         // determine by how much we need to translate the table to ensure that
 858         // the start position of this column lines up with the left edge of the
 859         // tableview, and also that the columns don't become detached from the
 860         // right edge of the table
 861         double pos = flow.getHbar().getValue();
 862         double max = flow.getHbar().getMax();
 863         double newPos;
 864 
 865         if (start < pos && start >= 0) {
 866             newPos = start;
 867         } else {
 868             double delta = start < 0 || end > headerWidth ? start - pos : 0;
 869             newPos = pos + delta > max ? max : pos + delta;
 870         }
 871 
 872         // FIXME we should add API in VirtualFlow so we don't end up going
 873         // direct to the hbar.
 874         // actually shift the flow - this will result in the header moving
 875         // as well
 876         flow.getHbar().setValue(newPos);
 877     }
 878 
 879     private boolean isCellSelected(int row) {
 880         TableSelectionModel<S> sm = getSelectionModel();
 881         if (sm == null) return false;
 882         if (! sm.isCellSelectionEnabled()) return false;
 883 
 884         int columnCount = getVisibleLeafColumns().size();
 885         for (int col = 0; col < columnCount; col++) {
 886             if (sm.isSelected(row, TableSkinUtils.getVisibleLeafColumn(this,col))) {
 887                 return true;
 888             }
 889         }
 890 
 891         return false;
 892     }
 893 
 894     private boolean isCellFocused(int row) {
 895         TableFocusModel<S,TC> fm = (TableFocusModel<S,TC>)(Object)getFocusModel();
 896         if (fm == null) return false;
 897 
 898         int columnCount = getVisibleLeafColumns().size();
 899         for (int col = 0; col < columnCount; col++) {
 900             if (fm.isFocused(row, TableSkinUtils.getVisibleLeafColumn(this,col))) {
 901                 return true;
 902             }
 903         }
 904 
 905         return false;
 906     }
 907 
 908 
 909 
 910     /***************************************************************************
 911      *                                                                         *
 912      * A11y                                                                    *
 913      *                                                                         *
 914      **************************************************************************/
 915 
 916     /** {@inheritDoc} */
 917     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 918         switch (attribute) {
 919             case FOCUS_ITEM: {
 920                 TableFocusModel<M,?> fm = getFocusModel();
 921                 int focusedIndex = fm.getFocusedIndex();
 922                 if (focusedIndex == -1) {
 923                     if (placeholderRegion != null && placeholderRegion.isVisible()) {
 924                         return placeholderRegion.getChildren().get(0);
 925                     }
 926                     if (getItemCount() > 0) {
 927                         focusedIndex = 0;
 928                     } else {
 929                         return null;
 930                     }
 931                 }
 932                 return flow.getPrivateCell(focusedIndex);
 933             }
 934             case CELL_AT_ROW_COLUMN: {
 935                 int rowIndex = (Integer)parameters[0];
 936                 return flow.getPrivateCell(rowIndex);
 937             }
 938             case COLUMN_AT_INDEX: {
 939                 int index = (Integer)parameters[0];
 940                 TableColumnBase<S,?> column = TableSkinUtils.getVisibleLeafColumn(this,index);
 941                 return getTableHeaderRow().getColumnHeaderFor(column);
 942             }
 943             case HEADER: {
 944                 /* Not sure how this is used by Accessibility, but without this VoiceOver will not
 945                  * look for column headers */
 946                 return getTableHeaderRow();
 947             }
 948             case VERTICAL_SCROLLBAR: return flow.getVbar();
 949             case HORIZONTAL_SCROLLBAR: return flow.getHbar();
 950             default: return super.queryAccessibleAttribute(attribute, parameters);
 951         }
 952     }
 953 
 954 }