1 /* 2 * Copyright (c) 2012, 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 com.sun.javafx.scene.control.skin; 27 28 import com.sun.javafx.scene.control.behavior.BehaviorBase; 29 30 import javafx.application.Platform; 31 import javafx.beans.InvalidationListener; 32 import javafx.beans.Observable; 33 import javafx.beans.value.ChangeListener; 34 import javafx.collections.FXCollections; 35 import javafx.collections.ListChangeListener; 36 import javafx.collections.MapChangeListener; 37 import javafx.collections.ObservableList; 38 import javafx.collections.ObservableMap; 39 import javafx.geometry.Insets; 40 import javafx.scene.AccessibleAction; 41 import javafx.scene.AccessibleAttribute; 42 import javafx.scene.Node; 43 import javafx.scene.control.*; 44 45 import javafx.scene.layout.Region; 46 import javafx.scene.layout.StackPane; 47 import javafx.util.Callback; 48 49 import javafx.collections.WeakListChangeListener; 50 import com.sun.javafx.scene.control.skin.resources.ControlResources; 51 52 import java.lang.ref.WeakReference; 53 import java.util.List; 54 import javafx.beans.WeakInvalidationListener; 55 import javafx.beans.property.BooleanProperty; 56 import javafx.beans.property.ObjectProperty; 57 import javafx.beans.value.WeakChangeListener; 58 import javafx.geometry.HPos; 59 import javafx.geometry.VPos; 60 61 import java.security.AccessController; 62 import java.security.PrivilegedAction; 63 import java.util.Map; 64 65 /** 66 * 67 * @param <M> The type of the item stored in the cell 68 * @param <S> The type of the item represented by the virtualised control (e.g. TableView<S>) 69 * @param <C> The type of the virtualised control (e.g TableView, TreeTableView) 70 * @param <B> The Behavior of the virtualised control 71 * @param <I> The type of cell used by this virtualised control (e.g. TableRow, TreeTableRow) 72 * @param <TC> The type of TableColumnBase used by this virtualised control (e.g. TableColumn, TreeTableColumn) 73 */ 74 public abstract class TableViewSkinBase<M, S, C extends Control, B extends BehaviorBase<C>, I extends IndexedCell<M>, TC extends TableColumnBase<S,?>> extends VirtualContainerBase<C, B, I> { 75 76 /*************************************************************************** 77 * * 78 * Static Fields * 79 * * 80 **************************************************************************/ 81 82 public static final String REFRESH = "tableRefreshKey"; 83 public static final String RECREATE = "tableRecreateKey"; 84 85 86 87 /*************************************************************************** 88 * * 89 * Internal Fields * 90 * * 91 **************************************************************************/ 92 93 private boolean contentWidthDirty = true; 94 95 /** 96 * This region is used to overlay atop the table when the user is performing 97 * a column resize operation or a column reordering operation. It is a line 98 * that runs the height of the table to indicate either the final width of 99 * of the selected column, or the position the column will be 'dropped' into 100 * when the reordering operation completes. 101 */ 102 private Region columnReorderLine; 103 104 /** 105 * A region which is resized and positioned such that it perfectly matches 106 * the dimensions of any TableColumn that is being reordered by the user. 107 * This is useful, for example, as a semi-transparent overlay to give 108 * feedback to the user as to which column is currently being moved. 109 */ 110 private Region columnReorderOverlay; 111 112 /** 113 * The entire header region for all columns. This header region handles 114 * column reordering and resizing. It also handles the positioning and 115 * resizing of thte columnReorderLine and columnReorderOverlay. 116 */ 117 private TableHeaderRow tableHeaderRow; 118 119 private Callback<C, I> rowFactory; 120 121 /** 122 * Region placed over the top of the flow (and possibly the header row) if 123 * there is no data and/or there are no columns specified. 124 */ 125 private StackPane placeholderRegion; 126 private Label placeholderLabel; 127 private static final String EMPTY_TABLE_TEXT = ControlResources.getString("TableView.noContent"); 128 private static final String NO_COLUMNS_TEXT = ControlResources.getString("TableView.noColumns"); 129 130 private int visibleColCount; 131 132 protected boolean needCellsRebuilt = true; 133 protected boolean needCellsRecreated = true; 134 protected boolean needCellsReconfigured = false; 135 136 private int itemCount = -1; 137 protected boolean forceCellRecreate = false; 138 139 // RT-34744 : IS_PANNABLE will be false unless 140 // com.sun.javafx.scene.control.skin.TableViewSkin.pannable 141 // is set to true. This is done in order to make TableView functional 142 // on embedded systems with touch screens which do not generate scroll 143 // events for touch drag gestures. 144 private static final boolean IS_PANNABLE = 145 AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("com.sun.javafx.scene.control.skin.TableViewSkin.pannable")); 146 147 /*************************************************************************** 148 * * 149 * Constructors * 150 * * 151 **************************************************************************/ 152 153 public TableViewSkinBase(final C control, final B behavior) { 154 super(control, behavior); 155 156 // init(control) should not be called here - it should be called by the 157 // subclass after initialising itself. This is to prevent NPEs (for 158 // example, getVisibleLeafColumns() throws a NPE as the control itself 159 // is not yet set in subclasses). 160 } 161 162 // init isn't a constructor, but it is part of the initialisation routine 163 protected void init(final C control) { 164 // init the VirtualFlow 165 flow.setPannable(IS_PANNABLE); 166 flow.setCreateCell(flow1 -> TableViewSkinBase.this.createCell()); 167 168 /* 169 * Listening for scrolling along the X axis, but we need to be careful 170 * to handle the situation appropriately when the hbar is invisible. 171 */ 172 final InvalidationListener hbarValueListener = valueModel -> { 173 horizontalScroll(); 174 }; 175 flow.getHbar().valueProperty().addListener(hbarValueListener); 176 177 // RT-37152 178 flow.getHbar().setUnitIncrement(15); 179 flow.getHbar().setBlockIncrement(TableColumnHeader.DEFAULT_COLUMN_WIDTH); 180 181 columnReorderLine = new Region(); 182 columnReorderLine.getStyleClass().setAll("column-resize-line"); 183 columnReorderLine.setManaged(false); 184 columnReorderLine.setVisible(false); 185 186 columnReorderOverlay = new Region(); 187 columnReorderOverlay.getStyleClass().setAll("column-overlay"); 188 columnReorderOverlay.setVisible(false); 189 columnReorderOverlay.setManaged(false); 190 191 tableHeaderRow = createTableHeaderRow(); 192 // tableHeaderRow.setColumnReorderLine(columnReorderLine); 193 tableHeaderRow.setFocusTraversable(false); 194 195 getChildren().addAll(tableHeaderRow, flow, columnReorderOverlay, columnReorderLine); 196 197 updateVisibleColumnCount(); 198 updateVisibleLeafColumnWidthListeners(getVisibleLeafColumns(), FXCollections.<TC>emptyObservableList()); 199 200 tableHeaderRow.reorderingProperty().addListener(valueModel -> { 201 getSkinnable().requestLayout(); 202 }); 203 204 getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener); 205 206 updateTableItems(null, itemsProperty().get()); 207 itemsChangeListener = new InvalidationListener() { 208 private WeakReference<ObservableList<S>> weakItemsRef = new WeakReference<>(itemsProperty().get()); 209 210 @Override public void invalidated(Observable observable) { 211 ObservableList<S> oldItems = weakItemsRef.get(); 212 weakItemsRef = new WeakReference<>(itemsProperty().get()); 213 updateTableItems(oldItems, itemsProperty().get()); 214 } 215 }; 216 weakItemsChangeListener = new WeakInvalidationListener(itemsChangeListener); 217 itemsProperty().addListener(weakItemsChangeListener); 218 219 final ObservableMap<Object, Object> properties = control.getProperties(); 220 properties.remove(REFRESH); 221 properties.remove(RECREATE); 222 properties.addListener(propertiesMapListener); 223 224 control.addEventHandler(ScrollToEvent.<TC>scrollToColumn(), event -> { 225 scrollHorizontally(event.getScrollTarget()); 226 }); 227 228 // flow and flow.vbar width observer 229 InvalidationListener widthObserver = valueModel -> { 230 contentWidthDirty = true; 231 getSkinnable().requestLayout(); 232 }; 233 flow.widthProperty().addListener(widthObserver); 234 flow.getVbar().widthProperty().addListener(widthObserver); 235 236 registerChangeListener(rowFactoryProperty(), "ROW_FACTORY"); 237 registerChangeListener(placeholderProperty(), "PLACEHOLDER"); 238 registerChangeListener(control.widthProperty(), "WIDTH"); 239 registerChangeListener(flow.getVbar().visibleProperty(), "VBAR_VISIBLE"); 240 } 241 242 243 244 /*************************************************************************** 245 * * 246 * Listeners * 247 * * 248 **************************************************************************/ 249 250 private MapChangeListener<Object, Object> propertiesMapListener = c -> { 251 if (! c.wasAdded()) return; 252 if (REFRESH.equals(c.getKey())) { 253 refreshView(); 254 getSkinnable().getProperties().remove(REFRESH); 255 } else if (RECREATE.equals(c.getKey())) { 256 forceCellRecreate = true; 257 refreshView(); 258 getSkinnable().getProperties().remove(RECREATE); 259 } 260 }; 261 262 private ListChangeListener<S> rowCountListener = c -> { 263 while (c.next()) { 264 if (c.wasReplaced()) { 265 // RT-28397: Support for when an item is replaced with itself (but 266 // updated internal values that should be shown visually). 267 268 // The ListViewSkin equivalent code here was updated to use the 269 // flow.setDirtyCell(int) API, but it was left alone here, otherwise 270 // our unit test for RT-36220 fails as we do not handle the case 271 // where the TableCell gets updated (only the TableRow does). 272 // Ideally we would use the dirtyCell API: 273 // 274 // for (int i = c.getFrom(); i < c.getTo(); i++) { 275 // flow.setCellDirty(i); 276 // } 277 itemCount = 0; 278 break; 279 } else if (c.getRemovedSize() == itemCount) { 280 // RT-22463: If the user clears out an items list then we 281 // should reset all cells (in particular their contained 282 // items) such that a subsequent addition to the list of 283 // an item which equals the old item (but is rendered 284 // differently) still displays as expected (i.e. with the 285 // updated display, not the old display). 286 itemCount = 0; 287 break; 288 } 289 } 290 291 // fix for RT-37853 292 if (getSkinnable() instanceof TableView) { 293 edit(-1, null); 294 } 295 296 rowCountDirty = true; 297 getSkinnable().requestLayout(); 298 }; 299 300 private ListChangeListener<TC> visibleLeafColumnsListener = 301 c -> { 302 updateVisibleColumnCount(); 303 while (c.next()) { 304 updateVisibleLeafColumnWidthListeners(c.getAddedSubList(), c.getRemoved()); 305 } 306 }; 307 308 private InvalidationListener widthListener = observable -> { 309 // This forces the horizontal scrollbar to show when the column 310 // resizing occurs. It is not ideal, but will work for now. 311 312 // using 'needCellsReconfigured' here rather than 'needCellsRebuilt' 313 // as otherwise performance suffers massively (RT-27831) 314 needCellsReconfigured = true; 315 if (getSkinnable() != null) { 316 getSkinnable().requestLayout(); 317 } 318 }; 319 320 private InvalidationListener itemsChangeListener; 321 322 private WeakListChangeListener<S> weakRowCountListener = 323 new WeakListChangeListener<S>(rowCountListener); 324 private WeakListChangeListener<TC> weakVisibleLeafColumnsListener = 325 new WeakListChangeListener<TC>(visibleLeafColumnsListener); 326 private WeakInvalidationListener weakWidthListener = 327 new WeakInvalidationListener(widthListener); 328 private WeakInvalidationListener weakItemsChangeListener; 329 330 331 332 /*************************************************************************** 333 * * 334 * Abstract Methods * 335 * * 336 **************************************************************************/ 337 338 // returns the selection model of the control 339 protected abstract TableSelectionModel<S> getSelectionModel(); 340 341 // returns the focus model of the control 342 protected abstract TableFocusModel<S,TC> getFocusModel(); 343 344 // returns the currently focused cell in the focus model 345 protected abstract TablePositionBase<? extends TC> getFocusedCell(); 346 347 // returns an ObservableList of the visible leaf columns of the control 348 protected abstract ObservableList<? extends TC> getVisibleLeafColumns(); 349 350 // returns the index of a column in the visible leaf columns 351 protected abstract int getVisibleLeafIndex(TC tc); 352 353 // returns the leaf column at the given index 354 protected abstract TC getVisibleLeafColumn(int col); 355 356 // returns a list of the root columns 357 protected abstract ObservableList<TC> getColumns(); 358 359 // returns the sort order of the control 360 protected abstract ObservableList<TC> getSortOrder(); 361 362 // returns a property representing the list of items in the control 363 protected abstract ObjectProperty<ObservableList<S>> itemsProperty(); 364 365 // returns a property representing the row factory in the control 366 protected abstract ObjectProperty<Callback<C, I>> rowFactoryProperty(); 367 368 // returns the placeholder property for the control 369 protected abstract ObjectProperty<Node> placeholderProperty(); 370 371 // returns the property used to represent whether the tableMenuButton should 372 // be visible 373 protected abstract BooleanProperty tableMenuButtonVisibleProperty(); 374 375 // returns a property representing the column resize properyt in the control 376 protected abstract ObjectProperty<Callback<ResizeFeaturesBase, Boolean>> columnResizePolicyProperty(); 377 378 // Method to resize the given column by the given delta, returning a boolean 379 // to indicate success or failure 380 protected abstract boolean resizeColumn(TC tc, double delta); 381 382 // Method to resize the column based on the content in that column, based on 383 // the maxRows number of rows 384 protected abstract void resizeColumnToFitContent(TC tc, int maxRows); 385 386 protected abstract void edit(int index, TC column); 387 388 389 390 /*************************************************************************** 391 * * 392 * Public API * 393 * * 394 **************************************************************************/ 395 396 @Override protected void handleControlPropertyChanged(String p) { 397 super.handleControlPropertyChanged(p); 398 399 if ("ROW_FACTORY".equals(p)) { 400 Callback<C, I> oldFactory = rowFactory; 401 rowFactory = rowFactoryProperty().get(); 402 if (oldFactory != rowFactory) { 403 needCellsRebuilt = true; 404 getSkinnable().requestLayout(); 405 } 406 } else if ("PLACEHOLDER".equals(p)) { 407 updatePlaceholderRegionVisibility(); 408 // } else if ("WIDTH".equals(p)) { 409 // tableHeaderRow.setTablePadding(getSkinnable().getInsets()); 410 } else if ("VBAR_VISIBLE".equals(p)) { 411 updateContentWidth(); 412 } 413 } 414 415 @Override public void dispose() { 416 getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener); 417 itemsProperty().removeListener(weakItemsChangeListener); 418 getSkinnable().getProperties().removeListener(propertiesMapListener); 419 updateTableItems(itemsProperty().get(), null); 420 421 super.dispose(); 422 } 423 424 protected TableHeaderRow createTableHeaderRow() { 425 return new TableHeaderRow(this); 426 } 427 428 /** 429 * 430 */ 431 public TableHeaderRow getTableHeaderRow() { 432 return tableHeaderRow; 433 } 434 435 public Region getColumnReorderLine() { 436 return columnReorderLine; 437 } 438 439 /** 440 * Function used to scroll the container down by one 'page', although 441 * if this is a horizontal container, then the scrolling will be to the right. 442 */ 443 public int onScrollPageDown(boolean isFocusDriven) { 444 TableSelectionModel<S> sm = getSelectionModel(); 445 if (sm == null) return -1; 446 447 final int itemCount = getItemCount(); 448 449 I lastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 450 if (lastVisibleCell == null) return -1; 451 452 int lastVisibleCellIndex = lastVisibleCell.getIndex(); 453 454 // we include this test here as the virtual flow will return cells that 455 // exceed past the item count, so we need to clamp here (and further down 456 // in this method also). See RT-19053 for more information. 457 lastVisibleCellIndex = lastVisibleCellIndex >= itemCount ? itemCount - 1 : lastVisibleCellIndex; 458 459 // isSelected represents focus OR selection 460 boolean isSelected; 461 if (isFocusDriven) { 462 isSelected = lastVisibleCell.isFocused() || isCellFocused(lastVisibleCellIndex); 463 } else { 464 isSelected = lastVisibleCell.isSelected() || isCellSelected(lastVisibleCellIndex); 465 } 466 467 if (isSelected) { 468 boolean isLeadIndex = isLeadIndex(isFocusDriven, lastVisibleCellIndex); 469 470 if (isLeadIndex) { 471 // if the last visible cell is selected, we want to shift that cell up 472 // to be the top-most cell, or at least as far to the top as we can go. 473 flow.showAsFirst(lastVisibleCell); 474 475 I newLastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 476 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell; 477 } 478 } 479 480 int newSelectionIndex = lastVisibleCell.getIndex(); 481 newSelectionIndex = newSelectionIndex >= itemCount ? itemCount - 1 : newSelectionIndex; 482 flow.show(newSelectionIndex); 483 return newSelectionIndex; 484 } 485 486 /** 487 * Function used to scroll the container up by one 'page', although 488 * if this is a horizontal container, then the scrolling will be to the left. 489 */ 490 public int onScrollPageUp(boolean isFocusDriven) { 491 I firstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 492 if (firstVisibleCell == null) return -1; 493 494 int firstVisibleCellIndex = firstVisibleCell.getIndex(); 495 496 // isSelected represents focus OR selection 497 boolean isSelected = false; 498 if (isFocusDriven) { 499 isSelected = firstVisibleCell.isFocused() || isCellFocused(firstVisibleCellIndex); 500 } else { 501 isSelected = firstVisibleCell.isSelected() || isCellSelected(firstVisibleCellIndex); 502 } 503 504 if (isSelected) { 505 boolean isLeadIndex = isLeadIndex(isFocusDriven, firstVisibleCellIndex); 506 507 if (isLeadIndex) { 508 // if the first visible cell is selected, we want to shift that cell down 509 // to be the bottom-most cell, or at least as far to the bottom as we can go. 510 flow.showAsLast(firstVisibleCell); 511 512 I newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 513 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell; 514 } 515 } 516 517 int newSelectionIndex = firstVisibleCell.getIndex(); 518 flow.show(newSelectionIndex); 519 return newSelectionIndex; 520 } 521 522 private boolean isLeadIndex(boolean isFocusDriven, int index) { 523 final TableSelectionModel<S> sm = getSelectionModel(); 524 final FocusModel<S> fm = getFocusModel(); 525 526 return (isFocusDriven && fm.getFocusedIndex() == index) 527 || (! isFocusDriven && sm.getSelectedIndex() == index); 528 } 529 530 boolean isColumnPartiallyOrFullyVisible(TC col) { 531 if (col == null || !col.isVisible()) return false; 532 533 double scrollX = flow.getHbar().getValue(); 534 535 // work out where this column header is, and it's width (start -> end) 536 double start = 0; 537 final ObservableList<? extends TC> visibleLeafColumns = getVisibleLeafColumns(); 538 for (int i = 0, max = visibleLeafColumns.size(); i < max; i++) { 539 TableColumnBase<S,?> c = visibleLeafColumns.get(i); 540 if (c.equals(col)) break; 541 start += c.getWidth(); 542 } 543 double end = start + col.getWidth(); 544 545 // determine the width of the table 546 final Insets padding = getSkinnable().getPadding(); 547 double headerWidth = getSkinnable().getWidth() - padding.getLeft() + padding.getRight(); 548 549 return (start >= scrollX || end > scrollX) && (start < (headerWidth + scrollX) || end <= (headerWidth + scrollX)); 550 } 551 552 protected void horizontalScroll() { 553 tableHeaderRow.updateScrollX(); 554 } 555 556 @Override protected void updateRowCount() { 557 updatePlaceholderRegionVisibility(); 558 559 int oldCount = itemCount; 560 int newCount = getItemCount(); 561 562 itemCount = newCount; 563 564 // if this is not called even when the count is the same, we get a 565 // memory leak in VirtualFlow.sheet.children. This can probably be 566 // optimised in the future when time permits. 567 flow.setCellCount(newCount); 568 569 if (forceCellRecreate) { 570 needCellsRecreated = true; 571 forceCellRecreate = false; 572 } else if (newCount != oldCount) { 573 // FIXME updateRowCount is called _a lot_. Perhaps we can make rebuildCells 574 // smarter. Imagine if items has one million items added - do we really 575 // need to rebuildCells a million times? Maybe this is better now that 576 // we do rebuildCells instead of recreateCells. 577 needCellsRebuilt = true; 578 } else { 579 needCellsReconfigured = true; 580 } 581 } 582 583 protected void onFocusPreviousCell() { 584 TableFocusModel<S,TC> fm = getFocusModel(); 585 if (fm == null) return; 586 587 flow.show(fm.getFocusedIndex()); 588 } 589 590 protected void onFocusNextCell() { 591 TableFocusModel<S,TC> fm = getFocusModel(); 592 if (fm == null) return; 593 594 flow.show(fm.getFocusedIndex()); 595 } 596 597 protected void onSelectPreviousCell() { 598 SelectionModel<S> sm = getSelectionModel(); 599 if (sm == null) return; 600 601 flow.show(sm.getSelectedIndex()); 602 } 603 604 protected void onSelectNextCell() { 605 SelectionModel<S> sm = getSelectionModel(); 606 if (sm == null) return; 607 608 flow.show(sm.getSelectedIndex()); 609 } 610 611 protected void onSelectLeftCell() { 612 scrollHorizontally(); 613 } 614 615 protected void onSelectRightCell() { 616 scrollHorizontally(); 617 } 618 619 protected void onMoveToFirstCell() { 620 flow.show(0); 621 flow.setPosition(0); 622 } 623 624 protected void onMoveToLastCell() { 625 int endPos = getItemCount(); 626 flow.show(endPos); 627 flow.setPosition(1); 628 } 629 630 public void updateTableItems(ObservableList<S> oldList, ObservableList<S> newList) { 631 if (oldList != null) { 632 oldList.removeListener(weakRowCountListener); 633 } 634 635 if (newList != null) { 636 newList.addListener(weakRowCountListener); 637 } 638 639 rowCountDirty = true; 640 getSkinnable().requestLayout(); 641 } 642 643 644 /*************************************************************************** 645 * * 646 * Layout * 647 * * 648 **************************************************************************/ 649 650 private static final double GOLDEN_RATIO_MULTIPLIER = 0.618033987; 651 652 private void checkContentWidthState() { 653 // we test for item count here to resolve RT-14855, where the column 654 // widths weren't being resized properly when in constrained layout mode 655 // if there were no items. 656 if (contentWidthDirty || getItemCount() == 0) { 657 updateContentWidth(); 658 contentWidthDirty = false; 659 } 660 } 661 662 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 663 return 400; 664 } 665 666 /** {@inheritDoc} */ 667 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 668 double prefHeight = computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset); 669 670 List<? extends TC> cols = getVisibleLeafColumns(); 671 if (cols == null || cols.isEmpty()) { 672 return prefHeight * GOLDEN_RATIO_MULTIPLIER; 673 } 674 675 double pw = leftInset + rightInset; 676 for (int i = 0, max = cols.size(); i < max; i++) { 677 TC tc = cols.get(i); 678 pw += Math.max(tc.getPrefWidth(), tc.getMinWidth()); 679 } 680 // return pw; 681 return Math.max(pw, prefHeight * GOLDEN_RATIO_MULTIPLIER); 682 } 683 684 /** {@inheritDoc} */ 685 @Override protected void layoutChildren(final double x, double y, 686 final double w, final double h) { 687 688 C table = getSkinnable(); 689 690 // an unlikely scenario, but it does pop up in unit tests, so guarding 691 // here to prevent test failures seems ok. 692 if (table == null) { 693 return; 694 } 695 696 super.layoutChildren(x, y, w, h); 697 698 if (needCellsRecreated) { 699 flow.recreateCells(); 700 } else if (needCellsRebuilt) { 701 flow.rebuildCells(); 702 } else if (needCellsReconfigured) { 703 flow.reconfigureCells(); 704 } 705 706 needCellsRebuilt = false; 707 needCellsRecreated = false; 708 needCellsReconfigured = false; 709 710 final double baselineOffset = table.getLayoutBounds().getHeight() / 2; 711 712 // position the table header 713 double tableHeaderRowHeight = tableHeaderRow.prefHeight(-1); 714 layoutInArea(tableHeaderRow, x, y, w, tableHeaderRowHeight, baselineOffset, 715 HPos.CENTER, VPos.CENTER); 716 y += tableHeaderRowHeight; 717 718 // let the virtual flow take up all remaining space 719 // TODO this calculation is to ensure the bottom border is visible when 720 // placed in a Pane. It is not ideal, but will suffice for now. See 721 // RT-14335 for more information. 722 double flowHeight = Math.floor(h - tableHeaderRowHeight); 723 if (getItemCount() == 0 || visibleColCount == 0) { 724 // show message overlay instead of empty table 725 layoutInArea(placeholderRegion, x, y, 726 w, flowHeight, 727 baselineOffset, HPos.CENTER, VPos.CENTER); 728 } else { 729 layoutInArea(flow, x, y, 730 w, flowHeight, 731 baselineOffset, HPos.CENTER, VPos.CENTER); 732 } 733 734 // painting the overlay over the column being reordered 735 if (tableHeaderRow.getReorderingRegion() != null) { 736 TableColumnHeader reorderingColumnHeader = tableHeaderRow.getReorderingRegion(); 737 TableColumnBase reorderingColumn = reorderingColumnHeader.getTableColumn(); 738 if (reorderingColumn != null) { 739 Node n = tableHeaderRow.getReorderingRegion(); 740 741 // determine where to draw the column header overlay, it's 742 // either from the left-edge of the column, or 0, if the column 743 // is off the left-side of the TableView (i.e. horizontal 744 // scrolling has occured). 745 double minX = tableHeaderRow.sceneToLocal(n.localToScene(n.getBoundsInLocal())).getMinX(); 746 double overlayWidth = reorderingColumnHeader.getWidth(); 747 if (minX < 0) { 748 overlayWidth += minX; 749 } 750 minX = minX < 0 ? 0 : minX; 751 752 // prevent the overlay going out the right-hand side of the 753 // TableView 754 if (minX + overlayWidth > w) { 755 overlayWidth = w - minX; 756 757 if (flow.getVbar().isVisible()) { 758 overlayWidth -= flow.getVbar().getWidth() - 1; 759 } 760 } 761 762 double contentAreaHeight = flowHeight; 763 if (flow.getHbar().isVisible()) { 764 contentAreaHeight -= flow.getHbar().getHeight(); 765 } 766 767 columnReorderOverlay.resize(overlayWidth, contentAreaHeight); 768 769 columnReorderOverlay.setLayoutX(minX); 770 columnReorderOverlay.setLayoutY(tableHeaderRow.getHeight()); 771 } 772 773 // paint the reorder line as well 774 double cw = columnReorderLine.snappedLeftInset() + columnReorderLine.snappedRightInset(); 775 double lineHeight = h - (flow.getHbar().isVisible() ? flow.getHbar().getHeight() - 1 : 0); 776 columnReorderLine.resizeRelocate(0, columnReorderLine.snappedTopInset(), cw, lineHeight); 777 } 778 779 columnReorderLine.setVisible(tableHeaderRow.isReordering()); 780 columnReorderOverlay.setVisible(tableHeaderRow.isReordering()); 781 782 checkContentWidthState(); 783 } 784 785 786 787 /*************************************************************************** 788 * * 789 * Private methods * 790 * * 791 **************************************************************************/ 792 793 /** 794 * Keeps track of how many leaf columns are currently visible in this table. 795 */ 796 private void updateVisibleColumnCount() { 797 visibleColCount = getVisibleLeafColumns().size(); 798 799 updatePlaceholderRegionVisibility(); 800 needCellsRebuilt = true; 801 getSkinnable().requestLayout(); 802 } 803 804 private void updateVisibleLeafColumnWidthListeners( 805 List<? extends TC> added, List<? extends TC> removed) { 806 807 for (int i = 0, max = removed.size(); i < max; i++) { 808 TC tc = removed.get(i); 809 tc.widthProperty().removeListener(weakWidthListener); 810 } 811 for (int i = 0, max = added.size(); i < max; i++) { 812 TC tc = added.get(i); 813 tc.widthProperty().addListener(weakWidthListener); 814 } 815 needCellsRebuilt = true; 816 getSkinnable().requestLayout(); 817 } 818 819 protected final void updatePlaceholderRegionVisibility() { 820 boolean visible = visibleColCount == 0 || getItemCount() == 0; 821 822 if (visible) { 823 if (placeholderRegion == null) { 824 placeholderRegion = new StackPane(); 825 placeholderRegion.getStyleClass().setAll("placeholder"); 826 getChildren().add(placeholderRegion); 827 } 828 829 Node placeholderNode = placeholderProperty().get(); 830 831 if (placeholderNode == null) { 832 if (placeholderLabel == null) { 833 placeholderLabel = new Label(); 834 } 835 String s = visibleColCount == 0 ? NO_COLUMNS_TEXT : EMPTY_TABLE_TEXT; 836 placeholderLabel.setText(s); 837 838 placeholderRegion.getChildren().setAll(placeholderLabel); 839 } else { 840 placeholderRegion.getChildren().setAll(placeholderNode); 841 } 842 } 843 844 flow.setVisible(! visible); 845 if (placeholderRegion != null) { 846 placeholderRegion.setVisible(visible); 847 } 848 } 849 850 /* 851 * It's often important to know how much width is available for content 852 * within the table, and this needs to exclude the width of any vertical 853 * scrollbar. 854 */ 855 private void updateContentWidth() { 856 double contentWidth = flow.getWidth(); 857 858 if (flow.getVbar().isVisible()) { 859 contentWidth -= flow.getVbar().getWidth(); 860 } 861 862 if (contentWidth <= 0) { 863 // Fix for RT-14855 when there is no content in the TableView. 864 Control c = getSkinnable(); 865 contentWidth = c.getWidth() - (snappedLeftInset() + snappedRightInset()); 866 } 867 868 contentWidth = Math.max(0.0, contentWidth); 869 870 // FIXME this isn't perfect, but it prevents RT-14885, which results in 871 // undesired horizontal scrollbars when in constrained resize mode 872 getSkinnable().getProperties().put("TableView.contentWidth", Math.floor(contentWidth)); 873 } 874 875 private void refreshView() { 876 rowCountDirty = true; 877 Control c = getSkinnable(); 878 if (c != null) { 879 c.requestLayout(); 880 } 881 } 882 883 // Handles the horizontal scrolling when the selection mode is cell-based 884 // and the newly selected cell belongs to a column which is not totally 885 // visible. 886 protected void scrollHorizontally() { 887 TableFocusModel<S,TC> fm = getFocusModel(); 888 if (fm == null) return; 889 890 TC col = getFocusedCell().getTableColumn(); 891 scrollHorizontally(col); 892 } 893 894 protected void scrollHorizontally(TC col) { 895 if (col == null || !col.isVisible()) return; 896 897 final Control control = getSkinnable(); 898 899 // RT-37060 - if we are trying to scroll to a column that has not 900 // yet even been rendered, we must wait until the layout pass has 901 // happened and then do the scroll. The laziest way to do this is to 902 // queue up the task to run later, at which point we will have hopefully 903 // fully run the column through layout and css. 904 TableColumnHeader header = tableHeaderRow.getColumnHeaderFor(col); 905 if (header == null || header.getWidth() <= 0) { 906 Platform.runLater(() -> scrollHorizontally(col)); 907 return; 908 } 909 910 // work out where this column header is, and it's width (start -> end) 911 double start = 0; 912 for (TC c : getVisibleLeafColumns()) { 913 if (c.equals(col)) break; 914 start += c.getWidth(); 915 } 916 double end = start + col.getWidth(); 917 918 // determine the visible width of the table 919 double headerWidth = control.getWidth() - snappedLeftInset() - snappedRightInset(); 920 921 // determine by how much we need to translate the table to ensure that 922 // the start position of this column lines up with the left edge of the 923 // tableview, and also that the columns don't become detached from the 924 // right edge of the table 925 double pos = flow.getHbar().getValue(); 926 double max = flow.getHbar().getMax(); 927 double newPos; 928 929 if (start < pos && start >= 0) { 930 newPos = start; 931 } else { 932 double delta = start < 0 || end > headerWidth ? start - pos : 0; 933 newPos = pos + delta > max ? max : pos + delta; 934 } 935 936 // FIXME we should add API in VirtualFlow so we don't end up going 937 // direct to the hbar. 938 // actually shift the flow - this will result in the header moving 939 // as well 940 flow.getHbar().setValue(newPos); 941 } 942 943 private boolean isCellSelected(int row) { 944 TableSelectionModel<S> sm = getSelectionModel(); 945 if (sm == null) return false; 946 if (! sm.isCellSelectionEnabled()) return false; 947 948 int columnCount = getVisibleLeafColumns().size(); 949 for (int col = 0; col < columnCount; col++) { 950 if (sm.isSelected(row, getVisibleLeafColumn(col))) { 951 return true; 952 } 953 } 954 955 return false; 956 } 957 958 private boolean isCellFocused(int row) { 959 TableFocusModel<S,TC> fm = getFocusModel(); 960 if (fm == null) return false; 961 962 int columnCount = getVisibleLeafColumns().size(); 963 for (int col = 0; col < columnCount; col++) { 964 if (fm.isFocused(row, getVisibleLeafColumn(col))) { 965 return true; 966 } 967 } 968 969 return false; 970 } 971 972 @Override 973 protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 974 switch (attribute) { 975 case FOCUS_ITEM: { 976 TableFocusModel<S,?> fm = getFocusModel(); 977 int focusedIndex = fm.getFocusedIndex(); 978 if (focusedIndex == -1) { 979 if (placeholderRegion != null && placeholderRegion.isVisible()) { 980 return placeholderRegion.getChildren().get(0); 981 } 982 if (getItemCount() > 0) { 983 focusedIndex = 0; 984 } else { 985 return null; 986 } 987 } 988 return flow.getPrivateCell(focusedIndex); 989 } 990 case CELL_AT_ROW_COLUMN: { 991 int rowIndex = (Integer)parameters[0]; 992 return flow.getPrivateCell(rowIndex); 993 } 994 case COLUMN_AT_INDEX: { 995 int index = (Integer)parameters[0]; 996 TableColumnBase<S,?> column = getVisibleLeafColumn(index); 997 return getTableHeaderRow().getColumnHeaderFor(column); 998 } 999 case HEADER: { 1000 /* Not sure how this is used by Accessibility, but without this VoiceOver will not 1001 * look for column headers */ 1002 return getTableHeaderRow(); 1003 } 1004 case VERTICAL_SCROLLBAR: return flow.getVbar(); 1005 case HORIZONTAL_SCROLLBAR: return flow.getHbar(); 1006 default: return super.queryAccessibleAttribute(attribute, parameters); 1007 } 1008 } 1009 1010 }