1 /* 2 * Copyright (c) 2010, 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 java.util.ArrayList; 29 import java.util.List; 30 31 import com.sun.javafx.scene.control.Properties; 32 import com.sun.javafx.scene.control.behavior.BehaviorBase; 33 import javafx.beans.InvalidationListener; 34 import javafx.beans.WeakInvalidationListener; 35 import javafx.collections.FXCollections; 36 import javafx.collections.ListChangeListener; 37 import javafx.collections.MapChangeListener; 38 import javafx.collections.ObservableList; 39 import javafx.collections.ObservableMap; 40 import javafx.collections.WeakListChangeListener; 41 import javafx.event.EventHandler; 42 import javafx.geometry.Orientation; 43 import javafx.scene.AccessibleAction; 44 import javafx.scene.AccessibleAttribute; 45 import javafx.scene.Node; 46 import javafx.scene.control.Button; 47 import javafx.scene.control.Control; 48 import javafx.scene.control.FocusModel; 49 import javafx.scene.control.IndexedCell; 50 import javafx.scene.control.Label; 51 import javafx.scene.control.ListCell; 52 import javafx.scene.control.ListView; 53 import javafx.scene.control.MultipleSelectionModel; 54 import javafx.scene.control.SelectionModel; 55 import com.sun.javafx.scene.control.behavior.ListViewBehavior; 56 import javafx.scene.input.MouseEvent; 57 import javafx.scene.layout.StackPane; 58 59 import java.security.AccessController; 60 import java.security.PrivilegedAction; 61 62 import com.sun.javafx.scene.control.skin.resources.ControlResources; 63 64 /** 65 * Default skin implementation for the {@link ListView} control. 66 * 67 * @see ListView 68 * @since 9 69 */ 70 public class ListViewSkin<T> extends VirtualContainerBase<ListView<T>, ListCell<T>> { 71 72 /*************************************************************************** 73 * * 74 * Static Fields * 75 * * 76 **************************************************************************/ 77 78 private static final String EMPTY_LIST_TEXT = ControlResources.getString("ListView.noContent"); 79 80 // RT-34744 : IS_PANNABLE will be false unless 81 // javafx.scene.control.skin.ListViewSkin.pannable 82 // is set to true. This is done in order to make ListView functional 83 // on embedded systems with touch screens which do not generate scroll 84 // events for touch drag gestures. 85 private static final boolean IS_PANNABLE = 86 AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.ListViewSkin.pannable")); 87 88 89 90 /*************************************************************************** 91 * * 92 * Internal Fields * 93 * * 94 **************************************************************************/ 95 96 private final VirtualFlow<ListCell<T>> flow; 97 98 /** 99 * Region placed over the top of the flow (and possibly the header row) if 100 * there is no data. 101 */ 102 // FIXME this should not be a StackPane 103 private StackPane placeholderRegion; 104 private Node placeholderNode; 105 106 private ObservableList<T> listViewItems; 107 private final InvalidationListener itemsChangeListener = observable -> updateListViewItems(); 108 109 private boolean needCellsRebuilt = true; 110 private boolean needCellsReconfigured = false; 111 112 private int itemCount = -1; 113 private ListViewBehavior<T> behavior; 114 115 116 117 /*************************************************************************** 118 * * 119 * Listeners * 120 * * 121 **************************************************************************/ 122 123 private MapChangeListener<Object, Object> propertiesMapListener = c -> { 124 if (! c.wasAdded()) return; 125 if (Properties.RECREATE.equals(c.getKey())) { 126 needCellsRebuilt = true; 127 getSkinnable().requestLayout(); 128 getSkinnable().getProperties().remove(Properties.RECREATE); 129 } 130 }; 131 132 private final ListChangeListener<T> listViewItemsListener = new ListChangeListener<T>() { 133 @Override public void onChanged(Change<? extends T> c) { 134 while (c.next()) { 135 if (c.wasReplaced()) { 136 // RT-28397: Support for when an item is replaced with itself (but 137 // updated internal values that should be shown visually). 138 // This code was updated for RT-36714 to not update all cells, 139 // just those affected by the change 140 for (int i = c.getFrom(); i < c.getTo(); i++) { 141 flow.setCellDirty(i); 142 } 143 144 break; 145 } else if (c.getRemovedSize() == itemCount) { 146 // RT-22463: If the user clears out an items list then we 147 // should reset all cells (in particular their contained 148 // items) such that a subsequent addition to the list of 149 // an item which equals the old item (but is rendered 150 // differently) still displays as expected (i.e. with the 151 // updated display, not the old display). 152 itemCount = 0; 153 break; 154 } 155 } 156 157 // fix for RT-37853 158 getSkinnable().edit(-1); 159 160 rowCountDirty = true; 161 getSkinnable().requestLayout(); 162 } 163 }; 164 165 private final WeakListChangeListener<T> weakListViewItemsListener = 166 new WeakListChangeListener<T>(listViewItemsListener); 167 168 169 170 /*************************************************************************** 171 * * 172 * Constructors * 173 * * 174 **************************************************************************/ 175 176 /** 177 * Creates a new ListViewSkin instance, installing the necessary child 178 * nodes into the Control {@link Control#getChildren() children} list, as 179 * well as the necessary input mappings for handling key, mouse, etc events. 180 * 181 * @param control The control that this skin should be installed onto. 182 */ 183 public ListViewSkin(final ListView<T> control) { 184 super(control); 185 186 // install default input map for the ListView control 187 behavior = new ListViewBehavior<>(control); 188 // control.setInputMap(behavior.getInputMap()); 189 190 // init the behavior 'closures' 191 behavior.setOnFocusPreviousRow(() -> onFocusPreviousCell()); 192 behavior.setOnFocusNextRow(() -> onFocusNextCell()); 193 behavior.setOnMoveToFirstCell(() -> onMoveToFirstCell()); 194 behavior.setOnMoveToLastCell(() -> onMoveToLastCell()); 195 behavior.setOnSelectPreviousRow(() -> onSelectPreviousCell()); 196 behavior.setOnSelectNextRow(() -> onSelectNextCell()); 197 behavior.setOnScrollPageDown(this::onScrollPageDown); 198 behavior.setOnScrollPageUp(this::onScrollPageUp); 199 200 updateListViewItems(); 201 202 // init the VirtualFlow 203 flow = getVirtualFlow(); 204 flow.setId("virtual-flow"); 205 flow.setPannable(IS_PANNABLE); 206 flow.setVertical(control.getOrientation() == Orientation.VERTICAL); 207 flow.setCellFactory(flow -> createCell()); 208 flow.setFixedCellSize(control.getFixedCellSize()); 209 getChildren().add(flow); 210 211 EventHandler<MouseEvent> ml = event -> { 212 // RT-15127: cancel editing on scroll. This is a bit extreme 213 // (we are cancelling editing on touching the scrollbars). 214 // This can be improved at a later date. 215 if (control.getEditingIndex() > -1) { 216 control.edit(-1); 217 } 218 219 // This ensures that the list maintains the focus, even when the vbar 220 // and hbar controls inside the flow are clicked. Without this, the 221 // focus border will not be shown when the user interacts with the 222 // scrollbars, and more importantly, keyboard navigation won't be 223 // available to the user. 224 if (control.isFocusTraversable()) { 225 control.requestFocus(); 226 } 227 }; 228 flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml); 229 flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml); 230 231 updateRowCount(); 232 233 control.itemsProperty().addListener(new WeakInvalidationListener(itemsChangeListener)); 234 235 final ObservableMap<Object, Object> properties = control.getProperties(); 236 properties.remove(Properties.RECREATE); 237 properties.addListener(propertiesMapListener); 238 239 // Register listeners 240 registerChangeListener(control.itemsProperty(), o -> updateListViewItems()); 241 registerChangeListener(control.orientationProperty(), o -> 242 flow.setVertical(control.getOrientation() == Orientation.VERTICAL) 243 ); 244 registerChangeListener(control.cellFactoryProperty(), o -> flow.recreateCells()); 245 registerChangeListener(control.parentProperty(), o -> { 246 if (control.getParent() != null && control.isVisible()) { 247 control.requestLayout(); 248 } 249 }); 250 registerChangeListener(control.placeholderProperty(), o -> updatePlaceholderRegionVisibility()); 251 registerChangeListener(control.fixedCellSizeProperty(), o -> 252 flow.setFixedCellSize(control.getFixedCellSize()) 253 ); 254 } 255 256 257 258 /*************************************************************************** 259 * * 260 * Public API * 261 * * 262 **************************************************************************/ 263 264 /** {@inheritDoc} */ 265 @Override public void dispose() { 266 super.dispose(); 267 268 if (behavior != null) { 269 behavior.dispose(); 270 } 271 } 272 273 /** {@inheritDoc} */ 274 @Override protected void layoutChildren(final double x, final double y, 275 final double w, final double h) { 276 super.layoutChildren(x, y, w, h); 277 278 if (needCellsRebuilt) { 279 flow.rebuildCells(); 280 } else if (needCellsReconfigured) { 281 flow.reconfigureCells(); 282 } 283 284 needCellsRebuilt = false; 285 needCellsReconfigured = false; 286 287 if (getItemCount() == 0) { 288 // show message overlay instead of empty listview 289 if (placeholderRegion != null) { 290 placeholderRegion.setVisible(w > 0 && h > 0); 291 placeholderRegion.resizeRelocate(x, y, w, h); 292 } 293 } else { 294 flow.resizeRelocate(x, y, w, h); 295 } 296 } 297 298 /** {@inheritDoc} */ 299 @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { 300 checkState(); 301 302 if (getItemCount() == 0) { 303 if (placeholderRegion == null) { 304 updatePlaceholderRegionVisibility(); 305 } 306 if (placeholderRegion != null) { 307 return placeholderRegion.prefWidth(height) + leftInset + rightInset; 308 } 309 } 310 311 return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987; 312 } 313 314 /** {@inheritDoc} */ 315 @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { 316 return 400; 317 } 318 319 /** {@inheritDoc} */ 320 @Override int getItemCount() { 321 return itemCount; 322 } 323 324 /** {@inheritDoc} */ 325 @Override void updateRowCount() { 326 if (flow == null) return; 327 328 int oldCount = itemCount; 329 int newCount = listViewItems == null ? 0 : listViewItems.size(); 330 331 itemCount = newCount; 332 333 flow.setCellCount(newCount); 334 335 updatePlaceholderRegionVisibility(); 336 if (newCount != oldCount) { 337 needCellsRebuilt = true; 338 } else { 339 needCellsReconfigured = true; 340 } 341 } 342 343 /** {@inheritDoc} */ 344 @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { 345 switch (attribute) { 346 case FOCUS_ITEM: { 347 FocusModel<?> fm = getSkinnable().getFocusModel(); 348 int focusedIndex = fm.getFocusedIndex(); 349 if (focusedIndex == -1) { 350 if (placeholderRegion != null && placeholderRegion.isVisible()) { 351 return placeholderRegion.getChildren().get(0); 352 } 353 if (getItemCount() > 0) { 354 focusedIndex = 0; 355 } else { 356 return null; 357 } 358 } 359 return flow.getPrivateCell(focusedIndex); 360 } 361 case ITEM_COUNT: return getItemCount(); 362 case ITEM_AT_INDEX: { 363 Integer rowIndex = (Integer)parameters[0]; 364 if (rowIndex == null) return null; 365 if (0 <= rowIndex && rowIndex < getItemCount()) { 366 return flow.getPrivateCell(rowIndex); 367 } 368 return null; 369 } 370 case SELECTED_ITEMS: { 371 MultipleSelectionModel<T> sm = getSkinnable().getSelectionModel(); 372 ObservableList<Integer> indices = sm.getSelectedIndices(); 373 List<Node> selection = new ArrayList<>(indices.size()); 374 for (int i : indices) { 375 ListCell<T> row = flow.getPrivateCell(i); 376 if (row != null) selection.add(row); 377 } 378 return FXCollections.observableArrayList(selection); 379 } 380 case VERTICAL_SCROLLBAR: return flow.getVbar(); 381 case HORIZONTAL_SCROLLBAR: return flow.getHbar(); 382 default: return super.queryAccessibleAttribute(attribute, parameters); 383 } 384 } 385 386 /** {@inheritDoc} */ 387 @Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) { 388 switch (action) { 389 case SHOW_ITEM: { 390 Node item = (Node)parameters[0]; 391 if (item instanceof ListCell) { 392 @SuppressWarnings("unchecked") 393 ListCell<T> cell = (ListCell<T>)item; 394 flow.scrollTo(cell.getIndex()); 395 } 396 break; 397 } 398 case SET_SELECTED_ITEMS: { 399 @SuppressWarnings("unchecked") 400 ObservableList<Node> items = (ObservableList<Node>)parameters[0]; 401 if (items != null) { 402 MultipleSelectionModel<T> sm = getSkinnable().getSelectionModel(); 403 if (sm != null) { 404 sm.clearSelection(); 405 for (Node item : items) { 406 if (item instanceof ListCell) { 407 @SuppressWarnings("unchecked") 408 ListCell<T> cell = (ListCell<T>)item; 409 sm.select(cell.getIndex()); 410 } 411 } 412 } 413 } 414 break; 415 } 416 default: super.executeAccessibleAction(action, parameters); 417 } 418 } 419 420 421 422 /*************************************************************************** 423 * * 424 * Private implementation * 425 * * 426 **************************************************************************/ 427 428 /** {@inheritDoc} */ 429 private ListCell<T> createCell() { 430 ListCell<T> cell; 431 if (getSkinnable().getCellFactory() != null) { 432 cell = getSkinnable().getCellFactory().call(getSkinnable()); 433 } else { 434 cell = createDefaultCellImpl(); 435 } 436 437 cell.updateListView(getSkinnable()); 438 439 return cell; 440 } 441 442 private void updateListViewItems() { 443 if (listViewItems != null) { 444 listViewItems.removeListener(weakListViewItemsListener); 445 } 446 447 this.listViewItems = getSkinnable().getItems(); 448 449 if (listViewItems != null) { 450 listViewItems.addListener(weakListViewItemsListener); 451 } 452 453 rowCountDirty = true; 454 getSkinnable().requestLayout(); 455 } 456 457 private final void updatePlaceholderRegionVisibility() { 458 boolean visible = getItemCount() == 0; 459 460 if (visible) { 461 placeholderNode = getSkinnable().getPlaceholder(); 462 if (placeholderNode == null && (EMPTY_LIST_TEXT != null && ! EMPTY_LIST_TEXT.isEmpty())) { 463 placeholderNode = new Label(); 464 ((Label)placeholderNode).setText(EMPTY_LIST_TEXT); 465 } 466 467 if (placeholderNode != null) { 468 if (placeholderRegion == null) { 469 placeholderRegion = new StackPane(); 470 placeholderRegion.getStyleClass().setAll("placeholder"); 471 getChildren().add(placeholderRegion); 472 } 473 474 placeholderRegion.getChildren().setAll(placeholderNode); 475 } 476 } 477 478 flow.setVisible(!visible); 479 if (placeholderRegion != null) { 480 placeholderRegion.setVisible(visible); 481 } 482 } 483 484 private static <T> ListCell<T> createDefaultCellImpl() { 485 return new ListCell<T>() { 486 @Override public void updateItem(T item, boolean empty) { 487 super.updateItem(item, empty); 488 489 if (empty) { 490 setText(null); 491 setGraphic(null); 492 } else if (item instanceof Node) { 493 setText(null); 494 Node currentNode = getGraphic(); 495 Node newNode = (Node) item; 496 if (currentNode == null || ! currentNode.equals(newNode)) { 497 setGraphic(newNode); 498 } 499 } else { 500 /** 501 * This label is used if the item associated with this cell is to be 502 * represented as a String. While we will lazily instantiate it 503 * we never clear it, being more afraid of object churn than a minor 504 * "leak" (which will not become a "major" leak). 505 */ 506 setText(item == null ? "null" : item.toString()); 507 setGraphic(null); 508 } 509 } 510 }; 511 } 512 513 private void onFocusPreviousCell() { 514 FocusModel<T> fm = getSkinnable().getFocusModel(); 515 if (fm == null) return; 516 flow.scrollTo(fm.getFocusedIndex()); 517 } 518 519 private void onFocusNextCell() { 520 FocusModel<T> fm = getSkinnable().getFocusModel(); 521 if (fm == null) return; 522 flow.scrollTo(fm.getFocusedIndex()); 523 } 524 525 private void onSelectPreviousCell() { 526 SelectionModel<T> sm = getSkinnable().getSelectionModel(); 527 if (sm == null) return; 528 529 int pos = sm.getSelectedIndex(); 530 flow.scrollTo(pos); 531 532 // Fix for RT-11299 533 IndexedCell<T> cell = flow.getFirstVisibleCell(); 534 if (cell == null || pos < cell.getIndex()) { 535 flow.setPosition(pos / (double) getItemCount()); 536 } 537 } 538 539 private void onSelectNextCell() { 540 SelectionModel<T> sm = getSkinnable().getSelectionModel(); 541 if (sm == null) return; 542 543 int pos = sm.getSelectedIndex(); 544 flow.scrollTo(pos); 545 546 // Fix for RT-11299 547 ListCell<T> cell = flow.getLastVisibleCell(); 548 if (cell == null || cell.getIndex() < pos) { 549 flow.setPosition(pos / (double) getItemCount()); 550 } 551 } 552 553 private void onMoveToFirstCell() { 554 flow.scrollTo(0); 555 flow.setPosition(0); 556 } 557 558 private void onMoveToLastCell() { 559 // SelectionModel sm = getSkinnable().getSelectionModel(); 560 // if (sm == null) return; 561 // 562 int endPos = getItemCount() - 1; 563 // sm.select(endPos); 564 flow.scrollTo(endPos); 565 flow.setPosition(1); 566 } 567 568 /** 569 * Function used to scroll the container down by one 'page', although 570 * if this is a horizontal container, then the scrolling will be to the right. 571 */ 572 private int onScrollPageDown(boolean isFocusDriven) { 573 ListCell<T> lastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 574 if (lastVisibleCell == null) return -1; 575 576 final SelectionModel<T> sm = getSkinnable().getSelectionModel(); 577 final FocusModel<T> fm = getSkinnable().getFocusModel(); 578 if (sm == null || fm == null) return -1; 579 580 int lastVisibleCellIndex = lastVisibleCell.getIndex(); 581 582 // boolean isSelected = sm.isSelected(lastVisibleCellIndex) || fm.isFocused(lastVisibleCellIndex) || lastVisibleCellIndex == anchor; 583 // isSelected represents focus OR selection 584 boolean isSelected = false; 585 if (isFocusDriven) { 586 isSelected = lastVisibleCell.isFocused() || fm.isFocused(lastVisibleCellIndex); 587 } else { 588 isSelected = lastVisibleCell.isSelected() || sm.isSelected(lastVisibleCellIndex); 589 } 590 591 if (isSelected) { 592 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == lastVisibleCellIndex) 593 || (! isFocusDriven && sm.getSelectedIndex() == lastVisibleCellIndex); 594 595 if (isLeadIndex) { 596 // if the last visible cell is selected, we want to shift that cell up 597 // to be the top-most cell, or at least as far to the top as we can go. 598 flow.scrollToTop(lastVisibleCell); 599 600 ListCell<T> newLastVisibleCell = flow.getLastVisibleCellWithinViewPort(); 601 lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell; 602 } 603 } else { 604 // if the selection is not on the 'bottom' most cell, we firstly move 605 // the selection down to that, without scrolling the contents, so 606 // this is a no-op 607 } 608 609 int newSelectionIndex = lastVisibleCell.getIndex(); 610 flow.scrollTo(lastVisibleCell); 611 return newSelectionIndex; 612 } 613 614 /** 615 * Function used to scroll the container up by one 'page', although 616 * if this is a horizontal container, then the scrolling will be to the left. 617 */ 618 private int onScrollPageUp(boolean isFocusDriven) { 619 ListCell<T> firstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 620 if (firstVisibleCell == null) return -1; 621 622 final SelectionModel<T> sm = getSkinnable().getSelectionModel(); 623 final FocusModel<T> fm = getSkinnable().getFocusModel(); 624 if (sm == null || fm == null) return -1; 625 626 int firstVisibleCellIndex = firstVisibleCell.getIndex(); 627 628 // isSelected represents focus OR selection 629 boolean isSelected = false; 630 if (isFocusDriven) { 631 isSelected = firstVisibleCell.isFocused() || fm.isFocused(firstVisibleCellIndex); 632 } else { 633 isSelected = firstVisibleCell.isSelected() || sm.isSelected(firstVisibleCellIndex); 634 } 635 636 if (isSelected) { 637 boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == firstVisibleCellIndex) 638 || (! isFocusDriven && sm.getSelectedIndex() == firstVisibleCellIndex); 639 640 if (isLeadIndex) { 641 // if the first visible cell is selected, we want to shift that cell down 642 // to be the bottom-most cell, or at least as far to the bottom as we can go. 643 flow.scrollToBottom(firstVisibleCell); 644 645 ListCell<T> newFirstVisibleCell = flow.getFirstVisibleCellWithinViewPort(); 646 firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell; 647 } 648 } else { 649 // if the selection is not on the 'top' most cell, we firstly move 650 // the selection up to that, without scrolling the contents, so 651 // this is a no-op 652 } 653 654 int newSelectionIndex = firstVisibleCell.getIndex(); 655 flow.scrollTo(firstVisibleCell); 656 return newSelectionIndex; 657 } 658 }