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