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

Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
   1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.scene.control.skin;
  27 

  28 import javafx.collections.WeakListChangeListener;
  29 import java.util.ArrayList;
  30 import java.util.List;
  31 import java.util.Map;
  32 import java.util.WeakHashMap;
  33 
  34 import javafx.collections.FXCollections;
  35 import javafx.collections.ListChangeListener;
  36 import javafx.collections.ObservableList;
  37 import javafx.event.EventHandler;
  38 import javafx.geometry.NodeOrientation;
  39 import javafx.scene.Cursor;
  40 import javafx.scene.Node;
  41 import javafx.scene.control.*;
  42 import javafx.scene.input.MouseEvent;
  43 import javafx.scene.paint.Color;
  44 import javafx.scene.shape.Rectangle;
  45 import javafx.util.Callback;
  46 
  47 /**
  48  * <p>This class is used to construct the header of a TableView. We take the approach
  49  * that every TableView header is nested - even if it isn't. This allows for us
  50  * to use the same code for building a single row of TableColumns as we would
  51  * with a heavily nested sequences of TableColumns. Because of this, the
  52  * TableHeaderRow class consists of just one instance of a NestedTableColumnHeader.
  53  *




  54  */
  55 public class NestedTableColumnHeader extends TableColumnHeader {
  56 
  57     /***************************************************************************
  58      *                                                                         *
  59      * Static Fields                                                           *
  60      *                                                                         *
  61      **************************************************************************/
  62 
  63     private static final int DRAG_RECT_WIDTH = 4;
  64 
  65     private static final String TABLE_COLUMN_KEY = "TableColumn";
  66     private static final String TABLE_COLUMN_HEADER_KEY = "TableColumnHeader";
  67 
  68 
  69 
  70     /***************************************************************************
  71      *                                                                         *
  72      * Private Fields                                                          *
  73      *                                                                         *
  74      **************************************************************************/
  75 
  76     /**
  77      * Represents the actual columns directly contained in this nested column.
  78      * It does NOT include ANY of the children of these columns, if any exist.
  79      */
  80     private ObservableList<? extends TableColumnBase> columns;
  81 
  82     private TableColumnHeader label;
  83 
  84     private ObservableList<TableColumnHeader> columnHeaders;

  85 
  86     // used for column resizing
  87     private double lastX = 0.0F;
  88     private double dragAnchorX = 0.0;
  89 
  90     // drag rectangle overlays
  91     private Map<TableColumnBase<?,?>, Rectangle> dragRects = new WeakHashMap<>();
  92 
  93     boolean updateColumns = true;
  94 
  95 
  96     
  97     /***************************************************************************
  98      *                                                                         *
  99      * Constructor                                                             *
 100      *                                                                         *
 101      **************************************************************************/
 102     







 103     public NestedTableColumnHeader(final TableViewSkinBase skin, final TableColumnBase tc) {
 104         super(skin, tc);
 105         
 106         getStyleClass().setAll("nested-column-header");
 107         setFocusTraversable(false);
 108 
 109         // init UI
 110         label = new TableColumnHeader(skin, getTableColumn());
 111         label.setTableHeaderRow(getTableHeaderRow());
 112         label.setParentHeader(getParentHeader());
 113         label.setNestedColumnHeader(this);
 114 
 115         if (getTableColumn() != null) {
 116             changeListenerHandler.registerChangeListener(getTableColumn().textProperty(), "TABLE_COLUMN_TEXT");

 117         }
 118 
 119         changeListenerHandler.registerChangeListener(skin.columnResizePolicyProperty(), "TABLE_VIEW_COLUMN_RESIZE_POLICY");
 120     }
 121 
 122     
 123     
 124     /***************************************************************************
 125      *                                                                         *
 126      * Listeners                                                               *
 127      *                                                                         *
 128      **************************************************************************/
 129     
 130     private final ListChangeListener<TableColumnBase> columnsListener = c -> {
 131         setHeadersNeedUpdate();
 132     };
 133     
 134     private final WeakListChangeListener weakColumnsListener =
 135             new WeakListChangeListener(columnsListener);
 136 
 137     private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
 138         @Override public void handle(MouseEvent me) {
 139             Rectangle rect = (Rectangle) me.getSource();
 140             TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 141             NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 142 
 143             if (! header.isColumnResizingEnabled()) return;
 144 
 145             if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
 146                 // the user wants to resize the column such that its
 147                 // width is equal to the widest element in the column
 148                 header.getTableViewSkin().resizeColumnToFitContent(column, -1);
 149             } else {
 150                 // rather than refer to the rect variable, we just grab
 151                 // it from the source to prevent a small memory leak.
 152                 Rectangle innerRect = (Rectangle) me.getSource();
 153                 double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
 154                 header.dragAnchorX = me.getSceneX();
 155                 header.columnResizingStarted(startX);
 156             }
 157             me.consume();
 158         }
 159     };
 160 
 161     private static final EventHandler<MouseEvent> rectMouseDragged = new EventHandler<MouseEvent>() {
 162         @Override public void handle(MouseEvent me) {
 163             Rectangle rect = (Rectangle) me.getSource();
 164             TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 165             NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 166 
 167             if (! header.isColumnResizingEnabled()) return;
 168 
 169             header.columnResizing(column, me);
 170             me.consume();
 171         }
 172     };
 173 
 174     private static final EventHandler<MouseEvent> rectMouseReleased = new EventHandler<MouseEvent>() {
 175         @Override public void handle(MouseEvent me) {
 176             Rectangle rect = (Rectangle) me.getSource();
 177             TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 178             NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 179 
 180             if (! header.isColumnResizingEnabled()) return;
 181 
 182             header.columnResizingComplete(column, me);
 183             me.consume();
 184         }
 185     };
 186 
 187     private static final EventHandler<MouseEvent> rectCursorChangeListener = new EventHandler<MouseEvent>() {
 188         @Override public void handle(MouseEvent me) {
 189             Rectangle rect = (Rectangle) me.getSource();
 190             TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 191             NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 192 
 193             if (header.getCursor() == null) { // If there's a cursor for the whole header, don't override it
 194                 rect.setCursor(header.isColumnResizingEnabled() && rect.isHover() &&
 195                         column.isResizable() ? Cursor.H_RESIZE : null);
 196             }
 197         }
 198     };
 199     
 200 
 201 
 202     /***************************************************************************
 203      *                                                                         *
 204      * Public Methods                                                          *
 205      *                                                                         *
 206      **************************************************************************/
 207 
 208     @Override protected void handlePropertyChanged(String p) {
 209         super.handlePropertyChanged(p);

 210 
 211         if ("TABLE_VIEW_COLUMN_RESIZE_POLICY".equals(p)) {
 212             updateContent();
 213         } else if ("TABLE_COLUMN_TEXT".equals(p)) {
 214             label.setVisible(getTableColumn().getText() != null && ! getTableColumn().getText().isEmpty());





























 215         }














 216     }
 217 
 218     @Override public void setTableHeaderRow(TableHeaderRow header) {












































































 219         super.setTableHeaderRow(header);
 220 
 221         label.setTableHeaderRow(header);
 222 
 223         // tell all children columns what TableHeader they belong to
 224         for (TableColumnHeader c : getColumnHeaders()) {
 225             c.setTableHeaderRow(header);
 226         }
 227     }
 228 
 229     @Override public void setParentHeader(NestedTableColumnHeader parentHeader) {
 230         super.setParentHeader(parentHeader);
 231         label.setParentHeader(parentHeader);
 232     }
 233 
 234     ObservableList<? extends TableColumnBase> getColumns() {
 235         return columns;
 236     }
 237 
 238     void setColumns(ObservableList<? extends TableColumnBase> newColumns) {
 239         if (this.columns != null) {
 240             this.columns.removeListener(weakColumnsListener);
 241         }
 242         
 243         this.columns = newColumns;  
 244         
 245         if (this.columns != null) {
 246             this.columns.addListener(weakColumnsListener);
 247         }
 248     }
 249     


 260         // iterate through all columns, unless we've got no child columns
 261         // any longer, in which case we should switch to a TableColumnHeader 
 262         // instead
 263         if (getColumns().isEmpty()) {
 264             // iterate through all current headers, telling them to clean up
 265             for (int i = 0; i < getColumnHeaders().size(); i++) {
 266                 TableColumnHeader header = getColumnHeaders().get(i);
 267                 header.dispose();
 268             }
 269 
 270             // switch out to be a TableColumn instead, if we have a parent header
 271             NestedTableColumnHeader parentHeader = getParentHeader();
 272             if (parentHeader != null) {
 273                 List<TableColumnHeader> parentColumnHeaders = parentHeader.getColumnHeaders();
 274                 int index = parentColumnHeaders.indexOf(this);
 275                 if (index >= 0 && index < parentColumnHeaders.size()) {
 276                     parentColumnHeaders.set(index, createColumnHeader(getTableColumn()));
 277                 }
 278             } else {
 279                 // otherwise just remove all the columns
 280                 getColumnHeaders().clear();
 281             }
 282         } else {
 283             List<TableColumnHeader> oldHeaders = new ArrayList<>(getColumnHeaders());
 284             List<TableColumnHeader> newHeaders = new ArrayList<>();
 285             
 286             for (int i = 0; i < getColumns().size(); i++) {
 287                 TableColumnBase<?,?> column = getColumns().get(i);
 288                 if (column == null || ! column.isVisible()) continue;
 289 
 290                 // check if the header already exists and reuse it
 291                 boolean found = false;
 292                 for (int j = 0; j < oldHeaders.size(); j++) {
 293                     TableColumnHeader oldColumn = oldHeaders.get(j);
 294                     if (column == oldColumn.getTableColumn()) {
 295                         newHeaders.add(oldColumn);
 296                         found = true;
 297                         break;
 298                     }
 299                 }
 300 
 301                 // otherwise create a new table column header
 302                 if (!found) {
 303                     newHeaders.add(createColumnHeader(column));
 304                 }
 305             }
 306             
 307             getColumnHeaders().setAll(newHeaders);
 308 
 309             // dispose all old headers
 310             oldHeaders.removeAll(newHeaders);
 311             for (int i = 0; i < oldHeaders.size(); i++) {
 312                 oldHeaders.get(i).dispose();
 313             }
 314         }
 315         
 316         // update the content
 317         updateContent();
 318         
 319         // RT-33596: Do CSS now, as we are in the middle of layout pass and the headers are new Nodes w/o CSS done
 320         for (TableColumnHeader header : getColumnHeaders()) {
 321             header.applyCss();
 322         }
 323     }
 324     
 325     @Override void dispose() {
 326         super.dispose();
 327         
 328         if (label != null) {
 329             label.dispose();
 330         }
 331         
 332         if (getColumns() != null) {
 333             getColumns().removeListener(weakColumnsListener);
 334         }
 335 
 336         for (int i = 0; i < getColumnHeaders().size(); i++) {
 337             TableColumnHeader header = getColumnHeaders().get(i);
 338             header.dispose();
 339         }
 340 
 341         for (Rectangle rect : dragRects.values()) {
 342             if (rect != null) {
 343                 rect.visibleProperty().unbind();
 344             }
 345         }
 346         dragRects.clear();
 347         getChildren().clear();
 348         
 349         changeListenerHandler.dispose();
 350     }
 351 
 352     public ObservableList<TableColumnHeader> getColumnHeaders() {
 353         if (columnHeaders == null) columnHeaders = FXCollections.<TableColumnHeader>observableArrayList();
 354         return columnHeaders; 
 355     }
 356 
 357     @Override protected void layoutChildren() {
 358         double w = getWidth() - snappedLeftInset() - snappedRightInset();
 359         double h = getHeight() - snappedTopInset() - snappedBottomInset();
 360 
 361         int labelHeight = (int) label.prefHeight(-1);
 362 
 363         if (label.isVisible()) {
 364             // label gets to span whole width and sits at top
 365             label.resize(w, labelHeight);
 366             label.relocate(snappedLeftInset(), snappedTopInset());
 367         }
 368 
 369         // children columns need to share the total available width
 370         double x = snappedLeftInset();
 371         int pos = 0;
 372         for (int i = 0, max = getColumnHeaders().size(); i < max; i++) {
 373             TableColumnHeader n = getColumnHeaders().get(i);
 374             if (! n.isVisible()) continue;
 375 
 376             double prefWidth = snapSize(n.prefWidth(-1));
 377 //            double prefHeight = n.prefHeight(-1);
 378 
 379             // position the column header in the default location...
 380             n.resize(prefWidth, snapSize(h - labelHeight));
 381             n.relocate(x, labelHeight + snappedTopInset());
 382 
 383 //            // ...but, if there are no children of this column, we should ensure
 384 //            // that it is resized vertically such that it goes to the very
 385 //            // bottom of the table header row.
 386 //            if (getTableHeaderRow() != null && n.getCol().getColumns().isEmpty()) {
 387 //                Bounds bounds = getTableHeaderRow().sceneToLocal(n.localToScene(n.getBoundsInLocal()));
 388 //                prefHeight = getTableHeaderRow().getHeight() - bounds.getMinY();
 389 //                n.resize(prefWidth, prefHeight);
 390 //            }
 391 
 392             // shuffle along the x-axis appropriately
 393             x += prefWidth;
 394 
 395             // position drag overlay to intercept column resize requests
 396             Rectangle dragRect = dragRects.get(n.getTableColumn());
 397             if (dragRect != null) {
 398                 dragRect.setHeight(n.getDragRectHeight());
 399                 dragRect.relocate(x - DRAG_RECT_WIDTH / 2, snappedTopInset() + labelHeight);
 400             }
 401         }
 402     }
 403 
 404     @Override
 405     double getDragRectHeight() {
 406         return label.prefHeight(-1);
 407     }
 408 
 409     // sum up all children columns
 410     @Override protected double computePrefWidth(double height) {
 411         checkState();
 412 
 413         double width = 0.0F;
 414 
 415         if (getColumns() != null) {
 416             for (TableColumnHeader c : getColumnHeaders()) {
 417                 if (c.isVisible()) {
 418                     width += snapSize(c.computePrefWidth(height));
 419                 }
 420             }
 421         }
 422 
 423         return width;
 424     }
 425 
 426     @Override protected double computePrefHeight(double width) {
 427         checkState();
 428 
 429         double height = 0.0F;
 430 
 431         if (getColumnHeaders() != null) {
 432             for (TableColumnHeader n : getColumnHeaders()) {
 433                 height = Math.max(height, n.prefHeight(-1));
 434             }
 435         }
 436 
 437         return height + label.prefHeight(-1) + snappedTopInset() + snappedBottomInset();
 438     }
 439 
 440     // protected to allow subclasses to customise the column header types
 441     protected TableColumnHeader createTableColumnHeader(TableColumnBase col) {
 442         return col.getColumns().isEmpty() ?
 443                 new TableColumnHeader(getTableViewSkin(), col) :
 444                 new NestedTableColumnHeader(getTableViewSkin(), col);
 445     }
 446 
 447     // allowing subclasses to force an update on the headers
 448     protected void setHeadersNeedUpdate() {
 449         updateColumns = true;
 450 
 451         // go through children columns - they should update too
 452         for (int i = 0; i < getColumnHeaders().size(); i++) {
 453             TableColumnHeader header = getColumnHeaders().get(i);
 454             if (header instanceof NestedTableColumnHeader) {
 455                 ((NestedTableColumnHeader)header).setHeadersNeedUpdate();
 456             }
 457         }
 458         requestLayout();
 459     }
 460 
 461 
 462 
 463     /***************************************************************************
 464      *                                                                         *
 465      * Private Implementation                                                  *
 466      *                                                                         *
 467      **************************************************************************/
 468 
 469     private void updateContent() {
 470         // create a temporary list so we only do addAll into the main content
 471         // observableArrayList once.
 472         final List<Node> content = new ArrayList<Node>();
 473 
 474         // the label is the region that sits above the children columns
 475         content.add(label);
 476 
 477         // all children columns
 478         content.addAll(getColumnHeaders());
 479 
 480         // Small transparent overlays that sit at the start and end of each
 481         // column to intercept user drag gestures to enable column resizing.
 482         if (isColumnResizingEnabled()) {
 483             rebuildDragRects();
 484             content.addAll(dragRects.values());
 485         }
 486 
 487         getChildren().setAll(content);
 488     }
 489     
 490     private void rebuildDragRects() {
 491         if (! isColumnResizingEnabled()) return;
 492         
 493         getChildren().removeAll(dragRects.values());
 494         
 495         for (Rectangle rect : dragRects.values()) {
 496             rect.visibleProperty().unbind();
 497         }
 498         dragRects.clear();
 499 
 500         List<? extends TableColumnBase> columns = getColumns();
 501 
 502         if (columns == null) {
 503             return;
 504         }
 505 
 506         final TableViewSkinBase<?,?,?,?,?,?> skin = getTableViewSkin();
 507         Callback<ResizeFeaturesBase, Boolean> columnResizePolicy = skin.columnResizePolicyProperty().get();
 508         boolean isConstrainedResize =
 509                 skin instanceof TableViewSkin ? TableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
 510                 skin instanceof TreeTableViewSkin ? TreeTableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
 511                 false;
 512 
 513         // RT-32547 - don't show resize cursor when in constrained resize mode
 514         // and there is only one column
 515         if (isConstrainedResize && skin.getVisibleLeafColumns().size() == 1) {
 516             return;
 517         }
 518 
 519         for (int col = 0; col < columns.size(); col++) {
 520             if (isConstrainedResize && col == getColumns().size() - 1) {
 521                 break;
 522             }
 523 
 524             final TableColumnBase c = columns.get(col);
 525             final Rectangle rect = new Rectangle();
 526             rect.getProperties().put(TABLE_COLUMN_KEY, c);


   1 /*
   2  * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
  29 import javafx.collections.WeakListChangeListener;
  30 import java.util.ArrayList;
  31 import java.util.List;
  32 import java.util.Map;
  33 import java.util.WeakHashMap;
  34 
  35 import javafx.collections.FXCollections;
  36 import javafx.collections.ListChangeListener;
  37 import javafx.collections.ObservableList;
  38 import javafx.event.EventHandler;
  39 import javafx.geometry.NodeOrientation;
  40 import javafx.scene.Cursor;
  41 import javafx.scene.Node;
  42 import javafx.scene.control.*;
  43 import javafx.scene.input.MouseEvent;
  44 import javafx.scene.paint.Color;
  45 import javafx.scene.shape.Rectangle;
  46 import javafx.util.Callback;
  47 
  48 /**
  49  * <p>This class is used to construct the header of a TableView. We take the approach
  50  * that every TableView header is nested - even if it isn't. This allows for us
  51  * to use the same code for building a single row of TableColumns as we would
  52  * with a heavily nested sequences of TableColumns. Because of this, the
  53  * TableHeaderRow class consists of just one instance of a NestedTableColumnHeader.
  54  *
  55  * @since 9
  56  * @see TableColumnHeader
  57  * @see TableHeaderRow
  58  * @see TableColumnBase
  59  */
  60 public class NestedTableColumnHeader extends TableColumnHeader {
  61 
  62     /***************************************************************************
  63      *                                                                         *
  64      * Static Fields                                                           *
  65      *                                                                         *
  66      **************************************************************************/
  67 
  68     private static final int DRAG_RECT_WIDTH = 4;
  69 
  70     private static final String TABLE_COLUMN_KEY = "TableColumn";
  71     private static final String TABLE_COLUMN_HEADER_KEY = "TableColumnHeader";
  72 
  73 
  74 
  75     /***************************************************************************
  76      *                                                                         *
  77      * Private Fields                                                          *
  78      *                                                                         *
  79      **************************************************************************/
  80 
  81     /**
  82      * Represents the actual columns directly contained in this nested column.
  83      * It does NOT include ANY of the children of these columns, if any exist.
  84      */
  85     private ObservableList<? extends TableColumnBase> columns;
  86 
  87     private TableColumnHeader label;
  88 
  89     private ObservableList<TableColumnHeader> columnHeaders;
  90     private ObservableList<TableColumnHeader> unmodifiableColumnHeaders;
  91 
  92     // used for column resizing
  93     private double lastX = 0.0F;
  94     private double dragAnchorX = 0.0;
  95 
  96     // drag rectangle overlays
  97     private Map<TableColumnBase<?,?>, Rectangle> dragRects = new WeakHashMap<>();
  98 
  99     boolean updateColumns = true;
 100 
 101 
 102     
 103     /***************************************************************************
 104      *                                                                         *
 105      * Constructor                                                             *
 106      *                                                                         *
 107      **************************************************************************/
 108 
 109     /**
 110      * Creates a new NestedTableColumnHeader instance to visually represent the given
 111      * {@link TableColumnBase} instance.
 112      *
 113      * @param skin The skin used by the UI control.
 114      * @param tc The table column to be visually represented by this instance.
 115      */
 116     public NestedTableColumnHeader(final TableViewSkinBase skin, final TableColumnBase tc) {
 117         super(skin, tc);
 118         
 119         getStyleClass().setAll("nested-column-header");
 120         setFocusTraversable(false);
 121 
 122         // init UI
 123         label = new TableColumnHeader(skin, getTableColumn());
 124         label.setTableHeaderRow(getTableHeaderRow());
 125         label.setParentHeader(getParentHeader());
 126         label.setNestedColumnHeader(this);
 127 
 128         if (getTableColumn() != null) {
 129             changeListenerHandler.registerChangeListener(getTableColumn().textProperty(), e ->
 130                     label.setVisible(getTableColumn().getText() != null && ! getTableColumn().getText().isEmpty()));
 131         }
 132 
 133         changeListenerHandler.registerChangeListener(skin.columnResizePolicyProperty(), e -> updateContent());
 134     }
 135 
 136     
 137     
 138     /***************************************************************************
 139      *                                                                         *
 140      * Listeners                                                               *
 141      *                                                                         *
 142      **************************************************************************/
 143     
 144     private final ListChangeListener<TableColumnBase> columnsListener = c -> {
 145         setHeadersNeedUpdate();
 146     };
 147     
 148     private final WeakListChangeListener weakColumnsListener =
 149             new WeakListChangeListener(columnsListener);
 150 
 151     private static final EventHandler<MouseEvent> rectMousePressed = me -> {

 152         Rectangle rect = (Rectangle) me.getSource();
 153         TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 154         NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 155 
 156         if (! header.isColumnResizingEnabled()) return;
 157 
 158         if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
 159             // the user wants to resize the column such that its
 160             // width is equal to the widest element in the column
 161             header.getTableViewSkin().resizeColumnToFitContent(column, -1);
 162         } else {
 163             // rather than refer to the rect variable, we just grab
 164             // it from the source to prevent a small memory leak.
 165             Rectangle innerRect = (Rectangle) me.getSource();
 166             double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
 167             header.dragAnchorX = me.getSceneX();
 168             header.columnResizingStarted(startX);
 169         }
 170         me.consume();

 171     };
 172 
 173     private static final EventHandler<MouseEvent> rectMouseDragged = me -> {

 174         Rectangle rect = (Rectangle) me.getSource();
 175         TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 176         NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 177 
 178         if (! header.isColumnResizingEnabled()) return;
 179 
 180         header.columnResizing(column, me);
 181         me.consume();

 182     };
 183 
 184     private static final EventHandler<MouseEvent> rectMouseReleased = me -> {

 185         Rectangle rect = (Rectangle) me.getSource();
 186         TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 187         NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 188 
 189         if (! header.isColumnResizingEnabled()) return;
 190 
 191         header.columnResizingComplete(column, me);
 192         me.consume();

 193     };
 194 
 195     private static final EventHandler<MouseEvent> rectCursorChangeListener = me -> {

 196         Rectangle rect = (Rectangle) me.getSource();
 197         TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
 198         NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
 199 
 200         if (header.getCursor() == null) { // If there's a cursor for the whole header, don't override it
 201             rect.setCursor(header.isColumnResizingEnabled() && rect.isHover() &&
 202                     column.isResizable() ? Cursor.H_RESIZE : null);
 203         }

 204     };
 205     
 206 
 207 
 208     /***************************************************************************
 209      *                                                                         *
 210      * Public Methods                                                          *
 211      *                                                                         *
 212      **************************************************************************/
 213 
 214     /** {@inheritDoc} */
 215     @Override void dispose() {
 216         super.dispose();
 217         
 218         if (label != null) {
 219             label.dispose();
 220         }
 221         
 222         if (getColumns() != null) {
 223             getColumns().removeListener(weakColumnsListener);
 224         }
 225 
 226         for (int i = 0; i < getColumnHeaders().size(); i++) {
 227             TableColumnHeader header = getColumnHeaders().get(i);
 228             header.dispose();
 229         }
 230 
 231         for (Rectangle rect : dragRects.values()) {
 232             if (rect != null) {
 233                 rect.visibleProperty().unbind();
 234             }
 235         }
 236         dragRects.clear();
 237         getChildren().clear();
 238         
 239         changeListenerHandler.dispose();
 240     }
 241 
 242     /**
 243      * Returns an unmodifiable list of the {@link TableColumnHeader} instances
 244      * that are children of this NestedTableColumnHeader.
 245      */
 246     @ReturnsUnmodifiableCollection
 247     public final ObservableList<TableColumnHeader> getColumnHeaders() {
 248         if (columnHeaders == null) {
 249             columnHeaders = FXCollections.<TableColumnHeader>observableArrayList();
 250             unmodifiableColumnHeaders = FXCollections.unmodifiableObservableList(columnHeaders);
 251         }
 252         return unmodifiableColumnHeaders;
 253     }
 254 
 255     /** {@inheritDoc} */
 256     @Override protected void layoutChildren() {
 257         double w = getWidth() - snappedLeftInset() - snappedRightInset();
 258         double h = getHeight() - snappedTopInset() - snappedBottomInset();
 259 
 260         int labelHeight = (int) label.prefHeight(-1);
 261 
 262         if (label.isVisible()) {
 263             // label gets to span whole width and sits at top
 264             label.resize(w, labelHeight);
 265             label.relocate(snappedLeftInset(), snappedTopInset());
 266         }
 267 
 268         // children columns need to share the total available width
 269         double x = snappedLeftInset();
 270         int pos = 0;
 271         for (int i = 0, max = getColumnHeaders().size(); i < max; i++) {
 272             TableColumnHeader n = getColumnHeaders().get(i);
 273             if (! n.isVisible()) continue;
 274 
 275             double prefWidth = snapSize(n.prefWidth(-1));
 276 //            double prefHeight = n.prefHeight(-1);
 277 
 278             // position the column header in the default location...
 279             n.resize(prefWidth, snapSize(h - labelHeight));
 280             n.relocate(x, labelHeight + snappedTopInset());
 281 
 282 //            // ...but, if there are no children of this column, we should ensure
 283 //            // that it is resized vertically such that it goes to the very
 284 //            // bottom of the table header row.
 285 //            if (getTableHeaderRow() != null && n.getCol().getColumns().isEmpty()) {
 286 //                Bounds bounds = getTableHeaderRow().sceneToLocal(n.localToScene(n.getBoundsInLocal()));
 287 //                prefHeight = getTableHeaderRow().getHeight() - bounds.getMinY();
 288 //                n.resize(prefWidth, prefHeight);
 289 //            }
 290 
 291             // shuffle along the x-axis appropriately
 292             x += prefWidth;
 293 
 294             // position drag overlay to intercept column resize requests
 295             Rectangle dragRect = dragRects.get(n.getTableColumn());
 296             if (dragRect != null) {
 297                 dragRect.setHeight(n.getDragRectHeight());
 298                 dragRect.relocate(x - DRAG_RECT_WIDTH / 2, snappedTopInset() + labelHeight);
 299             }
 300         }
 301     }
 302 
 303     // sum up all children columns
 304     /** {@inheritDoc} */
 305     @Override protected double computePrefWidth(double height) {
 306         checkState();
 307 
 308         double width = 0.0F;
 309 
 310         if (getColumns() != null) {
 311             for (TableColumnHeader c : getColumnHeaders()) {
 312                 if (c.isVisible()) {
 313                     width += snapSize(c.computePrefWidth(height));
 314                 }
 315             }
 316         }
 317 
 318         return width;
 319     }
 320 
 321     /** {@inheritDoc} */
 322     @Override protected double computePrefHeight(double width) {
 323         checkState();
 324 
 325         double height = 0.0F;
 326 
 327         if (getColumnHeaders() != null) {
 328             for (TableColumnHeader n : getColumnHeaders()) {
 329                 height = Math.max(height, n.prefHeight(-1));
 330             }
 331         }
 332 
 333         return height + label.prefHeight(-1) + snappedTopInset() + snappedBottomInset();
 334     }
 335 
 336 
 337 
 338     /***************************************************************************
 339      *                                                                         *
 340      * Private Implementation                                                  *
 341      *                                                                         *
 342      **************************************************************************/
 343 
 344     @Override void setTableHeaderRow(TableHeaderRow header) {
 345         super.setTableHeaderRow(header);
 346 
 347         label.setTableHeaderRow(header);
 348 
 349         // tell all children columns what TableHeader they belong to
 350         for (TableColumnHeader c : getColumnHeaders()) {
 351             c.setTableHeaderRow(header);
 352         }
 353     }
 354 
 355     @Override void setParentHeader(NestedTableColumnHeader parentHeader) {
 356         super.setParentHeader(parentHeader);
 357         label.setParentHeader(parentHeader);
 358     }
 359 
 360     ObservableList<? extends TableColumnBase> getColumns() {
 361         return columns;
 362     }
 363 
 364     void setColumns(ObservableList<? extends TableColumnBase> newColumns) {
 365         if (this.columns != null) {
 366             this.columns.removeListener(weakColumnsListener);
 367         }
 368 
 369         this.columns = newColumns;
 370 
 371         if (this.columns != null) {
 372             this.columns.addListener(weakColumnsListener);
 373         }
 374     }
 375 


 386         // iterate through all columns, unless we've got no child columns
 387         // any longer, in which case we should switch to a TableColumnHeader
 388         // instead
 389         if (getColumns().isEmpty()) {
 390             // iterate through all current headers, telling them to clean up
 391             for (int i = 0; i < getColumnHeaders().size(); i++) {
 392                 TableColumnHeader header = getColumnHeaders().get(i);
 393                 header.dispose();
 394             }
 395 
 396             // switch out to be a TableColumn instead, if we have a parent header
 397             NestedTableColumnHeader parentHeader = getParentHeader();
 398             if (parentHeader != null) {
 399                 List<TableColumnHeader> parentColumnHeaders = parentHeader.getColumnHeaders();
 400                 int index = parentColumnHeaders.indexOf(this);
 401                 if (index >= 0 && index < parentColumnHeaders.size()) {
 402                     parentColumnHeaders.set(index, createColumnHeader(getTableColumn()));
 403                 }
 404             } else {
 405                 // otherwise just remove all the columns
 406                 columnHeaders.clear();
 407             }
 408         } else {
 409             List<TableColumnHeader> oldHeaders = new ArrayList<>(getColumnHeaders());
 410             List<TableColumnHeader> newHeaders = new ArrayList<>();
 411 
 412             for (int i = 0; i < getColumns().size(); i++) {
 413                 TableColumnBase<?,?> column = getColumns().get(i);
 414                 if (column == null || ! column.isVisible()) continue;
 415 
 416                 // check if the header already exists and reuse it
 417                 boolean found = false;
 418                 for (int j = 0; j < oldHeaders.size(); j++) {
 419                     TableColumnHeader oldColumn = oldHeaders.get(j);
 420                     if (column == oldColumn.getTableColumn()) {
 421                         newHeaders.add(oldColumn);
 422                         found = true;
 423                         break;
 424                     }
 425                 }
 426 
 427                 // otherwise create a new table column header
 428                 if (!found) {
 429                     newHeaders.add(createColumnHeader(column));
 430                 }
 431             }
 432 
 433             columnHeaders.setAll(newHeaders);
 434 
 435             // dispose all old headers
 436             oldHeaders.removeAll(newHeaders);
 437             for (int i = 0; i < oldHeaders.size(); i++) {
 438                 oldHeaders.get(i).dispose();
 439             }
 440         }
 441 
 442         // update the content
 443         updateContent();
 444 
 445         // RT-33596: Do CSS now, as we are in the middle of layout pass and the headers are new Nodes w/o CSS done
 446         for (TableColumnHeader header : getColumnHeaders()) {
 447             header.applyCss();
 448         }
 449     }
 450 
 451     /** {@inheritDoc} */
 452     @Override double getDragRectHeight() {















































































 453         return label.prefHeight(-1);
 454     }
 455 
 456     TableColumnHeader createTableColumnHeader(TableColumnBase col) {
































 457         return col.getColumns().isEmpty() ?
 458                 new TableColumnHeader(getTableViewSkin(), col) :
 459                 new NestedTableColumnHeader(getTableViewSkin(), col);
 460     }
 461 
 462     void setHeadersNeedUpdate() {

 463         updateColumns = true;
 464 
 465         // go through children columns - they should update too
 466         for (int i = 0; i < getColumnHeaders().size(); i++) {
 467             TableColumnHeader header = getColumnHeaders().get(i);
 468             if (header instanceof NestedTableColumnHeader) {
 469                 ((NestedTableColumnHeader)header).setHeadersNeedUpdate();
 470             }
 471         }
 472         requestLayout();
 473     }
 474 








 475     private void updateContent() {
 476         // create a temporary list so we only do addAll into the main content
 477         // observableArrayList once.
 478         final List<Node> content = new ArrayList<Node>();
 479 
 480         // the label is the region that sits above the children columns
 481         content.add(label);
 482 
 483         // all children columns
 484         content.addAll(getColumnHeaders());
 485 
 486         // Small transparent overlays that sit at the start and end of each
 487         // column to intercept user drag gestures to enable column resizing.
 488         if (isColumnResizingEnabled()) {
 489             rebuildDragRects();
 490             content.addAll(dragRects.values());
 491         }
 492 
 493         getChildren().setAll(content);
 494     }
 495     
 496     private void rebuildDragRects() {
 497         if (! isColumnResizingEnabled()) return;
 498         
 499         getChildren().removeAll(dragRects.values());
 500         
 501         for (Rectangle rect : dragRects.values()) {
 502             rect.visibleProperty().unbind();
 503         }
 504         dragRects.clear();
 505 
 506         List<? extends TableColumnBase> columns = getColumns();
 507 
 508         if (columns == null) {
 509             return;
 510         }
 511 
 512         final TableViewSkinBase<?,?,?,?,?> skin = getTableViewSkin();
 513         Callback<ResizeFeaturesBase, Boolean> columnResizePolicy = skin.columnResizePolicyProperty().get();
 514         boolean isConstrainedResize =
 515                 skin instanceof TableViewSkin ? TableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
 516                 skin instanceof TreeTableViewSkin ? TreeTableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
 517                 false;
 518 
 519         // RT-32547 - don't show resize cursor when in constrained resize mode
 520         // and there is only one column
 521         if (isConstrainedResize && skin.getVisibleLeafColumns().size() == 1) {
 522             return;
 523         }
 524 
 525         for (int col = 0; col < columns.size(); col++) {
 526             if (isConstrainedResize && col == getColumns().size() - 1) {
 527                 break;
 528             }
 529 
 530             final TableColumnBase c = columns.get(col);
 531             final Rectangle rect = new Rectangle();
 532             rect.getProperties().put(TABLE_COLUMN_KEY, c);