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