--- old/modules/controls/src/main/java/com/sun/javafx/scene/control/skin/TableRowSkinBase.java 2015-09-03 15:22:59.484896600 -0700 +++ /dev/null 2015-09-03 15:23:00.000000000 -0700 @@ -1,731 +0,0 @@ -/* - * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. Oracle designates this - * particular file as subject to the "Classpath" exception as provided - * by Oracle in the LICENSE file that accompanied this code. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -package com.sun.javafx.scene.control.skin; - - -import java.lang.ref.Reference; -import java.lang.ref.WeakReference; -import java.util.*; - -import com.sun.javafx.PlatformUtil; -import javafx.animation.FadeTransition; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ObjectProperty; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.collections.WeakListChangeListener; -import javafx.css.StyleOrigin; -import javafx.css.StyleableObjectProperty; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.Parent; -import javafx.scene.control.Control; -import javafx.scene.control.IndexedCell; -import javafx.scene.control.TableColumnBase; -import javafx.util.Duration; - -import com.sun.javafx.scene.control.behavior.CellBehaviorBase; -import com.sun.javafx.tk.Toolkit; - -public abstract class TableRowSkinBase*/, - B extends CellBehaviorBase, - R extends IndexedCell> extends CellSkinBase { - - /*************************************************************************** - * * - * Static Fields * - * * - **************************************************************************/ - - // There appears to be a memory leak when using the stub toolkit. Therefore, - // to prevent tests from failing we disable the animations below when the - // stub toolkit is being used. - // Filed as RT-29163. - private static boolean IS_STUB_TOOLKIT = Toolkit.getToolkit().toString().contains("StubToolkit"); - - // lets save the CPU and not do animations when on embedded platforms - private static boolean DO_ANIMATIONS = ! IS_STUB_TOOLKIT && ! PlatformUtil.isEmbedded(); - - private static final Duration FADE_DURATION = Duration.millis(200); - - /* - * This is rather hacky - but it is a quick workaround to resolve the - * issue that we don't know maximum width of a disclosure node for a given - * TreeView. If we don't know the maximum width, we have no way to ensure - * consistent indentation for a given TreeView. - * - * To work around this, we create a single WeakHashMap to store a max - * disclosureNode width per TreeView. We use WeakHashMap to help prevent - * any memory leaks. - */ - static final Map maxDisclosureWidthMap = new WeakHashMap(); - - // Specifies the number of times we will call 'recreateCells()' before we blow - // out the cellsMap structure and rebuild all cells. This helps to prevent - // against memory leaks in certain extreme circumstances. - private static final int DEFAULT_FULL_REFRESH_COUNTER = 100; - - - - /*************************************************************************** - * * - * Private Fields * - * * - **************************************************************************/ - - /* - * A map that maps from TableColumn to TableCell (i.e. model to view). - * This is recreated whenever the leaf columns change, however to increase - * efficiency we create cells for all columns, even if they aren't visible, - * and we only create new cells if we don't already have it cached in this - * map. - * - * Note that this means that it is possible for this map to therefore be - * a memory leak if an application uses TableView and is creating and removing - * a large number of tableColumns. This is mitigated in the recreateCells() - * function below - refer to that to learn more. - */ - protected WeakHashMap> cellsMap; - - // This observableArrayList contains the currently visible table cells for this row. - protected final List cells = new ArrayList(); - - private int fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER; - - protected boolean isDirty = false; - protected boolean updateCells = false; - - private double fixedCellSize; - private boolean fixedCellSizeEnabled; - - - - /*************************************************************************** - * * - * Constructors * - * * - **************************************************************************/ - - public TableRowSkinBase(C control, B behavior) { - super(control, behavior); - - // init(control) should not be called here - it should be called by the - // subclass after initialising itself. This is to prevent NPEs (for - // example, getVisibleLeafColumns() throws a NPE as the control itself - // is not yet set in subclasses). - } - - // init isn't a constructor, but it is part of the initialisation routine - protected void init(C control) { - getSkinnable().setPickOnBounds(false); - - recreateCells(); - updateCells(true); - - // init bindings - // watches for any change in the leaf columns observableArrayList - this will indicate - // that the column order has changed and that we should update the row - // such that the cells are in the new order - getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener); - // --- end init bindings - - - // use invalidation listener here to update even when item equality is true - // (e.g. see RT-22463) - control.itemProperty().addListener(o -> requestCellUpdate()); - registerChangeListener(control.indexProperty(), "INDEX"); - - if (fixedCellSizeProperty() != null) { - registerChangeListener(fixedCellSizeProperty(), "FIXED_CELL_SIZE"); - fixedCellSize = fixedCellSizeProperty().get(); - fixedCellSizeEnabled = fixedCellSize > 0; - } - } - - - - /*************************************************************************** - * * - * Listeners * - * * - **************************************************************************/ - - private ListChangeListener visibleLeafColumnsListener = c -> { - isDirty = true; - getSkinnable().requestLayout(); - }; - - private WeakListChangeListener weakVisibleLeafColumnsListener = - new WeakListChangeListener(visibleLeafColumnsListener); - - - - /*************************************************************************** - * * - * Abstract Methods * - * * - **************************************************************************/ - - /** - * Returns the graphic to draw on the inside of the disclosure node. Null - * is acceptable when no graphic should be shown. Commonly this is the - * graphic associated with a TreeItem (i.e. treeItem.getGraphic()), rather - * than a graphic associated with a cell. - */ - protected abstract ObjectProperty graphicProperty(); - - // return TableView / TreeTableView / etc - protected abstract Control getVirtualFlowOwner(); - - protected abstract ObservableList*/> getVisibleLeafColumns(); - - // cell.updateTableRow(skinnable); (i.e cell.updateTableRow(row)) - protected abstract void updateCell(R cell, C row); - - protected abstract DoubleProperty fixedCellSizeProperty(); - - protected abstract boolean isColumnPartiallyOrFullyVisible(TableColumnBase tc); - - protected abstract R getCell(TableColumnBase tc); - - protected abstract TableColumnBase getTableColumnBase(R cell); - - - - /*************************************************************************** - * * - * Public Methods * - * * - **************************************************************************/ - - @Override protected void handleControlPropertyChanged(String p) { - super.handleControlPropertyChanged(p); - - if ("INDEX".equals(p)) { - // Fix for RT-36661, where empty table cells were showing content, as they - // had incorrect table cell indices (but the table row index was correct). - // Note that we only do the update on empty cells to avoid the issue - // noted below in requestCellUpdate(). - if (getSkinnable().isEmpty()) { - requestCellUpdate(); - } - } else if ("FIXED_CELL_SIZE".equals(p)) { - fixedCellSize = fixedCellSizeProperty().get(); - fixedCellSizeEnabled = fixedCellSize > 0; - } - } - - @Override protected void layoutChildren(double x, final double y, final double w, final double h) { - checkState(); - if (cellsMap.isEmpty()) return; - - ObservableList visibleLeafColumns = getVisibleLeafColumns(); - if (visibleLeafColumns.isEmpty()) { - super.layoutChildren(x,y,w,h); - return; - } - - C control = getSkinnable(); - - /////////////////////////////////////////// - // indentation code starts here - /////////////////////////////////////////// - double leftMargin = 0; - double disclosureWidth = 0; - double graphicWidth = 0; - boolean indentationRequired = isIndentationRequired(); - boolean disclosureVisible = isDisclosureNodeVisible(); - int indentationColumnIndex = 0; - Node disclosureNode = null; - if (indentationRequired) { - // Determine the column in which we want to put the disclosure node. - // By default it is null, which means the 0th column should be - // where the indentation occurs. - TableColumnBase treeColumn = getTreeColumn(); - indentationColumnIndex = treeColumn == null ? 0 : visibleLeafColumns.indexOf(treeColumn); - indentationColumnIndex = indentationColumnIndex < 0 ? 0 : indentationColumnIndex; - - int indentationLevel = getIndentationLevel(control); - if (! isShowRoot()) indentationLevel--; - final double indentationPerLevel = getIndentationPerLevel(); - leftMargin = indentationLevel * indentationPerLevel; - - // position the disclosure node so that it is at the proper indent - Control c = getVirtualFlowOwner(); - final double defaultDisclosureWidth = maxDisclosureWidthMap.containsKey(c) ? - maxDisclosureWidthMap.get(c) : 0; - disclosureWidth = defaultDisclosureWidth; - - disclosureNode = getDisclosureNode(); - if (disclosureNode != null) { - disclosureNode.setVisible(disclosureVisible); - - if (disclosureVisible) { - disclosureWidth = disclosureNode.prefWidth(h); - if (disclosureWidth > defaultDisclosureWidth) { - maxDisclosureWidthMap.put(c, disclosureWidth); - - // RT-36359: The recorded max width of the disclosure node - // has increased. We need to go back and request all - // earlier rows to update themselves to take into account - // this increased indentation. - final VirtualFlow flow = getVirtualFlow(); - final int thisIndex = getSkinnable().getIndex(); - for (int i = 0; i < flow.cells.size(); i++) { - C cell = flow.cells.get(i); - if (cell == null || cell.isEmpty()) continue; - cell.requestLayout(); - cell.layout(); - } - } - } - } - } - /////////////////////////////////////////// - // indentation code ends here - /////////////////////////////////////////// - - // layout the individual column cells - double width; - double height; - - final double verticalPadding = snappedTopInset() + snappedBottomInset(); - final double horizontalPadding = snappedLeftInset() + snappedRightInset(); - final double controlHeight = control.getHeight(); - - /** - * RT-26743:TreeTableView: Vertical Line looks unfinished. - * We used to not do layout on cells whose row exceeded the number - * of items, but now we do so as to ensure we get vertical lines - * where expected in cases where the vertical height exceeds the - * number of items. - */ - int index = control.getIndex(); - if (index < 0/* || row >= itemsProperty().get().size()*/) return; - - for (int column = 0, max = cells.size(); column < max; column++) { - R tableCell = cells.get(column); - TableColumnBase tableColumn = getTableColumnBase(tableCell); - - boolean isVisible = true; - if (fixedCellSizeEnabled) { - // we determine if the cell is visible, and if not we have the - // ability to take it out of the scenegraph to help improve - // performance. However, we only do this when there is a - // fixed cell length specified in the TableView. This is because - // when we have a fixed cell length it is possible to know with - // certainty the height of each TableCell - it is the fixed value - // provided by the developer, and this means that we do not have - // to concern ourselves with the possibility that the height - // may be variable and / or dynamic. - isVisible = isColumnPartiallyOrFullyVisible(tableColumn); - - height = fixedCellSize; - } else { - height = Math.max(controlHeight, tableCell.prefHeight(-1)); - height = snapSize(height) - snapSize(verticalPadding); - } - - if (isVisible) { - if (fixedCellSizeEnabled && tableCell.getParent() == null) { - getChildren().add(tableCell); - } - - width = snapSize(tableCell.prefWidth(-1)) - snapSize(horizontalPadding); - - // Added for RT-32700, and then updated for RT-34074. - // We change the alignment from CENTER_LEFT to TOP_LEFT if the - // height of the row is greater than the default size, and if - // the alignment is the default alignment. - // What I would rather do is only change the alignment if the - // alignment has not been manually changed, but for now this will - // do. - final boolean centreContent = h <= 24.0; - - // if the style origin is null then the property has not been - // set (or it has been reset to its default), which means that - // we can set it without overwriting someone elses settings. - final StyleOrigin origin = ((StyleableObjectProperty) tableCell.alignmentProperty()).getStyleOrigin(); - if (! centreContent && origin == null) { - tableCell.setAlignment(Pos.TOP_LEFT); - } - // --- end of RT-32700 fix - - /////////////////////////////////////////// - // further indentation code starts here - /////////////////////////////////////////// - if (indentationRequired && column == indentationColumnIndex) { - if (disclosureVisible) { - double ph = disclosureNode.prefHeight(disclosureWidth); - - if (width > 0 && width < (disclosureWidth + leftMargin)) { - fadeOut(disclosureNode); - } else { - fadeIn(disclosureNode); - disclosureNode.resize(disclosureWidth, ph); - - disclosureNode.relocate(x + leftMargin, - centreContent ? (h / 2.0 - ph / 2.0) : - (y + tableCell.getPadding().getTop())); - disclosureNode.toFront(); - } - } - - // determine starting point of the graphic or cell node, and the - // remaining width available to them - ObjectProperty graphicProperty = graphicProperty(); - Node graphic = graphicProperty == null ? null : graphicProperty.get(); - - if (graphic != null) { - graphicWidth = graphic.prefWidth(-1) + 3; - double ph = graphic.prefHeight(graphicWidth); - - if (width > 0 && width < disclosureWidth + leftMargin + graphicWidth) { - fadeOut(graphic); - } else { - fadeIn(graphic); - - graphic.relocate(x + leftMargin + disclosureWidth, - centreContent ? (h / 2.0 - ph / 2.0) : - (y + tableCell.getPadding().getTop())); - - graphic.toFront(); - } - } - } - /////////////////////////////////////////// - // further indentation code ends here - /////////////////////////////////////////// - - tableCell.resize(width, height); - tableCell.relocate(x, snappedTopInset()); - - // Request layout is here as (partial) fix for RT-28684. - // This does not appear to impact performance... - tableCell.requestLayout(); - } else { - if (fixedCellSizeEnabled) { - // we only add/remove to the scenegraph if the fixed cell - // length support is enabled - otherwise we keep all - // TableCells in the scenegraph - getChildren().remove(tableCell); - } - - width = snapSize(tableCell.prefWidth(-1)) - snapSize(horizontalPadding); - } - - x += width; - } - } - - protected int getIndentationLevel(C control) { - return 0; - } - - protected double getIndentationPerLevel() { - return 0; - } - - /** - * Used to represent whether the current virtual flow owner is wanting - * indentation to be used in this table row. - */ - protected boolean isIndentationRequired() { - return false; - } - - /** - * Returns the table column that should show the disclosure nodes and / or - * a graphic. By default this is the left-most column. - */ - protected TableColumnBase getTreeColumn() { - return null; - } - - protected Node getDisclosureNode() { - return null; - } - - /** - * Used to represent whether a disclosure node is visible for _this_ - * table row. Not to be confused with isIndentationRequired(), which is the - * more general API. - */ - protected boolean isDisclosureNodeVisible() { - return false; - } - - protected boolean isShowRoot() { - return true; - } - - protected TableColumnBase getVisibleLeafColumn(int column) { - final List*/> visibleLeafColumns = getVisibleLeafColumns(); - if (column < 0 || column >= visibleLeafColumns.size()) return null; - return visibleLeafColumns.get(column); - } - - protected void updateCells(boolean resetChildren) { - // To avoid a potential memory leak (when the TableColumns in the - // TableView are created/inserted/removed/deleted, we have a 'refresh - // counter' that when we reach 0 will delete all cells in this row - // and recreate all of them. - if (resetChildren) { - if (fullRefreshCounter == 0) { - recreateCells(); - } - fullRefreshCounter--; - } - - // if clear isn't called first, we can run into situations where the - // cells aren't updated properly. - final boolean cellsEmpty = cells.isEmpty(); - cells.clear(); - - final C skinnable = getSkinnable(); - final int skinnableIndex = skinnable.getIndex(); - final List*/> visibleLeafColumns = getVisibleLeafColumns(); - - for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) { - TableColumnBase col = visibleLeafColumns.get(i); - - R cell = null; - if (cellsMap.containsKey(col)) { - cell = cellsMap.get(col).get(); - - // the reference has been gc'd, remove key entry from map - if (cell == null) { - cellsMap.remove(col); - } - } - - if (cell == null) { - // if the cell is null it means we don't have it in cache and - // need to create it - cell = createCell(col); - } - - updateCell(cell, skinnable); - cell.updateIndex(skinnableIndex); - cells.add(cell); - } - - // update children of each row - if (!fixedCellSizeEnabled && (resetChildren || cellsEmpty)) { - getChildren().setAll(cells); - } - } - - private VirtualFlow getVirtualFlow() { - Parent p = getSkinnable(); - while (p != null) { - if (p instanceof VirtualFlow) { - return (VirtualFlow) p; - } - p = p.getParent(); - } - return null; - } - - @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - double prefWidth = 0.0; - - final List*/> visibleLeafColumns = getVisibleLeafColumns(); - for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) { - prefWidth += visibleLeafColumns.get(i).getWidth(); - } - - return prefWidth; - } - - @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - if (fixedCellSizeEnabled) { - return fixedCellSize; - } - - // fix for RT-29080 - checkState(); - - // Support for RT-18467: making it easier to specify a height for - // cells via CSS, where the desired height is less than the height - // of the TableCells. Essentially, -fx-cell-size is given higher - // precedence now - if (getCellSize() < CellSkinBase.DEFAULT_CELL_SIZE) { - return getCellSize(); - } - - // FIXME according to profiling, this method is slow and should - // be optimised - double prefHeight = 0.0f; - final int count = cells.size(); - for (int i=0; i> cells = cellsMap.values(); - Iterator> cellsIter = cells.iterator(); - while (cellsIter.hasNext()) { - Reference cellRef = cellsIter.next(); - R cell = cellRef.get(); - if (cell != null) { - cell.updateIndex(-1); - cell.getSkin().dispose(); - cell.setSkin(null); - } - } - cellsMap.clear(); - } - - ObservableList*/> columns = getVisibleLeafColumns(); - - cellsMap = new WeakHashMap>(columns.size()); - fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER; - getChildren().clear(); - - for (TableColumnBase col : columns) { - if (cellsMap.containsKey(col)) { - continue; - } - - // create a TableCell for this column and store it in the cellsMap - // for future use - createCell(col); - } - } - - private R createCell(TableColumnBase col) { - // we must create a TableCell for this table column - R cell = getCell(col); - - // and store this in our HashMap until needed - cellsMap.put(col, new WeakReference(cell)); - - return cell; - } - - private void fadeOut(final Node node) { - if (node.getOpacity() < 1.0) return; - - if (! DO_ANIMATIONS) { - node.setOpacity(0); - return; - } - - final FadeTransition fader = new FadeTransition(FADE_DURATION, node); - fader.setToValue(0.0); - fader.play(); - } - - private void fadeIn(final Node node) { - if (node.getOpacity() > 0.0) return; - - if (! DO_ANIMATIONS) { - node.setOpacity(1); - return; - } - - final FadeTransition fader = new FadeTransition(FADE_DURATION, node); - fader.setToValue(1.0); - fader.play(); - } -} --- /dev/null 2015-09-03 15:23:00.000000000 -0700 +++ new/modules/controls/src/main/java/javafx/scene/control/skin/TableRowSkinBase.java 2015-09-03 15:22:58.706852100 -0700 @@ -0,0 +1,738 @@ +/* + * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javafx.scene.control.skin; + + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.*; + +import com.sun.javafx.PlatformUtil; +import javafx.animation.FadeTransition; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.WeakListChangeListener; +import javafx.css.StyleOrigin; +import javafx.css.StyleableObjectProperty; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.control.Control; +import javafx.scene.control.IndexedCell; +import javafx.scene.control.TableColumnBase; +import javafx.util.Duration; + +import com.sun.javafx.tk.Toolkit; + +/** + * TableRowSkinBase is the base skin class used by controls such as + * {@link javafx.scene.control.TableRow} and {@link javafx.scene.control.TreeTableRow} + * (the concrete classes are {@link TableRowSkin} and {@link TreeTableRowSkin}, + * respectively). + * + * @param The type of the cell (i.e. the generic type of the {@link IndexedCell} subclass). + * @param The cell type (e.g. TableRow or TreeTableRow) + * @param The type of cell that is contained within each row (e.g. + * {@link javafx.scene.control.TableCell or {@link javafx.scene.control.TreeTableCell}}). + * + * @since 9 + * @see javafx.scene.control.TableRow + * @see javafx.scene.control.TreeTableRow + * @see TableRowSkin + * @see TreeTableRowSkin + */ +public abstract class TableRowSkinBase*/, + R extends IndexedCell> extends CellSkinBase { + + /*************************************************************************** + * * + * Static Fields * + * * + **************************************************************************/ + + // There appears to be a memory leak when using the stub toolkit. Therefore, + // to prevent tests from failing we disable the animations below when the + // stub toolkit is being used. + // Filed as RT-29163. + private static boolean IS_STUB_TOOLKIT = Toolkit.getToolkit().toString().contains("StubToolkit"); + + // lets save the CPU and not do animations when on embedded platforms + private static boolean DO_ANIMATIONS = ! IS_STUB_TOOLKIT && ! PlatformUtil.isEmbedded(); + + private static final Duration FADE_DURATION = Duration.millis(200); + + /* + * This is rather hacky - but it is a quick workaround to resolve the + * issue that we don't know maximum width of a disclosure node for a given + * TreeView. If we don't know the maximum width, we have no way to ensure + * consistent indentation for a given TreeView. + * + * To work around this, we create a single WeakHashMap to store a max + * disclosureNode width per TreeView. We use WeakHashMap to help prevent + * any memory leaks. + */ + static final Map maxDisclosureWidthMap = new WeakHashMap(); + + // Specifies the number of times we will call 'recreateCells()' before we blow + // out the cellsMap structure and rebuild all cells. This helps to prevent + // against memory leaks in certain extreme circumstances. + private static final int DEFAULT_FULL_REFRESH_COUNTER = 100; + + + + /*************************************************************************** + * * + * Private Fields * + * * + **************************************************************************/ + + /* + * A map that maps from TableColumn to TableCell (i.e. model to view). + * This is recreated whenever the leaf columns change, however to increase + * efficiency we create cells for all columns, even if they aren't visible, + * and we only create new cells if we don't already have it cached in this + * map. + * + * Note that this means that it is possible for this map to therefore be + * a memory leak if an application uses TableView and is creating and removing + * a large number of tableColumns. This is mitigated in the recreateCells() + * function below - refer to that to learn more. + */ + WeakHashMap> cellsMap; + + // This observableArrayList contains the currently visible table cells for this row. + final List cells = new ArrayList<>(); + + private int fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER; + + boolean isDirty = false; + boolean updateCells = false; + + private double fixedCellSize; + private boolean fixedCellSizeEnabled; + + + + /*************************************************************************** + * * + * Constructors * + * * + **************************************************************************/ + + /** + * Creates a new instance of TableRowSkinBase, although note that this + * instance does not handle any behavior / input mappings - this needs to be + * handled appropriately by subclasses. + * + * @param control The control that this skin should be installed onto. + */ + public TableRowSkinBase(C control) { + super(control); + getSkinnable().setPickOnBounds(false); + + recreateCells(); + updateCells(true); + + // init bindings + // watches for any change in the leaf columns observableArrayList - this will indicate + // that the column order has changed and that we should update the row + // such that the cells are in the new order + getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener); + // --- end init bindings + + + // use invalidation listener here to update even when item equality is true + // (e.g. see RT-22463) + control.itemProperty().addListener(o -> requestCellUpdate()); + registerChangeListener(control.indexProperty(), e -> { + // Fix for RT-36661, where empty table cells were showing content, as they + // had incorrect table cell indices (but the table row index was correct). + // Note that we only do the update on empty cells to avoid the issue + // noted below in requestCellUpdate(). + if (getSkinnable().isEmpty()) { + requestCellUpdate(); + } + }); + + if (fixedCellSizeProperty() != null) { + registerChangeListener(fixedCellSizeProperty(), e -> { + fixedCellSize = fixedCellSizeProperty().get(); + fixedCellSizeEnabled = fixedCellSize > 0; + }); + fixedCellSize = fixedCellSizeProperty().get(); + fixedCellSizeEnabled = fixedCellSize > 0; + } + } + + + + /*************************************************************************** + * * + * Listeners * + * * + **************************************************************************/ + + private ListChangeListener visibleLeafColumnsListener = c -> { + isDirty = true; + getSkinnable().requestLayout(); + }; + + private WeakListChangeListener weakVisibleLeafColumnsListener = + new WeakListChangeListener(visibleLeafColumnsListener); + + + + /*************************************************************************** + * * + * Abstract Methods * + * * + **************************************************************************/ + + /** + * Returns the graphic to draw on the inside of the disclosure node. Null + * is acceptable when no graphic should be shown. Commonly this is the + * graphic associated with a TreeItem (i.e. treeItem.getGraphic()), rather + * than a graphic associated with a cell. + */ + abstract ObjectProperty graphicProperty(); + + // return TableView / TreeTableView / etc + abstract Control getVirtualFlowOwner(); + + abstract ObservableList*/> getVisibleLeafColumns(); + + // cell.updateTableRow(skinnable); (i.e cell.updateTableRow(row)) + abstract void updateCell(R cell, C row); + + abstract DoubleProperty fixedCellSizeProperty(); + + abstract boolean isColumnPartiallyOrFullyVisible(TableColumnBase tc); + + abstract R getCell(TableColumnBase tc); + + abstract TableColumnBase getTableColumnBase(R cell); + + + + /*************************************************************************** + * * + * Public Methods * + * * + **************************************************************************/ + + /** {@inheritDoc} */ + @Override protected void layoutChildren(double x, final double y, final double w, final double h) { + checkState(); + if (cellsMap.isEmpty()) return; + + ObservableList visibleLeafColumns = getVisibleLeafColumns(); + if (visibleLeafColumns.isEmpty()) { + super.layoutChildren(x,y,w,h); + return; + } + + C control = getSkinnable(); + + /////////////////////////////////////////// + // indentation code starts here + /////////////////////////////////////////// + double leftMargin = 0; + double disclosureWidth = 0; + double graphicWidth = 0; + boolean indentationRequired = isIndentationRequired(); + boolean disclosureVisible = isDisclosureNodeVisible(); + int indentationColumnIndex = 0; + Node disclosureNode = null; + if (indentationRequired) { + // Determine the column in which we want to put the disclosure node. + // By default it is null, which means the 0th column should be + // where the indentation occurs. + TableColumnBase treeColumn = getTreeColumn(); + indentationColumnIndex = treeColumn == null ? 0 : visibleLeafColumns.indexOf(treeColumn); + indentationColumnIndex = indentationColumnIndex < 0 ? 0 : indentationColumnIndex; + + int indentationLevel = getIndentationLevel(control); + if (! isShowRoot()) indentationLevel--; + final double indentationPerLevel = getIndentationPerLevel(); + leftMargin = indentationLevel * indentationPerLevel; + + // position the disclosure node so that it is at the proper indent + Control c = getVirtualFlowOwner(); + final double defaultDisclosureWidth = maxDisclosureWidthMap.containsKey(c) ? + maxDisclosureWidthMap.get(c) : 0; + disclosureWidth = defaultDisclosureWidth; + + disclosureNode = getDisclosureNode(); + if (disclosureNode != null) { + disclosureNode.setVisible(disclosureVisible); + + if (disclosureVisible) { + disclosureWidth = disclosureNode.prefWidth(h); + if (disclosureWidth > defaultDisclosureWidth) { + maxDisclosureWidthMap.put(c, disclosureWidth); + + // RT-36359: The recorded max width of the disclosure node + // has increased. We need to go back and request all + // earlier rows to update themselves to take into account + // this increased indentation. + final VirtualFlow flow = getVirtualFlow(); + final int thisIndex = getSkinnable().getIndex(); + for (int i = 0; i < flow.cells.size(); i++) { + C cell = flow.cells.get(i); + if (cell == null || cell.isEmpty()) continue; + cell.requestLayout(); + cell.layout(); + } + } + } + } + } + /////////////////////////////////////////// + // indentation code ends here + /////////////////////////////////////////// + + // layout the individual column cells + double width; + double height; + + final double verticalPadding = snappedTopInset() + snappedBottomInset(); + final double horizontalPadding = snappedLeftInset() + snappedRightInset(); + final double controlHeight = control.getHeight(); + + /** + * RT-26743:TreeTableView: Vertical Line looks unfinished. + * We used to not do layout on cells whose row exceeded the number + * of items, but now we do so as to ensure we get vertical lines + * where expected in cases where the vertical height exceeds the + * number of items. + */ + int index = control.getIndex(); + if (index < 0/* || row >= itemsProperty().get().size()*/) return; + + for (int column = 0, max = cells.size(); column < max; column++) { + R tableCell = cells.get(column); + TableColumnBase tableColumn = getTableColumnBase(tableCell); + + boolean isVisible = true; + if (fixedCellSizeEnabled) { + // we determine if the cell is visible, and if not we have the + // ability to take it out of the scenegraph to help improve + // performance. However, we only do this when there is a + // fixed cell length specified in the TableView. This is because + // when we have a fixed cell length it is possible to know with + // certainty the height of each TableCell - it is the fixed value + // provided by the developer, and this means that we do not have + // to concern ourselves with the possibility that the height + // may be variable and / or dynamic. + isVisible = isColumnPartiallyOrFullyVisible(tableColumn); + + height = fixedCellSize; + } else { + height = Math.max(controlHeight, tableCell.prefHeight(-1)); + height = snapSize(height) - snapSize(verticalPadding); + } + + if (isVisible) { + if (fixedCellSizeEnabled && tableCell.getParent() == null) { + getChildren().add(tableCell); + } + + width = snapSize(tableCell.prefWidth(-1)) - snapSize(horizontalPadding); + + // Added for RT-32700, and then updated for RT-34074. + // We change the alignment from CENTER_LEFT to TOP_LEFT if the + // height of the row is greater than the default size, and if + // the alignment is the default alignment. + // What I would rather do is only change the alignment if the + // alignment has not been manually changed, but for now this will + // do. + final boolean centreContent = h <= 24.0; + + // if the style origin is null then the property has not been + // set (or it has been reset to its default), which means that + // we can set it without overwriting someone elses settings. + final StyleOrigin origin = ((StyleableObjectProperty) tableCell.alignmentProperty()).getStyleOrigin(); + if (! centreContent && origin == null) { + tableCell.setAlignment(Pos.TOP_LEFT); + } + // --- end of RT-32700 fix + + /////////////////////////////////////////// + // further indentation code starts here + /////////////////////////////////////////// + if (indentationRequired && column == indentationColumnIndex) { + if (disclosureVisible) { + double ph = disclosureNode.prefHeight(disclosureWidth); + + if (width > 0 && width < (disclosureWidth + leftMargin)) { + fadeOut(disclosureNode); + } else { + fadeIn(disclosureNode); + disclosureNode.resize(disclosureWidth, ph); + + disclosureNode.relocate(x + leftMargin, + centreContent ? (h / 2.0 - ph / 2.0) : + (y + tableCell.getPadding().getTop())); + disclosureNode.toFront(); + } + } + + // determine starting point of the graphic or cell node, and the + // remaining width available to them + ObjectProperty graphicProperty = graphicProperty(); + Node graphic = graphicProperty == null ? null : graphicProperty.get(); + + if (graphic != null) { + graphicWidth = graphic.prefWidth(-1) + 3; + double ph = graphic.prefHeight(graphicWidth); + + if (width > 0 && width < disclosureWidth + leftMargin + graphicWidth) { + fadeOut(graphic); + } else { + fadeIn(graphic); + + graphic.relocate(x + leftMargin + disclosureWidth, + centreContent ? (h / 2.0 - ph / 2.0) : + (y + tableCell.getPadding().getTop())); + + graphic.toFront(); + } + } + } + /////////////////////////////////////////// + // further indentation code ends here + /////////////////////////////////////////// + + tableCell.resize(width, height); + tableCell.relocate(x, snappedTopInset()); + + // Request layout is here as (partial) fix for RT-28684. + // This does not appear to impact performance... + tableCell.requestLayout(); + } else { + if (fixedCellSizeEnabled) { + // we only add/remove to the scenegraph if the fixed cell + // length support is enabled - otherwise we keep all + // TableCells in the scenegraph + getChildren().remove(tableCell); + } + + width = snapSize(tableCell.prefWidth(-1)) - snapSize(horizontalPadding); + } + + x += width; + } + } + + int getIndentationLevel(C control) { + return 0; + } + + double getIndentationPerLevel() { + return 0; + } + + /** + * Used to represent whether the current virtual flow owner is wanting + * indentation to be used in this table row. + */ + boolean isIndentationRequired() { + return false; + } + + /** + * Returns the table column that should show the disclosure nodes and / or + * a graphic. By default this is the left-most column. + */ + TableColumnBase getTreeColumn() { + return null; + } + + Node getDisclosureNode() { + return null; + } + + /** + * Used to represent whether a disclosure node is visible for _this_ + * table row. Not to be confused with isIndentationRequired(), which is the + * more general API. + */ + boolean isDisclosureNodeVisible() { + return false; + } + + boolean isShowRoot() { + return true; + } + + TableColumnBase getVisibleLeafColumn(int column) { + final List*/> visibleLeafColumns = getVisibleLeafColumns(); + if (column < 0 || column >= visibleLeafColumns.size()) return null; + return visibleLeafColumns.get(column); + } + + void updateCells(boolean resetChildren) { + // To avoid a potential memory leak (when the TableColumns in the + // TableView are created/inserted/removed/deleted, we have a 'refresh + // counter' that when we reach 0 will delete all cells in this row + // and recreate all of them. + if (resetChildren) { + if (fullRefreshCounter == 0) { + recreateCells(); + } + fullRefreshCounter--; + } + + // if clear isn't called first, we can run into situations where the + // cells aren't updated properly. + final boolean cellsEmpty = cells.isEmpty(); + cells.clear(); + + final C skinnable = getSkinnable(); + final int skinnableIndex = skinnable.getIndex(); + final List*/> visibleLeafColumns = getVisibleLeafColumns(); + + for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) { + TableColumnBase col = visibleLeafColumns.get(i); + + R cell = null; + if (cellsMap.containsKey(col)) { + cell = cellsMap.get(col).get(); + + // the reference has been gc'd, remove key entry from map + if (cell == null) { + cellsMap.remove(col); + } + } + + if (cell == null) { + // if the cell is null it means we don't have it in cache and + // need to create it + cell = createCell(col); + } + + updateCell(cell, skinnable); + cell.updateIndex(skinnableIndex); + cells.add(cell); + } + + // update children of each row + if (!fixedCellSizeEnabled && (resetChildren || cellsEmpty)) { + getChildren().setAll(cells); + } + } + + private VirtualFlow getVirtualFlow() { + Parent p = getSkinnable(); + while (p != null) { + if (p instanceof VirtualFlow) { + return (VirtualFlow) p; + } + p = p.getParent(); + } + return null; + } + + @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + double prefWidth = 0.0; + + final List*/> visibleLeafColumns = getVisibleLeafColumns(); + for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) { + prefWidth += visibleLeafColumns.get(i).getWidth(); + } + + return prefWidth; + } + + @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + if (fixedCellSizeEnabled) { + return fixedCellSize; + } + + // fix for RT-29080 + checkState(); + + // Support for RT-18467: making it easier to specify a height for + // cells via CSS, where the desired height is less than the height + // of the TableCells. Essentially, -fx-cell-size is given higher + // precedence now + if (getCellSize() < DEFAULT_CELL_SIZE) { + return getCellSize(); + } + + // FIXME according to profiling, this method is slow and should + // be optimised + double prefHeight = 0.0f; + final int count = cells.size(); + for (int i=0; i> cells = cellsMap.values(); + Iterator> cellsIter = cells.iterator(); + while (cellsIter.hasNext()) { + Reference cellRef = cellsIter.next(); + R cell = cellRef.get(); + if (cell != null) { + cell.updateIndex(-1); + cell.getSkin().dispose(); + cell.setSkin(null); + } + } + cellsMap.clear(); + } + + ObservableList*/> columns = getVisibleLeafColumns(); + + cellsMap = new WeakHashMap>(columns.size()); + fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER; + getChildren().clear(); + + for (TableColumnBase col : columns) { + if (cellsMap.containsKey(col)) { + continue; + } + + // create a TableCell for this column and store it in the cellsMap + // for future use + createCell(col); + } + } + + private R createCell(TableColumnBase col) { + // we must create a TableCell for this table column + R cell = getCell(col); + + // and store this in our HashMap until needed + cellsMap.put(col, new WeakReference(cell)); + + return cell; + } + + private void fadeOut(final Node node) { + if (node.getOpacity() < 1.0) return; + + if (! DO_ANIMATIONS) { + node.setOpacity(0); + return; + } + + final FadeTransition fader = new FadeTransition(FADE_DURATION, node); + fader.setToValue(0.0); + fader.play(); + } + + private void fadeIn(final Node node) { + if (node.getOpacity() > 0.0) return; + + if (! DO_ANIMATIONS) { + node.setOpacity(1); + return; + } + + final FadeTransition fader = new FadeTransition(FADE_DURATION, node); + fader.setToValue(1.0); + fader.play(); + } +}