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