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