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