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