1 /*
   2  * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler;
  29 import com.sun.javafx.scene.control.Properties;
  30 import com.sun.javafx.scene.control.TableColumnBaseHelper;
  31 import javafx.beans.property.DoubleProperty;
  32 import javafx.beans.property.ReadOnlyObjectProperty;
  33 import javafx.beans.property.ReadOnlyObjectWrapper;
  34 import javafx.beans.value.WritableValue;
  35 import javafx.collections.ListChangeListener;
  36 import javafx.collections.ObservableList;
  37 import javafx.collections.WeakListChangeListener;
  38 import javafx.css.CssMetaData;
  39 import javafx.css.PseudoClass;
  40 import javafx.css.Styleable;
  41 import javafx.css.StyleableDoubleProperty;
  42 import javafx.css.StyleableProperty;
  43 import javafx.event.EventHandler;
  44 import javafx.geometry.HPos;
  45 import javafx.geometry.Insets;
  46 import javafx.geometry.Pos;
  47 import javafx.geometry.VPos;
  48 import javafx.scene.AccessibleAttribute;
  49 import javafx.scene.AccessibleRole;
  50 import javafx.scene.Node;
  51 import javafx.scene.control.ContextMenu;
  52 import javafx.scene.control.Label;
  53 import javafx.scene.control.TableColumn;
  54 import javafx.scene.control.TableColumnBase;
  55 import javafx.scene.input.ContextMenuEvent;
  56 import javafx.scene.input.MouseEvent;
  57 import javafx.scene.layout.GridPane;
  58 import javafx.scene.layout.HBox;
  59 import javafx.scene.layout.Priority;
  60 import javafx.scene.layout.Region;
  61 
  62 import java.util.ArrayList;
  63 import java.util.Collections;
  64 import java.util.List;
  65 import java.util.Locale;
  66 
  67 import javafx.css.converter.SizeConverter;
  68 
  69 import static com.sun.javafx.scene.control.TableColumnSortTypeWrapper.getSortTypeName;
  70 import static com.sun.javafx.scene.control.TableColumnSortTypeWrapper.getSortTypeProperty;
  71 import static com.sun.javafx.scene.control.TableColumnSortTypeWrapper.isAscending;
  72 import static com.sun.javafx.scene.control.TableColumnSortTypeWrapper.isDescending;
  73 import static com.sun.javafx.scene.control.TableColumnSortTypeWrapper.setSortType;
  74 
  75 
  76 /**
  77  * Region responsible for painting a single column header. A subcomponent used by
  78  * subclasses of {@link TableViewSkinBase}.
  79  *
  80  * @since 9
  81  */
  82 public class TableColumnHeader extends Region {
  83 
  84     /***************************************************************************
  85      *                                                                         *
  86      * Static Fields                                                           *
  87      *                                                                         *
  88      **************************************************************************/
  89 
  90     static final String DEFAULT_STYLE_CLASS = "column-header";
  91 
  92     // Copied from TableColumn. The value here should always be in-sync with
  93     // the value in TableColumn
  94     static final double DEFAULT_COLUMN_WIDTH = 80.0F;
  95 
  96 
  97 
  98     /***************************************************************************
  99      *                                                                         *
 100      * Private Fields                                                          *
 101      *                                                                         *
 102      **************************************************************************/
 103 
 104     private boolean autoSizeComplete = false;
 105 
 106     private double dragOffset;
 107 //    TableViewSkinBase<?,?,?,?,?> tableSkin;
 108     private NestedTableColumnHeader nestedColumnHeader;
 109     private TableHeaderRow tableHeaderRow;
 110     private NestedTableColumnHeader parentHeader;
 111 
 112     // work out where this column currently is within its parent
 113     Label label;
 114 
 115     // sort order
 116     int sortPos = -1;
 117     private Region arrow;
 118     private Label sortOrderLabel;
 119     private HBox sortOrderDots;
 120     private Node sortArrow;
 121     private boolean isSortColumn;
 122 
 123     private boolean isSizeDirty = false;
 124 
 125     boolean isLastVisibleColumn = false;
 126 
 127     // package for testing
 128     int columnIndex = -1;
 129 
 130     private int newColumnPos;
 131 
 132     // the line drawn in the table when a user presses and moves a column header
 133     // to indicate where the column will be dropped. This is provided by the
 134     // table skin, but manipulated by the header
 135     Region columnReorderLine;
 136 
 137 
 138 
 139     /***************************************************************************
 140      *                                                                         *
 141      * Constructor                                                             *
 142      *                                                                         *
 143      **************************************************************************/
 144 
 145     /**
 146      * Creates a new TableColumnHeader instance to visually represent the given
 147      * {@link TableColumnBase} instance.
 148      *
 149      * @param tc The table column to be visually represented by this instance.
 150      */
 151     public TableColumnHeader(final TableColumnBase tc) {
 152         setTableColumn(tc);
 153         setFocusTraversable(false);
 154 
 155         updateColumnIndex();
 156         initUI();
 157 
 158         // change listener for multiple properties
 159         changeListenerHandler = new LambdaMultiplePropertyChangeListenerHandler();
 160         changeListenerHandler.registerChangeListener(sceneProperty(), e -> updateScene());
 161 
 162         if (getTableColumn() != null) {
 163             changeListenerHandler.registerChangeListener(tc.idProperty(), e -> setId(tc.getId()));
 164             changeListenerHandler.registerChangeListener(tc.styleProperty(), e -> setStyle(tc.getStyle()));
 165             changeListenerHandler.registerChangeListener(tc.widthProperty(), e -> {
 166                 // It is this that ensures that when a column is resized that the header
 167                 // visually adjusts its width as necessary.
 168                 isSizeDirty = true;
 169                 requestLayout();
 170             });
 171             changeListenerHandler.registerChangeListener(tc.visibleProperty(), e -> setVisible(getTableColumn().isVisible()));
 172             changeListenerHandler.registerChangeListener(tc.sortNodeProperty(), e -> updateSortGrid());
 173             changeListenerHandler.registerChangeListener(tc.sortableProperty(), e -> {
 174                 // we need to notify all headers that a sortable state has changed,
 175                 // in case the sort grid in other columns needs to be updated.
 176                 if (TableSkinUtils.getSortOrder(getTableSkin()).contains(getTableColumn())) {
 177                     NestedTableColumnHeader root = getTableHeaderRow().getRootHeader();
 178                     updateAllHeaders(root);
 179                 }
 180             });
 181             changeListenerHandler.registerChangeListener(tc.textProperty(), e -> label.setText(tc.getText()));
 182             changeListenerHandler.registerChangeListener(tc.graphicProperty(), e -> label.setGraphic(tc.getGraphic()));
 183 
 184             tc.getStyleClass().addListener(weakStyleClassListener);
 185 
 186             setId(tc.getId());
 187             setStyle(tc.getStyle());
 188             updateStyleClass();
 189             /* Having TableColumn role parented by TableColumn causes VoiceOver to be unhappy */
 190             setAccessibleRole(AccessibleRole.TABLE_COLUMN);
 191         }
 192     }
 193 
 194 
 195 
 196     /***************************************************************************
 197      *                                                                         *
 198      * Listeners                                                               *
 199      *                                                                         *
 200      **************************************************************************/
 201 
 202     final LambdaMultiplePropertyChangeListenerHandler changeListenerHandler;
 203 
 204     private ListChangeListener<TableColumnBase<?,?>> sortOrderListener = c -> {
 205         updateSortPosition();
 206     };
 207 
 208     private ListChangeListener<TableColumnBase<?,?>> visibleLeafColumnsListener = c -> {
 209         updateColumnIndex();
 210         updateSortPosition();
 211     };
 212 
 213     private ListChangeListener<String> styleClassListener = c -> {
 214         updateStyleClass();
 215     };
 216 
 217     private WeakListChangeListener<TableColumnBase<?,?>> weakSortOrderListener =
 218             new WeakListChangeListener<TableColumnBase<?,?>>(sortOrderListener);
 219     private final WeakListChangeListener<TableColumnBase<?,?>> weakVisibleLeafColumnsListener =
 220             new WeakListChangeListener<TableColumnBase<?,?>>(visibleLeafColumnsListener);
 221     private final WeakListChangeListener<String> weakStyleClassListener =
 222             new WeakListChangeListener<String>(styleClassListener);
 223 
 224     private static final EventHandler<MouseEvent> mousePressedHandler = me -> {
 225         TableColumnHeader header = (TableColumnHeader) me.getSource();
 226         TableColumnBase tableColumn = header.getTableColumn();
 227 
 228         ContextMenu menu = tableColumn.getContextMenu();
 229         if (menu != null && menu.isShowing()) {
 230             menu.hide();
 231         }
 232 
 233         if (me.isConsumed()) return;
 234         me.consume();
 235 
 236         header.getTableHeaderRow().columnDragLock = true;
 237 
 238         // pass focus to the table, so that the user immediately sees
 239         // the focus rectangle around the table control.
 240         header.getTableSkin().getSkinnable().requestFocus();
 241 
 242         if (me.isPrimaryButtonDown() && header.isColumnReorderingEnabled()) {
 243             header.columnReorderingStarted(me.getX());
 244         }
 245     };
 246 
 247     private static final EventHandler<MouseEvent> mouseDraggedHandler = me -> {
 248         if (me.isConsumed()) return;
 249         me.consume();
 250 
 251         TableColumnHeader header = (TableColumnHeader) me.getSource();
 252 
 253         if (me.isPrimaryButtonDown() && header.isColumnReorderingEnabled()) {
 254             header.columnReordering(me.getSceneX(), me.getSceneY());
 255         }
 256     };
 257 
 258     private static final EventHandler<MouseEvent> mouseReleasedHandler = me -> {
 259         if (me.isPopupTrigger()) return;
 260         if (me.isConsumed()) return;
 261         me.consume();
 262 
 263         TableColumnHeader header = (TableColumnHeader) me.getSource();
 264         header.getTableHeaderRow().columnDragLock = false;
 265 
 266         if (header.getTableHeaderRow().isReordering() && header.isColumnReorderingEnabled()) {
 267             header.columnReorderingComplete();
 268         } else if (me.isStillSincePress()) {
 269             header.sortColumn(me.isShiftDown());
 270         }
 271     };
 272 
 273     private static final EventHandler<ContextMenuEvent> contextMenuRequestedHandler = me -> {
 274         TableColumnHeader header = (TableColumnHeader) me.getSource();
 275         TableColumnBase tableColumn = header.getTableColumn();
 276 
 277         ContextMenu menu = tableColumn.getContextMenu();
 278         if (menu != null) {
 279             menu.show(header, me.getScreenX(), me.getScreenY());
 280             me.consume();
 281         }
 282     };
 283 
 284 
 285 
 286     /***************************************************************************
 287      *                                                                         *
 288      * Properties                                                              *
 289      *                                                                         *
 290      **************************************************************************/
 291 
 292     // --- size
 293     private DoubleProperty size;
 294     private final double getSize() {
 295         return size == null ? 20.0 : size.doubleValue();
 296     }
 297     private final DoubleProperty sizeProperty() {
 298         if (size == null) {
 299             size = new StyleableDoubleProperty(20) {
 300                 @Override
 301                 protected void invalidated() {
 302                     double value = get();
 303                     if (value <= 0) {
 304                         if (isBound()) {
 305                             unbind();
 306                         }
 307                         set(20);
 308                         throw new IllegalArgumentException("Size cannot be 0 or negative");
 309                     }
 310                 }
 311 
 312 
 313 
 314                 @Override public Object getBean() {
 315                     return TableColumnHeader.this;
 316                 }
 317 
 318                 @Override public String getName() {
 319                     return "size";
 320                 }
 321 
 322                 @Override public CssMetaData<TableColumnHeader,Number> getCssMetaData() {
 323                     return StyleableProperties.SIZE;
 324                 }
 325             };
 326         }
 327         return size;
 328     }
 329 
 330 
 331     /**
 332      * A property that refers to the {@link TableColumnBase} instance that this
 333      * header is visually represents.
 334      */
 335     // --- table column
 336     private ReadOnlyObjectWrapper<TableColumnBase<?,?>> tableColumn = new ReadOnlyObjectWrapper<>(this, "tableColumn");
 337     private final void setTableColumn(TableColumnBase<?,?> column) {
 338         tableColumn.set(column);
 339     }
 340     public final TableColumnBase<?,?> getTableColumn() {
 341         return tableColumn.get();
 342     }
 343     public final ReadOnlyObjectProperty<TableColumnBase<?,?>> tableColumnProperty() {
 344         return tableColumn.getReadOnlyProperty();
 345     }
 346 
 347 
 348 
 349     /***************************************************************************
 350      *                                                                         *
 351      * Public API                                                              *
 352      *                                                                         *
 353      **************************************************************************/
 354 
 355     /** {@inheritDoc} */
 356     @Override protected void layoutChildren() {
 357         if (isSizeDirty) {
 358             resize(getTableColumn().getWidth(), getHeight());
 359             isSizeDirty = false;
 360         }
 361 
 362         double sortWidth = 0;
 363         double w = snapSizeX(getWidth()) - (snappedLeftInset() + snappedRightInset());
 364         double h = getHeight() - (snappedTopInset() + snappedBottomInset());
 365         double x = w;
 366 
 367         // a bit hacky, but we REALLY don't want the arrow shape to fluctuate
 368         // in size
 369         if (arrow != null) {
 370             arrow.setMaxSize(arrow.prefWidth(-1), arrow.prefHeight(-1));
 371         }
 372 
 373         if (sortArrow != null && sortArrow.isVisible()) {
 374             sortWidth = sortArrow.prefWidth(-1);
 375             x -= sortWidth;
 376             sortArrow.resize(sortWidth, sortArrow.prefHeight(-1));
 377             positionInArea(sortArrow, x, snappedTopInset(),
 378                     sortWidth, h, 0, HPos.CENTER, VPos.CENTER);
 379         }
 380 
 381         if (label != null) {
 382             double labelWidth = w - sortWidth;
 383             label.resizeRelocate(snappedLeftInset(), 0, labelWidth, getHeight());
 384         }
 385     }
 386 
 387     /** {@inheritDoc} */
 388     @Override protected double computePrefWidth(double height) {
 389         if (getNestedColumnHeader() != null) {
 390             double width = getNestedColumnHeader().prefWidth(height);
 391 
 392             if (getTableColumn() != null) {
 393                 TableColumnBaseHelper.setWidth(getTableColumn(), width);
 394             }
 395 
 396             return width;
 397         } else if (getTableColumn() != null && getTableColumn().isVisible()) {
 398             return getTableColumn().getWidth();
 399         }
 400 
 401         return 0;
 402     }
 403 
 404     /** {@inheritDoc} */
 405     @Override protected double computeMinHeight(double width) {
 406         return label == null ? 0 : label.minHeight(width);
 407     }
 408 
 409     /** {@inheritDoc} */
 410     @Override protected double computePrefHeight(double width) {
 411         if (getTableColumn() == null) return 0;
 412         return Math.max(getSize(), label.prefHeight(-1));
 413     }
 414 
 415     /** {@inheritDoc} */
 416     @Override public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
 417         return getClassCssMetaData();
 418     }
 419 
 420     /** {@inheritDoc} */
 421     @Override  public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 422         switch (attribute) {
 423             case INDEX: return getIndex(getTableColumn());
 424             case TEXT: return getTableColumn() != null ? getTableColumn().getText() : null;
 425             default: return super.queryAccessibleAttribute(attribute, parameters);
 426         }
 427     }
 428 
 429 
 430 
 431     /***************************************************************************
 432      *                                                                         *
 433      * Private Implementation                                                  *
 434      *                                                                         *
 435      **************************************************************************/
 436 
 437     NestedTableColumnHeader getNestedColumnHeader() { return nestedColumnHeader; }
 438     void setNestedColumnHeader(NestedTableColumnHeader nch) { nestedColumnHeader = nch; }
 439 
 440     TableHeaderRow getTableHeaderRow() { return tableHeaderRow; }
 441     void setTableHeaderRow(TableHeaderRow thr) {
 442         tableHeaderRow = thr;
 443         updateTableSkin();
 444     }
 445 
 446     private void updateTableSkin() {
 447         // when we get the table header row, we are also given the skin,
 448         // so this is the time to look up listeners, etc.
 449         if (tableHeaderRow == null) return;
 450 
 451         TableViewSkinBase<?,?,?,?,?> tableSkin = getTableSkin();
 452         if (tableSkin == null) return;
 453 
 454         this.columnReorderLine = tableSkin.getColumnReorderLine();
 455 
 456         if (getTableColumn() != null) {
 457             updateSortPosition();
 458             TableSkinUtils.getSortOrder(tableSkin).addListener(weakSortOrderListener);
 459             TableSkinUtils.getVisibleLeafColumns(tableSkin).addListener(weakVisibleLeafColumnsListener);
 460         }
 461     }
 462 
 463     TableViewSkinBase<?,?,?,?,?> getTableSkin() {
 464         return tableHeaderRow == null ? null : tableHeaderRow.tableSkin;
 465     }
 466 
 467     NestedTableColumnHeader getParentHeader() { return parentHeader; }
 468     void setParentHeader(NestedTableColumnHeader ph) { parentHeader = ph; }
 469 
 470     // RT-29682: When the sortable property of a TableColumnBase changes this
 471     // may impact other TableColumnHeaders, as they may need to change their
 472     // sort order representation. Rather than install listeners across all
 473     // TableColumn in the sortOrder list for their sortable property, we simply
 474     // update the sortPosition of all headers whenever the sortOrder property
 475     // changes, assuming the column is within the sortOrder list.
 476     private void updateAllHeaders(TableColumnHeader header) {
 477         if (header instanceof NestedTableColumnHeader) {
 478             List<TableColumnHeader> children = ((NestedTableColumnHeader)header).getColumnHeaders();
 479             for (int i = 0; i < children.size(); i++) {
 480                 updateAllHeaders(children.get(i));
 481             }
 482         } else {
 483             header.updateSortPosition();
 484         }
 485     }
 486 
 487     private void updateStyleClass() {
 488         // For now we leave the 'nested-table-column-header' / 'column-header' style class intact so that the
 489         // appropriate border styles are shown, etc.
 490         getStyleClass().setAll((this instanceof NestedTableColumnHeader) ?
 491                 NestedTableColumnHeader.DEFAULT_STYLE_CLASS : TableColumnHeader.DEFAULT_STYLE_CLASS);
 492         getStyleClass().addAll(getTableColumn().getStyleClass());
 493     }
 494 
 495     private void updateScene() {
 496         // RT-17684: If the TableColumn widths are all currently the default,
 497         // we attempt to 'auto-size' based on the preferred width of the first
 498         // n rows (we can't do all rows, as that could conceivably be an unlimited
 499         // number of rows retrieved from a very slow (e.g. remote) data source.
 500         // Obviously, the bigger the value of n, the more likely the default
 501         // width will be suitable for most values in the column
 502         final int n = 30;
 503         if (! autoSizeComplete) {
 504             if (getTableColumn() == null || getTableColumn().getWidth() != DEFAULT_COLUMN_WIDTH || getScene() == null) {
 505                 return;
 506             }
 507             doColumnAutoSize(getTableColumn(), n);
 508             autoSizeComplete = true;
 509         }
 510     }
 511 
 512     void dispose() {
 513         TableViewSkinBase tableSkin = getTableSkin();
 514         if (tableSkin != null) {
 515             TableSkinUtils.getVisibleLeafColumns(tableSkin).removeListener(weakVisibleLeafColumnsListener);
 516             TableSkinUtils.getSortOrder(tableSkin).removeListener(weakSortOrderListener);
 517         }
 518 
 519         changeListenerHandler.dispose();
 520     }
 521 
 522     private boolean isSortingEnabled() {
 523         // this used to check if ! PlatformUtil.isEmbedded(), but has been changed
 524         // to always return true (for now), as we want to support column sorting
 525         // everywhere
 526         return true;
 527     }
 528 
 529     private boolean isColumnReorderingEnabled() {
 530         // we only allow for column reordering if there are more than one column,
 531         return !Properties.IS_TOUCH_SUPPORTED && TableSkinUtils.getVisibleLeafColumns(getTableSkin()).size() > 1;
 532     }
 533 
 534     private void initUI() {
 535         // TableColumn will be null if we are dealing with the root NestedTableColumnHeader
 536         if (getTableColumn() == null) return;
 537 
 538         // set up mouse events
 539         setOnMousePressed(mousePressedHandler);
 540         setOnMouseDragged(mouseDraggedHandler);
 541         setOnDragDetected(event -> event.consume());
 542         setOnContextMenuRequested(contextMenuRequestedHandler);
 543         setOnMouseReleased(mouseReleasedHandler);
 544 
 545         // --- label
 546         label = new Label();
 547         label.setText(getTableColumn().getText());
 548         label.setGraphic(getTableColumn().getGraphic());
 549         label.setVisible(getTableColumn().isVisible());
 550 
 551         // ---- container for the sort arrow (which is not supported on embedded
 552         // platforms)
 553         if (isSortingEnabled()) {
 554             // put together the grid
 555             updateSortGrid();
 556         }
 557     }
 558 
 559     private void doColumnAutoSize(TableColumnBase<?,?> column, int cellsToMeasure) {
 560         double prefWidth = column.getPrefWidth();
 561 
 562         // if the prefWidth has been set, we do _not_ autosize columns
 563         if (prefWidth == DEFAULT_COLUMN_WIDTH) {
 564             TableSkinUtils.resizeColumnToFitContent(getTableSkin(), column, cellsToMeasure);
 565 //            getTableViewSkin().resizeColumnToFitContent(column, cellsToMeasure);
 566         }
 567     }
 568 
 569     private void updateSortPosition() {
 570         this.sortPos = ! getTableColumn().isSortable() ? -1 : getSortPosition();
 571         updateSortGrid();
 572     }
 573 
 574     private void updateSortGrid() {
 575         // Fix for RT-14488
 576         if (this instanceof NestedTableColumnHeader) return;
 577 
 578         getChildren().clear();
 579         getChildren().add(label);
 580 
 581         // we do not support sorting in embedded devices
 582         if (! isSortingEnabled()) return;
 583 
 584         isSortColumn = sortPos != -1;
 585         if (! isSortColumn) {
 586             if (sortArrow != null) {
 587                 sortArrow.setVisible(false);
 588             }
 589             return;
 590         }
 591 
 592         // RT-28016: if the tablecolumn is not a visible leaf column, we should ignore this
 593         int visibleLeafIndex = TableSkinUtils.getVisibleLeafIndex(getTableSkin(), getTableColumn());
 594         if (visibleLeafIndex == -1) return;
 595 
 596         final int sortColumnCount = getVisibleSortOrderColumnCount();
 597         boolean showSortOrderDots = sortPos <= 3 && sortColumnCount > 1;
 598 
 599         Node _sortArrow = null;
 600         if (getTableColumn().getSortNode() != null) {
 601             _sortArrow = getTableColumn().getSortNode();
 602             getChildren().add(_sortArrow);
 603         } else {
 604             GridPane sortArrowGrid = new GridPane();
 605             _sortArrow = sortArrowGrid;
 606             sortArrowGrid.setPadding(new Insets(0, 3, 0, 0));
 607             getChildren().add(sortArrowGrid);
 608 
 609             // if we are here, and the sort arrow is null, we better create it
 610             if (arrow == null) {
 611                 arrow = new Region();
 612                 arrow.getStyleClass().setAll("arrow");
 613                 arrow.setVisible(true);
 614                 arrow.setRotate(isAscending(getTableColumn()) ? 180.0F : 0.0F);
 615                 changeListenerHandler.registerChangeListener(getSortTypeProperty(getTableColumn()), e -> {
 616                     updateSortGrid();
 617                     if (arrow != null) {
 618                         arrow.setRotate(isAscending(getTableColumn()) ? 180 : 0.0);
 619                     }
 620                 });
 621             }
 622 
 623             arrow.setVisible(isSortColumn);
 624 
 625             if (sortPos > 2) {
 626                 if (sortOrderLabel == null) {
 627                     // ---- sort order label (for sort positions greater than 3)
 628                     sortOrderLabel = new Label();
 629                     sortOrderLabel.getStyleClass().add("sort-order");
 630                 }
 631 
 632                 // only show the label if the sortPos is greater than 3 (for sortPos
 633                 // values less than three, we show the sortOrderDots instead)
 634                 sortOrderLabel.setText("" + (sortPos + 1));
 635                 sortOrderLabel.setVisible(sortColumnCount > 1);
 636 
 637                 // update the grid layout
 638                 sortArrowGrid.add(arrow, 1, 1);
 639                 GridPane.setHgrow(arrow, Priority.NEVER);
 640                 GridPane.setVgrow(arrow, Priority.NEVER);
 641                 sortArrowGrid.add(sortOrderLabel, 2, 1);
 642             } else if (showSortOrderDots) {
 643                 if (sortOrderDots == null) {
 644                     sortOrderDots = new HBox(0);
 645                     sortOrderDots.getStyleClass().add("sort-order-dots-container");
 646                 }
 647 
 648                 // show the sort order dots
 649                 boolean isAscending = isAscending(getTableColumn());
 650                 int arrowRow = isAscending ? 1 : 2;
 651                 int dotsRow = isAscending ? 2 : 1;
 652 
 653                 sortArrowGrid.add(arrow, 1, arrowRow);
 654                 GridPane.setHalignment(arrow, HPos.CENTER);
 655                 sortArrowGrid.add(sortOrderDots, 1, dotsRow);
 656 
 657                 updateSortOrderDots(sortPos);
 658             } else {
 659                 // only show the arrow
 660                 sortArrowGrid.add(arrow, 1, 1);
 661                 GridPane.setHgrow(arrow, Priority.NEVER);
 662                 GridPane.setVgrow(arrow, Priority.ALWAYS);
 663             }
 664         }
 665 
 666         sortArrow = _sortArrow;
 667         if (sortArrow != null) {
 668             sortArrow.setVisible(isSortColumn);
 669         }
 670 
 671         requestLayout();
 672     }
 673 
 674     private void updateSortOrderDots(int sortPos) {
 675         double arrowWidth = arrow.prefWidth(-1);
 676 
 677         sortOrderDots.getChildren().clear();
 678 
 679         for (int i = 0; i <= sortPos; i++) {
 680             Region r = new Region();
 681             r.getStyleClass().add("sort-order-dot");
 682 
 683             String sortTypeName = getSortTypeName(getTableColumn());
 684             if (sortTypeName != null && ! sortTypeName.isEmpty()) {
 685                 r.getStyleClass().add(sortTypeName.toLowerCase(Locale.ROOT));
 686             }
 687 
 688             sortOrderDots.getChildren().add(r);
 689 
 690             // RT-34914: fine tuning the placement of the sort dots. We could have gone to a custom layout, but for now
 691             // this works fine.
 692             if (i < sortPos) {
 693                 Region spacer = new Region();
 694                 double lp = sortPos == 1 ? 1 : 0;
 695                 spacer.setPadding(new Insets(0, 1, 0, lp));
 696                 sortOrderDots.getChildren().add(spacer);
 697             }
 698         }
 699 
 700         sortOrderDots.setAlignment(Pos.TOP_CENTER);
 701         sortOrderDots.setMaxWidth(arrowWidth);
 702     }
 703 
 704     // Package for testing purposes only.
 705     void moveColumn(TableColumnBase column, final int newColumnPos) {
 706         if (column == null || newColumnPos < 0) return;
 707 
 708         ObservableList<TableColumnBase<?,?>> columns = getColumns(column);
 709 
 710         final int columnsCount = columns.size();
 711         final int currentPos = columns.indexOf(column);
 712 
 713         int actualNewColumnPos = newColumnPos;
 714 
 715         // Fix for RT-35141: We need to account for hidden columns.
 716         // We keep iterating until we see 'requiredVisibleColumns' number of visible columns
 717         final int requiredVisibleColumns = actualNewColumnPos;
 718         int visibleColumnsSeen = 0;
 719         for (int i = 0; i < columnsCount; i++) {
 720             if (visibleColumnsSeen == (requiredVisibleColumns + 1)) {
 721                 break;
 722             }
 723 
 724             if (columns.get(i).isVisible()) {
 725                 visibleColumnsSeen++;
 726             } else {
 727                 actualNewColumnPos++;
 728             }
 729         }
 730         // --- end of RT-35141 fix
 731 
 732         if (actualNewColumnPos >= columnsCount) {
 733             actualNewColumnPos = columnsCount - 1;
 734         } else if (actualNewColumnPos < 0) {
 735             actualNewColumnPos = 0;
 736         }
 737 
 738         if (actualNewColumnPos == currentPos) return;
 739 
 740         List<TableColumnBase<?,?>> tempList = new ArrayList<>(columns);
 741         tempList.remove(column);
 742         tempList.add(actualNewColumnPos, column);
 743 
 744         columns.setAll(tempList);
 745     }
 746 
 747     private ObservableList<TableColumnBase<?,?>> getColumns(TableColumnBase column) {
 748         return column.getParentColumn() == null ?
 749                 TableSkinUtils.getColumns(getTableSkin()) :
 750                 column.getParentColumn().getColumns();
 751     }
 752 
 753     private int getIndex(TableColumnBase<?,?> column) {
 754         if (column == null) return -1;
 755 
 756         ObservableList<? extends TableColumnBase<?,?>> columns = getColumns(column);
 757 
 758         int index = -1;
 759         for (int i = 0; i < columns.size(); i++) {
 760             TableColumnBase<?,?> _column = columns.get(i);
 761             if (! _column.isVisible()) continue;
 762 
 763             index++;
 764             if (column.equals(_column)) break;
 765         }
 766 
 767         return index;
 768     }
 769 
 770     private void updateColumnIndex() {
 771 //        TableView tv = getTableView();
 772         TableColumnBase tc = getTableColumn();
 773         TableViewSkinBase tableSkin = getTableSkin();
 774         columnIndex = tableSkin == null || tc == null ? -1 :TableSkinUtils.getVisibleLeafIndex(tableSkin,tc);
 775 
 776         // update the pseudo class state regarding whether this is the last
 777         // visible cell (i.e. the right-most).
 778         isLastVisibleColumn = getTableColumn() != null &&
 779                 columnIndex != -1 &&
 780                 columnIndex == TableSkinUtils.getVisibleLeafColumns(tableSkin).size() - 1;
 781         pseudoClassStateChanged(PSEUDO_CLASS_LAST_VISIBLE, isLastVisibleColumn);
 782     }
 783 
 784     private void sortColumn(final boolean addColumn) {
 785         if (! isSortingEnabled()) return;
 786 
 787         // we only allow sorting on the leaf columns and columns
 788         // that actually have comparators defined, and are sortable
 789         if (getTableColumn() == null || getTableColumn().getColumns().size() != 0 || getTableColumn().getComparator() == null || !getTableColumn().isSortable()) return;
 790 //        final int sortPos = getTable().getSortOrder().indexOf(column);
 791 //        final boolean isSortColumn = sortPos != -1;
 792 
 793         final ObservableList<TableColumnBase<?,?>> sortOrder = TableSkinUtils.getSortOrder(getTableSkin());
 794 
 795         // addColumn is true e.g. when the user is holding down Shift
 796         if (addColumn) {
 797             if (!isSortColumn) {
 798                 setSortType(getTableColumn(), TableColumn.SortType.ASCENDING);
 799                 sortOrder.add(getTableColumn());
 800             } else if (isAscending(getTableColumn())) {
 801                 setSortType(getTableColumn(), TableColumn.SortType.DESCENDING);
 802             } else {
 803                 int i = sortOrder.indexOf(getTableColumn());
 804                 if (i != -1) {
 805                     sortOrder.remove(i);
 806                 }
 807             }
 808         } else {
 809             // the user has clicked on a column header - we should add this to
 810             // the TableView sortOrder list if it isn't already there.
 811             if (isSortColumn && sortOrder.size() == 1) {
 812                 // the column is already being sorted, and it's the only column.
 813                 // We therefore move through the 2nd or 3rd states:
 814                 //   1st click: sort ascending
 815                 //   2nd click: sort descending
 816                 //   3rd click: natural sorting (sorting is switched off)
 817                 if (isAscending(getTableColumn())) {
 818                     setSortType(getTableColumn(), TableColumn.SortType.DESCENDING);
 819                 } else {
 820                     // remove from sort
 821                     sortOrder.remove(getTableColumn());
 822                 }
 823             } else if (isSortColumn) {
 824                 // the column is already being used to sort, so we toggle its
 825                 // sortAscending property, and also make the column become the
 826                 // primary sort column
 827                 if (isAscending(getTableColumn())) {
 828                     setSortType(getTableColumn(), TableColumn.SortType.DESCENDING);
 829                 } else if (isDescending(getTableColumn())) {
 830                     setSortType(getTableColumn(), TableColumn.SortType.ASCENDING);
 831                 }
 832 
 833                 // to prevent multiple sorts, we make a copy of the sort order
 834                 // list, moving the column value from the current position to
 835                 // its new position at the front of the list
 836                 List<TableColumnBase<?,?>> sortOrderCopy = new ArrayList<TableColumnBase<?,?>>(sortOrder);
 837                 sortOrderCopy.remove(getTableColumn());
 838                 sortOrderCopy.add(0, getTableColumn());
 839                 sortOrder.setAll(getTableColumn());
 840             } else {
 841                 // add to the sort order, in ascending form
 842                 setSortType(getTableColumn(), TableColumn.SortType.ASCENDING);
 843                 sortOrder.setAll(getTableColumn());
 844             }
 845         }
 846     }
 847 
 848     // Because it is possible that some columns are in the sortOrder list but are
 849     // not themselves sortable, we cannot just do sortOrderList.indexOf(column).
 850     // Therefore, this method does the proper work required of iterating through
 851     // and ignoring non-sortable (and null) columns in the sortOrder list.
 852     private int getSortPosition() {
 853         if (getTableColumn() == null) {
 854             return -1;
 855         }
 856 
 857         final List<TableColumnBase> sortOrder = getVisibleSortOrderColumns();
 858         int pos = 0;
 859         for (int i = 0; i < sortOrder.size(); i++) {
 860             TableColumnBase _tc = sortOrder.get(i);
 861 
 862             if (getTableColumn().equals(_tc)) {
 863                 return pos;
 864             }
 865 
 866             pos++;
 867         }
 868 
 869         return -1;
 870     }
 871 
 872     private List<TableColumnBase> getVisibleSortOrderColumns() {
 873         final ObservableList<TableColumnBase<?,?>> sortOrder = TableSkinUtils.getSortOrder(getTableSkin());
 874 
 875         List<TableColumnBase> visibleSortOrderColumns = new ArrayList<>();
 876         for (int i = 0; i < sortOrder.size(); i++) {
 877             TableColumnBase _tc = sortOrder.get(i);
 878             if (_tc == null || ! _tc.isSortable() || ! _tc.isVisible()) {
 879                 continue;
 880             }
 881 
 882             visibleSortOrderColumns.add(_tc);
 883         }
 884 
 885         return visibleSortOrderColumns;
 886     }
 887 
 888     // as with getSortPosition above, this method iterates through the sortOrder
 889     // list ignoring the null and non-sortable columns, so that we get the correct
 890     // number of columns in the sortOrder list.
 891     private int getVisibleSortOrderColumnCount() {
 892         return getVisibleSortOrderColumns().size();
 893     }
 894 
 895 
 896 
 897     /***************************************************************************
 898      *                                                                         *
 899      * Private Implementation: Column Reordering                               *
 900      *                                                                         *
 901      **************************************************************************/
 902 
 903     // package for testing
 904     void columnReorderingStarted(double dragOffset) {
 905         if (! getTableColumn().isReorderable()) return;
 906 
 907         // Used to ensure the column ghost is positioned relative to where the
 908         // user clicked on the column header
 909         this.dragOffset = dragOffset;
 910 
 911         // Note here that we only allow for reordering of 'root' columns
 912         getTableHeaderRow().setReorderingColumn(getTableColumn());
 913         getTableHeaderRow().setReorderingRegion(this);
 914     }
 915 
 916     // package for testing
 917     void columnReordering(double sceneX, double sceneY) {
 918         if (! getTableColumn().isReorderable()) return;
 919 
 920         // this is for handling the column drag to reorder columns.
 921         // It shows a line to indicate where the 'drop' will be.
 922 
 923         // indicate that we've started dragging so that the dragging
 924         // line overlay is shown
 925         getTableHeaderRow().setReordering(true);
 926 
 927         // Firstly we need to determine where to draw the line.
 928         // Find which column we're over
 929         TableColumnHeader hoverHeader = null;
 930 
 931         // x represents where the mouse is relative to the parent
 932         // NestedTableColumnHeader
 933         final double x = getParentHeader().sceneToLocal(sceneX, sceneY).getX();
 934 
 935         // calculate where the ghost column header should be
 936         double dragX = getTableSkin().getSkinnable().sceneToLocal(sceneX, sceneY).getX() - dragOffset;
 937         getTableHeaderRow().setDragHeaderX(dragX);
 938 
 939         double startX = 0;
 940         double endX = 0;
 941         double headersWidth = 0;
 942         newColumnPos = 0;
 943         for (TableColumnHeader header : getParentHeader().getColumnHeaders()) {
 944             if (! header.isVisible()) continue;
 945 
 946             double headerWidth = header.prefWidth(-1);
 947             headersWidth += headerWidth;
 948 
 949             startX = header.getBoundsInParent().getMinX();
 950             endX = startX + headerWidth;
 951 
 952             if (x >= startX && x < endX) {
 953                 hoverHeader = header;
 954                 break;
 955             }
 956             newColumnPos++;
 957         }
 958 
 959         // hoverHeader will be null if the drag occurs outside of the
 960         // tableview. In this case we handle the newColumnPos specially
 961         // and then short-circuit. This results in the drop action
 962         // resulting in the correct result (the column will drop at
 963         // the start or end of the table).
 964         if (hoverHeader == null) {
 965             newColumnPos = x > headersWidth ? (getParentHeader().getColumns().size() - 1) : 0;
 966             return;
 967         }
 968 
 969         // This is the x-axis value midway through hoverHeader. It's
 970         // used to determine whether the drop should be to the left
 971         // or the right of hoverHeader.
 972         double midPoint = startX + (endX - startX) / 2;
 973         boolean beforeMidPoint = x <= midPoint;
 974 
 975         // Based on where the mouse actually is, we have to shuffle
 976         // where we want the column to end up. This code handles that.
 977         int currentPos = getIndex(getTableColumn());
 978         newColumnPos += newColumnPos > currentPos && beforeMidPoint ?
 979             -1 : (newColumnPos < currentPos && !beforeMidPoint ? 1 : 0);
 980 
 981         double lineX = getTableHeaderRow().sceneToLocal(hoverHeader.localToScene(hoverHeader.getBoundsInLocal())).getMinX();
 982         lineX = lineX + ((beforeMidPoint) ? (0) : (hoverHeader.getWidth()));
 983 
 984         if (lineX >= -0.5 && lineX <= getTableSkin().getSkinnable().getWidth()) {
 985             columnReorderLine.setTranslateX(lineX);
 986 
 987             // then if this is the first event, we set the property to true
 988             // so that the line becomes visible until the drop is completed.
 989             // We also set reordering to true so that the various reordering
 990             // effects become visible (ghost, transparent overlay, etc).
 991             columnReorderLine.setVisible(true);
 992         }
 993 
 994         getTableHeaderRow().setReordering(true);
 995     }
 996 
 997     // package for testing
 998     void columnReorderingComplete() {
 999         if (! getTableColumn().isReorderable()) return;
1000 
1001         // Move col from where it is now to the new position.
1002         moveColumn(getTableColumn(), newColumnPos);
1003 
1004         // cleanup
1005         columnReorderLine.setTranslateX(0.0F);
1006         columnReorderLine.setLayoutX(0.0F);
1007         newColumnPos = 0;
1008 
1009         getTableHeaderRow().setReordering(false);
1010         columnReorderLine.setVisible(false);
1011         getTableHeaderRow().setReorderingColumn(null);
1012         getTableHeaderRow().setReorderingRegion(null);
1013         dragOffset = 0.0F;
1014     }
1015 
1016     double getDragRectHeight() {
1017         return getHeight();
1018     }
1019 
1020     // Used to test whether this column header properly represents the given column.
1021     // In particular, whether it has child column headers for all child columns
1022     boolean represents(TableColumnBase<?, ?> column) {
1023         if (!column.getColumns().isEmpty()) {
1024             // this column has children, but we are in a TableColumnHeader instance,
1025             // so the match is bad.
1026             return false;
1027         }
1028         return column == getTableColumn();
1029     }
1030 
1031 
1032 
1033     /***************************************************************************
1034      *                                                                         *
1035      * Stylesheet Handling                                                     *
1036      *                                                                         *
1037      **************************************************************************/
1038 
1039     private static final PseudoClass PSEUDO_CLASS_LAST_VISIBLE =
1040             PseudoClass.getPseudoClass("last-visible");
1041 
1042     /*
1043      * Super-lazy instantiation pattern from Bill Pugh.
1044      */
1045      private static class StyleableProperties {
1046          private static final CssMetaData<TableColumnHeader,Number> SIZE =
1047             new CssMetaData<TableColumnHeader,Number>("-fx-size",
1048                  SizeConverter.getInstance(), 20.0) {
1049 
1050             @Override
1051             public boolean isSettable(TableColumnHeader n) {
1052                 return n.size == null || !n.size.isBound();
1053             }
1054 
1055             @Override
1056             public StyleableProperty<Number> getStyleableProperty(TableColumnHeader n) {
1057                 return (StyleableProperty<Number>)(WritableValue<Number>)n.sizeProperty();
1058             }
1059         };
1060 
1061          private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1062          static {
1063 
1064             final List<CssMetaData<? extends Styleable, ?>> styleables =
1065                 new ArrayList<CssMetaData<? extends Styleable, ?>>(Region.getClassCssMetaData());
1066             styleables.add(SIZE);
1067             STYLEABLES = Collections.unmodifiableList(styleables);
1068 
1069          }
1070     }
1071 
1072     /**
1073      * Returnst he CssMetaData associated with this class, which may include the
1074      * CssMetaData of its super classes.
1075      */
1076     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1077         return StyleableProperties.STYLEABLES;
1078     }
1079 }