/*
 * Copyright (c) 2011, 2018, 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.util.*;

import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumnBase;
import javafx.scene.layout.Border;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;

import com.sun.javafx.scene.control.skin.resources.ControlResources;

Region responsible for painting the entire row of column headers.
See Also:
Since:9
/** * Region responsible for painting the entire row of column headers. * * @since 9 * @see javafx.scene.control.TableView * @see TableViewSkin * @see javafx.scene.control.TreeTableView * @see TreeTableViewSkin */
public class TableHeaderRow extends StackPane { /*************************************************************************** * * * Static Fields * * * **************************************************************************/
* Private Fields * *
/*************************************************************************** * * * Private Fields * * * **************************************************************************/
// JDK-8090129: This constant should not be static, because the // Locale may change between instances. private final String MENU_SEPARATOR = ControlResources.getString("TableView.nestedColumnControlMenuSeparator"); private final VirtualFlow flow; final TableViewSkinBase<?,?,?,?,?> tableSkin; private Map<TableColumnBase, CheckMenuItem> columnMenuItems = new HashMap<TableColumnBase, CheckMenuItem>(); private double scrollX; private double tableWidth; private Rectangle clip; private TableColumnHeader reorderingRegion;
This is the ghosted region representing the table column that is being dragged. It moves along the x-axis but is fixed in the y-axis.
/** * This is the ghosted region representing the table column that is being * dragged. It moves along the x-axis but is fixed in the y-axis. */
private StackPane dragHeader; private final Label dragHeaderLabel = new Label(); private Region filler;
This is the region where the user can interact with to show/hide columns. It is positioned in the top-right hand corner of the TableHeaderRow, and when clicked shows a PopupMenu consisting of all leaf columns.
/** * This is the region where the user can interact with to show/hide columns. * It is positioned in the top-right hand corner of the TableHeaderRow, and * when clicked shows a PopupMenu consisting of all leaf columns. */
private Pane cornerRegion;
PopupMenu shown to users to allow for them to hide/show columns in the table.
/** * PopupMenu shown to users to allow for them to hide/show columns in the * table. */
private ContextMenu columnPopupMenu;
There are two different mouse dragged event handlers in the header code. Firstly, the column reordering functionality, and secondly, the column resizing functionality. Because these are handled in separate classes and with separate event handlers, we occasionally run into the issue where both event handlers were being called, resulting in bad UX. To remove this issue, we lock when the column dragging happens, and prevent resize operations from taking place.
/** * There are two different mouse dragged event handlers in the header code. * Firstly, the column reordering functionality, and secondly, the column * resizing functionality. Because these are handled in separate classes and * with separate event handlers, we occasionally run into the issue where * both event handlers were being called, resulting in bad UX. To remove this * issue, we lock when the column dragging happens, and prevent resize operations * from taking place. */
boolean columnDragLock = false;
* Listeners * *
/*************************************************************************** * * * Listeners * * * **************************************************************************/
private InvalidationListener tableWidthListener = o -> updateTableWidth(); private InvalidationListener tablePaddingListener = o -> updateTableWidth(); // This is necessary for RT-20300 (but was updated for RT-20840) private ListChangeListener visibleLeafColumnsListener = c -> getRootHeader().setHeadersNeedUpdate(); private final ListChangeListener tableColumnsListener = c -> { while (c.next()) { updateTableColumnListeners(c.getAddedSubList(), c.getRemoved()); } }; private final InvalidationListener columnTextListener = observable -> { TableColumnBase<?,?> column = (TableColumnBase<?,?>) ((StringProperty)observable).getBean(); CheckMenuItem menuItem = columnMenuItems.get(column); if (menuItem != null) { menuItem.setText(getText(column.getText(), column)); } }; private final WeakInvalidationListener weakTableWidthListener = new WeakInvalidationListener(tableWidthListener); private final WeakInvalidationListener weakTablePaddingListener = new WeakInvalidationListener(tablePaddingListener); private final WeakListChangeListener weakVisibleLeafColumnsListener = new WeakListChangeListener(visibleLeafColumnsListener); private final WeakListChangeListener weakTableColumnsListener = new WeakListChangeListener(tableColumnsListener); private final WeakInvalidationListener weakColumnTextListener = new WeakInvalidationListener(columnTextListener); /*************************************************************************** * * * Constructor * * * **************************************************************************/
Creates a new TableHeaderRow instance to visually represent the column header area of controls such as TableView and TreeTableView.
Params:
  • skin – The skin used by the UI control.
/** * Creates a new TableHeaderRow instance to visually represent the column * header area of controls such as {@link javafx.scene.control.TableView} and * {@link javafx.scene.control.TreeTableView}. * * @param skin The skin used by the UI control. */
public TableHeaderRow(final TableViewSkinBase skin) { this.tableSkin = skin; this.flow = skin.flow; getStyleClass().setAll("column-header-background"); // clip the header so it doesn't show outside of the table bounds clip = new Rectangle(); clip.setSmooth(false); clip.heightProperty().bind(heightProperty()); setClip(clip); // listen to table width to keep header in sync updateTableWidth(); tableSkin.getSkinnable().widthProperty().addListener(weakTableWidthListener); tableSkin.getSkinnable().paddingProperty().addListener(weakTablePaddingListener); TableSkinUtils.getVisibleLeafColumns(skin).addListener(weakVisibleLeafColumnsListener); // popup menu for hiding/showing columns columnPopupMenu = new ContextMenu(); updateTableColumnListeners(TableSkinUtils.getColumns(tableSkin), Collections.<TableColumnBase<?,?>>emptyList()); TableSkinUtils.getVisibleLeafColumns(skin).addListener(weakTableColumnsListener); TableSkinUtils.getColumns(tableSkin).addListener(weakTableColumnsListener); // drag header region. Used to indicate the current column being reordered dragHeader = new StackPane(); dragHeader.setVisible(false); dragHeader.getStyleClass().setAll("column-drag-header"); dragHeader.setManaged(false); dragHeader.setMouseTransparent(true); dragHeader.getChildren().add(dragHeaderLabel); // the header lives inside a NestedTableColumnHeader NestedTableColumnHeader rootHeader = createRootHeader(); setRootHeader(rootHeader); rootHeader.setFocusTraversable(false); rootHeader.setTableHeaderRow(this); // The 'filler' area that extends from the right-most column to the edge // of the tableview, or up to the 'column control' button filler = new Region(); filler.getStyleClass().setAll("filler"); // Give focus to the table when an empty area of the header row is clicked. // This ensures the user knows that the table has focus. setOnMousePressed(e -> { skin.getSkinnable().requestFocus(); }); // build the corner region button for showing the popup menu final StackPane image = new StackPane(); image.setSnapToPixel(false); image.getStyleClass().setAll("show-hide-column-image"); cornerRegion = new StackPane() { @Override protected void layoutChildren() { double imageWidth = image.snappedLeftInset() + image.snappedRightInset(); double imageHeight = image.snappedTopInset() + image.snappedBottomInset(); image.resize(imageWidth, imageHeight); positionInArea(image, 0, 0, getWidth(), getHeight() - 3, 0, HPos.CENTER, VPos.CENTER); } }; cornerRegion.getStyleClass().setAll("show-hide-columns-button"); cornerRegion.getChildren().addAll(image); BooleanProperty tableMenuButtonVisibleProperty = TableSkinUtils.tableMenuButtonVisibleProperty(skin); if (tableMenuButtonVisibleProperty != null) { cornerRegion.visibleProperty().bind(tableMenuButtonVisibleProperty); }; cornerRegion.setOnMousePressed(me -> { // show a popupMenu which lists all columns columnPopupMenu.show(cornerRegion, Side.BOTTOM, 0, 0); me.consume(); }); // the actual header // the region that is anchored above the vertical scrollbar // a 'ghost' of the header being dragged by the user to force column // reordering getChildren().addAll(filler, rootHeader, cornerRegion, dragHeader); } /*************************************************************************** * * * Properties * * * **************************************************************************/
Indicates if a reordering operation of a column is in progress. The value is true during a column reordering operation, and false otherwise. When a column is reordered (for example, by dragging its header), this property is updated automatically. Setting the value manually should be done when a subclass overrides the default reordering behavior. Calling setReorderingRegion(TableColumnHeader) before setting this property is required as well.
Since:12
/** * Indicates if a reordering operation of a column is in progress. The value is {@code true} during a column * reordering operation, and {@code false} otherwise. When a column is reordered (for example, by dragging its * header), this property is updated automatically. Setting the value manually should be done when a subclass * overrides the default reordering behavior. Calling {@link #setReorderingRegion(TableColumnHeader)} before setting * this property is required as well. * * @since 12 */
private BooleanProperty reordering = new SimpleBooleanProperty(this, "reordering", false) { @Override protected void invalidated() { TableColumnHeader r = getReorderingRegion(); if (r != null) { double dragHeaderHeight = r.getNestedColumnHeader() != null ? r.getNestedColumnHeader().getHeight() : getReorderingRegion().getHeight(); dragHeader.resize(dragHeader.getWidth(), dragHeaderHeight); dragHeader.setTranslateY(getHeight() - dragHeaderHeight); } dragHeader.setVisible(isReordering()); } }; public final void setReordering(boolean value) { this.reordering.set(value); } public final boolean isReordering() { return reordering.get(); } public final BooleanProperty reorderingProperty() { return reordering; } // --- root header /* * The header row is actually just one NestedTableColumnHeader that spans * the entire width. Nested within this is the TableColumnHeader's and * NestedTableColumnHeader's, as necessary. This makes it nice and clean * to handle column reordering - we basically enforce the rule that column * reordering only occurs within a single NestedTableColumnHeader, and only * at that level. */ private ReadOnlyObjectWrapper<NestedTableColumnHeader> rootHeader = new ReadOnlyObjectWrapper<>(this, "rootHeader"); private final ReadOnlyObjectProperty<NestedTableColumnHeader> rootHeaderProperty() { return rootHeader.getReadOnlyProperty(); }
Returns the root header for all columns. The root header is a NestedTableColumnHeader that contains the NestedTableColumnHeaders that represent each column. It spans the entire width of the TableView. This allows any developer overriding a TableColumnHeader to easily access the root header and all others TableColumnHeaders.
Returns:the root header
Implementation Note:This design enforces that column reordering occurs only within a single NestedTableColumnHeader and only at that level.
Since:12
/** * Returns the root header for all columns. The root header is a {@link NestedTableColumnHeader} that contains the * {@code NestedTableColumnHeader}s that represent each column. It spans the entire width of the {@code TableView}. * This allows any developer overriding a {@code TableColumnHeader} to easily access the root header and all others * {@code TableColumnHeader}s. * * @return the root header * @implNote This design enforces that column reordering occurs only within a single {@code NestedTableColumnHeader} * and only at that level. * @since 12 */
public final NestedTableColumnHeader getRootHeader() { return rootHeader.get(); } private final void setRootHeader(NestedTableColumnHeader value) { rootHeader.set(value); } /*************************************************************************** * * * Public API * * * **************************************************************************/
{@inheritDoc}
/** {@inheritDoc} */
@Override protected void layoutChildren() { double x = scrollX; double headerWidth = snapSizeX(getRootHeader().prefWidth(-1)); double prefHeight = getHeight() - snappedTopInset() - snappedBottomInset(); double cornerWidth = snapSizeX(flow.getVbar().prefWidth(-1)); // position the main nested header getRootHeader().resizeRelocate(x, snappedTopInset(), headerWidth, prefHeight); // position the filler region final Control control = tableSkin.getSkinnable(); if (control == null) { return; } final BooleanProperty tableMenuButtonVisibleProperty = TableSkinUtils.tableMenuButtonVisibleProperty(tableSkin); final double controlInsets = control.snappedLeftInset() + control.snappedRightInset(); double fillerWidth = tableWidth - headerWidth + filler.getInsets().getLeft() - controlInsets; fillerWidth -= tableMenuButtonVisibleProperty != null && tableMenuButtonVisibleProperty.get() ? cornerWidth : 0; filler.setVisible(fillerWidth > 0); if (fillerWidth > 0) { filler.resizeRelocate(x + headerWidth, snappedTopInset(), fillerWidth, prefHeight); } // position the top-right rectangle (which sits above the scrollbar) cornerRegion.resizeRelocate(tableWidth - cornerWidth, snappedTopInset(), cornerWidth, prefHeight); }
{@inheritDoc}
/** {@inheritDoc} */
@Override protected double computePrefWidth(double height) { return getRootHeader().prefWidth(height); }
{@inheritDoc}
/** {@inheritDoc} */
@Override protected double computeMinHeight(double width) { return computePrefHeight(width); }
{@inheritDoc}
/** {@inheritDoc} */
@Override protected double computePrefHeight(double width) { // we hardcode 24.0 here to avoid RT-37616, where the // entire header row would disappear when all columns were hidden. double headerPrefHeight = getRootHeader().prefHeight(width); headerPrefHeight = headerPrefHeight == 0.0 ? 24.0 : headerPrefHeight; return snappedTopInset() + headerPrefHeight + snappedBottomInset(); }
Called whenever the value of the horizontal scrollbar changes in order to request layout changes, shifting the TableColumnHeaders.

For example, if custom components are added around a TableColumnHeader (such as icons above), they will also need to be shifted. When overriding, calling super() is required to shift the TableColumnHeaders, and it's up to the developer to notify its own custom components of this change.

Since:12
/** * Called whenever the value of the horizontal scrollbar changes in order to request layout changes, shifting the * {@code TableColumnHeader}s. * <p> * For example, if custom components are added around a {@code TableColumnHeader} (such as icons above), they will * also need to be shifted. When overriding, calling {@code super()} is required to shift the {@code * TableColumnHeader}s, and it's up to the developer to notify its own custom components of this change. * * @since 12 */
protected void updateScrollX() { scrollX = flow.getHbar().isVisible() ? -flow.getHbar().getValue() : 0.0F; requestLayout(); // Fix for RT-36392: without this call even though we call requestLayout() // we don't seem to ever see the layoutChildren() method above called, // which means the layout is not always updated to use the latest scrollX. layout(); }
Updates the table width when a resize operation occurs. This method is called continuously when the control width is resizing in order to properly clip this TableHeaderRow. Overriding this method allows a subclass to customize the resizing behavior.

Normally, the TableHeaderRow is using the full space (TableView width), but in some cases that space may be reduced. For example, if a vertical header that will display the row number is introduced, the TableHeaderRow would need to be clipped a bit shorter in order not to overlap that vertical header. Calling super() first when overriding this method allows Node.getClip() to compute the right width in order apply a transformation.

Since:12
/** * Updates the table width when a resize operation occurs. This method is called continuously when the control width * is resizing in order to properly clip this {@code TableHeaderRow}. Overriding this method allows a subclass to * customize the resizing behavior. * <p> * Normally, the {@code TableHeaderRow} is using the full space ({@code TableView} width), but in some cases that * space may be reduced. For example, if a vertical header that will display the row number is introduced, the * {@code TableHeaderRow} would need to be clipped a bit shorter in order not to overlap that vertical header. * Calling {@code super()} first when overriding this method allows {@link #getClip()} to compute the right width in * order apply a transformation. * * @since 12 */
protected void updateTableWidth() { // snapping added for RT-19428 final Control c = tableSkin.getSkinnable(); if (c == null) { this.tableWidth = 0; } else { Insets insets = c.getInsets() == null ? Insets.EMPTY : c.getInsets(); double padding = snapSizeX(insets.getLeft()) + snapSizeX(insets.getRight()); this.tableWidth = snapSizeX(c.getWidth()) - padding; } clip.setWidth(tableWidth); }
Creates a new NestedTableColumnHeader instance. By default this method should not be overridden, but in some circumstances it makes sense (e.g. testing, or when extreme customization is desired).
Returns:A new NestedTableColumnHeader instance.
/** * Creates a new NestedTableColumnHeader instance. By default this method should not be overridden, but in some * circumstances it makes sense (e.g. testing, or when extreme customization is desired). * * @return A new NestedTableColumnHeader instance. */
protected NestedTableColumnHeader createRootHeader() { return new NestedTableColumnHeader(null); } /*************************************************************************** * * * Private Implementation * * * **************************************************************************/
Returns the current TableColumnHeader being moved during reordering.
Returns:the current TableColumnHeader being moved
Since:12
/** * Returns the current {@link TableColumnHeader} being moved during reordering. * * @return the current {@code TableColumnHeader} being moved * @since 12 */
protected TableColumnHeader getReorderingRegion() { return reorderingRegion; } void setReorderingColumn(TableColumnBase rc) { dragHeaderLabel.setText(rc == null ? "" : rc.getText()); }
Sets the TableColumnHeader that is being moved during a reordering operation. This is automatically set by the TableColumnHeader when reordering starts. This method should only be called manually if the default reordering behavior is overridden. Calling setReordering(boolean) after the call is required.
Params:
  • reorderingRegion – the TableColumnHeader being reordered
Since:12
/** * Sets the {@code TableColumnHeader} that is being moved during a reordering operation. This is automatically set * by the {@code TableColumnHeader} when reordering starts. This method should only be called manually if the * default reordering behavior is overridden. Calling {@link #setReordering(boolean)} after the call is required. * * @param reorderingRegion the {@code TableColumnHeader} being reordered * @since 12 */
protected void setReorderingRegion(TableColumnHeader reorderingRegion) { this.reorderingRegion = reorderingRegion; if (reorderingRegion != null) { dragHeader.resize(reorderingRegion.getWidth(), dragHeader.getHeight()); } } void setDragHeaderX(double dragHeaderX) { dragHeader.setTranslateX(dragHeaderX); } TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col) { if (col == null) return null; List<TableColumnBase<?,?>> columnChain = new ArrayList<>(); columnChain.add(col); TableColumnBase<?,?> parent = col.getParentColumn(); while (parent != null) { columnChain.add(0, parent); parent = parent.getParentColumn(); } // we now have a list from top to bottom of a nested column hierarchy, // and we can now navigate down to retrieve the header with ease TableColumnHeader currentHeader = getRootHeader(); for (int depth = 0; depth < columnChain.size(); depth++) { // this is the column we are looking for at this depth TableColumnBase<?,?> column = columnChain.get(depth); // and now we iterate through the nested table column header at this // level to get the header currentHeader = getColumnHeaderFor(column, currentHeader); } return currentHeader; } private TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col, TableColumnHeader currentHeader) { if (currentHeader instanceof NestedTableColumnHeader) { List<TableColumnHeader> headers = ((NestedTableColumnHeader)currentHeader).getColumnHeaders(); for (int i = 0; i < headers.size(); i++) { TableColumnHeader header = headers.get(i); if (header.getTableColumn() == col) { return header; } } } return null; } private void updateTableColumnListeners(List<? extends TableColumnBase<?,?>> added, List<? extends TableColumnBase<?,?>> removed) { // remove binding from all removed items for (TableColumnBase tc : removed) { remove(tc); } rebuildColumnMenu(); } private void remove(TableColumnBase<?,?> col) { if (col == null) return; CheckMenuItem item = columnMenuItems.remove(col); if (item != null) { col.textProperty().removeListener(weakColumnTextListener); item.selectedProperty().unbindBidirectional(col.visibleProperty()); columnPopupMenu.getItems().remove(item); } if (! col.getColumns().isEmpty()) { for (TableColumnBase tc : col.getColumns()) { remove(tc); } } } private void rebuildColumnMenu() { columnPopupMenu.getItems().clear(); for (TableColumnBase<?,?> col : TableSkinUtils.getColumns(tableSkin)) { // we only create menu items for leaf columns, visible or not if (col.getColumns().isEmpty()) { createMenuItem(col); } else { List<TableColumnBase<?,?>> leafColumns = getLeafColumns(col); for (TableColumnBase<?,?> _col : leafColumns) { createMenuItem(_col); } } } } private List<TableColumnBase<?,?>> getLeafColumns(TableColumnBase<?,?> col) { List<TableColumnBase<?,?>> leafColumns = new ArrayList<>(); for (TableColumnBase<?,?> _col : col.getColumns()) { if (_col.getColumns().isEmpty()) { leafColumns.add(_col); } else { leafColumns.addAll(getLeafColumns(_col)); } } return leafColumns; } private void createMenuItem(TableColumnBase<?,?> col) { CheckMenuItem item = columnMenuItems.get(col); if (item == null) { item = new CheckMenuItem(); columnMenuItems.put(col, item); } // bind column text and isVisible so that the menu item is always correct item.setText(getText(col.getText(), col)); col.textProperty().addListener(weakColumnTextListener); // ideally we would have API to observe the binding status of a property, // but for now that doesn't exist, so we set this once and then forget item.setDisable(col.visibleProperty().isBound()); // fake bidrectional binding (a real one was used here but resulted in JBS-8136468) item.setSelected(col.isVisible()); final CheckMenuItem _item = item; item.selectedProperty().addListener(o -> { if (col.visibleProperty().isBound()) return; col.setVisible(_item.isSelected()); }); col.visibleProperty().addListener(o -> _item.setSelected(col.isVisible())); columnPopupMenu.getItems().add(item); } /* * Function used for building the strings in the popup menu */ private String getText(String text, TableColumnBase col) { String s = text; TableColumnBase parentCol = col.getParentColumn(); while (parentCol != null) { if (isColumnVisibleInHeader(parentCol, TableSkinUtils.getColumns(tableSkin))) { s = parentCol.getText() + MENU_SEPARATOR + s; } parentCol = parentCol.getParentColumn(); } return s; } // We need to show strings properly. If a column has a parent column which is // not inserted into the TableView columns list, it effectively doesn't have // a parent column from the users perspective. As such, we shouldn't include // the parent column text in the menu. Fixes RT-14482. private boolean isColumnVisibleInHeader(TableColumnBase col, List columns) { if (col == null) return false; for (int i = 0; i < columns.size(); i++) { TableColumnBase column = (TableColumnBase) columns.get(i); if (col.equals(column)) return true; if (! column.getColumns().isEmpty()) { boolean isVisible = isColumnVisibleInHeader(col, column.getColumns()); if (isVisible) return true; } } return false; } }