Skip to content

Commit

Permalink
Fine-tuning behaviour of PagingListView.
Browse files Browse the repository at this point in the history
  • Loading branch information
dlemmermann committed Dec 11, 2024
1 parent 98ac6c8 commit 07d0cbd
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.scenicview.ScenicView;
Expand All @@ -15,10 +19,52 @@ public class SimplePagingListViewApp extends Application {
public void start(Stage stage) {
SimplePagingListView<String> pagingListView = new SimplePagingListView<>();
pagingListView.setPrefWidth(400);
for (int i = 0; i < 200; i++) {
for (int i = 0; i < 100; i++) {
pagingListView.getItems().add("Item " + (i + 1));
}

pagingListView.setCellFactory(lv -> new ListCell<>() {
{
setOnContextMenuRequested(request -> {
MenuItem deleteItem = new MenuItem("Delete");
deleteItem.setOnAction(evt -> pagingListView.getItems().remove(getItem()));

MenuItem duplicateItem = new MenuItem("Duplicate");
duplicateItem.setOnAction(evt -> {
int index = pagingListView.getItems().indexOf(getItem());
pagingListView.getItems().add(index + 1, getItem() + " (copy)");
});

MenuItem replaceItem = new MenuItem("Replace");
replaceItem.setOnAction(evt -> {
int index = pagingListView.getItems().indexOf(getItem());
pagingListView.getItems().set(index, getItem() + " (replacement)");
});

MenuItem showItem = new MenuItem("Show item at current index + 10");
showItem.setOnAction(evt -> {
int index = pagingListView.getItems().indexOf(getItem());
pagingListView.show(pagingListView.getItems().get(index + 10));
pagingListView.getSelectionModel().select(pagingListView.getItems().get(index + 10));
});

ContextMenu contextMenu = new ContextMenu(deleteItem, duplicateItem, replaceItem, showItem);
contextMenu.show(lv, request.getScreenX(), request.getScreenY());
});

}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);

if (item != null) {
setText(item);
} else {
setText("");
}
}
});

Button scenicView = new Button("Scenic View");
scenicView.setOnAction(evt -> ScenicView.show(scenicView.getScene()));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dlsc.gemsfx.demo;

import com.dlsc.gemsfx.PagingListView;
import com.dlsc.gemsfx.SimplePagingListView;
import com.dlsc.gemsfx.TextView;
import com.dlsc.gemsfx.util.StageManager;
import javafx.application.Application;
Expand All @@ -25,7 +26,7 @@ public class TextViewWithPagingListViewApp extends Application {

@Override
public void start(Stage stage) {
PagingListView<String> listView = new PagingListView<>();
SimplePagingListView<String> listView = new SimplePagingListView<>();
listView.setUsingScrollPane(false);
listView.setPageSize(3);
listView.setFillLastPage(false);
Expand All @@ -35,8 +36,7 @@ public void start(Stage stage) {
data.add("Item " + i + "\n\nLorem ipsum dolor sit amet consectetur adipiscing elit nunc hendrerit purus, nisi dapibus primis nibh volutpat fringilla ad nisl urna pos-uere!\nCubilia sagittis egestas pharetra sociis montes nullam netus erat.\n\nFusce mauris condimentum neque morbi nunc ligula pretium vehicula nulla, platea dictum mus sapien pulvinar eget porta mi praesent, orci hac dignissim suscipit imperdiet sem per a.\nMauris pellentesque dui vitae velit netus venenatis diam felis urna ultrices, potenti pretium sociosqu eros dictumst dis aenean nibh cursus, leo sagittis integer nullam malesuada aliquet et metus vulputate. Interdum facilisis congue ac proin libero mus ullamcorper mauris leo imperdiet eleifend porta, posuere dignissim erat tincidunt vehicula habitant taciti porttitor scelerisque laoreet neque. Habitant etiam cubilia tempor inceptos ad aptent est et varius, vitae imperdiet phasellus feugiat class purus curabitur ullamcorper maecenas, venenatis mollis fusce cras leo eros metus proin. Fusce aenean sociosqu dis habitant mi sapien inceptos, orci lacinia nisi nascetur convallis at erat sociis, purus integer arcu feugiat sollicitudin libero.");
}

listView.setLoader(new PagingListView.SimpleLoader<>(listView, data));

listView.setItems(data);
listView.setCellFactory(lv -> new ListCell<>() {

private final TextView textView = new TextView();
Expand Down
17 changes: 9 additions & 8 deletions gemsfx/src/main/java/com/dlsc/gemsfx/LoadingPane.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import javafx.application.Platform;
import javafx.beans.DefaultProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
Expand All @@ -15,7 +17,6 @@
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;

import java.time.Duration;
import java.util.Objects;

/**
Expand Down Expand Up @@ -96,8 +97,8 @@ public LoadingPane() {
});

commitDelay.addListener(it -> {
if (getCommitDelay() == null) {
throw new IllegalArgumentException("commit delay can not be null");
if (getCommitDelay() < 0) {
throw new IllegalArgumentException("commit delay must be greater than or equal to zero");
}
});

Expand Down Expand Up @@ -234,7 +235,7 @@ public void run() {
try {
if (status.equals(Status.LOADING)) {
// only delay the change when switching to the state that represents that loading is currently ongoing.
Thread.sleep(getCommitDelay().toMillis());
Thread.sleep(getCommitDelay());
}
if (!stopped) {
Platform.runLater(() -> committedStatus.set(status));
Expand All @@ -249,9 +250,9 @@ public void abort() {
}
}

private final ObjectProperty<Duration> commitDelay = new SimpleObjectProperty<>(this, "commitDelay", Duration.ofMillis(200));
private final LongProperty commitDelay = new SimpleLongProperty(this, "commitDelay", 200L);

public final Duration getCommitDelay() {
public final long getCommitDelay() {
return commitDelay.get();
}

Expand All @@ -261,11 +262,11 @@ public final Duration getCommitDelay() {
*
* @return the delay duration in milliseconds
*/
public final ObjectProperty<Duration> commitDelayProperty() {
public final LongProperty commitDelayProperty() {
return commitDelay;
}

public final void setCommitDelay(Duration commitDelay) {
public final void setCommitDelay(long commitDelay) {
this.commitDelay.set(commitDelay);
}

Expand Down
119 changes: 73 additions & 46 deletions gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@
import com.dlsc.gemsfx.LoadingPane.Status;
import com.dlsc.gemsfx.skins.InnerListViewSkin;
import com.dlsc.gemsfx.skins.PagingListViewSkin;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Orientation;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Cell;
import javafx.scene.control.ListCell;
Expand Down Expand Up @@ -83,8 +82,7 @@ public PagingListView() {
loadingService.setOnRunning(evt -> loadingStatus.set(Status.LOADING));
loadingService.setOnFailed(evt -> loadingStatus.set(Status.ERROR));

InvalidationListener loadListener = it -> loadingService.restart();

InvalidationListener loadListener = it -> reload();
pageProperty().addListener(loadListener);
pageSizeProperty().addListener(loadListener);
totalItemCountProperty().addListener(loadListener);
Expand Down Expand Up @@ -116,6 +114,12 @@ protected void updateItem(T item, boolean empty) {
throw new IllegalArgumentException("unsupported location for the paging controls: " + newLocation);
}
});

loadDelayInMillis.addListener(it -> {
if (getLoadDelayInMillis() < 0) {
throw new IllegalArgumentException("load delay must be >= 0");
}
});
}

@Override
Expand Down Expand Up @@ -228,6 +232,50 @@ public final void setLoadingStatus(Status loadingStatus) {
this.loadingStatus.set(loadingStatus);
}

private final LongProperty loadDelayInMillis = new SimpleLongProperty(this, "loadDelayInMillis", 200L);

public final long getLoadDelayInMillis() {
return loadDelayInMillis.get();
}

/**
* The delay in milliseconds before the loading service will actually try to retrieve the data from (for example)
* a backend. This delay is around a few hundred milliseconds by default. Delaying the loading has the advantage
* that sudden property changes will not trigger multiple backend queries but will get batched together to a single
* reload operation.
*
* @return the delay before data will actually be loaded
*/
public final LongProperty loadDelayInMillisProperty() {
return loadDelayInMillis;
}

public final void setLoadDelayInMillis(long loadDelayInMillis) {
this.loadDelayInMillis.set(loadDelayInMillis);
}

private final LongProperty commitLoadStatusDelay = new SimpleLongProperty(this, "commitLoadStatusDelay", 400L);

public final long getCommitLoadStatusDelay() {
return commitLoadStatusDelay.get();
}

/**
* The delay in milliseconds before the list view will display the progress indicator for long running
* load operations.
*
* @see LoadingPane#commitDelayProperty()
*
* @return the commit delay for the nested loading pane
*/
public final LongProperty commitLoadStatusDelayProperty() {
return commitLoadStatusDelay;
}

public final void setCommitLoadStatusDelay(long commitLoadStatusDelay) {
this.commitLoadStatusDelay.set(commitLoadStatusDelay);
}

private class LoadingService extends Service<List<T>> {

@Override
Expand All @@ -238,17 +286,28 @@ protected Task<List<T>> createTask() {

@Override
protected List<T> call() {
try {
System.out.println("sleeping " + getLoadDelayInMillis());
Thread.sleep(getLoadDelayInMillis());
} catch (InterruptedException e) {
// do nothing
}

if (!isCancelled()) {
Callback<LoadRequest, List<T>> loader = PagingListView.this.loader.get();
if (loader != null) {

actualLoads++;
System.out.println("reloads: " + reloads + ", actual loads: " + actualLoads);
/*
* Important to wrap in a list, otherwise we can get a concurrent modification
* exception when the result gets applied to the "items" list in the service
* event handler for "success". Not sure why this fixes that issue.
*/
return new ArrayList<>(loader.call(loadRequest));
}
} else {
System.out.println("was cancelled");
}

return Collections.emptyList();
Expand All @@ -257,10 +316,15 @@ protected List<T> call() {
}
}

private int reloads;
private int actualLoads;

/**
* Triggers an explicit reload of the list view.
*/
public final void reload() {
reloads++;
System.out.println("reloading");
loadingService.restart();
}

Expand Down Expand Up @@ -442,48 +506,11 @@ public final ObjectProperty<Callback<ListView<T>, ListCell<T>>> cellFactoryPrope
return cellFactory;
}

public void refresh() {
getProperties().remove("refresh-items");
getProperties().put("refresh-items", true);
}

/**
* A convenience class to easily provide a loader for paging when the data is given as an
* observable list.
*
* @param <S> the type of the items
* Triggers a rebuild of the view without reloading data.
*/
public static class SimpleLoader<S> implements Callback<LoadRequest, List<S>> {

private final ObservableList<S> data;
private final PagingListView<S> listView;

/**
* Constructs a new simple loader for the given list view and the given data.
*
* @param listView the list view where the loader will be used
* @param data the observable list that is providing the data / the items
*/
public SimpleLoader(PagingListView<S> listView, ObservableList<S> data) {
this.listView = Objects.requireNonNull(listView);
this.data = Objects.requireNonNull(data);
listView.totalItemCountProperty().bind(Bindings.size(data));
}

@Override
public List<S> call(LoadRequest param) {
int page = param.getPage();
int pageSize = param.getPageSize();
int offset = page * pageSize;
return data.subList(offset, Math.min(data.size(), offset + pageSize));
}

public final PagingListView<S> getListView() {
return listView;
}

public final ObservableList<S> getData() {
return data;
}
public final void refresh() {
getProperties().remove("refresh-items");
getProperties().put("refresh-items", true);
}
}
Loading

0 comments on commit 07d0cbd

Please sign in to comment.