1 /*
   2  * Copyright (c) 2010, 2014, 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 }