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 
  29 import java.lang.ref.Reference;
  30 import java.lang.ref.WeakReference;
  31 import java.util.*;
  32 
  33 import com.sun.javafx.PlatformUtil;
  34 import javafx.animation.FadeTransition;
  35 import javafx.beans.property.DoubleProperty;
  36 import javafx.beans.property.ObjectProperty;
  37 import javafx.collections.ListChangeListener;
  38 import javafx.collections.ObservableList;
  39 import javafx.collections.WeakListChangeListener;
  40 import javafx.css.StyleOrigin;
  41 import javafx.css.StyleableObjectProperty;
  42 import javafx.geometry.Insets;
  43 import javafx.geometry.Pos;
  44 import javafx.scene.Node;
  45 import javafx.scene.Parent;
  46 import javafx.scene.control.*;
  47 import javafx.util.Duration;
  48 
  49 import com.sun.javafx.tk.Toolkit;
  50 
  51 /**
  52  * TableRowSkinBase is the base skin class used by controls such as
  53  * {@link javafx.scene.control.TableRow} and {@link javafx.scene.control.TreeTableRow}
  54  * (the concrete classes are {@link TableRowSkin} and {@link TreeTableRowSkin},
  55  * respectively).
  56  *
  57  * @param <T> The type of the cell (i.e. the generic type of the {@link IndexedCell} subclass).
  58  * @param <C> The cell type (e.g. TableRow or TreeTableRow)
  59  * @param <R> The type of cell that is contained within each row (e.g.
  60  *           {@link javafx.scene.control.TableCell or {@link javafx.scene.control.TreeTableCell}}).
  61  *
  62  * @since 9
  63  * @see javafx.scene.control.TableRow
  64  * @see javafx.scene.control.TreeTableRow
  65  * @see TableRowSkin
  66  * @see TreeTableRowSkin
  67  */
  68 public abstract class TableRowSkinBase<T,
  69                                        C extends IndexedCell/*<T>*/,
  70                                        R extends IndexedCell> extends CellSkinBase<C> {
  71 
  72     /***************************************************************************
  73      *                                                                         *
  74      * Static Fields                                                           *
  75      *                                                                         *
  76      **************************************************************************/
  77 
  78     // There appears to be a memory leak when using the stub toolkit. Therefore,
  79     // to prevent tests from failing we disable the animations below when the
  80     // stub toolkit is being used.
  81     // Filed as RT-29163.
  82     private static boolean IS_STUB_TOOLKIT = Toolkit.getToolkit().toString().contains("StubToolkit");
  83 
  84     // lets save the CPU and not do animations when on embedded platforms
  85     private static boolean DO_ANIMATIONS = ! IS_STUB_TOOLKIT && ! PlatformUtil.isEmbedded();
  86 
  87     private static final Duration FADE_DURATION = Duration.millis(200);
  88 
  89     /*
  90      * This is rather hacky - but it is a quick workaround to resolve the
  91      * issue that we don't know maximum width of a disclosure node for a given
  92      * control. If we don't know the maximum width, we have no way to ensure
  93      * consistent indentation.
  94      *
  95      * To work around this, we create a single WeakHashMap to store a max
  96      * disclosureNode width per TableColumnBase. We use WeakHashMap to help prevent
  97      * any memory leaks.
  98      */
  99     static final Map<TableColumnBase<?,?>, Double> maxDisclosureWidthMap = new WeakHashMap<>();
 100 
 101     // Specifies the number of times we will call 'recreateCells()' before we blow
 102     // out the cellsMap structure and rebuild all cells. This helps to prevent
 103     // against memory leaks in certain extreme circumstances.
 104     private static final int DEFAULT_FULL_REFRESH_COUNTER = 100;
 105 
 106 
 107 
 108     /***************************************************************************
 109      *                                                                         *
 110      * Private Fields                                                          *
 111      *                                                                         *
 112      **************************************************************************/
 113 
 114     /*
 115      * A map that maps from TableColumn to TableCell (i.e. model to view).
 116      * This is recreated whenever the leaf columns change, however to increase
 117      * efficiency we create cells for all columns, even if they aren't visible,
 118      * and we only create new cells if we don't already have it cached in this
 119      * map.
 120      *
 121      * Note that this means that it is possible for this map to therefore be
 122      * a memory leak if an application uses TableView and is creating and removing
 123      * a large number of tableColumns. This is mitigated in the recreateCells()
 124      * function below - refer to that to learn more.
 125      */
 126     WeakHashMap<TableColumnBase, Reference<R>> cellsMap;
 127 
 128     // This observableArrayList contains the currently visible table cells for this row.
 129     final List<R> cells = new ArrayList<>();
 130 
 131     private int fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER;
 132 
 133     boolean isDirty = false;
 134     boolean updateCells = false;
 135 
 136     double fixedCellSize;
 137     boolean fixedCellSizeEnabled;
 138 
 139 
 140 
 141     /***************************************************************************
 142      *                                                                         *
 143      * Constructors                                                            *
 144      *                                                                         *
 145      **************************************************************************/
 146 
 147     /**
 148      * Creates a new instance of TableRowSkinBase, although note that this
 149      * instance does not handle any behavior / input mappings - this needs to be
 150      * handled appropriately by subclasses.
 151      *
 152      * @param control The control that this skin should be installed onto.
 153      */
 154     public TableRowSkinBase(C control) {
 155         super(control);
 156         getSkinnable().setPickOnBounds(false);
 157 
 158         recreateCells();
 159         updateCells(true);
 160 
 161         // init bindings
 162         // watches for any change in the leaf columns observableArrayList - this will indicate
 163         // that the column order has changed and that we should update the row
 164         // such that the cells are in the new order
 165         getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
 166         // --- end init bindings
 167 
 168 
 169         // use invalidation listener here to update even when item equality is true
 170         // (e.g. see RT-22463)
 171         control.itemProperty().addListener(o -> requestCellUpdate());
 172         registerChangeListener(control.indexProperty(), e -> {
 173             // Fix for RT-36661, where empty table cells were showing content, as they
 174             // had incorrect table cell indices (but the table row index was correct).
 175             // Note that we only do the update on empty cells to avoid the issue
 176             // noted below in requestCellUpdate().
 177             if (getSkinnable().isEmpty()) {
 178                 requestCellUpdate();
 179             }
 180         });
 181     }
 182 
 183 
 184 
 185     /***************************************************************************
 186      *                                                                         *
 187      * Listeners                                                               *
 188      *                                                                         *
 189      **************************************************************************/
 190 
 191     private ListChangeListener<TableColumnBase> visibleLeafColumnsListener = c -> {
 192         isDirty = true;
 193         getSkinnable().requestLayout();
 194     };
 195 
 196     private WeakListChangeListener<TableColumnBase> weakVisibleLeafColumnsListener =
 197             new WeakListChangeListener<>(visibleLeafColumnsListener);
 198 
 199 
 200 
 201     /***************************************************************************
 202      *                                                                         *
 203      * Abstract Methods                                                        *
 204      *                                                                         *
 205      **************************************************************************/
 206 
 207     /**
 208      * Creates a new cell instance that is suitable for representing the given table column instance.
 209      * @param tc the table column
 210      * @return the created cell
 211      */
 212     protected abstract R createCell(TableColumnBase<T,?> tc);
 213 
 214     /**
 215      * A method to allow the given cell to be told that it is a member of the given row.
 216      * How this is implemented is dependent on the actual cell implementation.
 217      * @param cell The cell for which we want to inform it of its owner row.
 218      * @param row The row which will be set on the given cell.
 219      */
 220     protected abstract void updateCell(R cell, C row);
 221 
 222     /**
 223      * Returns the {@link TableColumnBase} instance for the given cell instance.
 224      * @param cell The cell for which a TableColumn is desired.
 225      * @return the table column
 226      */
 227     protected abstract TableColumnBase<T,?> getTableColumn(R cell);
 228 
 229     /**
 230      * Returns an unmodifiable list containing the currently visible leaf columns.
 231      * @return the list of visible leaf columns
 232      */
 233     protected abstract ObservableList<? extends TableColumnBase/*<T,?>*/> getVisibleLeafColumns();
 234 
 235 
 236 
 237     /***************************************************************************
 238      *                                                                         *
 239      * Public Methods                                                          *
 240      *                                                                         *
 241      **************************************************************************/
 242 
 243     /**
 244      * Returns the graphic to draw on the inside of the disclosure node. Null
 245      * is acceptable when no graphic should be shown. Commonly this is the
 246      * graphic associated with a TreeItem (i.e. treeItem.getGraphic()), rather
 247      * than a graphic associated with a cell.
 248      * @return the graphic to draw on the inside of the disclosure node
 249      */
 250     protected ObjectProperty<Node> graphicProperty() {
 251         return null;
 252     }
 253 
 254     /** {@inheritDoc} */
 255     @Override protected void layoutChildren(double x, final double y, final double w, final double h) {
 256         checkState();
 257         if (cellsMap.isEmpty()) return;
 258 
 259         ObservableList<? extends TableColumnBase> visibleLeafColumns = getVisibleLeafColumns();
 260         if (visibleLeafColumns.isEmpty()) {
 261             super.layoutChildren(x,y,w,h);
 262             return;
 263         }
 264 
 265         C control = getSkinnable();
 266 
 267         ///////////////////////////////////////////
 268         // indentation code starts here
 269         ///////////////////////////////////////////
 270         double leftMargin = 0;
 271         double disclosureWidth = 0;
 272         double graphicWidth = 0;
 273         boolean indentationRequired = isIndentationRequired();
 274         boolean disclosureVisible = isDisclosureNodeVisible();
 275         int indentationColumnIndex = 0;
 276         Node disclosureNode = null;
 277         if (indentationRequired) {
 278             // Determine the column in which we want to put the disclosure node.
 279             // By default it is null, which means the 0th column should be
 280             // where the indentation occurs.
 281             TableColumnBase<?,?> treeColumn = getTreeColumn();
 282             indentationColumnIndex = treeColumn == null ? 0 : visibleLeafColumns.indexOf(treeColumn);
 283             indentationColumnIndex = indentationColumnIndex < 0 ? 0 : indentationColumnIndex;
 284 
 285             int indentationLevel = getIndentationLevel(control);
 286             if (! isShowRoot()) indentationLevel--;
 287             final double indentationPerLevel = getIndentationPerLevel();
 288             leftMargin = indentationLevel * indentationPerLevel;
 289 
 290             // position the disclosure node so that it is at the proper indent
 291             final double defaultDisclosureWidth = maxDisclosureWidthMap.containsKey(treeColumn) ?
 292                 maxDisclosureWidthMap.get(treeColumn) : 0;
 293             disclosureWidth = defaultDisclosureWidth;
 294 
 295             disclosureNode = getDisclosureNode();
 296             if (disclosureNode != null) {
 297                 disclosureNode.setVisible(disclosureVisible);
 298 
 299                 if (disclosureVisible) {
 300                     disclosureWidth = disclosureNode.prefWidth(h);
 301                     if (disclosureWidth > defaultDisclosureWidth) {
 302                         maxDisclosureWidthMap.put(treeColumn, disclosureWidth);
 303 
 304                         // RT-36359: The recorded max width of the disclosure node
 305                         // has increased. We need to go back and request all
 306                         // earlier rows to update themselves to take into account
 307                         // this increased indentation.
 308                         final VirtualFlow<C> flow = getVirtualFlow();
 309                         final int thisIndex = getSkinnable().getIndex();
 310                         for (int i = 0; i < flow.cells.size(); i++) {
 311                             C cell = flow.cells.get(i);
 312                             if (cell == null || cell.isEmpty()) continue;
 313                             cell.requestLayout();
 314                             cell.layout();
 315                         }
 316                     }
 317                 }
 318             }
 319         }
 320         ///////////////////////////////////////////
 321         // indentation code ends here
 322         ///////////////////////////////////////////
 323 
 324         // layout the individual column cells
 325         double width;
 326         double height;
 327 
 328         final double verticalPadding = snappedTopInset() + snappedBottomInset();
 329         final double horizontalPadding = snappedLeftInset() + snappedRightInset();
 330         final double controlHeight = control.getHeight();
 331 
 332         /**
 333          * RT-26743:TreeTableView: Vertical Line looks unfinished.
 334          * We used to not do layout on cells whose row exceeded the number
 335          * of items, but now we do so as to ensure we get vertical lines
 336          * where expected in cases where the vertical height exceeds the
 337          * number of items.
 338          */
 339         int index = control.getIndex();
 340         if (index < 0/* || row >= itemsProperty().get().size()*/) return;
 341 
 342         for (int column = 0, max = cells.size(); column < max; column++) {
 343             R tableCell = cells.get(column);
 344             TableColumnBase<T, ?> tableColumn = getTableColumn(tableCell);
 345 
 346             boolean isVisible = true;
 347             if (fixedCellSizeEnabled) {
 348                 // we determine if the cell is visible, and if not we have the
 349                 // ability to take it out of the scenegraph to help improve
 350                 // performance. However, we only do this when there is a
 351                 // fixed cell length specified in the TableView. This is because
 352                 // when we have a fixed cell length it is possible to know with
 353                 // certainty the height of each TableCell - it is the fixed value
 354                 // provided by the developer, and this means that we do not have
 355                 // to concern ourselves with the possibility that the height
 356                 // may be variable and / or dynamic.
 357                 isVisible = isColumnPartiallyOrFullyVisible(tableColumn);
 358 
 359                 height = fixedCellSize;
 360             } else {
 361                 height = Math.max(controlHeight, tableCell.prefHeight(-1));
 362                 height = snapSizeY(height) - snapSizeY(verticalPadding);
 363             }
 364 
 365             if (isVisible) {
 366                 if (fixedCellSizeEnabled && tableCell.getParent() == null) {
 367                     getChildren().add(tableCell);
 368                 }
 369 
 370                 width = tableCell.prefWidth(height) - snapSizeX(horizontalPadding);
 371 
 372                 // Added for RT-32700, and then updated for RT-34074.
 373                 // We change the alignment from CENTER_LEFT to TOP_LEFT if the
 374                 // height of the row is greater than the default size, and if
 375                 // the alignment is the default alignment.
 376                 // What I would rather do is only change the alignment if the
 377                 // alignment has not been manually changed, but for now this will
 378                 // do.
 379                 final boolean centreContent = h <= 24.0;
 380 
 381                 // if the style origin is null then the property has not been
 382                 // set (or it has been reset to its default), which means that
 383                 // we can set it without overwriting someone elses settings.
 384                 final StyleOrigin origin = ((StyleableObjectProperty<?>) tableCell.alignmentProperty()).getStyleOrigin();
 385                 if (! centreContent && origin == null) {
 386                     tableCell.setAlignment(Pos.TOP_LEFT);
 387                 }
 388                 // --- end of RT-32700 fix
 389 
 390                 ///////////////////////////////////////////
 391                 // further indentation code starts here
 392                 ///////////////////////////////////////////
 393                 if (indentationRequired && column == indentationColumnIndex) {
 394                     if (disclosureVisible) {
 395                         double ph = disclosureNode.prefHeight(disclosureWidth);
 396 
 397                         if (width > 0 && width < (disclosureWidth + leftMargin)) {
 398                             fadeOut(disclosureNode);
 399                         } else {
 400                             fadeIn(disclosureNode);
 401                             disclosureNode.resize(disclosureWidth, ph);
 402 
 403                             disclosureNode.relocate(x + leftMargin,
 404                                     centreContent ? (h / 2.0 - ph / 2.0) :
 405                                             (y + tableCell.getPadding().getTop()));
 406                             disclosureNode.toFront();
 407                         }
 408                     }
 409 
 410                     // determine starting point of the graphic or cell node, and the
 411                     // remaining width available to them
 412                     ObjectProperty<Node> graphicProperty = graphicProperty();
 413                     Node graphic = graphicProperty == null ? null : graphicProperty.get();
 414 
 415                     if (graphic != null) {
 416                         graphicWidth = graphic.prefWidth(-1) + 3;
 417                         double ph = graphic.prefHeight(graphicWidth);
 418 
 419                         if (width > 0 && width < disclosureWidth + leftMargin + graphicWidth) {
 420                             fadeOut(graphic);
 421                         } else {
 422                             fadeIn(graphic);
 423 
 424                             graphic.relocate(x + leftMargin + disclosureWidth,
 425                                     centreContent ? (h / 2.0 - ph / 2.0) :
 426                                             (y + tableCell.getPadding().getTop()));
 427 
 428                             graphic.toFront();
 429                         }
 430                     }
 431                 }
 432                 ///////////////////////////////////////////
 433                 // further indentation code ends here
 434                 ///////////////////////////////////////////
 435 
 436                 tableCell.resize(width, height);
 437                 tableCell.relocate(x, snappedTopInset());
 438 
 439                 // Request layout is here as (partial) fix for RT-28684.
 440                 // This does not appear to impact performance...
 441                 tableCell.requestLayout();
 442             } else {
 443                 width = snapSizeX(tableCell.prefWidth(-1)) - snapSizeX(horizontalPadding);
 444 
 445                 if (fixedCellSizeEnabled) {
 446                     // we only add/remove to the scenegraph if the fixed cell
 447                     // length support is enabled - otherwise we keep all
 448                     // TableCells in the scenegraph
 449                     getChildren().remove(tableCell);
 450                 }
 451             }
 452 
 453             x += width;
 454         }
 455     }
 456 
 457     int getIndentationLevel(C control) {
 458         return 0;
 459     }
 460 
 461     double getIndentationPerLevel() {
 462         return 0;
 463     }
 464 
 465     /**
 466      * Used to represent whether the current virtual flow owner is wanting
 467      * indentation to be used in this table row.
 468      */
 469     boolean isIndentationRequired() {
 470         return false;
 471     }
 472 
 473     /**
 474      * Returns the table column that should show the disclosure nodes and / or
 475      * a graphic. By default this is the left-most column.
 476      */
 477     TableColumnBase getTreeColumn() {
 478         return null;
 479     }
 480 
 481     Node getDisclosureNode() {
 482         return null;
 483     }
 484 
 485     /**
 486      * Used to represent whether a disclosure node is visible for _this_
 487      * table row. Not to be confused with isIndentationRequired(), which is the
 488      * more general API.
 489      */
 490     boolean isDisclosureNodeVisible() {
 491         return false;
 492     }
 493 
 494     boolean isShowRoot() {
 495         return true;
 496     }
 497 
 498     void updateCells(boolean resetChildren) {
 499         // To avoid a potential memory leak (when the TableColumns in the
 500         // TableView are created/inserted/removed/deleted, we have a 'refresh
 501         // counter' that when we reach 0 will delete all cells in this row
 502         // and recreate all of them.
 503         if (resetChildren) {
 504             if (fullRefreshCounter == 0) {
 505                 recreateCells();
 506             }
 507             fullRefreshCounter--;
 508         }
 509 
 510         // if clear isn't called first, we can run into situations where the
 511         // cells aren't updated properly.
 512         final boolean cellsEmpty = cells.isEmpty();
 513         cells.clear();
 514 
 515         final C skinnable = getSkinnable();
 516         final int skinnableIndex = skinnable.getIndex();
 517         final List<? extends TableColumnBase/*<T,?>*/> visibleLeafColumns = getVisibleLeafColumns();
 518 
 519         for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) {
 520             TableColumnBase<T,?> col = visibleLeafColumns.get(i);
 521 
 522             R cell = null;
 523             if (cellsMap.containsKey(col)) {
 524                 cell = cellsMap.get(col).get();
 525 
 526                 // the reference has been gc'd, remove key entry from map
 527                 if (cell == null) {
 528                     cellsMap.remove(col);
 529                 }
 530             }
 531 
 532             if (cell == null) {
 533                 // if the cell is null it means we don't have it in cache and
 534                 // need to create it
 535                 cell = createCellAndCache(col);
 536             }
 537 
 538             updateCell(cell, skinnable);
 539             cell.updateIndex(skinnableIndex);
 540             cells.add(cell);
 541         }
 542 
 543         // update children of each row
 544         if (fixedCellSizeEnabled) {
 545             // we leave the adding / removing up to the layoutChildren method mostly,
 546             // but here we remove any children cells that refer to columns that are
 547             // not visible
 548             List<Node> toRemove = new ArrayList<>();
 549             for (Node cell : getChildren()) {
 550                 if (! (cell instanceof IndexedCell)) continue;
 551                 if (!getTableColumn((R)cell).isVisible()) {
 552                     toRemove.add(cell);
 553                 }
 554             }
 555             getChildren().removeAll(toRemove);
 556         } else if (!fixedCellSizeEnabled && (resetChildren || cellsEmpty)) {
 557             getChildren().setAll(cells);
 558         }
 559     }
 560 
 561     VirtualFlow<C> getVirtualFlow() {
 562         Parent p = getSkinnable();
 563         while (p != null) {
 564             if (p instanceof VirtualFlow) {
 565                 return (VirtualFlow<C>) p;
 566             }
 567             p = p.getParent();
 568         }
 569         return null;
 570     }
 571 
 572     /** {@inheritDoc} */
 573     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 574         double prefWidth = 0.0;
 575         for (R cell : cells) {
 576             prefWidth += cell.prefWidth(height);
 577         }
 578         return prefWidth;
 579     }
 580 
 581     /** {@inheritDoc} */
 582     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 583         if (fixedCellSizeEnabled) {
 584             return fixedCellSize;
 585         }
 586 
 587         // fix for RT-29080
 588         checkState();
 589 
 590         // Support for RT-18467: making it easier to specify a height for
 591         // cells via CSS, where the desired height is less than the height
 592         // of the TableCells. Essentially, -fx-cell-size is given higher
 593         // precedence now
 594         if (getCellSize() < DEFAULT_CELL_SIZE) {
 595             return getCellSize();
 596         }
 597 
 598         // FIXME according to profiling, this method is slow and should
 599         // be optimised
 600         double prefHeight = 0.0f;
 601         final int count = cells.size();
 602         for (int i=0; i<count; i++) {
 603             final R tableCell = cells.get(i);
 604             prefHeight = Math.max(prefHeight, tableCell.prefHeight(-1));
 605         }
 606         double ph = Math.max(prefHeight, Math.max(getCellSize(), getSkinnable().minHeight(-1)));
 607 
 608         return ph;
 609     }
 610 
 611     /** {@inheritDoc} */
 612     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 613         if (fixedCellSizeEnabled) {
 614             return fixedCellSize;
 615         }
 616 
 617         // fix for RT-29080
 618         checkState();
 619 
 620         // Support for RT-18467: making it easier to specify a height for
 621         // cells via CSS, where the desired height is less than the height
 622         // of the TableCells. Essentially, -fx-cell-size is given higher
 623         // precedence now
 624         if (getCellSize() < DEFAULT_CELL_SIZE) {
 625             return getCellSize();
 626         }
 627 
 628         // FIXME according to profiling, this method is slow and should
 629         // be optimised
 630         double minHeight = 0.0f;
 631         final int count = cells.size();
 632         for (int i = 0; i < count; i++) {
 633             final R tableCell = cells.get(i);
 634             minHeight = Math.max(minHeight, tableCell.minHeight(-1));
 635         }
 636         return minHeight;
 637     }
 638 
 639     /** {@inheritDoc} */
 640     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 641         if (fixedCellSizeEnabled) {
 642             return fixedCellSize;
 643         }
 644         return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
 645     }
 646 
 647     final void checkState() {
 648         if (isDirty) {
 649             updateCells(true);
 650             isDirty = false;
 651             updateCells = false;
 652         } else if (updateCells) {
 653             updateCells(false);
 654             updateCells = false;
 655         }
 656     }
 657 
 658 
 659 
 660     /***************************************************************************
 661      *                                                                         *
 662      * Private Implementation                                                  *
 663      *                                                                         *
 664      **************************************************************************/
 665 
 666     private boolean isColumnPartiallyOrFullyVisible(TableColumnBase col) {
 667         if (col == null || !col.isVisible()) return false;
 668 
 669         final VirtualFlow<?> virtualFlow = getVirtualFlow();
 670         double scrollX = virtualFlow == null ? 0.0 : virtualFlow.getHbar().getValue();
 671 
 672         // work out where this column header is, and it's width (start -> end)
 673         double start = 0;
 674         final ObservableList<? extends TableColumnBase> visibleLeafColumns = getVisibleLeafColumns();
 675         for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) {
 676             TableColumnBase<?,?> c = visibleLeafColumns.get(i);
 677             if (c.equals(col)) break;
 678             start += c.getWidth();
 679         }
 680         double end = start + col.getWidth();
 681 
 682         // determine the width of the table
 683         final Insets padding = getSkinnable().getPadding();
 684         double headerWidth = getSkinnable().getWidth() - padding.getLeft() + padding.getRight();
 685 
 686         return (start >= scrollX || end > scrollX) && (start < (headerWidth + scrollX) || end <= (headerWidth + scrollX));
 687     }
 688 
 689     private void requestCellUpdate() {
 690         updateCells = true;
 691         getSkinnable().requestLayout();
 692 
 693         // update the index of all children cells (RT-29849).
 694         // Note that we do this after the TableRow item has been updated,
 695         // rather than when the TableRow index has changed (as this will be
 696         // before the row has updated its item). This will result in the
 697         // issue highlighted in RT-33602, where the table cell had the correct
 698         // item whilst the row had the old item.
 699         final int newIndex = getSkinnable().getIndex();
 700         for (int i = 0, max = cells.size(); i < max; i++) {
 701             cells.get(i).updateIndex(newIndex);
 702         }
 703     }
 704 
 705     private void recreateCells() {
 706         if (cellsMap != null) {
 707             Collection<Reference<R>> cells = cellsMap.values();
 708             Iterator<Reference<R>> cellsIter = cells.iterator();
 709             while (cellsIter.hasNext()) {
 710                 Reference<R> cellRef = cellsIter.next();
 711                 R cell = cellRef.get();
 712                 if (cell != null) {
 713                     cell.updateIndex(-1);
 714                     cell.getSkin().dispose();
 715                     cell.setSkin(null);
 716                 }
 717             }
 718             cellsMap.clear();
 719         }
 720 
 721         ObservableList<? extends TableColumnBase/*<T,?>*/> columns = getVisibleLeafColumns();
 722 
 723         cellsMap = new WeakHashMap<>(columns.size());
 724         fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER;
 725         getChildren().clear();
 726 
 727         for (TableColumnBase col : columns) {
 728             if (cellsMap.containsKey(col)) {
 729                 continue;
 730             }
 731 
 732             // create a TableCell for this column and store it in the cellsMap
 733             // for future use
 734             createCellAndCache(col);
 735         }
 736     }
 737 
 738     private R createCellAndCache(TableColumnBase<T,?> col) {
 739         // we must create a TableCell for this table column
 740         R cell = createCell(col);
 741 
 742         // and store this in our HashMap until needed
 743         cellsMap.put(col, new WeakReference<>(cell));
 744 
 745         return cell;
 746     }
 747 
 748     private void fadeOut(final Node node) {
 749         if (node.getOpacity() < 1.0) return;
 750 
 751         if (! DO_ANIMATIONS) {
 752             node.setOpacity(0);
 753             return;
 754         }
 755 
 756         final FadeTransition fader = new FadeTransition(FADE_DURATION, node);
 757         fader.setToValue(0.0);
 758         fader.play();
 759     }
 760 
 761     private void fadeIn(final Node node) {
 762         if (node.getOpacity() > 0.0) return;
 763 
 764         if (! DO_ANIMATIONS) {
 765             node.setOpacity(1);
 766             return;
 767         }
 768 
 769         final FadeTransition fader = new FadeTransition(FADE_DURATION, node);
 770         fader.setToValue(1.0);
 771         fader.play();
 772     }
 773 }