modules/controls/src/main/java/javafx/scene/control/skin/TableHeaderRow.java

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization


   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.scene.control.skin;
  27 
  28 import java.util.*;
  29 
  30 import javafx.beans.InvalidationListener;
  31 import javafx.beans.WeakInvalidationListener;
  32 import javafx.beans.property.BooleanProperty;



  33 import javafx.beans.property.SimpleBooleanProperty;

  34 import javafx.beans.property.StringProperty;
  35 import javafx.collections.ListChangeListener;
  36 import javafx.collections.WeakListChangeListener;
  37 import javafx.geometry.HPos;
  38 import javafx.geometry.Insets;
  39 import javafx.geometry.Side;
  40 import javafx.geometry.VPos;
  41 import javafx.scene.control.CheckMenuItem;
  42 import javafx.scene.control.ContextMenu;
  43 import javafx.scene.control.Control;
  44 import javafx.scene.control.Label;
  45 import javafx.scene.control.TableColumn;
  46 import javafx.scene.control.TableColumnBase;
  47 import javafx.scene.layout.Pane;
  48 import javafx.scene.layout.Region;
  49 import javafx.scene.layout.StackPane;
  50 import javafx.scene.shape.Rectangle;
  51 
  52 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  53 
  54 /**
  55  * Region responsible for painting the entire row of column headers.






  56  */
  57 public class TableHeaderRow extends StackPane {
  58 
  59     /***************************************************************************
  60      *                                                                         *
  61      * Static Fields                                                           *
  62      *                                                                         *
  63      **************************************************************************/
  64 
  65     private static final String MENU_SEPARATOR = 
  66             ControlResources.getString("TableView.nestedColumnControlMenuSeparator");
  67 
  68 
  69 
  70     /***************************************************************************
  71      *                                                                         *
  72      * Private Fields                                                          *
  73      *                                                                         *
  74      **************************************************************************/
  75 
  76     private final VirtualFlow flow;
  77 
  78     private final TableViewSkinBase tableSkin;
  79 
  80     private Map<TableColumnBase, CheckMenuItem> columnMenuItems = new HashMap<TableColumnBase, CheckMenuItem>();
  81 
  82     // Vertical line that is shown when columns are being reordered
  83 //    private Region columnReorderLine;
  84 
  85     private double scrollX;
  86 
  87     private double tableWidth;
  88 
  89     private Rectangle clip;
  90 
  91     private TableColumnHeader reorderingRegion;
  92 
  93     /**
  94      * This is the ghosted region representing the table column that is being
  95      * dragged. It moves along the x-axis but is fixed in the y-axis.
  96      */
  97     private StackPane dragHeader;
  98     private final Label dragHeaderLabel = new Label();
  99 
 100     /*
 101      * The header row is actually just one NestedTableColumnHeader that spans
 102      * the entire width. Nested within this is the TableColumnHeader's and
 103      * NestedTableColumnHeader's, as necessary. This makes it nice and clean
 104      * to handle column reordering - we basically enforce the rule that column
 105      * reordering only occurs within a single NestedTableColumnHeader, and only
 106      * at that level.
 107      */
 108     private final NestedTableColumnHeader header;
 109 
 110     private Region filler;
 111 
 112     /**
 113      * This is the region where the user can interact with to show/hide columns.
 114      * It is positioned in the top-right hand corner of the TableHeaderRow, and
 115      * when clicked shows a PopupMenu consisting of all leaf columns.
 116      */
 117     private Pane cornerRegion;
 118 
 119     /**
 120      * PopupMenu shown to users to allow for them to hide/show columns in the
 121      * table.
 122      */
 123     private ContextMenu columnPopupMenu;
 124 
 125     private BooleanProperty reordering = new SimpleBooleanProperty(this, "reordering", false) {
 126         @Override protected void invalidated() {
 127             TableColumnHeader r = getReorderingRegion();
 128             if (r != null) {
 129                 double dragHeaderHeight = r.getNestedColumnHeader() != null ?
 130                         r.getNestedColumnHeader().getHeight() :
 131                         getReorderingRegion().getHeight();
 132 
 133                 dragHeader.resize(dragHeader.getWidth(), dragHeaderHeight);
 134                 dragHeader.setTranslateY(getHeight() - dragHeaderHeight);















 135             }
 136             dragHeader.setVisible(isReordering());






 137         }
 138     };
 139 















 140 
 141 
 142     /***************************************************************************
 143      *                                                                         *
 144      * Constructor                                                             *
 145      *                                                                         *
 146      **************************************************************************/
 147     







 148     public TableHeaderRow(final TableViewSkinBase skin) {
 149         this.tableSkin = skin;
 150         this.flow = skin.flow;
 151 
 152         getStyleClass().setAll("column-header-background");
 153 
 154         // clip the header so it doesn't show outside of the table bounds
 155         clip = new Rectangle();
 156         clip.setSmooth(false);
 157         clip.heightProperty().bind(heightProperty());
 158         setClip(clip);
 159 
 160         // listen to table width to keep header in sync
 161         updateTableWidth();
 162         tableSkin.getSkinnable().widthProperty().addListener(weakTableWidthListener);
 163         tableSkin.getSkinnable().paddingProperty().addListener(weakTablePaddingListener);
 164         skin.getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
 165 
 166         // popup menu for hiding/showing columns
 167         columnPopupMenu = new ContextMenu();
 168         updateTableColumnListeners(tableSkin.getColumns(), Collections.<TableColumnBase<?,?>>emptyList());
 169         tableSkin.getVisibleLeafColumns().addListener(weakTableColumnsListener);
 170         tableSkin.getColumns().addListener(weakTableColumnsListener);
 171 
 172         // drag header region. Used to indicate the current column being reordered
 173         dragHeader = new StackPane();
 174         dragHeader.setVisible(false);
 175         dragHeader.getStyleClass().setAll("column-drag-header");
 176         dragHeader.setManaged(false);
 177         dragHeader.getChildren().add(dragHeaderLabel);
 178 
 179         // the header lives inside a NestedTableColumnHeader
 180         header = createRootHeader();
 181         header.setFocusTraversable(false);
 182         header.setTableHeaderRow(this);

 183 
 184         // The 'filler' area that extends from the right-most column to the edge
 185         // of the tableview, or up to the 'column control' button
 186         filler = new Region();
 187         filler.getStyleClass().setAll("filler");
 188 
 189         // Give focus to the table when an empty area of the header row is clicked.
 190         // This ensures the user knows that the table has focus.
 191         setOnMousePressed(e -> {
 192             skin.getSkinnable().requestFocus();
 193         });
 194 
 195         // build the corner region button for showing the popup menu
 196         final StackPane image = new StackPane();
 197         image.setSnapToPixel(false);
 198         image.getStyleClass().setAll("show-hide-column-image");
 199         cornerRegion = new StackPane() {
 200             @Override protected void layoutChildren() {
 201                 double imageWidth = image.snappedLeftInset() + image.snappedRightInset();
 202                 double imageHeight = image.snappedTopInset() + image.snappedBottomInset();


 206                         0, HPos.CENTER, VPos.CENTER);
 207             }
 208         };
 209         cornerRegion.getStyleClass().setAll("show-hide-columns-button");
 210         cornerRegion.getChildren().addAll(image);
 211         cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
 212         tableSkin.tableMenuButtonVisibleProperty().addListener(valueModel -> {
 213             cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
 214             requestLayout();
 215         });
 216         cornerRegion.setOnMousePressed(me -> {
 217             // show a popupMenu which lists all columns
 218             columnPopupMenu.show(cornerRegion, Side.BOTTOM, 0, 0);
 219             me.consume();
 220         });
 221 
 222         // the actual header
 223         // the region that is anchored above the vertical scrollbar
 224         // a 'ghost' of the header being dragged by the user to force column
 225         // reordering
 226         getChildren().addAll(filler, header, cornerRegion, dragHeader);
 227     }
 228     
 229 
 230 
 231     /***************************************************************************
 232      *                                                                         *
 233      * Listeners                                                               *
 234      *                                                                         *
 235      **************************************************************************/    
 236     
 237     private InvalidationListener tableWidthListener = valueModel -> {
 238         updateTableWidth();
 239     };
 240 
 241     private InvalidationListener tablePaddingListener = valueModel -> {
 242         updateTableWidth();
 243     };

 244     
 245     private ListChangeListener visibleLeafColumnsListener = new ListChangeListener<TableColumn<?,?>>() {
 246         @Override public void onChanged(ListChangeListener.Change<? extends TableColumn<?,?>> c) {
 247             // This is necessary for RT-20300 (but was updated for RT-20840)
 248             header.setHeadersNeedUpdate();
 249         }
 250     };
 251     
 252     private final ListChangeListener tableColumnsListener = c -> {
 253         while (c.next()) {
 254             updateTableColumnListeners(c.getAddedSubList(), c.getRemoved());
 255         }
 256     };
 257     
 258     private final InvalidationListener columnTextListener = observable -> {
 259         TableColumnBase<?,?> column = (TableColumnBase<?,?>) ((StringProperty)observable).getBean();
 260         CheckMenuItem menuItem = columnMenuItems.get(column);
 261         if (menuItem != null) {
 262             menuItem.setText(getText(column.getText(), column));


 263         }
 264     };
 265     
 266     private final WeakInvalidationListener weakTableWidthListener = 
 267             new WeakInvalidationListener(tableWidthListener);
 268 
 269     private final WeakInvalidationListener weakTablePaddingListener =
 270             new WeakInvalidationListener(tablePaddingListener);
 271 
 272     private final WeakListChangeListener weakVisibleLeafColumnsListener =
 273             new WeakListChangeListener(visibleLeafColumnsListener);
 274     
 275     private final WeakListChangeListener weakTableColumnsListener =
 276             new WeakListChangeListener(tableColumnsListener);
 277     
 278     private final WeakInvalidationListener weakColumnTextListener = 
 279             new WeakInvalidationListener(columnTextListener);

















 280 
 281 
 282 
 283     /***************************************************************************
 284      *                                                                         *
 285      * Public Methods                                                          *
 286      *                                                                         *
 287      **************************************************************************/
 288 
 289     /** {@inheritDoc} */
 290     @Override protected void layoutChildren() {
 291         double x = scrollX;
 292         double headerWidth = snapSize(header.prefWidth(-1));
 293         double prefHeight = getHeight() - snappedTopInset() - snappedBottomInset();
 294         double cornerWidth = snapSize(flow.getVbar().prefWidth(-1));
 295 
 296         // position the main nested header
 297         header.resizeRelocate(x, snappedTopInset(), headerWidth, prefHeight);
 298         
 299         // position the filler region
 300         final Control control = tableSkin.getSkinnable();
 301         if (control == null) {
 302             return;
 303         }
 304 
 305         final double controlInsets = control.snappedLeftInset() + control.snappedRightInset();
 306         double fillerWidth = tableWidth - headerWidth + filler.getInsets().getLeft() - controlInsets;
 307         fillerWidth -= tableSkin.tableMenuButtonVisibleProperty().get() ? cornerWidth : 0;
 308         filler.setVisible(fillerWidth > 0);
 309         if (fillerWidth > 0) {
 310             filler.resizeRelocate(x + headerWidth, snappedTopInset(), fillerWidth, prefHeight);
 311         }
 312 
 313         // position the top-right rectangle (which sits above the scrollbar)
 314         cornerRegion.resizeRelocate(tableWidth - cornerWidth, snappedTopInset(), cornerWidth, prefHeight);
 315     }
 316 
 317     /** {@inheritDoc} */
 318     @Override protected double computePrefWidth(double height) {
 319         return header.prefWidth(height);
 320     }
 321 
 322     /** {@inheritDoc} */
 323     @Override protected double computeMinHeight(double width) {
 324         return computePrefHeight(width);
 325     }
 326 
 327     /** {@inheritDoc} */
 328     @Override protected double computePrefHeight(double width) {
 329         // we hardcode 24.0 here to avoid RT-37616, where the
 330         // entire header row would disappear when all columns were hidden. 
 331         double headerPrefHeight = header.prefHeight(width);
 332         headerPrefHeight = headerPrefHeight == 0.0 ? 24.0 : headerPrefHeight;
 333         return snappedTopInset() + headerPrefHeight + snappedBottomInset();
 334     }
 335 
 336     // protected to allow subclasses to provide a custom root header
 337     protected NestedTableColumnHeader createRootHeader() {
 338         return new NestedTableColumnHeader(tableSkin, null);
 339     }
 340 
 341     // protected to allow subclasses access to the TableViewSkinBase instance
 342     protected TableViewSkinBase<?,?,?,?,?,?> getTableSkin() {
 343         return this.tableSkin;
 344     }
 345 
 346     // protected to allow subclasses to modify the horizontal scrolling
 347     protected void updateScrollX() {
 348         scrollX = flow.getHbar().isVisible() ? -flow.getHbar().getValue() : 0.0F;
 349         requestLayout();
 350 
 351         // Fix for RT-36392: without this call even though we call requestLayout()
 352         // we don't seem to ever see the layoutChildren() method above called,
 353         // which means the layout is not always updated to use the latest scrollX.
 354         layout();
 355     }
 356 
 357     public final void setReordering(boolean value) {
 358         this.reordering.set(value);









 359     }
 360 
 361     public final boolean isReordering() {
 362         return reordering.get();
 363     }
 364 
 365     public final BooleanProperty reorderingProperty() {
 366         return reordering;
 367     }
 368 
 369     public TableColumnHeader getReorderingRegion() {







 370         return reorderingRegion;
 371     }
 372 
 373     public void setReorderingColumn(TableColumnBase rc) {
 374         dragHeaderLabel.setText(rc == null ? "" : rc.getText());
 375     }
 376 
 377     public void setReorderingRegion(TableColumnHeader reorderingRegion) {
 378         this.reorderingRegion = reorderingRegion;
 379 
 380         if (reorderingRegion != null) {
 381             dragHeader.resize(reorderingRegion.getWidth(), dragHeader.getHeight());
 382         }
 383     }
 384 
 385     public void setDragHeaderX(double dragHeaderX) {
 386         dragHeader.setTranslateX(dragHeaderX);
 387     }
 388 
 389     public NestedTableColumnHeader getRootHeader() {
 390         return header;
 391     }
 392 
 393     // protected to allow subclass to customise the width, to allow for features
 394     // such as row headers
 395     protected void updateTableWidth() {
 396         // snapping added for RT-19428
 397         final Control c = tableSkin.getSkinnable();
 398         if (c == null) {
 399             this.tableWidth = 0;
 400         } else {
 401             Insets insets = c.getInsets() == null ? Insets.EMPTY : c.getInsets();
 402             double padding = snapSize(insets.getLeft()) + snapSize(insets.getRight());
 403             this.tableWidth = snapSize(c.getWidth()) - padding;
 404         }
 405 
 406         clip.setWidth(tableWidth);
 407     }
 408 
 409     public TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col) {
 410         if (col == null) return null;
 411         List<TableColumnBase<?,?>> columnChain = new ArrayList<>();
 412         columnChain.add(col);
 413 
 414         TableColumnBase<?,?> parent = col.getParentColumn();
 415         while (parent != null) {
 416             columnChain.add(0, parent);
 417             parent = parent.getParentColumn();
 418         }
 419 
 420         // we now have a list from top to bottom of a nested column hierarchy,
 421         // and we can now navigate down to retrieve the header with ease
 422         TableColumnHeader currentHeader = getRootHeader();
 423         for (int depth = 0; depth < columnChain.size(); depth++) {
 424             // this is the column we are looking for at this depth
 425             TableColumnBase<?,?> column = columnChain.get(depth);
 426 
 427             // and now we iterate through the nested table column header at this
 428             // level to get the header
 429             currentHeader = getColumnHeaderFor(column, currentHeader);
 430         }
 431         return currentHeader;
 432     }
 433 
 434     public TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col, TableColumnHeader currentHeader) {
 435         if (currentHeader instanceof NestedTableColumnHeader) {
 436             List<TableColumnHeader> headers = ((NestedTableColumnHeader)currentHeader).getColumnHeaders();
 437 
 438             for (int i = 0; i < headers.size(); i++) {
 439                 TableColumnHeader header = headers.get(i);
 440                 if (header.getTableColumn() == col) {
 441                     return header;
 442                 }
 443             }
 444         }
 445 
 446         return null;
 447     }
 448 
 449 
 450     /***************************************************************************
 451      *                                                                         *
 452      * Private Implementation                                                  *
 453      *                                                                         *
 454      **************************************************************************/
 455 
 456     private void updateTableColumnListeners(List<? extends TableColumnBase<?,?>> added, List<? extends TableColumnBase<?,?>> removed) {
 457         // remove binding from all removed items
 458         for (TableColumnBase tc : removed) {
 459             remove(tc);
 460         }
 461 
 462         rebuildColumnMenu();
 463     }
 464 
 465     private void remove(TableColumnBase<?,?> col) {
 466         if (col == null) return;
 467 
 468         CheckMenuItem item = columnMenuItems.remove(col);
 469         if (item != null) {
 470             col.textProperty().removeListener(weakColumnTextListener);
 471             item.selectedProperty().unbindBidirectional(col.visibleProperty());
 472 
 473             columnPopupMenu.getItems().remove(item);
 474         }
 475 
 476         if (! col.getColumns().isEmpty()) {
 477             for (TableColumnBase tc : col.getColumns()) {
 478                 remove(tc);
 479             }
 480         }
 481     }
 482 
 483     private void rebuildColumnMenu() {
 484         columnPopupMenu.getItems().clear();
 485 
 486         for (TableColumnBase<?,?> col : getTableSkin().getColumns()) {
 487             // we only create menu items for leaf columns, visible or not
 488             if (col.getColumns().isEmpty()) {
 489                 createMenuItem(col);
 490             } else {
 491                 List<TableColumnBase<?,?>> leafColumns = getLeafColumns(col);
 492                 for (TableColumnBase<?,?> _col : leafColumns) {
 493                     createMenuItem(_col);
 494                 }
 495             }
 496         }
 497     }
 498 
 499     private List<TableColumnBase<?,?>> getLeafColumns(TableColumnBase<?,?> col) {
 500         List<TableColumnBase<?,?>> leafColumns = new ArrayList<>();
 501 
 502         for (TableColumnBase<?,?> _col : col.getColumns()) {
 503             if (_col.getColumns().isEmpty()) {
 504                 leafColumns.add(_col);
 505             } else {
 506                 leafColumns.addAll(getLeafColumns(_col));




   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import java.util.*;
  29 
  30 import javafx.beans.InvalidationListener;
  31 import javafx.beans.WeakInvalidationListener;
  32 import javafx.beans.property.BooleanProperty;
  33 import javafx.beans.property.ObjectProperty;
  34 import javafx.beans.property.ReadOnlyObjectProperty;
  35 import javafx.beans.property.ReadOnlyObjectWrapper;
  36 import javafx.beans.property.SimpleBooleanProperty;
  37 import javafx.beans.property.SimpleObjectProperty;
  38 import javafx.beans.property.StringProperty;
  39 import javafx.collections.ListChangeListener;
  40 import javafx.collections.WeakListChangeListener;
  41 import javafx.geometry.HPos;
  42 import javafx.geometry.Insets;
  43 import javafx.geometry.Side;
  44 import javafx.geometry.VPos;
  45 import javafx.scene.control.CheckMenuItem;
  46 import javafx.scene.control.ContextMenu;
  47 import javafx.scene.control.Control;
  48 import javafx.scene.control.Label;
  49 import javafx.scene.control.TableColumn;
  50 import javafx.scene.control.TableColumnBase;
  51 import javafx.scene.layout.Pane;
  52 import javafx.scene.layout.Region;
  53 import javafx.scene.layout.StackPane;
  54 import javafx.scene.shape.Rectangle;
  55 
  56 import com.sun.javafx.scene.control.skin.resources.ControlResources;
  57 
  58 /**
  59  * Region responsible for painting the entire row of column headers.
  60  *
  61  * @since 9
  62  * @see TableView
  63  * @see TableViewSkin
  64  * @see TreeTableView
  65  * @see TreeTableViewSkin
  66  */
  67 public class TableHeaderRow extends StackPane {
  68 
  69     /***************************************************************************
  70      *                                                                         *
  71      * Static Fields                                                           *
  72      *                                                                         *
  73      **************************************************************************/
  74 
  75     private static final String MENU_SEPARATOR = 
  76             ControlResources.getString("TableView.nestedColumnControlMenuSeparator");
  77 
  78 
  79 
  80     /***************************************************************************
  81      *                                                                         *
  82      * Private Fields                                                          *
  83      *                                                                         *
  84      **************************************************************************/
  85 
  86     private final VirtualFlow flow;
  87     private final TableViewSkinBase<?,?,?,?,?> tableSkin;


  88     private Map<TableColumnBase, CheckMenuItem> columnMenuItems = new HashMap<TableColumnBase, CheckMenuItem>();




  89     private double scrollX;

  90     private double tableWidth;

  91     private Rectangle clip;

  92     private TableColumnHeader reorderingRegion;
  93 
  94     /**
  95      * This is the ghosted region representing the table column that is being
  96      * dragged. It moves along the x-axis but is fixed in the y-axis.
  97      */
  98     private StackPane dragHeader;
  99     private final Label dragHeaderLabel = new Label();
 100 










 101     private Region filler;
 102 
 103     /**
 104      * This is the region where the user can interact with to show/hide columns.
 105      * It is positioned in the top-right hand corner of the TableHeaderRow, and
 106      * when clicked shows a PopupMenu consisting of all leaf columns.
 107      */
 108     private Pane cornerRegion;
 109 
 110     /**
 111      * PopupMenu shown to users to allow for them to hide/show columns in the
 112      * table.
 113      */
 114     private ContextMenu columnPopupMenu;
 115 







 116 
 117 
 118     /***************************************************************************
 119      *                                                                         *
 120      * Listeners                                                               *
 121      *                                                                         *
 122      **************************************************************************/
 123 
 124     private InvalidationListener tableWidthListener = o -> updateTableWidth();
 125 
 126     private InvalidationListener tablePaddingListener = o -> updateTableWidth();
 127 
 128     // This is necessary for RT-20300 (but was updated for RT-20840)
 129     private ListChangeListener visibleLeafColumnsListener = c -> getRootHeader().setHeadersNeedUpdate();
 130 
 131     private final ListChangeListener tableColumnsListener = c -> {
 132         while (c.next()) {
 133             updateTableColumnListeners(c.getAddedSubList(), c.getRemoved());
 134         }
 135     };
 136 
 137     private final InvalidationListener columnTextListener = observable -> {
 138         TableColumnBase<?,?> column = (TableColumnBase<?,?>) ((StringProperty)observable).getBean();
 139         CheckMenuItem menuItem = columnMenuItems.get(column);
 140         if (menuItem != null) {
 141             menuItem.setText(getText(column.getText(), column));
 142         }
 143     };
 144 
 145     private final WeakInvalidationListener weakTableWidthListener =
 146             new WeakInvalidationListener(tableWidthListener);
 147 
 148     private final WeakInvalidationListener weakTablePaddingListener =
 149             new WeakInvalidationListener(tablePaddingListener);
 150 
 151     private final WeakListChangeListener weakVisibleLeafColumnsListener =
 152             new WeakListChangeListener(visibleLeafColumnsListener);
 153 
 154     private final WeakListChangeListener weakTableColumnsListener =
 155             new WeakListChangeListener(tableColumnsListener);
 156 
 157     private final WeakInvalidationListener weakColumnTextListener =
 158             new WeakInvalidationListener(columnTextListener);
 159 
 160 
 161 
 162     /***************************************************************************
 163      *                                                                         *
 164      * Constructor                                                             *
 165      *                                                                         *
 166      **************************************************************************/
 167 
 168     /**
 169      * Creates a new TableHeaderRow instance to visually represent the column
 170      * header area of controls such as {@link javafx.scene.control.TableView} and
 171      * {@link javafx.scene.control.TreeTableView}.
 172      *
 173      * @param skin The skin used by the UI control.
 174      */
 175     public TableHeaderRow(final TableViewSkinBase skin) {
 176         this.tableSkin = skin;
 177         this.flow = skin.flow;
 178 
 179         getStyleClass().setAll("column-header-background");
 180 
 181         // clip the header so it doesn't show outside of the table bounds
 182         clip = new Rectangle();
 183         clip.setSmooth(false);
 184         clip.heightProperty().bind(heightProperty());
 185         setClip(clip);
 186 
 187         // listen to table width to keep header in sync
 188         updateTableWidth();
 189         tableSkin.getSkinnable().widthProperty().addListener(weakTableWidthListener);
 190         tableSkin.getSkinnable().paddingProperty().addListener(weakTablePaddingListener);
 191         skin.getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
 192 
 193         // popup menu for hiding/showing columns
 194         columnPopupMenu = new ContextMenu();
 195         updateTableColumnListeners(tableSkin.getColumns(), Collections.<TableColumnBase<?,?>>emptyList());
 196         tableSkin.getVisibleLeafColumns().addListener(weakTableColumnsListener);
 197         tableSkin.getColumns().addListener(weakTableColumnsListener);
 198 
 199         // drag header region. Used to indicate the current column being reordered
 200         dragHeader = new StackPane();
 201         dragHeader.setVisible(false);
 202         dragHeader.getStyleClass().setAll("column-drag-header");
 203         dragHeader.setManaged(false);
 204         dragHeader.getChildren().add(dragHeaderLabel);
 205 
 206         // the header lives inside a NestedTableColumnHeader
 207         NestedTableColumnHeader rootHeader = new NestedTableColumnHeader(tableSkin, null);
 208         setRootHeader(rootHeader);
 209         rootHeader.setFocusTraversable(false);
 210         rootHeader.setTableHeaderRow(this);
 211 
 212         // The 'filler' area that extends from the right-most column to the edge
 213         // of the tableview, or up to the 'column control' button
 214         filler = new Region();
 215         filler.getStyleClass().setAll("filler");
 216 
 217         // Give focus to the table when an empty area of the header row is clicked.
 218         // This ensures the user knows that the table has focus.
 219         setOnMousePressed(e -> {
 220             skin.getSkinnable().requestFocus();
 221         });
 222 
 223         // build the corner region button for showing the popup menu
 224         final StackPane image = new StackPane();
 225         image.setSnapToPixel(false);
 226         image.getStyleClass().setAll("show-hide-column-image");
 227         cornerRegion = new StackPane() {
 228             @Override protected void layoutChildren() {
 229                 double imageWidth = image.snappedLeftInset() + image.snappedRightInset();
 230                 double imageHeight = image.snappedTopInset() + image.snappedBottomInset();


 234                         0, HPos.CENTER, VPos.CENTER);
 235             }
 236         };
 237         cornerRegion.getStyleClass().setAll("show-hide-columns-button");
 238         cornerRegion.getChildren().addAll(image);
 239         cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
 240         tableSkin.tableMenuButtonVisibleProperty().addListener(valueModel -> {
 241             cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
 242             requestLayout();
 243         });
 244         cornerRegion.setOnMousePressed(me -> {
 245             // show a popupMenu which lists all columns
 246             columnPopupMenu.show(cornerRegion, Side.BOTTOM, 0, 0);
 247             me.consume();
 248         });
 249 
 250         // the actual header
 251         // the region that is anchored above the vertical scrollbar
 252         // a 'ghost' of the header being dragged by the user to force column
 253         // reordering
 254         getChildren().addAll(filler, rootHeader, cornerRegion, dragHeader);
 255     }
 256     
 257 
 258 
 259     /***************************************************************************
 260      *                                                                         *
 261      * Properties                                                              *
 262      *                                                                         *
 263      **************************************************************************/
 264 
 265     // --- reordering
 266     private BooleanProperty reordering = new SimpleBooleanProperty(this, "reordering", false) {
 267         @Override protected void invalidated() {
 268             TableColumnHeader r = getReorderingRegion();
 269             if (r != null) {
 270                 double dragHeaderHeight = r.getNestedColumnHeader() != null ?
 271                         r.getNestedColumnHeader().getHeight() :
 272                         getReorderingRegion().getHeight();
 273 
 274                 dragHeader.resize(dragHeader.getWidth(), dragHeaderHeight);
 275                 dragHeader.setTranslateY(getHeight() - dragHeaderHeight);


 276             }
 277             dragHeader.setVisible(isReordering());




 278         }
 279     };
 280     final void setReordering(boolean value) {
 281         this.reordering.set(value);
 282     }
 283     final boolean isReordering() {
 284         return reordering.get();
 285     }
 286     final BooleanProperty reorderingProperty() {
 287         return reordering;
 288     }













 289 
 290     // --- root header
 291     /*
 292      * The header row is actually just one NestedTableColumnHeader that spans
 293      * the entire width. Nested within this is the TableColumnHeader's and
 294      * NestedTableColumnHeader's, as necessary. This makes it nice and clean
 295      * to handle column reordering - we basically enforce the rule that column
 296      * reordering only occurs within a single NestedTableColumnHeader, and only
 297      * at that level.
 298      */
 299     private ReadOnlyObjectWrapper<NestedTableColumnHeader> rootHeader = new ReadOnlyObjectWrapper<>(this, "rootHeader");
 300     private final ReadOnlyObjectProperty<NestedTableColumnHeader> rootHeaderProperty() {
 301         return rootHeader.getReadOnlyProperty();
 302     }
 303     final NestedTableColumnHeader getRootHeader() {
 304         return rootHeader.get();
 305     }
 306     private final void setRootHeader(NestedTableColumnHeader value) {
 307         rootHeader.set(value);
 308     }
 309 
 310 
 311 
 312     /***************************************************************************
 313      *                                                                         *
 314      * Public API                                                              *
 315      *                                                                         *
 316      **************************************************************************/
 317 
 318     /** {@inheritDoc} */
 319     @Override protected void layoutChildren() {
 320         double x = scrollX;
 321         double headerWidth = snapSize(getRootHeader().prefWidth(-1));
 322         double prefHeight = getHeight() - snappedTopInset() - snappedBottomInset();
 323         double cornerWidth = snapSize(flow.getVbar().prefWidth(-1));
 324 
 325         // position the main nested header
 326         getRootHeader().resizeRelocate(x, snappedTopInset(), headerWidth, prefHeight);
 327         
 328         // position the filler region
 329         final Control control = tableSkin.getSkinnable();
 330         if (control == null) {
 331             return;
 332         }
 333 
 334         final double controlInsets = control.snappedLeftInset() + control.snappedRightInset();
 335         double fillerWidth = tableWidth - headerWidth + filler.getInsets().getLeft() - controlInsets;
 336         fillerWidth -= tableSkin.tableMenuButtonVisibleProperty().get() ? cornerWidth : 0;
 337         filler.setVisible(fillerWidth > 0);
 338         if (fillerWidth > 0) {
 339             filler.resizeRelocate(x + headerWidth, snappedTopInset(), fillerWidth, prefHeight);
 340         }
 341 
 342         // position the top-right rectangle (which sits above the scrollbar)
 343         cornerRegion.resizeRelocate(tableWidth - cornerWidth, snappedTopInset(), cornerWidth, prefHeight);
 344     }
 345 
 346     /** {@inheritDoc} */
 347     @Override protected double computePrefWidth(double height) {
 348         return getRootHeader().prefWidth(height);
 349     }
 350 
 351     /** {@inheritDoc} */
 352     @Override protected double computeMinHeight(double width) {
 353         return computePrefHeight(width);
 354     }
 355 
 356     /** {@inheritDoc} */
 357     @Override protected double computePrefHeight(double width) {
 358         // we hardcode 24.0 here to avoid RT-37616, where the
 359         // entire header row would disappear when all columns were hidden. 
 360         double headerPrefHeight = getRootHeader().prefHeight(width);
 361         headerPrefHeight = headerPrefHeight == 0.0 ? 24.0 : headerPrefHeight;
 362         return snappedTopInset() + headerPrefHeight + snappedBottomInset();
 363     }
 364 
 365     // used to be protected to allow subclasses to modify the horizontal scrolling,
 366     // but made private again for JDK 9
 367     void updateScrollX() {









 368         scrollX = flow.getHbar().isVisible() ? -flow.getHbar().getValue() : 0.0F;
 369         requestLayout();
 370 
 371         // Fix for RT-36392: without this call even though we call requestLayout()
 372         // we don't seem to ever see the layoutChildren() method above called,
 373         // which means the layout is not always updated to use the latest scrollX.
 374         layout();
 375     }
 376 
 377     // used to be protected to allow subclass to customise the width, to allow for features
 378     // such as row headers, but made private again for JDK 9
 379     private void updateTableWidth() {
 380         // snapping added for RT-19428
 381         final Control c = tableSkin.getSkinnable();
 382         if (c == null) {
 383             this.tableWidth = 0;
 384         } else {
 385             Insets insets = c.getInsets() == null ? Insets.EMPTY : c.getInsets();
 386             double padding = snapSize(insets.getLeft()) + snapSize(insets.getRight());
 387             this.tableWidth = snapSize(c.getWidth()) - padding;
 388         }
 389 
 390         clip.setWidth(tableWidth);

 391     }
 392 



 393 
 394 
 395     /***************************************************************************
 396      *                                                                         *
 397      * Private Implementation                                                  *
 398      *                                                                         *
 399      **************************************************************************/
 400 
 401     TableColumnHeader getReorderingRegion() {
 402         return reorderingRegion;
 403     }
 404 
 405     void setReorderingColumn(TableColumnBase rc) {
 406         dragHeaderLabel.setText(rc == null ? "" : rc.getText());
 407     }
 408 
 409     void setReorderingRegion(TableColumnHeader reorderingRegion) {
 410         this.reorderingRegion = reorderingRegion;
 411 
 412         if (reorderingRegion != null) {
 413             dragHeader.resize(reorderingRegion.getWidth(), dragHeader.getHeight());
 414         }
 415     }
 416 
 417     void setDragHeaderX(double dragHeaderX) {
 418         dragHeader.setTranslateX(dragHeaderX);
 419     }
 420 
 421     TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col) {




















 422         if (col == null) return null;
 423         List<TableColumnBase<?,?>> columnChain = new ArrayList<>();
 424         columnChain.add(col);
 425 
 426         TableColumnBase<?,?> parent = col.getParentColumn();
 427         while (parent != null) {
 428             columnChain.add(0, parent);
 429             parent = parent.getParentColumn();
 430         }
 431 
 432         // we now have a list from top to bottom of a nested column hierarchy,
 433         // and we can now navigate down to retrieve the header with ease
 434         TableColumnHeader currentHeader = getRootHeader();
 435         for (int depth = 0; depth < columnChain.size(); depth++) {
 436             // this is the column we are looking for at this depth
 437             TableColumnBase<?,?> column = columnChain.get(depth);
 438 
 439             // and now we iterate through the nested table column header at this
 440             // level to get the header
 441             currentHeader = getColumnHeaderFor(column, currentHeader);
 442         }
 443         return currentHeader;
 444     }
 445 
 446     private TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col, TableColumnHeader currentHeader) {
 447         if (currentHeader instanceof NestedTableColumnHeader) {
 448             List<TableColumnHeader> headers = ((NestedTableColumnHeader)currentHeader).getColumnHeaders();
 449 
 450             for (int i = 0; i < headers.size(); i++) {
 451                 TableColumnHeader header = headers.get(i);
 452                 if (header.getTableColumn() == col) {
 453                     return header;
 454                 }
 455             }
 456         }
 457 
 458         return null;
 459     }
 460 







 461     private void updateTableColumnListeners(List<? extends TableColumnBase<?,?>> added, List<? extends TableColumnBase<?,?>> removed) {
 462         // remove binding from all removed items
 463         for (TableColumnBase tc : removed) {
 464             remove(tc);
 465         }
 466 
 467         rebuildColumnMenu();
 468     }
 469 
 470     private void remove(TableColumnBase<?,?> col) {
 471         if (col == null) return;
 472 
 473         CheckMenuItem item = columnMenuItems.remove(col);
 474         if (item != null) {
 475             col.textProperty().removeListener(weakColumnTextListener);
 476             item.selectedProperty().unbindBidirectional(col.visibleProperty());
 477 
 478             columnPopupMenu.getItems().remove(item);
 479         }
 480 
 481         if (! col.getColumns().isEmpty()) {
 482             for (TableColumnBase tc : col.getColumns()) {
 483                 remove(tc);
 484             }
 485         }
 486     }
 487 
 488     private void rebuildColumnMenu() {
 489         columnPopupMenu.getItems().clear();
 490 
 491         for (TableColumnBase<?,?> col : tableSkin.getColumns()) {
 492             // we only create menu items for leaf columns, visible or not
 493             if (col.getColumns().isEmpty()) {
 494                 createMenuItem(col);
 495             } else {
 496                 List<TableColumnBase<?,?>> leafColumns = getLeafColumns(col);
 497                 for (TableColumnBase<?,?> _col : leafColumns) {
 498                     createMenuItem(_col);
 499                 }
 500             }
 501         }
 502     }
 503 
 504     private List<TableColumnBase<?,?>> getLeafColumns(TableColumnBase<?,?> col) {
 505         List<TableColumnBase<?,?>> leafColumns = new ArrayList<>();
 506 
 507         for (TableColumnBase<?,?> _col : col.getColumns()) {
 508             if (_col.getColumns().isEmpty()) {
 509                 leafColumns.add(_col);
 510             } else {
 511                 leafColumns.addAll(getLeafColumns(_col));