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