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