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