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