/*
* Copyright (c) 2010, 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.control.skin;
import java.util.ArrayList;
import java.util.List;
import com.sun.javafx.scene.control.Properties;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.collections.WeakListChangeListener;
import javafx.collections.WeakMapChangeListener;
import javafx.event.EventHandler;
import javafx.geometry.Orientation;
import javafx.scene.AccessibleAction;
import javafx.scene.AccessibleAttribute;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.FocusModel;
import javafx.scene.control.IndexedCell;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionModel;
import com.sun.javafx.scene.control.behavior.ListViewBehavior;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import java.security.AccessController;
import java.security.PrivilegedAction;
import com.sun.javafx.scene.control.skin.resources.ControlResources;
Default skin implementation for the ListView
control. See Also: Since: 9
/**
* Default skin implementation for the {@link ListView} control.
*
* @see ListView
* @since 9
*/
public class ListViewSkin<T> extends VirtualContainerBase<ListView<T>, ListCell<T>> {
*
Static Fields *
*
/***************************************************************************
* *
* Static Fields *
* *
**************************************************************************/
// RT-34744 : IS_PANNABLE will be false unless
// javafx.scene.control.skin.ListViewSkin.pannable
// is set to true. This is done in order to make ListView functional
// on embedded systems with touch screens which do not generate scroll
// events for touch drag gestures.
private static final boolean IS_PANNABLE =
AccessController.doPrivileged((PrivilegedAction<Boolean>) () -> Boolean.getBoolean("javafx.scene.control.skin.ListViewSkin.pannable"));
*
Internal Fields *
*
/***************************************************************************
* *
* Internal Fields *
* *
**************************************************************************/
// JDK-8090129: This constant should not be static, because the
// Locale may change between instances.
private static final String EMPTY_LIST_TEXT = ControlResources.getString("ListView.noContent");
private final VirtualFlow<ListCell<T>> flow;
Region placed over the top of the flow (and possibly the header row) if
there is no data.
/**
* Region placed over the top of the flow (and possibly the header row) if
* there is no data.
*/
// FIXME this should not be a StackPane
private StackPane placeholderRegion;
private Node placeholderNode;
private ObservableList<T> listViewItems;
private boolean needCellsRebuilt = true;
private boolean needCellsReconfigured = false;
private int itemCount = -1;
private ListViewBehavior<T> behavior;
*
Listeners *
*
/***************************************************************************
* *
* Listeners *
* *
**************************************************************************/
private MapChangeListener<Object, Object> propertiesMapListener = c -> {
if (! c.wasAdded()) return;
if (Properties.RECREATE.equals(c.getKey())) {
needCellsRebuilt = true;
getSkinnable().requestLayout();
getSkinnable().getProperties().remove(Properties.RECREATE);
}
};
private WeakMapChangeListener<Object, Object> weakPropertiesMapListener =
new WeakMapChangeListener<>(propertiesMapListener);
private final ListChangeListener<T> listViewItemsListener = new ListChangeListener<T>() {
@Override public void onChanged(Change<? extends T> c) {
while (c.next()) {
if (c.wasReplaced()) {
// RT-28397: Support for when an item is replaced with itself (but
// updated internal values that should be shown visually).
// This code was updated for RT-36714 to not update all cells,
// just those affected by the change
for (int i = c.getFrom(); i < c.getTo(); i++) {
flow.setCellDirty(i);
}
break;
} else if (c.getRemovedSize() == itemCount) {
// RT-22463: If the user clears out an items list then we
// should reset all cells (in particular their contained
// items) such that a subsequent addition to the list of
// an item which equals the old item (but is rendered
// differently) still displays as expected (i.e. with the
// updated display, not the old display).
itemCount = 0;
break;
}
}
// fix for RT-37853
getSkinnable().edit(-1);
markItemCountDirty();
getSkinnable().requestLayout();
}
};
private final WeakListChangeListener<T> weakListViewItemsListener =
new WeakListChangeListener<T>(listViewItemsListener);
private final InvalidationListener itemsChangeListener = observable -> updateListViewItems();
private WeakInvalidationListener
weakItemsChangeListener = new WeakInvalidationListener(itemsChangeListener);
private EventHandler<MouseEvent> ml;
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
Creates a new ListViewSkin instance, installing the necessary child nodes into the Control children
list, as well as the necessary input mappings for handling key, mouse, etc events. Params: - control – The control that this skin should be installed onto.
/**
* Creates a new ListViewSkin instance, installing the necessary child
* nodes into the Control {@link Control#getChildren() children} list, as
* well as the necessary input mappings for handling key, mouse, etc events.
*
* @param control The control that this skin should be installed onto.
*/
public ListViewSkin(final ListView<T> control) {
super(control);
// install default input map for the ListView control
behavior = new ListViewBehavior<>(control);
// control.setInputMap(behavior.getInputMap());
// init the behavior 'closures'
behavior.setOnFocusPreviousRow(() -> onFocusPreviousCell());
behavior.setOnFocusNextRow(() -> onFocusNextCell());
behavior.setOnMoveToFirstCell(() -> onMoveToFirstCell());
behavior.setOnMoveToLastCell(() -> onMoveToLastCell());
behavior.setOnSelectPreviousRow(() -> onSelectPreviousCell());
behavior.setOnSelectNextRow(() -> onSelectNextCell());
behavior.setOnScrollPageDown(this::onScrollPageDown);
behavior.setOnScrollPageUp(this::onScrollPageUp);
updateListViewItems();
// init the VirtualFlow
flow = getVirtualFlow();
flow.setId("virtual-flow");
flow.setPannable(IS_PANNABLE);
flow.setVertical(control.getOrientation() == Orientation.VERTICAL);
flow.setCellFactory(flow -> createCell());
flow.setFixedCellSize(control.getFixedCellSize());
getChildren().add(flow);
ml = event -> {
// RT-15127: cancel editing on scroll. This is a bit extreme
// (we are cancelling editing on touching the scrollbars).
// This can be improved at a later date.
if (control.getEditingIndex() > -1) {
control.edit(-1);
}
// This ensures that the list maintains the focus, even when the vbar
// and hbar controls inside the flow are clicked. Without this, the
// focus border will not be shown when the user interacts with the
// scrollbars, and more importantly, keyboard navigation won't be
// available to the user.
if (control.isFocusTraversable()) {
control.requestFocus();
}
};
flow.getVbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
flow.getHbar().addEventFilter(MouseEvent.MOUSE_PRESSED, ml);
updateItemCount();
control.itemsProperty().addListener(weakItemsChangeListener);
final ObservableMap<Object, Object> properties = control.getProperties();
properties.remove(Properties.RECREATE);
properties.addListener(weakPropertiesMapListener);
// Register listeners
registerChangeListener(control.itemsProperty(), o -> updateListViewItems());
registerChangeListener(control.orientationProperty(), o ->
flow.setVertical(control.getOrientation() == Orientation.VERTICAL)
);
registerChangeListener(control.cellFactoryProperty(), o -> flow.recreateCells());
registerChangeListener(control.parentProperty(), o -> {
if (control.getParent() != null && control.isVisible()) {
control.requestLayout();
}
});
registerChangeListener(control.placeholderProperty(), o -> updatePlaceholderRegionVisibility());
registerChangeListener(control.fixedCellSizeProperty(), o ->
flow.setFixedCellSize(control.getFixedCellSize())
);
}
/***************************************************************************
* *
* Public API *
* *
**************************************************************************/
{@inheritDoc} /** {@inheritDoc} */
@Override public void dispose() {
if (getSkinnable() == null) return;
// listener cleanup fixes side-effects (NPE on refresh, setItems, modifyItems)
getSkinnable().getProperties().removeListener(weakPropertiesMapListener);
getSkinnable().itemsProperty().removeListener(weakItemsChangeListener);
if (listViewItems != null) {
listViewItems.removeListener(weakListViewItemsListener);
listViewItems = null;
}
// flow related cleanup
// leaking without nulling factory
flow.setCellFactory(null);
// for completeness - but no effect with/out?
flow.getVbar().removeEventFilter(MouseEvent.MOUSE_PRESSED, ml);
flow.getHbar().removeEventFilter(MouseEvent.MOUSE_PRESSED, ml);
super.dispose();
if (behavior != null) {
behavior.dispose();
}
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected void layoutChildren(final double x, final double y,
final double w, final double h) {
super.layoutChildren(x, y, w, h);
if (needCellsRebuilt) {
flow.rebuildCells();
} else if (needCellsReconfigured) {
flow.reconfigureCells();
}
needCellsRebuilt = false;
needCellsReconfigured = false;
if (getItemCount() == 0) {
// show message overlay instead of empty listview
if (placeholderRegion != null) {
placeholderRegion.setVisible(w > 0 && h > 0);
placeholderRegion.resizeRelocate(x, y, w, h);
}
} else {
flow.resizeRelocate(x, y, w, h);
}
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
checkState();
if (getItemCount() == 0) {
if (placeholderRegion == null) {
updatePlaceholderRegionVisibility();
}
if (placeholderRegion != null) {
return placeholderRegion.prefWidth(height) + leftInset + rightInset;
}
}
return computePrefHeight(-1, topInset, rightInset, bottomInset, leftInset) * 0.618033987;
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
return 400;
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected int getItemCount() {
return itemCount;
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected void updateItemCount() {
if (flow == null) return;
int oldCount = itemCount;
int newCount = listViewItems == null ? 0 : listViewItems.size();
itemCount = newCount;
flow.setCellCount(newCount);
updatePlaceholderRegionVisibility();
if (newCount != oldCount) {
requestRebuildCells();
} else {
needCellsReconfigured = true;
}
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
switch (attribute) {
case FOCUS_ITEM: {
FocusModel<?> fm = getSkinnable().getFocusModel();
int focusedIndex = fm.getFocusedIndex();
if (focusedIndex == -1) {
if (placeholderRegion != null && placeholderRegion.isVisible()) {
return placeholderRegion.getChildren().get(0);
}
if (getItemCount() > 0) {
focusedIndex = 0;
} else {
return null;
}
}
return flow.getPrivateCell(focusedIndex);
}
case ITEM_COUNT: return getItemCount();
case ITEM_AT_INDEX: {
Integer rowIndex = (Integer)parameters[0];
if (rowIndex == null) return null;
if (0 <= rowIndex && rowIndex < getItemCount()) {
return flow.getPrivateCell(rowIndex);
}
return null;
}
case SELECTED_ITEMS: {
MultipleSelectionModel<T> sm = getSkinnable().getSelectionModel();
ObservableList<Integer> indices = sm.getSelectedIndices();
List<Node> selection = new ArrayList<>(indices.size());
for (int i : indices) {
ListCell<T> row = flow.getPrivateCell(i);
if (row != null) selection.add(row);
}
return FXCollections.observableArrayList(selection);
}
case VERTICAL_SCROLLBAR: return flow.getVbar();
case HORIZONTAL_SCROLLBAR: return flow.getHbar();
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
{@inheritDoc} /** {@inheritDoc} */
@Override protected void executeAccessibleAction(AccessibleAction action, Object... parameters) {
switch (action) {
case SHOW_ITEM: {
Node item = (Node)parameters[0];
if (item instanceof ListCell) {
@SuppressWarnings("unchecked")
ListCell<T> cell = (ListCell<T>)item;
flow.scrollTo(cell.getIndex());
}
break;
}
case SET_SELECTED_ITEMS: {
@SuppressWarnings("unchecked")
ObservableList<Node> items = (ObservableList<Node>)parameters[0];
if (items != null) {
MultipleSelectionModel<T> sm = getSkinnable().getSelectionModel();
if (sm != null) {
sm.clearSelection();
for (Node item : items) {
if (item instanceof ListCell) {
@SuppressWarnings("unchecked")
ListCell<T> cell = (ListCell<T>)item;
sm.select(cell.getIndex());
}
}
}
}
break;
}
default: super.executeAccessibleAction(action, parameters);
}
}
/***************************************************************************
* *
* Private implementation *
* *
**************************************************************************/
{@inheritDoc} /** {@inheritDoc} */
private ListCell<T> createCell() {
ListCell<T> cell;
if (getSkinnable().getCellFactory() != null) {
cell = getSkinnable().getCellFactory().call(getSkinnable());
} else {
cell = createDefaultCellImpl();
}
cell.updateListView(getSkinnable());
return cell;
}
private void updateListViewItems() {
if (listViewItems != null) {
listViewItems.removeListener(weakListViewItemsListener);
}
this.listViewItems = getSkinnable().getItems();
if (listViewItems != null) {
listViewItems.addListener(weakListViewItemsListener);
}
markItemCountDirty();
getSkinnable().requestLayout();
}
private final void updatePlaceholderRegionVisibility() {
boolean visible = getItemCount() == 0;
if (visible) {
placeholderNode = getSkinnable().getPlaceholder();
if (placeholderNode == null && (EMPTY_LIST_TEXT != null && ! EMPTY_LIST_TEXT.isEmpty())) {
placeholderNode = new Label();
((Label)placeholderNode).setText(EMPTY_LIST_TEXT);
}
if (placeholderNode != null) {
if (placeholderRegion == null) {
placeholderRegion = new StackPane();
placeholderRegion.getStyleClass().setAll("placeholder");
getChildren().add(placeholderRegion);
}
placeholderRegion.getChildren().setAll(placeholderNode);
}
}
flow.setVisible(!visible);
if (placeholderRegion != null) {
placeholderRegion.setVisible(visible);
}
}
private static <T> ListCell<T> createDefaultCellImpl() {
return new ListCell<T>() {
@Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (item instanceof Node) {
setText(null);
Node currentNode = getGraphic();
Node newNode = (Node) item;
if (currentNode == null || ! currentNode.equals(newNode)) {
setGraphic(newNode);
}
} else {
/**
* This label is used if the item associated with this cell is to be
* represented as a String. While we will lazily instantiate it
* we never clear it, being more afraid of object churn than a minor
* "leak" (which will not become a "major" leak).
*/
setText(item == null ? "null" : item.toString());
setGraphic(null);
}
}
};
}
private void onFocusPreviousCell() {
FocusModel<T> fm = getSkinnable().getFocusModel();
if (fm == null) return;
flow.scrollTo(fm.getFocusedIndex());
}
private void onFocusNextCell() {
FocusModel<T> fm = getSkinnable().getFocusModel();
if (fm == null) return;
flow.scrollTo(fm.getFocusedIndex());
}
private void onSelectPreviousCell() {
SelectionModel<T> sm = getSkinnable().getSelectionModel();
if (sm == null) return;
int pos = sm.getSelectedIndex();
flow.scrollTo(pos);
// Fix for RT-11299
IndexedCell<T> cell = flow.getFirstVisibleCell();
if (cell == null || pos < cell.getIndex()) {
flow.setPosition(pos / (double) getItemCount());
}
}
private void onSelectNextCell() {
SelectionModel<T> sm = getSkinnable().getSelectionModel();
if (sm == null) return;
int pos = sm.getSelectedIndex();
flow.scrollTo(pos);
// Fix for RT-11299
ListCell<T> cell = flow.getLastVisibleCell();
if (cell == null || cell.getIndex() < pos) {
flow.setPosition(pos / (double) getItemCount());
}
}
private void onMoveToFirstCell() {
flow.scrollTo(0);
flow.setPosition(0);
}
private void onMoveToLastCell() {
// SelectionModel sm = getSkinnable().getSelectionModel();
// if (sm == null) return;
//
int endPos = getItemCount() - 1;
// sm.select(endPos);
flow.scrollTo(endPos);
flow.setPosition(1);
}
Function used to scroll the container down by one 'page', although
if this is a horizontal container, then the scrolling will be to the right.
/**
* Function used to scroll the container down by one 'page', although
* if this is a horizontal container, then the scrolling will be to the right.
*/
private int onScrollPageDown(boolean isFocusDriven) {
ListCell<T> lastVisibleCell = flow.getLastVisibleCellWithinViewport();
if (lastVisibleCell == null) return -1;
final SelectionModel<T> sm = getSkinnable().getSelectionModel();
final FocusModel<T> fm = getSkinnable().getFocusModel();
if (sm == null || fm == null) return -1;
int lastVisibleCellIndex = lastVisibleCell.getIndex();
// boolean isSelected = sm.isSelected(lastVisibleCellIndex) || fm.isFocused(lastVisibleCellIndex) || lastVisibleCellIndex == anchor;
// isSelected represents focus OR selection
boolean isSelected = false;
if (isFocusDriven) {
isSelected = lastVisibleCell.isFocused() || fm.isFocused(lastVisibleCellIndex);
} else {
isSelected = lastVisibleCell.isSelected() || sm.isSelected(lastVisibleCellIndex);
}
if (isSelected) {
boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == lastVisibleCellIndex)
|| (! isFocusDriven && sm.getSelectedIndex() == lastVisibleCellIndex);
if (isLeadIndex) {
// if the last visible cell is selected, we want to shift that cell up
// to be the top-most cell, or at least as far to the top as we can go.
flow.scrollToTop(lastVisibleCell);
ListCell<T> newLastVisibleCell = flow.getLastVisibleCellWithinViewport();
lastVisibleCell = newLastVisibleCell == null ? lastVisibleCell : newLastVisibleCell;
}
} else {
// if the selection is not on the 'bottom' most cell, we firstly move
// the selection down to that, without scrolling the contents, so
// this is a no-op
}
int newSelectionIndex = lastVisibleCell.getIndex();
flow.scrollTo(lastVisibleCell);
return newSelectionIndex;
}
Function used to scroll the container up by one 'page', although
if this is a horizontal container, then the scrolling will be to the left.
/**
* Function used to scroll the container up by one 'page', although
* if this is a horizontal container, then the scrolling will be to the left.
*/
private int onScrollPageUp(boolean isFocusDriven) {
ListCell<T> firstVisibleCell = flow.getFirstVisibleCellWithinViewport();
if (firstVisibleCell == null) return -1;
final SelectionModel<T> sm = getSkinnable().getSelectionModel();
final FocusModel<T> fm = getSkinnable().getFocusModel();
if (sm == null || fm == null) return -1;
int firstVisibleCellIndex = firstVisibleCell.getIndex();
// isSelected represents focus OR selection
boolean isSelected = false;
if (isFocusDriven) {
isSelected = firstVisibleCell.isFocused() || fm.isFocused(firstVisibleCellIndex);
} else {
isSelected = firstVisibleCell.isSelected() || sm.isSelected(firstVisibleCellIndex);
}
if (isSelected) {
boolean isLeadIndex = (isFocusDriven && fm.getFocusedIndex() == firstVisibleCellIndex)
|| (! isFocusDriven && sm.getSelectedIndex() == firstVisibleCellIndex);
if (isLeadIndex) {
// if the first visible cell is selected, we want to shift that cell down
// to be the bottom-most cell, or at least as far to the bottom as we can go.
flow.scrollToBottom(firstVisibleCell);
ListCell<T> newFirstVisibleCell = flow.getFirstVisibleCellWithinViewport();
firstVisibleCell = newFirstVisibleCell == null ? firstVisibleCell : newFirstVisibleCell;
}
} else {
// if the selection is not on the 'top' most cell, we firstly move
// the selection up to that, without scrolling the contents, so
// this is a no-op
}
int newSelectionIndex = firstVisibleCell.getIndex();
flow.scrollTo(firstVisibleCell);
return newSelectionIndex;
}
}