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