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