1 /* 2 * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package javafx.scene.control.skin; 27 28 import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection; 29 import javafx.collections.WeakListChangeListener; 30 import java.util.ArrayList; 31 import java.util.List; 32 import java.util.Map; 33 import java.util.WeakHashMap; 34 35 import javafx.collections.FXCollections; 36 import javafx.collections.ListChangeListener; 37 import javafx.collections.ObservableList; 38 import javafx.event.EventHandler; 39 import javafx.geometry.NodeOrientation; 40 import javafx.scene.Cursor; 41 import javafx.scene.Node; 42 import javafx.scene.control.*; 43 import javafx.scene.input.MouseEvent; 44 import javafx.scene.paint.Color; 45 import javafx.scene.shape.Rectangle; 46 import javafx.util.Callback; 47 48 /** 49 * <p>This class is used to construct the header of a TableView. We take the approach 50 * that every TableView header is nested - even if it isn't. This allows for us 51 * to use the same code for building a single row of TableColumns as we would 52 * with a heavily nested sequences of TableColumns. Because of this, the 53 * TableHeaderRow class consists of just one instance of a NestedTableColumnHeader. 54 * 55 * @since 9 56 * @see TableColumnHeader 57 * @see TableHeaderRow 58 * @see TableColumnBase 59 */ 60 public class NestedTableColumnHeader extends TableColumnHeader { 61 62 /*************************************************************************** 63 * * 64 * Static Fields * 65 * * 66 **************************************************************************/ 67 68 private static final int DRAG_RECT_WIDTH = 4; 69 70 private static final String TABLE_COLUMN_KEY = "TableColumn"; 71 private static final String TABLE_COLUMN_HEADER_KEY = "TableColumnHeader"; 72 73 74 75 /*************************************************************************** 76 * * 77 * Private Fields * 78 * * 79 **************************************************************************/ 80 81 /** 82 * Represents the actual columns directly contained in this nested column. 83 * It does NOT include ANY of the children of these columns, if any exist. 84 */ 85 private ObservableList<? extends TableColumnBase> columns; 86 87 private TableColumnHeader label; 88 89 private ObservableList<TableColumnHeader> columnHeaders; 90 private ObservableList<TableColumnHeader> unmodifiableColumnHeaders; 91 92 // used for column resizing 93 private double lastX = 0.0F; 94 private double dragAnchorX = 0.0; 95 96 // drag rectangle overlays 97 private Map<TableColumnBase<?,?>, Rectangle> dragRects = new WeakHashMap<>(); 98 99 boolean updateColumns = true; 100 101 102 103 /*************************************************************************** 104 * * 105 * Constructor * 106 * * 107 **************************************************************************/ 108 109 /** 110 * Creates a new NestedTableColumnHeader instance to visually represent the given 111 * {@link TableColumnBase} instance. 112 * 113 * @param skin The skin used by the UI control. 114 * @param tc The table column to be visually represented by this instance. 115 */ 116 public NestedTableColumnHeader(final TableViewSkinBase skin, final TableColumnBase tc) { 117 super(skin, tc); 118 119 getStyleClass().setAll("nested-column-header"); 120 setFocusTraversable(false); 121 122 // init UI 123 label = new TableColumnHeader(skin, getTableColumn()); 124 label.setTableHeaderRow(getTableHeaderRow()); 125 label.setParentHeader(getParentHeader()); 126 label.setNestedColumnHeader(this); 127 128 if (getTableColumn() != null) { 129 changeListenerHandler.registerChangeListener(getTableColumn().textProperty(), e -> 130 label.setVisible(getTableColumn().getText() != null && ! getTableColumn().getText().isEmpty())); 131 } 132 133 changeListenerHandler.registerChangeListener(skin.columnResizePolicyProperty(), e -> updateContent()); 134 } 135 136 137 138 /*************************************************************************** 139 * * 140 * Listeners * 141 * * 142 **************************************************************************/ 143 144 private final ListChangeListener<TableColumnBase> columnsListener = c -> { 145 setHeadersNeedUpdate(); 146 }; 147 148 private final WeakListChangeListener weakColumnsListener = 149 new WeakListChangeListener(columnsListener); 150 151 private static final EventHandler<MouseEvent> rectMousePressed = me -> { 152 Rectangle rect = (Rectangle) me.getSource(); 153 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY); 154 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY); 155 156 if (! header.isColumnResizingEnabled()) return; 157 158 if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) { 159 // the user wants to resize the column such that its 160 // width is equal to the widest element in the column 161 header.getTableViewSkin().resizeColumnToFitContent(column, -1); 162 } else { 163 // rather than refer to the rect variable, we just grab 164 // it from the source to prevent a small memory leak. 165 Rectangle innerRect = (Rectangle) me.getSource(); 166 double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2; 167 header.dragAnchorX = me.getSceneX(); 168 header.columnResizingStarted(startX); 169 } 170 me.consume(); 171 }; 172 173 private static final EventHandler<MouseEvent> rectMouseDragged = me -> { 174 Rectangle rect = (Rectangle) me.getSource(); 175 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY); 176 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY); 177 178 if (! header.isColumnResizingEnabled()) return; 179 180 header.columnResizing(column, me); 181 me.consume(); 182 }; 183 184 private static final EventHandler<MouseEvent> rectMouseReleased = me -> { 185 Rectangle rect = (Rectangle) me.getSource(); 186 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY); 187 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY); 188 189 if (! header.isColumnResizingEnabled()) return; 190 191 header.columnResizingComplete(column, me); 192 me.consume(); 193 }; 194 195 private static final EventHandler<MouseEvent> rectCursorChangeListener = me -> { 196 Rectangle rect = (Rectangle) me.getSource(); 197 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY); 198 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY); 199 200 if (header.getCursor() == null) { // If there's a cursor for the whole header, don't override it 201 rect.setCursor(header.isColumnResizingEnabled() && rect.isHover() && 202 column.isResizable() ? Cursor.H_RESIZE : null); 203 } 204 }; 205 206 207 208 /*************************************************************************** 209 * * 210 * Public Methods * 211 * * 212 **************************************************************************/ 213 214 /** {@inheritDoc} */ 215 @Override void dispose() { 216 super.dispose(); 217 218 if (label != null) { 219 label.dispose(); 220 } 221 222 if (getColumns() != null) { 223 getColumns().removeListener(weakColumnsListener); 224 } 225 226 for (int i = 0; i < getColumnHeaders().size(); i++) { 227 TableColumnHeader header = getColumnHeaders().get(i); 228 header.dispose(); 229 } 230 231 for (Rectangle rect : dragRects.values()) { 232 if (rect != null) { 233 rect.visibleProperty().unbind(); 234 } 235 } 236 dragRects.clear(); 237 getChildren().clear(); 238 239 changeListenerHandler.dispose(); 240 } 241 242 /** 243 * Returns an unmodifiable list of the {@link TableColumnHeader} instances 244 * that are children of this NestedTableColumnHeader. 245 */ 246 @ReturnsUnmodifiableCollection 247 public final ObservableList<TableColumnHeader> getColumnHeaders() { 248 if (columnHeaders == null) { 249 columnHeaders = FXCollections.<TableColumnHeader>observableArrayList(); 250 unmodifiableColumnHeaders = FXCollections.unmodifiableObservableList(columnHeaders); 251 } 252 return unmodifiableColumnHeaders; 253 } 254 255 /** {@inheritDoc} */ 256 @Override protected void layoutChildren() { 257 double w = getWidth() - snappedLeftInset() - snappedRightInset(); 258 double h = getHeight() - snappedTopInset() - snappedBottomInset(); 259 260 int labelHeight = (int) label.prefHeight(-1); 261 262 if (label.isVisible()) { 263 // label gets to span whole width and sits at top 264 label.resize(w, labelHeight); 265 label.relocate(snappedLeftInset(), snappedTopInset()); 266 } 267 268 // children columns need to share the total available width 269 double x = snappedLeftInset(); 270 int pos = 0; 271 for (int i = 0, max = getColumnHeaders().size(); i < max; i++) { 272 TableColumnHeader n = getColumnHeaders().get(i); 273 if (! n.isVisible()) continue; 274 275 double prefWidth = snapSize(n.prefWidth(-1)); 276 // double prefHeight = n.prefHeight(-1); 277 278 // position the column header in the default location... 279 n.resize(prefWidth, snapSize(h - labelHeight)); 280 n.relocate(x, labelHeight + snappedTopInset()); 281 282 // // ...but, if there are no children of this column, we should ensure 283 // // that it is resized vertically such that it goes to the very 284 // // bottom of the table header row. 285 // if (getTableHeaderRow() != null && n.getCol().getColumns().isEmpty()) { 286 // Bounds bounds = getTableHeaderRow().sceneToLocal(n.localToScene(n.getBoundsInLocal())); 287 // prefHeight = getTableHeaderRow().getHeight() - bounds.getMinY(); 288 // n.resize(prefWidth, prefHeight); 289 // } 290 291 // shuffle along the x-axis appropriately 292 x += prefWidth; 293 294 // position drag overlay to intercept column resize requests 295 Rectangle dragRect = dragRects.get(n.getTableColumn()); 296 if (dragRect != null) { 297 dragRect.setHeight(n.getDragRectHeight()); 298 dragRect.relocate(x - DRAG_RECT_WIDTH / 2, snappedTopInset() + labelHeight); 299 } 300 } 301 } 302 303 // sum up all children columns 304 /** {@inheritDoc} */ 305 @Override protected double computePrefWidth(double height) { 306 checkState(); 307 308 double width = 0.0F; 309 310 if (getColumns() != null) { 311 for (TableColumnHeader c : getColumnHeaders()) { 312 if (c.isVisible()) { 313 width += snapSize(c.computePrefWidth(height)); 314 } 315 } 316 } 317 318 return width; 319 } 320 321 /** {@inheritDoc} */ 322 @Override protected double computePrefHeight(double width) { 323 checkState(); 324 325 double height = 0.0F; 326 327 if (getColumnHeaders() != null) { 328 for (TableColumnHeader n : getColumnHeaders()) { 329 height = Math.max(height, n.prefHeight(-1)); 330 } 331 } 332 333 return height + label.prefHeight(-1) + snappedTopInset() + snappedBottomInset(); 334 } 335 336 337 338 /*************************************************************************** 339 * * 340 * Private Implementation * 341 * * 342 **************************************************************************/ 343 344 @Override void setTableHeaderRow(TableHeaderRow header) { 345 super.setTableHeaderRow(header); 346 347 label.setTableHeaderRow(header); 348 349 // tell all children columns what TableHeader they belong to 350 for (TableColumnHeader c : getColumnHeaders()) { 351 c.setTableHeaderRow(header); 352 } 353 } 354 355 @Override void setParentHeader(NestedTableColumnHeader parentHeader) { 356 super.setParentHeader(parentHeader); 357 label.setParentHeader(parentHeader); 358 } 359 360 ObservableList<? extends TableColumnBase> getColumns() { 361 return columns; 362 } 363 364 void setColumns(ObservableList<? extends TableColumnBase> newColumns) { 365 if (this.columns != null) { 366 this.columns.removeListener(weakColumnsListener); 367 } 368 369 this.columns = newColumns; 370 371 if (this.columns != null) { 372 this.columns.addListener(weakColumnsListener); 373 } 374 } 375 376 void updateTableColumnHeaders() { 377 // watching for changes to the view columns in either table or tableColumn. 378 if (getTableColumn() == null && getTableViewSkin() != null) { 379 setColumns(getTableViewSkin().getColumns()); 380 } else if (getTableColumn() != null) { 381 setColumns(getTableColumn().getColumns()); 382 } 383 384 // update the column headers... 385 386 // iterate through all columns, unless we've got no child columns 387 // any longer, in which case we should switch to a TableColumnHeader 388 // instead 389 if (getColumns().isEmpty()) { 390 // iterate through all current headers, telling them to clean up 391 for (int i = 0; i < getColumnHeaders().size(); i++) { 392 TableColumnHeader header = getColumnHeaders().get(i); 393 header.dispose(); 394 } 395 396 // switch out to be a TableColumn instead, if we have a parent header 397 NestedTableColumnHeader parentHeader = getParentHeader(); 398 if (parentHeader != null) { 399 List<TableColumnHeader> parentColumnHeaders = parentHeader.getColumnHeaders(); 400 int index = parentColumnHeaders.indexOf(this); 401 if (index >= 0 && index < parentColumnHeaders.size()) { 402 parentColumnHeaders.set(index, createColumnHeader(getTableColumn())); 403 } 404 } else { 405 // otherwise just remove all the columns 406 columnHeaders.clear(); 407 } 408 } else { 409 List<TableColumnHeader> oldHeaders = new ArrayList<>(getColumnHeaders()); 410 List<TableColumnHeader> newHeaders = new ArrayList<>(); 411 412 for (int i = 0; i < getColumns().size(); i++) { 413 TableColumnBase<?,?> column = getColumns().get(i); 414 if (column == null || ! column.isVisible()) continue; 415 416 // check if the header already exists and reuse it 417 boolean found = false; 418 for (int j = 0; j < oldHeaders.size(); j++) { 419 TableColumnHeader oldColumn = oldHeaders.get(j); 420 if (column == oldColumn.getTableColumn()) { 421 newHeaders.add(oldColumn); 422 found = true; 423 break; 424 } 425 } 426 427 // otherwise create a new table column header 428 if (!found) { 429 newHeaders.add(createColumnHeader(column)); 430 } 431 } 432 433 columnHeaders.setAll(newHeaders); 434 435 // dispose all old headers 436 oldHeaders.removeAll(newHeaders); 437 for (int i = 0; i < oldHeaders.size(); i++) { 438 oldHeaders.get(i).dispose(); 439 } 440 } 441 442 // update the content 443 updateContent(); 444 445 // RT-33596: Do CSS now, as we are in the middle of layout pass and the headers are new Nodes w/o CSS done 446 for (TableColumnHeader header : getColumnHeaders()) { 447 header.applyCss(); 448 } 449 } 450 451 /** {@inheritDoc} */ 452 @Override double getDragRectHeight() { 453 return label.prefHeight(-1); 454 } 455 456 TableColumnHeader createTableColumnHeader(TableColumnBase col) { 457 return col.getColumns().isEmpty() ? 458 new TableColumnHeader(getTableViewSkin(), col) : 459 new NestedTableColumnHeader(getTableViewSkin(), col); 460 } 461 462 void setHeadersNeedUpdate() { 463 updateColumns = true; 464 465 // go through children columns - they should update too 466 for (int i = 0; i < getColumnHeaders().size(); i++) { 467 TableColumnHeader header = getColumnHeaders().get(i); 468 if (header instanceof NestedTableColumnHeader) { 469 ((NestedTableColumnHeader)header).setHeadersNeedUpdate(); 470 } 471 } 472 requestLayout(); 473 } 474 475 private void updateContent() { 476 // create a temporary list so we only do addAll into the main content 477 // observableArrayList once. 478 final List<Node> content = new ArrayList<Node>(); 479 480 // the label is the region that sits above the children columns 481 content.add(label); 482 483 // all children columns 484 content.addAll(getColumnHeaders()); 485 486 // Small transparent overlays that sit at the start and end of each 487 // column to intercept user drag gestures to enable column resizing. 488 if (isColumnResizingEnabled()) { 489 rebuildDragRects(); 490 content.addAll(dragRects.values()); 491 } 492 493 getChildren().setAll(content); 494 } 495 496 private void rebuildDragRects() { 497 if (! isColumnResizingEnabled()) return; 498 499 getChildren().removeAll(dragRects.values()); 500 501 for (Rectangle rect : dragRects.values()) { 502 rect.visibleProperty().unbind(); 503 } 504 dragRects.clear(); 505 506 List<? extends TableColumnBase> columns = getColumns(); 507 508 if (columns == null) { 509 return; 510 } 511 512 final TableViewSkinBase<?,?,?,?,?> skin = getTableViewSkin(); 513 Callback<ResizeFeaturesBase, Boolean> columnResizePolicy = skin.columnResizePolicyProperty().get(); 514 boolean isConstrainedResize = 515 skin instanceof TableViewSkin ? TableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) : 516 skin instanceof TreeTableViewSkin ? TreeTableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) : 517 false; 518 519 // RT-32547 - don't show resize cursor when in constrained resize mode 520 // and there is only one column 521 if (isConstrainedResize && skin.getVisibleLeafColumns().size() == 1) { 522 return; 523 } 524 525 for (int col = 0; col < columns.size(); col++) { 526 if (isConstrainedResize && col == getColumns().size() - 1) { 527 break; 528 } 529 530 final TableColumnBase c = columns.get(col); 531 final Rectangle rect = new Rectangle(); 532 rect.getProperties().put(TABLE_COLUMN_KEY, c); 533 rect.getProperties().put(TABLE_COLUMN_HEADER_KEY, this); 534 rect.setWidth(DRAG_RECT_WIDTH); 535 rect.setHeight(getHeight() - label.getHeight()); 536 rect.setFill(Color.TRANSPARENT); 537 rect.visibleProperty().bind(c.visibleProperty()); 538 rect.setOnMousePressed(rectMousePressed); 539 rect.setOnMouseDragged(rectMouseDragged); 540 rect.setOnMouseReleased(rectMouseReleased); 541 rect.setOnMouseEntered(rectCursorChangeListener); 542 rect.setOnMouseExited(rectCursorChangeListener); 543 544 dragRects.put(c, rect); 545 } 546 } 547 548 private void checkState() { 549 if (updateColumns) { 550 updateTableColumnHeaders(); 551 updateColumns = false; 552 } 553 } 554 555 private TableColumnHeader createColumnHeader(TableColumnBase col) { 556 TableColumnHeader newCol = createTableColumnHeader(col); 557 newCol.setTableHeaderRow(getTableHeaderRow()); 558 newCol.setParentHeader(this); 559 return newCol; 560 } 561 562 563 564 /*************************************************************************** 565 * * 566 * Private Implementation: Column Resizing * 567 * * 568 **************************************************************************/ 569 570 private boolean isColumnResizingEnabled() { 571 // this used to check if ! PlatformUtil.isEmbedded(), but has been changed 572 // to always return true (for now), as we want to support column resizing 573 // everywhere 574 return true; 575 } 576 577 private void columnResizingStarted(double startX) { 578 setCursor(Cursor.H_RESIZE); 579 columnReorderLine.setLayoutX(startX); 580 } 581 582 private void columnResizing(TableColumnBase col, MouseEvent me) { 583 double draggedX = me.getSceneX() - dragAnchorX; 584 if (getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) { 585 draggedX = -draggedX; 586 } 587 double delta = draggedX - lastX; 588 boolean allowed = getTableViewSkin().resizeColumn(col, delta); 589 if (allowed) { 590 lastX = draggedX; 591 } 592 } 593 594 private void columnResizingComplete(TableColumnBase col, MouseEvent me) { 595 setCursor(null); 596 columnReorderLine.setTranslateX(0.0F); 597 columnReorderLine.setLayoutX(0.0F); 598 lastX = 0.0F; 599 } 600 }