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