org.datafx.control.cell.ExpandOnMouseEventListCell Maven / Gradle / Ivy
/**
* Copyright (c) 2011, 2013, Jonathan Giles, Johan Vos, Hendrik Ebbers
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of DataFX, the website javafxdata.org, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.datafx.control.cell;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.control.Cell;
import javafx.scene.control.ListCell;
import javafx.scene.input.MouseEvent;
import javafx.util.Callback;
import javafx.util.Duration;
import static org.datafx.control.cell.ExpandOnMouseEventCell.ANIMATION_DURATION;
import static org.datafx.control.cell.ExpandOnMouseEventCell.expandedCellsMap;
import static org.datafx.control.cell.ExpandOnMouseEventCell.expandedIndicesMap;
/**
* A class containing a {@link ListCell} implementation that expands when a
* certain mouse event occurs. When the cell expands, it allows for additional
* information to be shown to the user.
*
*
*
* @param The type of the elements contained within the ListView.
* @author Jonathan Giles
*/
public class ExpandOnMouseEventListCell extends ListCell {
public static final double DEFAULT_PREF_HEIGHT = 24;
public static final double DEFAULT_EXPAND_HEIGHT = DEFAULT_PREF_HEIGHT * 3;
private int maxExpandedCells = 1;
private final double expandedHeight;
public ExpandOnMouseEventListCell() {
this(1);
}
public ExpandOnMouseEventListCell(int maxExpandedCells) {
this(maxExpandedCells, DEFAULT_EXPAND_HEIGHT, MouseEvent.MOUSE_CLICKED);
}
public ExpandOnMouseEventListCell(int maxExpandedCells, double expandHeight) {
this(maxExpandedCells, expandHeight, MouseEvent.MOUSE_CLICKED);
}
public ExpandOnMouseEventListCell(int maxExpandedCells, double expandHeight, EventType mouseEventType) {
this.maxExpandedCells = maxExpandedCells >= 0 ? maxExpandedCells : 1;
this.expandedHeight = expandHeight < 0.0 ? DEFAULT_EXPAND_HEIGHT : expandHeight;
prefHeightProperty().addListener(new InvalidationListener() {
@Override public void invalidated(Observable arg0) {
requestLayout();
}
});
addEventFilter(mouseEventType == null ? MouseEvent.MOUSE_CLICKED : mouseEventType,
new EventHandler() {
@Override public void handle(MouseEvent t) {
if (isEmpty()) {
return;
}
// flip whether this cell index is expanded or not
flip(ExpandOnMouseEventListCell.this, getMaxExpandedCells());
}
});
}
@Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
setText(item == null ? "" : item.toString());
}
@Override public void updateIndex(int i) {
super.updateIndex(i);
if (isExpanded()) {
// immediately expand this cell
setPrefHeight(expandedHeight);
} else {
// immediately collapse this cell
setPrefHeight(DEFAULT_PREF_HEIGHT);
}
}
private Callback, CellUpdate> onCellResizeStarting;
public void setOnCellResizeStarting(Callback, CellUpdate> onCellResizeStarting) {
this.onCellResizeStarting = onCellResizeStarting;
}
private synchronized static void flip(ExpandOnMouseEventListCell> cell, int maxExpandedCells) {
Queue expandedCells = expandedCellsMap.get(cell.getListView());
if (expandedCells == null) {
expandedCells = new LinkedBlockingQueue();
expandedCellsMap.put(cell.getListView(), expandedCells);
}
List expandedIndices = expandedIndicesMap.get(cell.getListView());
if (expandedIndices == null) {
expandedIndices = new ArrayList();
expandedIndicesMap.put(cell.getListView(), expandedIndices);
}
boolean cellIsExpanded = expandedCells.contains(cell);
if (maxExpandedCells <= expandedCells.size() && !cellIsExpanded) {
ExpandOnMouseEventListCell collapsed = (ExpandOnMouseEventListCell) expandedCells.remove();
animate(collapsed, false);
expandedIndices.remove((Object) collapsed.getIndex());
}
if (cellIsExpanded) {
expandedCells.remove(cell);
expandedIndices.remove((Object) cell.getIndex());
} else {
expandedCells.add(cell);
expandedIndices.add(cell.getIndex());
}
animate(cell, !cellIsExpanded);
}
public boolean isExpanded() {
if (expandedIndicesMap.containsKey(getListView())) {
return expandedIndicesMap.get(getListView()).contains(getIndex());
}
return false;
}
public int getMaxExpandedCells() {
return maxExpandedCells;
}
public void setMaxExpandedCells(int maxExpandedCells) {
this.maxExpandedCells = maxExpandedCells;
}
@Override protected double computePrefHeight(double width) {
return isExpanded() ? getPrefHeight() : super.computePrefHeight(width);
}
@Override protected double computeMinHeight(double width) {
return computePrefHeight(width);
}
private static void animate(final ExpandOnMouseEventListCell cell, final boolean toExpanded) {
double startHeight = cell.getHeight();
// the end height is the opposite of the current state -
// we are animating out of this state after all
double endHeight = toExpanded ? cell.expandedHeight : DEFAULT_PREF_HEIGHT;
// create a timeline to expand/collapse the cell. All this
// really does is modify the height of the content
Timeline timeline = new Timeline();
timeline.setCycleCount(1);
timeline.setAutoReverse(false);
CellResizeFeatures crf = new CellResizeFeatures(cell, toExpanded);
final CellUpdate cellUpdate = cell.onCellResizeStarting == null ? null : cell.onCellResizeStarting.call(crf);
final String text = cellUpdate == null ? cell.getText() : cellUpdate.text;
final Node graphic = cellUpdate == null ? cell.getGraphic() : cellUpdate.graphic;
double endOpacity = toExpanded ? 1.0 : 0.0;
// first key frame
KeyFrame firstFrame = graphic == null
? new KeyFrame(Duration.ZERO,
new KeyValue(cell.prefHeightProperty(), startHeight, Interpolator.EASE_BOTH))
: new KeyFrame(Duration.ZERO,
new KeyValue(cell.prefHeightProperty(), startHeight, Interpolator.EASE_BOTH),
new KeyValue(graphic.opacityProperty(), 1.0, Interpolator.EASE_BOTH));
// second key frame
// at the middle point, the cell opacity is 0.0, so we switch in the new content
EventHandler replaceGraphicEvent = new EventHandler() {
@Override public void handle(ActionEvent arg0) {
// switch the content if we have the method
cell.setText(text);
cell.setGraphic(graphic);
}
};
KeyFrame secondFrame = graphic == null
? new KeyFrame(Duration.millis(ANIMATION_DURATION / 2.0), replaceGraphicEvent)
: new KeyFrame(Duration.millis(ANIMATION_DURATION / 2.0),
replaceGraphicEvent,
new KeyValue(graphic.opacityProperty(), 0.0, Interpolator.EASE_BOTH));
// third key frame
EventHandler fireEvent2 = new EventHandler() {
@Override public void handle(ActionEvent arg0) {
cell.setGraphic(graphic);
}
};
KeyFrame thirdFrame = graphic == null
? new KeyFrame(Duration.millis(ANIMATION_DURATION),
fireEvent2,
new KeyValue(cell.prefHeightProperty(), endHeight, Interpolator.EASE_BOTH))
: new KeyFrame(Duration.millis(ANIMATION_DURATION),
fireEvent2,
new KeyValue(cell.prefHeightProperty(), endHeight, Interpolator.EASE_BOTH),
new KeyValue(graphic.opacityProperty(), endOpacity, Interpolator.EASE_BOTH));
timeline.getKeyFrames().addAll(
firstFrame,
secondFrame,
thirdFrame);
timeline.playFromStart();
}
public static class CellResizeFeatures {
public final Cell cell;
public final boolean isExpanded;
public CellResizeFeatures(Cell cell, boolean isExpanded) {
this.cell = cell;
this.isExpanded = isExpanded;
}
}
public static class CellUpdate {
private Node graphic;
private String text;
public CellUpdate(String text, Node graphic) {
this.text = text;
this.graphic = graphic;
}
}
} | |