1 /* 2 * Copyright (c) 2012, 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.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 // RT-34744 : IS_PANNABLE will be false unless 93 // javafx.scene.control.skin.TableViewSkin.pannable 94 // is set to true. This is done in order to make TableView functional 95 // on embedded systems with touch screens which do not generate scroll 96 // events for touch drag gestures. 97 private static final boolean IS_PANNABLE = 98 AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.TableViewSkin.pannable")); 99 100 101 102 /*************************************************************************** 103 * * 104 * Internal Fields * 105 * * 106 **************************************************************************/ 107 108 // JDK-8090129: These constants should not be static, because the 109 // Locale may change between instances. 110 private final String EMPTY_TABLE_TEXT = ControlResources.getString("TableView.noContent"); 111 private final String NO_COLUMNS_TEXT = ControlResources.getString("TableView.noColumns"); 112 113 VirtualFlow<I> flow; 114 115 private boolean contentWidthDirty = true; 116 117 /** 118 * This region is used to overlay atop the table when the user is performing 119 * a column resize operation or a column reordering operation. It is a line 120 * that runs the height of the table to indicate either the final width of 121 * of the selected column, or the position the column will be 'dropped' into 122 * when the reordering operation completes. 123 */ 124 private Region columnReorderLine; 125 126 /** 127 * A region which is resized and positioned such that it perfectly matches 128 * the dimensions of any TableColumn that is being reordered by the user. 129 * This is useful, for example, as a semi-transparent overlay to give 130 * feedback to the user as to which column is currently being moved. 131 */ 132 private Region columnReorderOverlay; 133 134 /** 135 * The entire header region for all columns. This header region handles 136 * column reordering and resizing. It also handles the positioning and 137 * resizing of thte columnReorderLine and columnReorderOverlay. 138 */ 139 private TableHeaderRow tableHeaderRow; 140 141 private Callback<C, I> rowFactory; 142 143 /** 144 * Region placed over the top of the flow (and possibly the header row) if 145 * there is no data and/or there are no columns specified. 146 */ 147 private StackPane placeholderRegion; 148 private Label placeholderLabel; 149 150 private int visibleColCount; 151 152 boolean needCellsRecreated = true; 153 boolean needCellsReconfigured = 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 needCellsRecreated = 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 markItemCountDirty(); 212 getSkinnable().requestLayout(); 213 }; 214 215 private ListChangeListener<TC> visibleLeafColumnsListener = c -> { 216 updateVisibleColumnCount(); 217 while (c.next()) { 218 updateVisibleLeafColumnWidthListeners(c.getAddedSubList(), c.getRemoved()); 219 } 220 }; 221 222 private InvalidationListener widthListener = observable -> { 223 // This forces the horizontal scrollbar to show when the column 224 // resizing occurs. It is not ideal, but will work for now. 225 226 // using 'needCellsReconfigured' here rather than 'needCellsRebuilt' 227 // as otherwise performance suffers massively (RT-27831) 228 needCellsReconfigured = true; 229 if (getSkinnable() != null) { 230 getSkinnable().requestLayout(); 231 } 232 }; 233 234 private InvalidationListener itemsChangeListener; 235 236 private WeakListChangeListener<S> weakRowCountListener = 237 new WeakListChangeListener<>(rowCountListener); 238 private WeakListChangeListener<TC> weakVisibleLeafColumnsListener = 239 new WeakListChangeListener<>(visibleLeafColumnsListener); 240 private WeakInvalidationListener weakWidthListener = 241 new WeakInvalidationListener(widthListener); 242 private WeakInvalidationListener weakItemsChangeListener; 243 244 245 246 /*************************************************************************** 247 * * 248 * Constructors * 249 * * 250 **************************************************************************/ 251 252 /** 253 * 254 * @param control 255 */ 256 public TableViewSkinBase(final C control) { 257 super(control); 258 259 // init the VirtualFlow 260 flow = getVirtualFlow(); 261 flow.setPannable(IS_PANNABLE); 262 // flow.setCellFactory(flow1 -> TableViewSkinBase.this.createCell()); 263 264 /* 265 * Listening for scrolling along the X axis, but we need to be careful 266 * to handle the situation appropriately when the hbar is invisible. 267 */ 268 flow.getHbar().valueProperty().addListener(o -> horizontalScroll()); 269 270 // RT-37152 271 flow.getHbar().setUnitIncrement(15); 272 flow.getHbar().setBlockIncrement(TableColumnHeader.DEFAULT_COLUMN_WIDTH); 273 274 columnReorderLine = new Region(); 275 columnReorderLine.getStyleClass().setAll("column-resize-line"); 276 columnReorderLine.setManaged(false); 277 columnReorderLine.setVisible(false); 278 279 columnReorderOverlay = new Region(); 280 columnReorderOverlay.getStyleClass().setAll("column-overlay"); 281 columnReorderOverlay.setVisible(false); 282 columnReorderOverlay.setManaged(false); 283 284 tableHeaderRow = createTableHeaderRow(); 285 // tableHeaderRow.setColumnReorderLine(columnReorderLine); 286 tableHeaderRow.setFocusTraversable(false); 287 288 getChildren().addAll(tableHeaderRow, flow, columnReorderOverlay, columnReorderLine); 289 290 updateVisibleColumnCount(); 291 updateVisibleLeafColumnWidthListeners(getVisibleLeafColumns(), FXCollections.<TC>emptyObservableList()); 292 293 tableHeaderRow.reorderingProperty().addListener(valueModel -> { 294 getSkinnable().requestLayout(); 295 }); 296 297 getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener); 298 299 final ObjectProperty<ObservableList<S>> itemsProperty = TableSkinUtils.itemsProperty(this); 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 final ObjectProperty<Callback<C, I>> rowFactoryProperty = TableSkinUtils.rowFactoryProperty(this); 331 registerChangeListener(rowFactoryProperty, e -> { 332 Callback<C, I> oldFactory = rowFactory; 333 rowFactory = rowFactoryProperty.get(); 334 if (oldFactory != rowFactory) { 335 requestRebuildCells(); 336 } 337 }); 338 registerChangeListener(TableSkinUtils.placeholderProperty(this), e -> updatePlaceholderRegionVisibility()); 339 registerChangeListener(flow.getVbar().visibleProperty(), e -> updateContentWidth()); 340 } 341 342 343 344 /*************************************************************************** 345 * * 346 * Abstract Methods * 347 * * 348 **************************************************************************/ 349 350 351 352 353 354 /*************************************************************************** 355 * * 356 * Public API * 357 * * 358 **************************************************************************/ 359 360 /** {@inheritDoc} */ 361 @Override public void dispose() { 362 final ObjectProperty<ObservableList<S>> itemsProperty = TableSkinUtils.itemsProperty(this); 363 364 getVisibleLeafColumns().removeListener(weakVisibleLeafColumnsListener); 365 itemsProperty.removeListener(weakItemsChangeListener); 366 getSkinnable().getProperties().removeListener(propertiesMapListener); 367 updateTableItems(itemsProperty.get(), null); 368 369 super.dispose(); 370 } 371 372 /** {@inheritDoc} */ 373 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 374 return 400; 375 } 376 377 /** {@inheritDoc} */ 378 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 379 double prefHeight = computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset); 380 381 List<? extends TC> cols = getVisibleLeafColumns(); 382 if (cols == null || cols.isEmpty()) { 383 return prefHeight * GOLDEN_RATIO_MULTIPLIER; 384 } 385 386 double pw = leftInset + rightInset; 387 for (int i = 0, max = cols.size(); i < max; i++) { 388 TC tc = cols.get(i); 389 pw += Math.max(tc.getPrefWidth(), tc.getMinWidth()); 390 } 391 // return pw; 392 return Math.max(pw, prefHeight * GOLDEN_RATIO_MULTIPLIER); 393 } 394 395 /** {@inheritDoc} */ 396 @Override protected void layoutChildren(final double x, double y, 397 final double w, final double h) { 398 399 C table = getSkinnable(); 400 401 // an unlikely scenario, but it does pop up in unit tests, so guarding 402 // here to prevent test failures seems ok. 403 if (table == null) { 404 return; 405 } 406 407 super.layoutChildren(x, y, w, h); 408 409 if (needCellsRecreated) { 410 flow.recreateCells(); 411 } else if (needCellsReconfigured) { 412 flow.reconfigureCells(); 413 } 414 415 needCellsRecreated = false; 416 needCellsReconfigured = false; 417 418 final double baselineOffset = table.getLayoutBounds().getHeight() / 2; 419 420 // position the table header 421 double tableHeaderRowHeight = tableHeaderRow.prefHeight(-1); 422 layoutInArea(tableHeaderRow, x, y, w, tableHeaderRowHeight, baselineOffset, 423 HPos.CENTER, VPos.CENTER); 424 y += tableHeaderRowHeight; 425 426 // let the virtual flow take up all remaining space 427 // TODO this calculation is to ensure the bottom border is visible when 428 // placed in a Pane. It is not ideal, but will suffice for now. See 429 // RT-14335 for more information. 430 double flowHeight = Math.floor(h - tableHeaderRowHeight); 431 if (getItemCount() == 0 || visibleColCount == 0) { 432 // show message overlay instead of empty table 433 layoutInArea(placeholderRegion, x, y, 434 w, flowHeight, 435 baselineOffset, HPos.CENTER, VPos.CENTER); 436 } else { 437 layoutInArea(flow, x, y, 438 w, flowHeight, 439 baselineOffset, HPos.CENTER, VPos.CENTER); 440 } 441 442 // painting the overlay over the column being reordered 443 if (tableHeaderRow.getReorderingRegion() != null) { 444 TableColumnHeader reorderingColumnHeader = tableHeaderRow.getReorderingRegion(); 445 TableColumnBase reorderingColumn = reorderingColumnHeader.getTableColumn(); 446 if (reorderingColumn != null) { 447 Node n = tableHeaderRow.getReorderingRegion(); 448 449 // determine where to draw the column header overlay, it's 450 // either from the left-edge of the column, or 0, if the column 451 // is off the left-side of the TableView (i.e. horizontal 452 // scrolling has occured). 453 double minX = tableHeaderRow.sceneToLocal(n.localToScene(n.getBoundsInLocal())).getMinX(); 454 double overlayWidth = reorderingColumnHeader.getWidth(); 455 if (minX < 0) { 456 overlayWidth += minX; 457 } 458 minX = minX < 0 ? 0 : minX; 459 460 // prevent the overlay going out the right-hand side of the 461 // TableView 462 if (minX + overlayWidth > w) { 463 overlayWidth = w - minX; 464 465 if (flow.getVbar().isVisible()) { 466 overlayWidth -= flow.getVbar().getWidth() - 1; 467 } 468 } 469 470 double contentAreaHeight = flowHeight; 471 if (flow.getHbar().isVisible()) { 472 contentAreaHeight -= flow.getHbar().getHeight(); 473 } 474 475 columnReorderOverlay.resize(overlayWidth, contentAreaHeight); 476 477 columnReorderOverlay.setLayoutX(minX); 478 columnReorderOverlay.setLayoutY(tableHeaderRow.getHeight()); 479 } 480 481 // paint the reorder line as well 482 double cw = columnReorderLine.snappedLeftInset() + columnReorderLine.snappedRightInset(); 483 double lineHeight = h - (flow.getHbar().isVisible() ? flow.getHbar().getHeight() - 1 : 0); 484 columnReorderLine.resizeRelocate(0, columnReorderLine.snappedTopInset(), cw, lineHeight); 485 } 486 487 columnReorderLine.setVisible(tableHeaderRow.isReordering()); 488 columnReorderOverlay.setVisible(tableHeaderRow.isReordering()); 489 490 checkContentWidthState(); 491 } 492 493 /** 494 * Creates a new TableHeaderRow instance. By default this method should not be overridden, but in some 495 * circumstances it makes sense (e.g. testing, or when extreme customization is desired). 496 * 497 * @return A new TableHeaderRow instance. 498 */ 499 protected TableHeaderRow createTableHeaderRow() { 500 return new TableHeaderRow(this); 501 } 502 503 504 505 /*************************************************************************** 506 * * 507 * Private implementation * 508 * * 509 **************************************************************************/ 510 511 final TableHeaderRow getTableHeaderRow() { 512 return tableHeaderRow; 513 } 514 515 private TableSelectionModel<S> getSelectionModel() { 516 return TableSkinUtils.getSelectionModel(this); 517 } 518 519 private TableFocusModel<M,?> getFocusModel() { 520 return TableSkinUtils.getFocusModel(this); 521 } 522 523 // returns the currently focused cell in the focus model 524 private TablePositionBase<? extends TC> getFocusedCell() { 525 return TableSkinUtils.getFocusedCell(this); 526 } 527 528 // returns an ObservableList of the visible leaf columns of the control 529 private ObservableList<? extends TC> getVisibleLeafColumns() { 530 return TableSkinUtils.getVisibleLeafColumns(this); 531 } 532 533 /** {@inheritDoc} */ 534 @Override protected void updateItemCount() { 535 updatePlaceholderRegionVisibility(); 536 537 int oldCount = itemCount; 538 int newCount = getItemCount(); 539 540 itemCount = newCount; 541 542 if (itemCount == 0) { 543 flow.getHbar().setValue(0.0); 544 } 545 546 // if this is not called even when the count is the same, we get a 547 // memory leak in VirtualFlow.sheet.children. This can probably be 548 // optimised in the future when time permits. 549 flow.setCellCount(newCount); 550 551 if (newCount != oldCount) { 552 // FIXME updateItemCount is called _a lot_. Perhaps we can make rebuildCells 553 // smarter. Imagine if items has one million items added - do we really 554 // need to rebuildCells a million times? Maybe this is better now that 555 // we do rebuildCells instead of recreateCells. 556 requestRebuildCells(); 557 } else { 558 needCellsReconfigured = true; 559 } 560 } 561 562 private void checkContentWidthState() { 563 // we test for item count here to resolve RT-14855, where the column 564 // widths weren't being resized properly when in constrained layout mode 565 // if there were no items. 566 if (contentWidthDirty || getItemCount() == 0) { 567 updateContentWidth(); 568 contentWidthDirty = false; 569 } 570 } 571 572 void horizontalScroll() { 573 tableHeaderRow.updateScrollX(); 574 } 575 576 void onFocusPreviousCell() { 577 TableFocusModel<M,?> fm = getFocusModel(); 578 if (fm == null) return; 579 580 flow.scrollTo(fm.getFocusedIndex()); 581 } 582 583 void onFocusNextCell() { 584 TableFocusModel<M,?> fm = getFocusModel(); 585 if (fm == null) return; 586 587 flow.scrollTo(fm.getFocusedIndex()); 588 } 589 590 void onSelectPreviousCell() { 591 SelectionModel<S> sm = getSelectionModel(); 592 if (sm == null) return; 593 594 flow.scrollTo(sm.getSelectedIndex()); 595 } 596 597 void onSelectNextCell() { 598 SelectionModel<S> sm = getSelectionModel(); 599 if (sm == null) return; 600 601 flow.scrollTo(sm.getSelectedIndex()); 602 } 603 604 void onSelectLeftCell() { 605 scrollHorizontally(); 606 } 607 608 void onSelectRightCell() { 609 scrollHorizontally(); 610 } 611 612 void onMoveToFirstCell() { 613 flow.scrollTo(0); 614 flow.setPosition(0); 615 } 616 617 void onMoveToLastCell() { 618 int endPos = getItemCount(); 619 flow.scrollTo(endPos); 620 flow.setPosition(1); 621 } 622 623 private void updateTableItems(ObservableList<S> oldList, ObservableList<S> newList) { 624 if (oldList != null) { 625 oldList.removeListener(weakRowCountListener); 626 } 627 628 if (newList != null) { 629 newList.addListener(weakRowCountListener); 630 } 631 632 markItemCountDirty(); 633 getSkinnable().requestLayout(); 634 } 635 636 Region getColumnReorderLine() { 637 return columnReorderLine; 638 } 639 640 /** 641 * Function used to scroll the container down by one 'page', although 642 * if this is a horizontal container, then the scrolling will be to the right. 643 */ 644 int onScrollPageDown(boolean isFocusDriven) { 645 TableSelectionModel<S> sm = getSelectionModel(); 646 if (sm == null) return -1; 647 648 final int itemCount = getItemCount(); 649 650 I lastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 651 if (lastVisibleCell == null) return -1; 652 653 int lastVisibleCellIndex = lastVisibleCell.getIndex(); 654 655 // we include this test here as the virtual flow will return cells that 656 // exceed past the item count, so we need to clamp here (and further down 657 // in this method also). See RT-19053 for more information. 658 lastVisibleCellIndex = lastVisibleCellIndex >= itemCount ? itemCount - 1 : lastVisibleCellIndex; 659 660 // isSelected represents focus OR selection 661 boolean isSelected; 662 if (isFocusDriven) { 663 isSelected = lastVisibleCell.isFocused() || isCellFocused(lastVisibleCellIndex); 664 } else { 665 isSelected = lastVisibleCell.isSelected() || isCellSelected(lastVisibleCellIndex); 666 } 667 668 if (isSelected) { 669 boolean isLeadIndex = isLeadIndex(isFocusDriven, lastVisibleCellIndex); 670 671 if (isLeadIndex) { 672 // if the last visible cell is selected, we want to shift that cell up 673 // to be the top-most cell, or at least as far to the top as we can go. 674 flow.scrollToTop(lastVisibleCell); 675 676 I newLastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 677 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell; 678 } 679 } 680 681 int newSelectionIndex = lastVisibleCell.getIndex(); 682 newSelectionIndex = newSelectionIndex >= itemCount ? itemCount - 1 : newSelectionIndex; 683 flow.scrollTo(newSelectionIndex); 684 return newSelectionIndex; 685 } 686 687 /** 688 * Function used to scroll the container up by one 'page', although 689 * if this is a horizontal container, then the scrolling will be to the left. 690 */ 691 int onScrollPageUp(boolean isFocusDriven) { 692 I firstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 693 if (firstVisibleCell == null) return -1; 694 695 int firstVisibleCellIndex = firstVisibleCell.getIndex(); 696 697 // isSelected represents focus OR selection 698 boolean isSelected = false; 699 if (isFocusDriven) { 700 isSelected = firstVisibleCell.isFocused() || isCellFocused(firstVisibleCellIndex); 701 } else { 702 isSelected = firstVisibleCell.isSelected() || isCellSelected(firstVisibleCellIndex); 703 } 704 705 if (isSelected) { 706 boolean isLeadIndex = isLeadIndex(isFocusDriven, firstVisibleCellIndex); 707 708 if (isLeadIndex) { 709 // if the first visible cell is selected, we want to shift that cell down 710 // to be the bottom-most cell, or at least as far to the bottom as we can go. 711 flow.scrollToBottom(firstVisibleCell); 712 713 I newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 714 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell; 715 } 716 } 717 718 int newSelectionIndex = firstVisibleCell.getIndex(); 719 flow.scrollTo(newSelectionIndex); 720 return newSelectionIndex; 721 } 722 723 private boolean isLeadIndex(boolean isFocusDriven, int index) { 724 final TableSelectionModel<S> sm = getSelectionModel(); 725 final FocusModel<M> fm = getFocusModel(); 726 727 return (isFocusDriven && fm.getFocusedIndex() == index) 728 || (! isFocusDriven && sm.getSelectedIndex() == index); 729 } 730 731 /** 732 * Keeps track of how many leaf columns are currently visible in this table. 733 */ 734 private void updateVisibleColumnCount() { 735 visibleColCount = getVisibleLeafColumns().size(); 736 737 updatePlaceholderRegionVisibility(); 738 requestRebuildCells(); 739 } 740 741 private void updateVisibleLeafColumnWidthListeners( 742 List<? extends TC> added, List<? extends TC> removed) { 743 744 for (int i = 0, max = removed.size(); i < max; i++) { 745 TC tc = removed.get(i); 746 tc.widthProperty().removeListener(weakWidthListener); 747 } 748 for (int i = 0, max = added.size(); i < max; i++) { 749 TC tc = added.get(i); 750 tc.widthProperty().addListener(weakWidthListener); 751 } 752 requestRebuildCells(); 753 } 754 755 final void updatePlaceholderRegionVisibility() { 756 boolean visible = visibleColCount == 0 || getItemCount() == 0; 757 758 if (visible) { 759 if (placeholderRegion == null) { 760 placeholderRegion = new StackPane(); 761 placeholderRegion.getStyleClass().setAll("placeholder"); 762 getChildren().add(placeholderRegion); 763 } 764 765 Node placeholderNode = TableSkinUtils.placeholderProperty(this).get(); 766 767 if (placeholderNode == null) { 768 if (placeholderLabel == null) { 769 placeholderLabel = new Label(); 770 } 771 String s = visibleColCount == 0 ? NO_COLUMNS_TEXT : EMPTY_TABLE_TEXT; 772 placeholderLabel.setText(s); 773 774 placeholderRegion.getChildren().setAll(placeholderLabel); 775 } else { 776 placeholderRegion.getChildren().setAll(placeholderNode); 777 } 778 } 779 780 flow.setVisible(! visible); 781 if (placeholderRegion != null) { 782 placeholderRegion.setVisible(visible); 783 } 784 } 785 786 /* 787 * It's often important to know how much width is available for content 788 * within the table, and this needs to exclude the width of any vertical 789 * scrollbar. 790 */ 791 private void updateContentWidth() { 792 double contentWidth = flow.getWidth(); 793 794 if (flow.getVbar().isVisible()) { 795 contentWidth -= flow.getVbar().getWidth(); 796 } 797 798 if (contentWidth <= 0) { 799 // Fix for RT-14855 when there is no content in the TableView. 800 Control c = getSkinnable(); 801 contentWidth = c.getWidth() - (snappedLeftInset() + snappedRightInset()); 802 } 803 804 contentWidth = Math.max(0.0, contentWidth); 805 806 // FIXME this isn't perfect, but it prevents RT-14885, which results in 807 // undesired horizontal scrollbars when in constrained resize mode 808 getSkinnable().getProperties().put("TableView.contentWidth", Math.floor(contentWidth)); 809 } 810 811 private void refreshView() { 812 markItemCountDirty(); 813 Control c = getSkinnable(); 814 if (c != null) { 815 c.requestLayout(); 816 } 817 } 818 819 // Handles the horizontal scrolling when the selection mode is cell-based 820 // and the newly selected cell belongs to a column which is not totally 821 // visible. 822 void scrollHorizontally() { 823 TableFocusModel<M,?> fm = getFocusModel(); 824 if (fm == null) return; 825 826 TC col = getFocusedCell().getTableColumn(); 827 scrollHorizontally(col); 828 } 829 830 void scrollHorizontally(TC col) { 831 if (col == null || !col.isVisible()) return; 832 833 final Control control = getSkinnable(); 834 835 // RT-37060 - if we are trying to scroll to a column that has not 836 // yet even been rendered, we must wait until the layout pass has 837 // happened and then do the scroll. The laziest way to do this is to 838 // queue up the task to run later, at which point we will have hopefully 839 // fully run the column through layout and css. 840 TableColumnHeader header = tableHeaderRow.getColumnHeaderFor(col); 841 if (header == null || header.getWidth() <= 0) { 842 Platform.runLater(() -> scrollHorizontally(col)); 843 return; 844 } 845 846 // work out where this column header is, and it's width (start -> end) 847 double start = 0; 848 for (TC c : getVisibleLeafColumns()) { 849 if (c.equals(col)) break; 850 start += c.getWidth(); 851 } 852 double end = start + col.getWidth(); 853 854 // determine the visible width of the table 855 double headerWidth = control.getWidth() - snappedLeftInset() - snappedRightInset(); 856 857 // determine by how much we need to translate the table to ensure that 858 // the start position of this column lines up with the left edge of the 859 // tableview, and also that the columns don't become detached from the 860 // right edge of the table 861 double pos = flow.getHbar().getValue(); 862 double max = flow.getHbar().getMax(); 863 double newPos; 864 865 if (start < pos && start >= 0) { 866 newPos = start; 867 } else { 868 double delta = start < 0 || end > headerWidth ? start - pos : 0; 869 newPos = pos + delta > max ? max : pos + delta; 870 } 871 872 // FIXME we should add API in VirtualFlow so we don't end up going 873 // direct to the hbar. 874 // actually shift the flow - this will result in the header moving 875 // as well 876 flow.getHbar().setValue(newPos); 877 } 878 879 private boolean isCellSelected(int row) { 880 TableSelectionModel<S> sm = getSelectionModel(); 881 if (sm == null) return false; 882 if (! sm.isCellSelectionEnabled()) return false; 883 884 int columnCount = getVisibleLeafColumns().size(); 885 for (int col = 0; col < columnCount; col++) { 886 if (sm.isSelected(row, TableSkinUtils.getVisibleLeafColumn(this,col))) { 887 return true; 888 } 889 } 890 891 return false; 892 } 893 894 private boolean isCellFocused(int row) { 895 TableFocusModel<S,TC> fm = (TableFocusModel<S,TC>)(Object)getFocusModel(); 896 if (fm == null) return false; 897 898 int columnCount = getVisibleLeafColumns().size(); 899 for (int col = 0; col < columnCount; col++) { 900 if (fm.isFocused(row, TableSkinUtils.getVisibleLeafColumn(this,col))) { 901 return true; 902 } 903 } 904 905 return false; 906 } 907 908 909 910 /*************************************************************************** 911 * * 912 * A11y * 913 * * 914 **************************************************************************/ 915 916 /** {@inheritDoc} */ 917 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 918 switch (attribute) { 919 case FOCUS_ITEM: { 920 TableFocusModel<M,?> fm = getFocusModel(); 921 int focusedIndex = fm.getFocusedIndex(); 922 if (focusedIndex == -1) { 923 if (placeholderRegion != null && placeholderRegion.isVisible()) { 924 return placeholderRegion.getChildren().get(0); 925 } 926 if (getItemCount() > 0) { 927 focusedIndex = 0; 928 } else { 929 return null; 930 } 931 } 932 return flow.getPrivateCell(focusedIndex); 933 } 934 case CELL_AT_ROW_COLUMN: { 935 int rowIndex = (Integer)parameters[0]; 936 return flow.getPrivateCell(rowIndex); 937 } 938 case COLUMN_AT_INDEX: { 939 int index = (Integer)parameters[0]; 940 TableColumnBase<S,?> column = TableSkinUtils.getVisibleLeafColumn(this,index); 941 return getTableHeaderRow().getColumnHeaderFor(column); 942 } 943 case HEADER: { 944 /* Not sure how this is used by Accessibility, but without this VoiceOver will not 945 * look for column headers */ 946 return getTableHeaderRow(); 947 } 948 case VERTICAL_SCROLLBAR: return flow.getVbar(); 949 case HORIZONTAL_SCROLLBAR: return flow.getHbar(); 950 default: return super.queryAccessibleAttribute(attribute, parameters); 951 } 952 } 953 954 }