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