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