All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javafx.scene.control.ListView Maven / Gradle / Ivy

There is a newer version: 24-ea+15
Show newest version
/*
 * Copyright (c) 2010, 2024, 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;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;

import com.sun.javafx.scene.control.Properties;
import com.sun.javafx.scene.control.behavior.ListCellBehavior;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.css.StyleableDoubleProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Orientation;
import javafx.scene.layout.Region;
import javafx.util.Callback;
import javafx.css.StyleableObjectProperty;
import javafx.css.CssMetaData;

import javafx.css.converter.EnumConverter;

import javafx.collections.WeakListChangeListener;

import javafx.css.converter.SizeConverter;
import javafx.scene.control.skin.ListViewSkin;

import java.lang.ref.WeakReference;

import javafx.css.PseudoClass;
import javafx.beans.DefaultProperty;
import javafx.css.Styleable;
import javafx.css.StyleableProperty;
import javafx.scene.AccessibleAttribute;
import javafx.scene.AccessibleRole;
import javafx.scene.Node;
import javafx.util.Pair;

/**
 * A ListView displays a horizontal or vertical list of items from which the
 * user may select, or with which the user may interact. A ListView is able to
 * have its generic type set to represent the type of data in the backing model.
 * Doing this has the benefit of making various methods in the ListView, as well
 * as the supporting classes (mentioned below), type-safe. In addition, making
 * use of the generic type supports substantially simplified development of applications
 * making use of ListView, as all modern IDEs are able to auto-complete far
 * more successfully with the additional type information.
 *
 * 

Populating a ListView

*

A simple example of how to create and populate a ListView of names (Strings) * is shown here: * *

 {@code ObservableList names = FXCollections.observableArrayList(
 *          "Julia", "Ian", "Sue", "Matthew", "Hannah", "Stephan", "Denise");
 * ListView listView = new ListView(names);}
* *

The elements of the ListView are contained within the * {@link #itemsProperty() items} {@link ObservableList}. This * ObservableList is automatically observed by the ListView, such that any * changes that occur inside the ObservableList will be automatically shown in * the ListView itself. If passing the ObservableList in to the * ListView constructor is not feasible, the recommended approach for setting * the items is to simply call: * *

 {@code ObservableList content = ...
 * listView.setItems(content);}
* * Image of the ListView control * *

The end result of this is, as noted above, that the ListView will automatically * refresh the view to represent the items in the list. * *

Another approach, whilst accepted by the ListView, is not the * recommended approach: * *

 {@code List content = ...
 * getItems().setAll(content);}
* * The issue with the approach shown above is that the content list is being * copied into the items list - meaning that subsequent changes to the content * list are not observed, and will not be reflected visually within the ListView. * *

ListView Selection / Focus APIs

*

To track selection and focus, it is necessary to become familiar with the * {@link SelectionModel} and {@link FocusModel} classes. A ListView has at most * one instance of each of these classes, available from * {@link #selectionModelProperty() selectionModel} and * {@link #focusModelProperty() focusModel} properties respectively. * Whilst it is possible to use this API to set a new selection model, in * most circumstances this is not necessary - the default selection and focus * models should work in most circumstances. * *

The default {@link SelectionModel} used when instantiating a ListView is * an implementation of the {@link MultipleSelectionModel} abstract class. * However, as noted in the API documentation for * the {@link MultipleSelectionModel#selectionModeProperty() selectionMode} * property, the default value is {@link SelectionMode#SINGLE}. To enable * multiple selection in a default ListView instance, it is therefore necessary * to do the following: * *

 {@code listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);}
* *

Customizing ListView Visuals

*

The visuals of the ListView can be entirely customized by replacing the * default {@link #cellFactoryProperty() cell factory}. A cell factory is used to * generate {@link ListCell} instances, which are used to represent an item in the * ListView. See the {@link Cell} class documentation for a more complete * description of how to write custom Cells.

* *

Warning: Nodes should not be inserted directly into the items list

* {@code ListView} allows for the items list to contain elements of any type, including * {@link Node} instances. Putting nodes into * the items list is strongly discouraged, as it can * lead to unexpected results. *

Important points to note: *

    *
  • Avoid inserting {@code Node} instances directly into the items list or its data model.
  • *
  • The recommended approach is to put the relevant information into the items list, and * provide a custom {@link #cellFactoryProperty() cell factory} to create the nodes for a * given cell and update them on demand using the data stored in the item for that cell.
  • *
  • Avoid creating new {@code Node}s in the {@code updateItem} method of a custom {@link #cellFactoryProperty() cell factory}.
  • *
*

The following minimal example shows how to create a custom cell factory for {@code ListView} containing {@code Node}s: * *

{@code   ListView lv = new ListView<>();
 *  lv.getItems().addAll(Color.RED, Color.GREEN, Color.BLUE);
 *
 *  lv.setCellFactory(p -> {
 *    return new ListCell<>() {
 *        private final Rectangle rectangle;
 *        {
 *            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
 *            rectangle = new Rectangle(10, 10);
 *        }
 *
 *        @Override
 *        protected void updateItem(Color item, boolean empty) {
 *            super.updateItem(item, empty);
 *
 *            if (item == null || empty) {
 *                setGraphic(null);
 *            } else {
 *                rectangle.setFill(item);
 *                setGraphic(rectangle);
 *            }
 *         }
 *     };
 * });}
*

This example has an anonymous custom {@code ListCell} class in the custom cell factory. * Note that the {@code Rectangle} ({@code Node}) object needs to be created in the instance initialization block * or the constructor of the custom {@code ListCell} class and updated/used in its {@code updateItem} method. * *

Editing

*

This control supports inline editing of values, and this section attempts to * give an overview of the available APIs and how you should use them.

* *

Firstly, cell editing most commonly requires a different user interface * than when a cell is not being edited. This is the responsibility of the * {@link Cell} implementation being used. For ListView, this is the responsibility * of the {@link #cellFactoryProperty() cell factory}. It is your choice whether the cell is * permanently in an editing state (e.g. this is common for {@link CheckBox} cells), * or to switch to a different UI when editing begins (e.g. when a double-click * is received on a cell).

* *

To know when editing has been requested on a cell, * simply override the {@link javafx.scene.control.Cell#startEdit()} method, and * update the cell {@link javafx.scene.control.Cell#textProperty() text} and * {@link javafx.scene.control.Cell#graphicProperty() graphic} properties as * appropriate (e.g. set the text to null and set the graphic to be a * {@link TextField}). Additionally, you should also override * {@link Cell#cancelEdit()} to reset the UI back to its original visual state * when the editing concludes. In both cases it is important that you also * ensure that you call the super method to have the cell perform all duties it * must do to enter or exit its editing mode.

* *

Once your cell is in an editing state, the next thing you are most probably * interested in is how to commit or cancel the editing that is taking place. This is your * responsibility as the cell factory provider. Your cell implementation will know * when the editing is over, based on the user input (e.g. when the user presses * the Enter or ESC keys on their keyboard). When this happens, it is your * responsibility to call {@link Cell#commitEdit(Object)} or * {@link Cell#cancelEdit()}, as appropriate.

* *

When you call {@link Cell#commitEdit(Object)} an event is fired to the * ListView, which you can observe by adding an {@link EventHandler} via * {@link ListView#setOnEditCommit(javafx.event.EventHandler)}. Similarly, * you can also observe edit events for * {@link ListView#setOnEditStart(javafx.event.EventHandler) edit start} * and {@link ListView#setOnEditCancel(javafx.event.EventHandler) edit cancel}.

* *

By default the ListView edit commit handler is non-null, with a default * handler that attempts to overwrite the property value for the * item in the currently-being-edited row. It is able to do this as the * {@link Cell#commitEdit(Object)} method is passed in the new value, and this * is passed along to the edit commit handler via the * {@link EditEvent} that is fired. It is simply a matter of calling * {@link EditEvent#getNewValue()} to retrieve this value. * *

It is very important to note that if you call * {@link ListView#setOnEditCommit(javafx.event.EventHandler)} with your own * {@link EventHandler}, then you will be removing the default handler. Unless * you then handle the writeback to the property (or the relevant data source), * nothing will happen. You can work around this by using the * {@link ListView#addEventHandler(javafx.event.EventType, javafx.event.EventHandler)} * method to add a {@link ListView#editCommitEvent()} {@link EventType} with * your desired {@link EventHandler} as the second argument. Using this method, * you will not replace the default implementation, but you will be notified when * an edit commit has occurred.

* *

Hopefully this summary answers some of the commonly asked questions. * Fortunately, JavaFX ships with a number of pre-built cell factories that * handle all the editing requirements on your behalf. You can find these * pre-built cell factories in the javafx.scene.control.cell package.

* * @see ListCell * @see MultipleSelectionModel * @see FocusModel * @param This type is used to represent the type of the objects stored in * the ListViews {@link #itemsProperty() items} ObservableList. It is * also used in the {@link #selectionModelProperty() selection model} * and {@link #focusModelProperty() focus model}. * @since JavaFX 2.0 */ // TODO add code examples @DefaultProperty("items") public class ListView extends Control { /* ************************************************************************* * * * Static properties and methods * * * **************************************************************************/ /** * An EventType that indicates some edit event has occurred. It is the parent * type of all other edit events: {@link #editStartEvent()}, * {@link #editCommitEvent()} and {@link #editCancelEvent()}. * @param the type of the objects stored in this ListView * @return the event type */ @SuppressWarnings("unchecked") public static EventType> editAnyEvent() { return (EventType>) EDIT_ANY_EVENT; } private static final EventType EDIT_ANY_EVENT = new EventType<>(Event.ANY, "LIST_VIEW_EDIT"); /** * An EventType used to indicate that an edit event has started within the * ListView upon which the event was fired. * @param the type of the objects stored in this ListView * @return the event type */ @SuppressWarnings("unchecked") public static EventType> editStartEvent() { return (EventType>) EDIT_START_EVENT; } private static final EventType EDIT_START_EVENT = new EventType<>(editAnyEvent(), "EDIT_START"); /** * An EventType used to indicate that an edit event has just been canceled * within the ListView upon which the event was fired. * @param the type of the objects stored in this ListView * @return the event type */ @SuppressWarnings("unchecked") public static EventType> editCancelEvent() { return (EventType>) EDIT_CANCEL_EVENT; } private static final EventType EDIT_CANCEL_EVENT = new EventType<>(editAnyEvent(), "EDIT_CANCEL"); /** * An EventType used to indicate that an edit event has been committed * within the ListView upon which the event was fired. * @param the type of the objects stored in this ListView * @return the event type */ @SuppressWarnings("unchecked") public static EventType> editCommitEvent() { return (EventType>) EDIT_COMMIT_EVENT; } private static final EventType EDIT_COMMIT_EVENT = new EventType<>(editAnyEvent(), "EDIT_COMMIT"); /* ************************************************************************* * * * Fields * * * **************************************************************************/ // by default we always select the first row in the ListView, and when the // items list changes, we also reselect the first row. In some cases, such as // for the ComboBox, this is not desirable, so it can be disabled here. private boolean selectFirstRowByDefault = true; /* ************************************************************************* * * * Constructors * * * **************************************************************************/ /** * Creates a default ListView which will display contents stacked vertically. * As no {@link ObservableList} is provided in this constructor, an empty * ObservableList is created, meaning that it is legal to directly call * {@link #getItems()} if so desired. However, as noted elsewhere, this * is not the recommended approach * (instead call {@link #setItems(javafx.collections.ObservableList)}). * *

Refer to the {@link ListView} class documentation for details on the * default state of other properties. */ public ListView() { this(FXCollections.observableArrayList()); } /** * Creates a default ListView which will stack the contents retrieved from the * provided {@link ObservableList} vertically. * *

Attempts to add a listener to the {@link ObservableList}, such that all * subsequent changes inside the list will be shown to the user. * *

Refer to the {@link ListView} class documentation for details on the * default state of other properties. * @param items the list of items */ public ListView(ObservableList items) { getStyleClass().setAll(DEFAULT_STYLE_CLASS); setAccessibleRole(AccessibleRole.LIST_VIEW); setItems(items); // Install default.... // ...selection model setSelectionModel(new ListView.ListViewBitSetSelectionModel<>(this)); // ...focus model setFocusModel(new ListView.ListViewFocusModel<>(this)); // ...edit commit handler setOnEditCommit(DEFAULT_EDIT_COMMIT_HANDLER); // Fix for RT-36651, which was introduced by RT-35679 (above) and resolved // by having special-case code to remove the listener when requested. // This is done by ComboBoxListViewSkin, so that selection is not done // when a ComboBox is shown. getProperties().addListener((MapChangeListener) change -> { if (change.wasAdded() && "selectFirstRowByDefault".equals(change.getKey())) { Boolean _selectFirstRowByDefault = (Boolean) change.getValueAdded(); if (_selectFirstRowByDefault == null) return; selectFirstRowByDefault = _selectFirstRowByDefault; } }); pseudoClassStateChanged(PSEUDO_CLASS_VERTICAL, true); } /* ************************************************************************* * * * Callbacks and Events * * * **************************************************************************/ private EventHandler> DEFAULT_EDIT_COMMIT_HANDLER = t -> { int index = t.getIndex(); List list = getItems(); if (index < 0 || index >= list.size()) return; list.set(index, t.getNewValue()); }; /* ************************************************************************* * * * Properties * * * **************************************************************************/ // --- Items private ObjectProperty> items; /** * Sets the underlying data model for the ListView. Note that it has a generic * type that must match the type of the ListView itself. * @param value the list of items for this ListView */ public final void setItems(ObservableList value) { itemsProperty().set(value); } /** * Returns an {@link ObservableList} that contains the items currently being * shown to the user. This may be null if * {@link #setItems(javafx.collections.ObservableList)} has previously been * called, however, by default it is an empty ObservableList. * * @return An ObservableList containing the items to be shown to the user, or * null if the items have previously been set to null. */ public final ObservableList getItems() { return items == null ? null : items.get(); } /** * The underlying data model for the ListView. Note that it has a generic * type that must match the type of the ListView itself. * @return the items property for this ListView */ public final ObjectProperty> itemsProperty() { if (items == null) { items = new SimpleObjectProperty<>(this, "items"); } return items; } /** * The {@code Node} to show to the user when the {@code ListView} has no content to show. * This happens when the list model has no data or when a filter has been applied to the list model, resulting in * there being nothing to show the user. * * @since JavaFX 8.0 */ private ObjectProperty placeholder; public final ObjectProperty placeholderProperty() { if (placeholder == null) { placeholder = new SimpleObjectProperty<>(this, "placeholder"); } return placeholder; } public final void setPlaceholder(Node value) { placeholderProperty().set(value); } public final Node getPlaceholder() { return placeholder == null ? null : placeholder.get(); } // --- Selection Model private ObjectProperty> selectionModel = new SimpleObjectProperty<>(this, "selectionModel"); /** * Sets the {@link MultipleSelectionModel} to be used in the ListView. * Despite a ListView requiring a MultipleSelectionModel, it is possible * to configure it to only allow single selection (see * {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)} * for more information). * @param value the MultipleSelectionModel to be used in this ListView */ public final void setSelectionModel(MultipleSelectionModel value) { selectionModelProperty().set(value); } /** * Returns the currently installed selection model. * @return the currently installed selection model */ public final MultipleSelectionModel getSelectionModel() { return selectionModel == null ? null : selectionModel.get(); } /** * The SelectionModel provides the API through which it is possible * to select single or multiple items within a ListView, as well as inspect * which items have been selected by the user. Note that it has a generic * type that must match the type of the ListView itself. * @return the selectionModel property */ public final ObjectProperty> selectionModelProperty() { return selectionModel; } // --- Focus Model private ObjectProperty> focusModel; /** * Sets the {@link FocusModel} to be used in the ListView. * @param value the FocusModel to be used in the ListView */ public final void setFocusModel(FocusModel value) { focusModelProperty().set(value); } /** * Returns the currently installed {@link FocusModel}. * @return the currently installed FocusModel */ public final FocusModel getFocusModel() { return focusModel == null ? null : focusModel.get(); } /** * The FocusModel provides the API through which it is possible * to both get and set the focus on a single item within a ListView. Note * that it has a generic type that must match the type of the ListView itself. * @return the FocusModel property */ public final ObjectProperty> focusModelProperty() { if (focusModel == null) { focusModel = new SimpleObjectProperty<>(this, "focusModel"); } return focusModel; } // --- Orientation private ObjectProperty orientation; /** * Sets the orientation of the ListView, which dictates whether * it scrolls vertically or horizontally. * @param value the orientation of the ListView */ public final void setOrientation(Orientation value) { orientationProperty().set(value); } /** * Returns the current orientation of the ListView, which dictates whether * it scrolls vertically or horizontally. * @return the current orientation of the ListView */ public final Orientation getOrientation() { return orientation == null ? Orientation.VERTICAL : orientation.get(); } /** * The orientation of the {@code ListView} - this can either be horizontal * or vertical. * @return the orientation property of this ListView */ public final ObjectProperty orientationProperty() { if (orientation == null) { orientation = new StyleableObjectProperty(Orientation.VERTICAL) { @Override public void invalidated() { final boolean active = (get() == Orientation.VERTICAL); pseudoClassStateChanged(PSEUDO_CLASS_VERTICAL, active); pseudoClassStateChanged(PSEUDO_CLASS_HORIZONTAL, !active); } @Override public CssMetaData,Orientation> getCssMetaData() { return ListView.StyleableProperties.ORIENTATION; } @Override public Object getBean() { return ListView.this; } @Override public String getName() { return "orientation"; } }; } return orientation; } // --- Cell Factory private ObjectProperty, ListCell>> cellFactory; /** * Sets a new cell factory to use in the ListView. This forces all old * {@link ListCell}'s to be thrown away, and new ListCell's created with * the new cell factory. * @param value cell factory to use in this ListView */ public final void setCellFactory(Callback, ListCell> value) { cellFactoryProperty().set(value); } /** * Returns the current cell factory. * @return the current cell factory */ public final Callback, ListCell> getCellFactory() { return cellFactory == null ? null : cellFactory.get(); } /** *

Setting a custom cell factory has the effect of deferring all cell * creation, allowing for total customization of the cell. Internally, the * ListView is responsible for reusing ListCells - all that is necessary * is for the custom cell factory to return from this function a ListCell * which might be usable for representing any item in the ListView. * *

Refer to the {@link Cell} class documentation for more detail. * @return the cell factory property */ public final ObjectProperty, ListCell>> cellFactoryProperty() { if (cellFactory == null) { cellFactory = new SimpleObjectProperty<>(this, "cellFactory"); } return cellFactory; } // --- Fixed cell size private DoubleProperty fixedCellSize; /** * Sets the new fixed cell size for this control. Any value greater than * zero will enable fixed cell size mode, whereas a zero or negative value * (or Region.USE_COMPUTED_SIZE) will be used to disabled fixed cell size * mode. * * @param value The new fixed cell size value, or a value less than or equal * to zero (or Region.USE_COMPUTED_SIZE) to disable. * @since JavaFX 8.0 */ public final void setFixedCellSize(double value) { fixedCellSizeProperty().set(value); } /** * Returns the fixed cell size value. A value less than or equal to zero is * used to represent that fixed cell size mode is disabled, and a value * greater than zero represents the size of all cells in this control. * * @return A double representing the fixed cell size of this control, or a * value less than or equal to zero if fixed cell size mode is disabled. * @since JavaFX 8.0 */ public final double getFixedCellSize() { return fixedCellSize == null ? Region.USE_COMPUTED_SIZE : fixedCellSize.get(); } /** * Specifies whether this control has cells that are a fixed height (of the * specified value). If this value is less than or equal to zero, * then all cells are individually sized and positioned. This is a slow * operation. Therefore, when performance matters and developers are not * dependent on variable cell sizes it is a good idea to set the fixed cell * size value. Generally cells are around 24px, so setting a fixed cell size * of 24 is likely to result in very little difference in visuals, but a * improvement to performance. * *

To set this property via CSS, use the -fx-fixed-cell-size property. * This should not be confused with the -fx-cell-size property. The difference * between these two CSS properties is that -fx-cell-size will size all * cells to the specified size, but it will not enforce that this is the * only size (thus allowing for variable cell sizes, and preventing the * performance gains from being possible). Therefore, when performance matters * use -fx-fixed-cell-size, instead of -fx-cell-size. If both properties are * specified in CSS, -fx-fixed-cell-size takes precedence.

* * @return the fixed cell size property * @since JavaFX 8.0 */ public final DoubleProperty fixedCellSizeProperty() { if (fixedCellSize == null) { fixedCellSize = new StyleableDoubleProperty(Region.USE_COMPUTED_SIZE) { @Override public CssMetaData,Number> getCssMetaData() { return StyleableProperties.FIXED_CELL_SIZE; } @Override public Object getBean() { return ListView.this; } @Override public String getName() { return "fixedCellSize"; } }; } return fixedCellSize; } // --- Editable private BooleanProperty editable; public final void setEditable(boolean value) { editableProperty().set(value); } public final boolean isEditable() { return editable == null ? false : editable.get(); } /** * Specifies whether this ListView is editable - only if the ListView and * the ListCells within it are both editable will a ListCell be able to go * into their editing state. * @return the editable property */ public final BooleanProperty editableProperty() { if (editable == null) { editable = new SimpleBooleanProperty(this, "editable", false); } return editable; } // --- Editing Index private ReadOnlyIntegerWrapper editingIndex; private void setEditingIndex(int value) { editingIndexPropertyImpl().set(value); } /** * Returns the index of the item currently being edited in the ListView, * or -1 if no item is being edited. * @return the index of the item currently being edited */ public final int getEditingIndex() { return editingIndex == null ? -1 : editingIndex.get(); } /** *

A property used to represent the index of the item currently being edited * in the ListView, if editing is taking place, or -1 if no item is being edited. * *

It is not possible to set the editing index, instead it is required that * you call {@link #edit(int)}. * @return the editing index property */ public final ReadOnlyIntegerProperty editingIndexProperty() { return editingIndexPropertyImpl().getReadOnlyProperty(); } private ReadOnlyIntegerWrapper editingIndexPropertyImpl() { if (editingIndex == null) { editingIndex = new ReadOnlyIntegerWrapper(this, "editingIndex", -1); } return editingIndex; } // --- On Edit Start private ObjectProperty>> onEditStart; /** * Sets the {@link EventHandler} that will be called when the user begins * an edit. * *

This is a convenience method - the same result can be * achieved by calling * addEventHandler(ListView.EDIT_START_EVENT, eventHandler). * @param value the EventHandler that will be called when the user begins * an edit */ public final void setOnEditStart(EventHandler> value) { onEditStartProperty().set(value); } /** * Returns the {@link EventHandler} that will be called when the user begins * an edit. * @return the EventHandler that will be called when the user begins an edit */ public final EventHandler> getOnEditStart() { return onEditStart == null ? null : onEditStart.get(); } /** * This event handler will be fired when the user successfully initiates * editing. * @return the onEditStart event handler property */ public final ObjectProperty>> onEditStartProperty() { if (onEditStart == null) { onEditStart = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ListView.editStartEvent(), get()); } @Override public Object getBean() { return ListView.this; } @Override public String getName() { return "onEditStart"; } }; } return onEditStart; } // --- On Edit Commit private ObjectProperty>> onEditCommit; /** * Sets the {@link EventHandler} that will be called when the user has * completed their editing. This is called as part of the * {@link ListCell#commitEdit(java.lang.Object)} method. * *

This is a convenience method - the same result can be * achieved by calling * addEventHandler(ListView.EDIT_START_EVENT, eventHandler). * @param value the EventHandler that will be called when the user has * completed their editing */ public final void setOnEditCommit(EventHandler> value) { onEditCommitProperty().set(value); } /** * Returns the {@link EventHandler} that will be called when the user commits * an edit. * @return the EventHandler that will be called when the user commits an edit */ public final EventHandler> getOnEditCommit() { return onEditCommit == null ? null : onEditCommit.get(); } /** *

This property is used when the user performs an action that should * result in their editing input being persisted.

* *

The EventHandler in this property should not be called directly - * instead call {@link ListCell#commitEdit(java.lang.Object)} from within * your custom ListCell. This will handle firing this event, updating the * view, and switching out of the editing state.

* @return the onEditCommit event handler property */ public final ObjectProperty>> onEditCommitProperty() { if (onEditCommit == null) { onEditCommit = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ListView.editCommitEvent(), get()); } @Override public Object getBean() { return ListView.this; } @Override public String getName() { return "onEditCommit"; } }; } return onEditCommit; } // --- On Edit Cancel private ObjectProperty>> onEditCancel; /** * Sets the {@link EventHandler} that will be called when the user cancels * an edit. * @param value the EventHandler that will be called when the user cancels * an edit */ public final void setOnEditCancel(EventHandler> value) { onEditCancelProperty().set(value); } /** * Returns the {@link EventHandler} that will be called when the user cancels * an edit. * @return the EventHandler that will be called when the user cancels an edit */ public final EventHandler> getOnEditCancel() { return onEditCancel == null ? null : onEditCancel.get(); } /** * This event handler will be fired when the user cancels editing a cell. * @return the onEditCancel event handler property */ public final ObjectProperty>> onEditCancelProperty() { if (onEditCancel == null) { onEditCancel = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ListView.editCancelEvent(), get()); } @Override public Object getBean() { return ListView.this; } @Override public String getName() { return "onEditCancel"; } }; } return onEditCancel; } /* ************************************************************************* * * * Public API * * * **************************************************************************/ /** * Instructs the ListView to begin editing the item in the given index, if * the ListView is {@link #editableProperty() editable}. Once * this method is called, if the current {@link #cellFactoryProperty()} is * set up to support editing, the Cell will switch its visual state to enable * for user input to take place. * * @param itemIndex The index of the item in the ListView that should be * edited. */ public void edit(int itemIndex) { if (!isEditable()) return; setEditingIndex(itemIndex); } /** * Scrolls the ListView such that the item in the given index is visible to * the end user. * * @param index The index that should be made visible to the user, assuming * of course that it is greater than, or equal to 0, and less than the * size of the items list contained within the given ListView. */ public void scrollTo(int index) { ControlUtils.scrollToIndex(this, index); } /** * Scrolls the ListView so that the given object is visible within the viewport. * @param object The object that should be visible to the user. * @since JavaFX 8.0 */ public void scrollTo(T object) { if( getItems() != null ) { int idx = getItems().indexOf(object); if( idx >= 0 ) { ControlUtils.scrollToIndex(this, idx); } } } /** * Called when there's a request to scroll an index into view using {@link #scrollTo(int)} * or {@link #scrollTo(Object)} * @since JavaFX 8.0 */ private ObjectProperty>> onScrollTo; public final void setOnScrollTo(EventHandler> value) { onScrollToProperty().set(value); } public final EventHandler> getOnScrollTo() { if( onScrollTo != null ) { return onScrollTo.get(); } return null; } public final ObjectProperty>> onScrollToProperty() { if( onScrollTo == null ) { onScrollTo = new ObjectPropertyBase<>() { @Override protected void invalidated() { setEventHandler(ScrollToEvent.scrollToTopIndex(), get()); } @Override public Object getBean() { return ListView.this; } @Override public String getName() { return "onScrollTo"; } }; } return onScrollTo; } /** {@inheritDoc} */ @Override protected Skin createDefaultSkin() { return new ListViewSkin<>(this); } /** * Calling {@code refresh()} forces the ListView control to recreate and * repopulate the cells necessary to populate the visual bounds of the control. * In other words, this forces the ListView to update what it is showing to * the user. This is useful in cases where the underlying data source has * changed in a way that is not observed by the ListView itself. * * @since JavaFX 8u60 */ public void refresh() { getProperties().put(Properties.RECREATE, Boolean.TRUE); } /* ************************************************************************* * * * Private Implementation * * * **************************************************************************/ /* ************************************************************************* * * * Stylesheet Handling * * * **************************************************************************/ private static final String DEFAULT_STYLE_CLASS = "list-view"; private static class StyleableProperties { private static final CssMetaData,Orientation> ORIENTATION = new CssMetaData<>("-fx-orientation", new EnumConverter<>(Orientation.class), Orientation.VERTICAL) { @Override public Orientation getInitialValue(ListView node) { // A vertical ListView should remain vertical return node.getOrientation(); } @Override public boolean isSettable(ListView n) { return n.orientation == null || !n.orientation.isBound(); } @SuppressWarnings("unchecked") // orientationProperty() is a StyleableProperty @Override public StyleableProperty getStyleableProperty(ListView n) { return (StyleableProperty)n.orientationProperty(); } }; private static final CssMetaData,Number> FIXED_CELL_SIZE = new CssMetaData<>("-fx-fixed-cell-size", SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) { @Override public Double getInitialValue(ListView node) { return node.getFixedCellSize(); } @Override public boolean isSettable(ListView n) { return n.fixedCellSize == null || !n.fixedCellSize.isBound(); } @Override public StyleableProperty getStyleableProperty(ListView n) { return (StyleableProperty)n.fixedCellSizeProperty(); } }; private static final List> STYLEABLES; static { final List> styleables = new ArrayList<>(Control.getClassCssMetaData()); styleables.add(ORIENTATION); styleables.add(FIXED_CELL_SIZE); STYLEABLES = Collections.unmodifiableList(styleables); } } /** * Gets the {@code CssMetaData} associated with this class, which may include the * {@code CssMetaData} of its superclasses. * @return the {@code CssMetaData} * @since JavaFX 8.0 */ public static List> getClassCssMetaData() { return StyleableProperties.STYLEABLES; } /** * {@inheritDoc} * @since JavaFX 8.0 */ @Override public List> getControlCssMetaData() { return getClassCssMetaData(); } private static final PseudoClass PSEUDO_CLASS_VERTICAL = PseudoClass.getPseudoClass("vertical"); private static final PseudoClass PSEUDO_CLASS_HORIZONTAL = PseudoClass.getPseudoClass("horizontal"); /* ************************************************************************* * * * Accessibility handling * * * **************************************************************************/ /** {@inheritDoc} */ @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) { switch (attribute) { case MULTIPLE_SELECTION: { MultipleSelectionModel sm = getSelectionModel(); return sm != null && sm.getSelectionMode() == SelectionMode.MULTIPLE; } default: return super.queryAccessibleAttribute(attribute, parameters); } } /* ************************************************************************* * * * Support Interfaces * * * **************************************************************************/ /* ************************************************************************* * * * Support Classes * * * **************************************************************************/ /** * An {@link Event} subclass used specifically in ListView for representing * edit-related events. It provides additional API to easily access the * index that the edit event took place on, as well as the input provided * by the end user. * * @param The type of the input, which is the same type as the ListView * itself. * @since JavaFX 2.0 */ public static class EditEvent extends Event { @SuppressWarnings("doclint:missing") private final T newValue; @SuppressWarnings("doclint:missing") private final int editIndex; @SuppressWarnings("doclint:missing") private final ListView source; private static final long serialVersionUID = 20130724L; /** * Common supertype for all edit event types. * @since JavaFX 8.0 */ public static final EventType ANY = EDIT_ANY_EVENT; /** * Creates a new EditEvent instance to represent an edit event. This * event is used for {@link #editStartEvent()}, * {@link #editCommitEvent()} and {@link #editCancelEvent()} types. * @param source the source * @param eventType the event type * @param newValue the new value * @param editIndex the edit index */ public EditEvent(ListView source, EventType> eventType, T newValue, int editIndex) { super(source, Event.NULL_SOURCE_TARGET, eventType); this.source = source; this.editIndex = editIndex; this.newValue = newValue; } /** * Returns the ListView upon which the edit took place. */ @Override public ListView getSource() { return source; } /** * Returns the index in which the edit took place. * @return the index in which the edit took place */ public int getIndex() { return editIndex; } /** * Returns the value of the new input provided by the end user. * @return the value of the new input provided by the end user */ public T getNewValue() { return newValue; } /** * Returns a string representation of this {@code EditEvent} object. * @return a string representation of this {@code EditEvent} object. */ @Override public String toString() { return "ListViewEditEvent [ newValue: " + getNewValue() + ", ListView: " + getSource() + " ]"; } } // package for testing static class ListViewBitSetSelectionModel extends MultipleSelectionModelBase { /* ********************************************************************* * * * Constructors * * * **********************************************************************/ public ListViewBitSetSelectionModel(final ListView listView) { if (listView == null) { throw new IllegalArgumentException("ListView can not be null"); } this.listView = listView; /* * The following two listeners are used in conjunction with * SelectionModel.select(T obj) to allow for a developer to select * an item that is not actually in the data model. When this occurs, * we actively try to find an index that matches this object, going * so far as to actually watch for all changes to the items list, * rechecking each time. */ itemsObserver = new InvalidationListener() { private WeakReference> weakItemsRef = new WeakReference<>(listView.getItems()); @Override public void invalidated(Observable observable) { ObservableList oldItems = weakItemsRef.get(); weakItemsRef = new WeakReference<>(listView.getItems()); updateItemsObserver(oldItems, listView.getItems()); } }; this.listView.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); if (listView.getItems() != null) { this.listView.getItems().addListener(weakItemsContentObserver); } updateItemCount(); updateDefaultSelection(); } // watching for changes to the items list content private final ListChangeListener itemsContentObserver = new ListChangeListener<>() { @Override public void onChanged(Change c) { updateItemCount(); boolean doSelectionUpdate = true; while (c.next()) { final T selectedItem = getSelectedItem(); final int selectedIndex = getSelectedIndex(); if (listView.getItems() == null || listView.getItems().isEmpty()) { selectedItemChange = c; clearSelection(); selectedItemChange = null; } else if (selectedIndex == -1 && selectedItem != null) { int newIndex = listView.getItems().indexOf(selectedItem); if (newIndex != -1) { setSelectedIndex(newIndex); doSelectionUpdate = false; } } else if (c.wasRemoved() && c.getRemovedSize() == 1 && ! c.wasAdded() && selectedItem != null && selectedItem.equals(c.getRemoved().get(0))) { // Bug fix for RT-28637 if (getSelectedIndex() < getItemCount()) { final int previousRow = selectedIndex == 0 ? 0 : selectedIndex - 1; T newSelectedItem = getModelItem(previousRow); if (! selectedItem.equals(newSelectedItem)) { startAtomic(); clearSelection(selectedIndex); stopAtomic(); select(newSelectedItem); } } } } if (doSelectionUpdate) { updateSelection(c); } } }; // watching for changes to the items list private final InvalidationListener itemsObserver; private WeakListChangeListener weakItemsContentObserver = new WeakListChangeListener<>(itemsContentObserver); /* ********************************************************************* * * * Internal properties * * * **********************************************************************/ private final ListView listView; private int itemCount = 0; private int previousModelSize = 0; // Listen to changes in the listview items list, such that when it // changes we can update the selected indices bitset to refer to the // new indices. // At present this is basically a left/right shift operation, which // seems to work ok. private void updateSelection(Change c) { // // debugging output // System.out.println(listView.getId()); // if (c.wasAdded()) { // System.out.println("\tAdded size: " + c.getAddedSize() + ", Added sublist: " + c.getAddedSubList()); // } // if (c.wasRemoved()) { // System.out.println("\tRemoved size: " + c.getRemovedSize() + ", Removed sublist: " + c.getRemoved()); // } // if (c.wasReplaced()) { // System.out.println("\tWas replaced"); // } // if (c.wasPermutated()) { // System.out.println("\tWas permutated"); // } c.reset(); List> shifts = new ArrayList<>(); while (c.next()) { if (c.wasReplaced()) { if (c.getList().isEmpty()) { // the entire items list was emptied - clear selection clearSelection(); } else { int index = getSelectedIndex(); if (previousModelSize == c.getRemovedSize()) { // all items were removed from the model clearSelection(); } else if (index < getItemCount() && index >= 0) { // Fix for RT-18969: the list had setAll called on it // Use of makeAtomic is a fix for RT-20945 startAtomic(); clearSelection(index); stopAtomic(); select(index); } else { // Fix for RT-22079 clearSelection(); } } } else if (c.wasAdded() || c.wasRemoved()) { int shift = c.wasAdded() ? c.getAddedSize() : -c.getRemovedSize(); shifts.add(new Pair<>(c.getFrom(), shift)); } else if (c.wasPermutated()) { // General approach: // -- detected a sort has happened // -- Create a permutation lookup map (1) // -- dump all the selected indices into a list (2) // -- clear the selected items / indexes (3) // -- create a list containing the new indices (4) // -- for each previously-selected index (5) // -- if index is in the permutation lookup map // -- add the new index to the new indices list // -- Perform batch selection (6) // (1) int length = c.getTo() - c.getFrom(); HashMap pMap = new HashMap<>(length); for (int i = c.getFrom(); i < c.getTo(); i++) { pMap.put(i, c.getPermutation(i)); } // (2) List selectedIndices = new ArrayList<>(getSelectedIndices()); // (3) clearSelection(); // (4) List newIndices = new ArrayList<>(getSelectedIndices().size()); // (5) for (int i = 0; i < selectedIndices.size(); i++) { int oldIndex = selectedIndices.get(i); if (pMap.containsKey(oldIndex)) { Integer newIndex = pMap.get(oldIndex); newIndices.add(newIndex); } } // (6) if (!newIndices.isEmpty()) { if (newIndices.size() == 1) { select(newIndices.get(0)); } else { int[] ints = new int[newIndices.size() - 1]; for (int i = 0; i < newIndices.size() - 1; i++) { ints[i] = newIndices.get(i + 1); } selectIndices(newIndices.get(0), ints); } } } } if (!shifts.isEmpty()) { shiftSelection(shifts, null); } previousModelSize = getItemCount(); } /* ********************************************************************* * * * Public selection API * * * **********************************************************************/ /** {@inheritDoc} */ @Override public void selectAll() { // when a selectAll happens, the anchor should not change, so we store it // before, and restore it afterwards final int anchor = ListCellBehavior.getAnchor(listView, -1); super.selectAll(); ListCellBehavior.setAnchor(listView, anchor, false); } /** {@inheritDoc} */ @Override public void clearAndSelect(int row) { ListCellBehavior.setAnchor(listView, row, false); super.clearAndSelect(row); } /** {@inheritDoc} */ @Override protected void focus(int row) { if (listView.getFocusModel() == null) return; listView.getFocusModel().focus(row); listView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); } /** {@inheritDoc} */ @Override protected int getFocusedIndex() { if (listView.getFocusModel() == null) return -1; return listView.getFocusModel().getFocusedIndex(); } @Override protected int getItemCount() { return itemCount; } @Override protected T getModelItem(int index) { List items = listView.getItems(); if (items == null) return null; if (index < 0 || index >= itemCount) return null; return items.get(index); } /* ********************************************************************* * * * Private implementation * * * **********************************************************************/ private void updateItemCount() { if (listView == null) { itemCount = -1; } else { List items = listView.getItems(); itemCount = items == null ? -1 : items.size(); } } private void updateItemsObserver(ObservableList oldList, ObservableList newList) { // update listeners if (oldList != null) { oldList.removeListener(weakItemsContentObserver); } if (newList != null) { newList.addListener(weakItemsContentObserver); } updateItemCount(); updateDefaultSelection(); } private void updateDefaultSelection() { // when the items list totally changes, we should clear out // the selection and focus int newSelectionIndex = -1; int newFocusIndex = -1; if (listView.getItems() != null) { T selectedItem = getSelectedItem(); if (selectedItem != null) { newSelectionIndex = listView.getItems().indexOf(selectedItem); newFocusIndex = newSelectionIndex; } // we put focus onto the first item, if there is at least // one item in the list if (listView.selectFirstRowByDefault && newFocusIndex == -1) { newFocusIndex = listView.getItems().size() > 0 ? 0 : -1; } } clearSelection(); select(newSelectionIndex); // focus(newFocusIndex); } } // package for testing static class ListViewFocusModel extends FocusModel { private final ListView listView; private int itemCount = 0; public ListViewFocusModel(final ListView listView) { if (listView == null) { throw new IllegalArgumentException("ListView can not be null"); } this.listView = listView; itemsObserver = new InvalidationListener() { private WeakReference> weakItemsRef = new WeakReference<>(listView.getItems()); @Override public void invalidated(Observable observable) { ObservableList oldItems = weakItemsRef.get(); weakItemsRef = new WeakReference<>(listView.getItems()); updateItemsObserver(oldItems, listView.getItems()); } }; this.listView.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver)); if (listView.getItems() != null) { this.listView.getItems().addListener(weakItemsContentListener); } updateItemCount(); updateDefaultFocus(); focusedIndexProperty().addListener(o -> { listView.notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM); }); } private void updateItemsObserver(ObservableList oldList, ObservableList newList) { // the listview items list has changed, we need to observe // the new list, and remove any observer we had from the old list if (oldList != null) oldList.removeListener(weakItemsContentListener); if (newList != null) newList.addListener(weakItemsContentListener); updateItemCount(); updateDefaultFocus(); } private final InvalidationListener itemsObserver; // Listen to changes in the listview items list, such that when it // changes we can update the focused index to refer to the new indices. private final ListChangeListener itemsContentListener = c -> { updateItemCount(); while (c.next()) { // looking at the first change int from = c.getFrom(); if (c.wasReplaced() || c.getAddedSize() == getItemCount()) { updateDefaultFocus(); return; } if (getFocusedIndex() == -1 || from > getFocusedIndex()) { return; } c.reset(); boolean added = false; boolean removed = false; int addedSize = 0; int removedSize = 0; while (c.next()) { added |= c.wasAdded(); removed |= c.wasRemoved(); addedSize += c.getAddedSize(); removedSize += c.getRemovedSize(); } if (added && !removed) { focus(Math.min(getItemCount() - 1, getFocusedIndex() + addedSize)); } else if (!added && removed) { focus(Math.max(0, getFocusedIndex() - removedSize)); } } }; private WeakListChangeListener weakItemsContentListener = new WeakListChangeListener<>(itemsContentListener); @Override protected int getItemCount() { return itemCount; } @Override protected T getModelItem(int index) { if (isEmpty()) return null; if (index < 0 || index >= itemCount) return null; return listView.getItems().get(index); } private boolean isEmpty() { return itemCount == -1; } private void updateItemCount() { if (listView == null) { itemCount = -1; } else { List items = listView.getItems(); itemCount = items == null ? -1 : items.size(); } } private void updateDefaultFocus() { // when the items list totally changes, we should clear out // the focus int newValueIndex = -1; if (listView.getItems() != null) { T focusedItem = getFocusedItem(); if (focusedItem != null) { newValueIndex = listView.getItems().indexOf(focusedItem); } // we put focus onto the first item, if there is at least // one item in the list if (newValueIndex == -1) { newValueIndex = listView.getItems().size() > 0 ? 0 : -1; } } focus(newValueIndex); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy