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