jidefx.utils.LazyLoadUtils Maven / Gradle / Ivy
/*
* @(#)LazyLoadUtils.java 5/19/2013
*
* Copyright 2002 - 2013 JIDE Software Inc. All rights reserved.
*/
package jidefx.utils;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox;
import javafx.util.Callback;
import javafx.util.Duration;
/**
* {@code LazyLoadUtils} provides an easy way to implement the lazy loading feature in a {@code Combobox} or a
* {@code ChoiceBox}. Sometimes it takes a long time to create the ObservableList for the control. By using this
* LazyLoadUtils, we will create the ObservableList only when the popup is about to show or after a delay, so that it
* doesn't block the UI thread. The UI will come up without the ObservableList and will be set later.
*
* To use it, simply call one of the install methods. The callback will take care of the creation of the
* ObservableList.
*
* There are two ways to trigger the callback. The first trigger is when the ComboBox or the ChoiceBox is clicked before
* the popup content is about to show. The beforeShowing flag will determine if this trigger will be triggered. Default
* is true. If this trigger is triggered, we will call the callback on the UI thread which means users will have to wait
* for the callback to finish. We will have to do that because the popup doesn't make sense to show without the
* ObservableList. The second trigger is triggered after a delay. The delay time is controlled by the delay parameter.
* When it reaches the specified delay duration, a worker thread will run to call the callback so that it doesn't block
* the UI. Either trigger can come first but it will be triggered only once. Ideally, when the UI is shown, if user
* never clicks the ComboBox or the ChoiceBox, the second trigger kicks in and populates the data behind the scene. Not
* ideally, user clicks on it right away and then he/she has to wait a while. However it is still much better than
* waiting for the same period of time before the UI showing.
*
* A typical use case is to create a ComboBox that list all the fonts in the system. However the Font.getFamilies call
* is expensive, especially the system has a lot of fonts. The following code will take care of it.
*
{@code
* ComboBox<String> fontComboBox = new ComboBox<>();
* fontComboBox.setValue("Arial"); // set a default value without setting the Items
* LazyLoadUtils.install(fontComboBox, new Callback<ComboBox<String>, ObservableList<String>>() {
* public ObservableList<String> call(ComboBox<String> comboBox) {
* return FXCollections.observableArrayList(Font.getFamilies());
* }
* });
* }
*/
public class LazyLoadUtils {
private final static double DELAY = 200;
private final static String PROPERTY_TASK = "LazyLoadUtils.Task"; //NON-NLS
/**
* Customizes the ComboBox to call the callback and setItems until user clicks on the ComboBox or after a 200 ms
* delay.
*
* @param comboBox the ComboBox.
* @param callback the callback to create the ObservableList.
* @param The type of the value that has been selected or otherwise entered in to this ComboBox.
*/
public static void install(ComboBox comboBox, Callback, ObservableList> callback) {
install(comboBox, callback, Duration.millis(DELAY), true, null);
}
/**
* Customizes the ComboBox to call the callback and setItems until user clicks on the ComboBox or after a delay.
*
* @param comboBox the ComboBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param The type of the value that has been selected or otherwise entered in to this ComboBox.
*/
public static void install(ComboBox comboBox, Callback, ObservableList> callback, Duration delay) {
install(comboBox, callback, delay, true, null);
}
/**
* Customizes the ComboBox to call the callback and setItems until user clicks on the ComboBox (if beforeShowing is
* true) or after a 200ms delay.
*
* @param comboBox the ComboBox.
* @param callback the callback to create the ObservableList.
* @param beforeShowing whether to trigger the callback before the ComboBox popup is about to show, if the callback
* @param The type of the value that has been selected or otherwise entered in to this ComboBox.
*/
public static void install(ComboBox comboBox, Callback, ObservableList> callback, boolean beforeShowing) {
install(comboBox, callback, Duration.millis(DELAY), beforeShowing, null);
}
/**
* Customizes the ComboBox to call the callback until user clicks on the ComboBox (if beforeShowing is true) or
* after a delay.
*
* @param comboBox the ComboBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param beforeShowing whether to trigger the callback before the ComboBox popup is about to show, if the callback
* hasn't called yet.
* @param The type of the value that has been selected or otherwise entered in to this ComboBox.
*/
public static void install(ComboBox comboBox, Callback, ObservableList> callback, Duration delay, boolean beforeShowing) {
customizeComboBox(comboBox, callback, delay, beforeShowing, null);
}
/**
* Customizes the ComboBox to call the callback until user clicks on the ComboBox (if beforeShowing is true) or
* after a delay. It will also set the initialValue to the ComboBox.
*
* @param comboBox the ComboBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param beforeShowing whether to trigger the callback before the ComboBox popup is about to show, if the callback
* hasn't called yet.
* @param initialValue the initial value. Null if the value has been set on the ComboBox.
* @param The type of the value that has been selected or otherwise entered in to this ComboBox.
*/
public static void install(ComboBox comboBox, Callback, ObservableList> callback, Duration delay, boolean beforeShowing, T initialValue) {
customizeComboBox(comboBox, callback, delay, beforeShowing, initialValue);
}
/**
* Customizes the ComboBox to call the callback until user clicks on the ComboBox or after a delay.
*
* @param comboBox the ComboBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param beforeShowing whether to trigger the callback before the ComboBox popup is about to show, if the callback
* hasn't called yet.
* @param initialValue the initial value. Null if the value has been set on the ComboBox.
* @param The type of the value that has been selected or otherwise entered in to this ComboBox.
*/
protected static void customizeComboBox(ComboBox comboBox, Callback, ObservableList> callback, Duration delay, boolean beforeShowing, T initialValue) {
Task oldTask = getTask(comboBox);
if (oldTask != null) {
oldTask.cancel(true);
}
if (initialValue != null) comboBox.setValue(initialValue);
Task> task = new Task>() {
@Override
protected ObservableList call() throws Exception {
return callback.call(comboBox);
}
};
task.setOnSucceeded(new EventHandler() {
@Override
public void handle(WorkerStateEvent event) {
T item = comboBox.getValue();
Object value = event.getSource().getValue();
if (value instanceof ObservableList) {
//noinspection unchecked
comboBox.setItems((ObservableList) value);
//noinspection unchecked
if (item == null || !((ObservableList) value).contains(item)) {
comboBox.setValue(null);
comboBox.getSelectionModel().select(0);
}
else {
// trick in order to show the item. should be a bug in JavaFX combobox
comboBox.setValue(null);
comboBox.setValue(item);
}
}
}
});
// when user clicks on the combobox, we will retrieve the fonts, not using thread because we don't want the empty
// list to show up. User has to wait for a while if the task takes a long time
if (beforeShowing) {
comboBox.setOnShowing(new EventHandler() {
@Override
public void handle(Event event) {
if (!task.isRunning() && !task.isDone()) {
task.run();
}
comboBox.setOnShowing(null);
}
});
}
// or use Timeline to get the font in a thread after 1 second, if user didn't click on the font combobox before that
startTimer(delay, task);
putTask(comboBox, task);
}
/**
* Customizes the ChoiceBox to call the callback and setItems until user clicks on the ChoiceBox or after a 200 ms
* delay.
*
* @param choiceBox the ChoiceBox.
* @param callback the callback to create the ObservableList.
* @param The type of the value that has been selected or otherwise entered in to this ChoiceBox.
*/
public static void install(ChoiceBox choiceBox, Callback, ObservableList> callback) {
install(choiceBox, callback, Duration.millis(DELAY), true, null);
}
/**
* Customizes the ChoiceBox to call the callback and setItems until user clicks on the ChoiceBox or after a delay.
*
* @param choiceBox the ChoiceBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param The type of the value that has been selected or otherwise entered in to this ChoiceBox.
*/
public static void install(ChoiceBox choiceBox, Callback, ObservableList> callback, Duration delay) {
install(choiceBox, callback, delay, true, null);
}
/**
* Customizes the ChoiceBox to call the callback and setItems until user clicks on the ChoiceBox (if beforeShowing
* is true) or after a 200ms delay.
*
* @param choiceBox the ChoiceBox.
* @param callback the callback to create the ObservableList.
* @param beforeShowing whether to trigger the callback before the ChoiceBox popup is about to show, if the
* callback
* @param The type of the value that has been selected or otherwise entered in to this ChoiceBox.
*/
public static void install(ChoiceBox choiceBox, Callback, ObservableList> callback, boolean beforeShowing) {
install(choiceBox, callback, Duration.millis(DELAY), beforeShowing, null);
}
/**
* Customizes the ChoiceBox to call the callback until user clicks on the ChoiceBox (if beforeShowing is true) or
* after a delay.
*
* @param choiceBox the ChoiceBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param beforeShowing whether to trigger the callback before the ChoiceBox popup is about to show, if the callback
* hasn't called yet.
* @param The type of the value that has been selected or otherwise entered in to this ChoiceBox.
*/
public static void install(ChoiceBox choiceBox, Callback, ObservableList> callback, Duration delay, boolean beforeShowing) {
customizeChoiceBox(choiceBox, callback, delay, beforeShowing, null);
}
/**
* Customizes the ChoiceBox to call the callback until user clicks on the ChoiceBox (if beforeShowing is true) or
* after a delay. It will also set the initialValue to the ChoiceBox.
*
* @param choiceBox the ChoiceBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param beforeShowing whether to trigger the callback before the ChoiceBox popup is about to show, if the callback
* hasn't called yet.
* @param initialValue the initial value. Null if the value has been set on the ChoiceBox.
* @param The type of the value that has been selected or otherwise entered in to this ChoiceBox.
*/
public static void install(ChoiceBox choiceBox, Callback, ObservableList> callback, Duration delay, boolean beforeShowing, T initialValue) {
customizeChoiceBox(choiceBox, callback, delay, beforeShowing, initialValue);
}
/**
* Customizes the ChoiceBox to call the callback until user clicks on the ChoiceBox or after a delay.
*
* @param choiceBox the ChoiceBox.
* @param callback the callback to create the ObservableList.
* @param delay the delay before the callback is called.
* @param beforeShowing whether to trigger the callback before the ChoiceBox popup is about to show, if the callback
* hasn't called yet.
* @param initialValue the initial value. Null if the value has been set on the ChoiceBox.
* @param The type of the value that has been selected or otherwise entered in to this ChoiceBox.
*/
protected static void customizeChoiceBox(ChoiceBox choiceBox, Callback, ObservableList> callback, Duration delay, boolean beforeShowing, T initialValue) {
Task oldTask = getTask(choiceBox);
if (oldTask != null) {
oldTask.cancel(true);
}
if (initialValue != null) choiceBox.setValue(initialValue);
Task> task = new Task>() {
@Override
protected ObservableList call() throws Exception {
return callback.call(choiceBox);
}
};
task.setOnSucceeded(new EventHandler() {
@Override
public void handle(WorkerStateEvent event) {
T item = choiceBox.getValue();
Object value = event.getSource().getValue();
if (value instanceof ObservableList) {
//noinspection unchecked
choiceBox.setItems((ObservableList) value);
//noinspection unchecked
if (item == null || !((ObservableList) value).contains(item)) {
choiceBox.setValue(null);
choiceBox.getSelectionModel().select(0);
}
else {
// trick in order to show the item. should be a bug in JavaFX combobox
choiceBox.setValue(null);
choiceBox.setValue(item);
}
}
}
});
// when user clicks on the choicebox, we will retrieve the fonts, not using thread because we don't want the empty
// list to show up. User has to wait for a while if the task takes a long time
if (beforeShowing) {
choiceBox.setOnContextMenuRequested(new EventHandler() {
@Override
public void handle(Event event) {
if (!task.isRunning() && !task.isDone()) {
task.run();
}
choiceBox.setOnContextMenuRequested(null);
}
});
}
// or use Timeline to get the font in a thread after 1 second, if user didn't click on the font choicebox before that
startTimer(delay, task);
putTask(choiceBox, task);
}
// common
private static void startTimer(Duration delay, Task> task) {
if (delay != null && !delay.isIndefinite()) {
Timeline timeline = new Timeline();
timeline.getKeyFrames().add(new KeyFrame(delay, new EventHandler() {
public void handle(ActionEvent t) {
if (!task.isRunning() && !task.isDone()) {
new Thread(task).start();
}
}
}));
timeline.play();
}
}
private static Task getTask(Node node) {
Object task = node.getProperties().get(PROPERTY_TASK);
if (task instanceof Task) {
return (Task) task;
}
else {
return null;
}
}
private static void putTask(Node node, Task task) {
node.getProperties().put(PROPERTY_TASK, task);
}
}