diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SimplePagingListViewApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SimplePagingListViewApp.java index f918b63d..1ee26fe0 100644 --- a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SimplePagingListViewApp.java +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SimplePagingListViewApp.java @@ -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; @@ -15,10 +19,52 @@ public class SimplePagingListViewApp extends Application { public void start(Stage stage) { SimplePagingListView 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())); diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/TextViewWithPagingListViewApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/TextViewWithPagingListViewApp.java index 0f4ea384..9494f361 100644 --- a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/TextViewWithPagingListViewApp.java +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/TextViewWithPagingListViewApp.java @@ -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; @@ -25,7 +26,7 @@ public class TextViewWithPagingListViewApp extends Application { @Override public void start(Stage stage) { - PagingListView listView = new PagingListView<>(); + SimplePagingListView listView = new SimplePagingListView<>(); listView.setUsingScrollPane(false); listView.setPageSize(3); listView.setFillLastPage(false); @@ -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(); diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/LoadingPane.java b/gemsfx/src/main/java/com/dlsc/gemsfx/LoadingPane.java index 9690f20e..079a698d 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/LoadingPane.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/LoadingPane.java @@ -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; @@ -15,7 +17,6 @@ import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; -import java.time.Duration; import java.util.Objects; /** @@ -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"); } }); @@ -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)); @@ -249,9 +250,9 @@ public void abort() { } } - private final ObjectProperty 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(); } @@ -261,11 +262,11 @@ public final Duration getCommitDelay() { * * @return the delay duration in milliseconds */ - public final ObjectProperty commitDelayProperty() { + public final LongProperty commitDelayProperty() { return commitDelay; } - public final void setCommitDelay(Duration commitDelay) { + public final void setCommitDelay(long commitDelay) { this.commitDelay.set(commitDelay); } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java index defd194b..da411c71 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java @@ -3,14 +3,14 @@ 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; @@ -18,7 +18,6 @@ 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; @@ -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); @@ -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 @@ -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> { @Override @@ -238,10 +286,19 @@ protected Task> createTask() { @Override protected List call() { + try { + System.out.println("sleeping " + getLoadDelayInMillis()); + Thread.sleep(getLoadDelayInMillis()); + } catch (InterruptedException e) { + // do nothing + } + if (!isCancelled()) { Callback> 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 @@ -249,6 +306,8 @@ protected List call() { */ return new ArrayList<>(loader.call(loadRequest)); } + } else { + System.out.println("was cancelled"); } return Collections.emptyList(); @@ -257,10 +316,15 @@ protected List 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(); } @@ -442,48 +506,11 @@ public final ObjectProperty, ListCell>> 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 the type of the items + * Triggers a rebuild of the view without reloading data. */ - public static class SimpleLoader implements Callback> { - - private final ObservableList data; - private final PagingListView 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 listView, ObservableList data) { - this.listView = Objects.requireNonNull(listView); - this.data = Objects.requireNonNull(data); - listView.totalItemCountProperty().bind(Bindings.size(data)); - } - - @Override - public List 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 getListView() { - return listView; - } - - public final ObservableList getData() { - return data; - } + public final void refresh() { + getProperties().remove("refresh-items"); + getProperties().put("refresh-items", true); } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/SimplePagingListView.java b/gemsfx/src/main/java/com/dlsc/gemsfx/SimplePagingListView.java index 3b7bc0a2..03376d6f 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/SimplePagingListView.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/SimplePagingListView.java @@ -1,10 +1,15 @@ package com.dlsc.gemsfx; import javafx.beans.Observable; +import javafx.beans.binding.Bindings; import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleListProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.util.Callback; + +import java.util.List; +import java.util.Objects; /** * A simple version of the paging list view that is completely based on a list of items, just like a normal @@ -21,35 +26,12 @@ public class SimplePagingListView extends PagingListView { * Constructs a new list view and sets a loader that uses the data list. */ public SimplePagingListView() { - setLoader(request -> { - int pageSize = request.getPageSize(); - int index = request.getPage() * pageSize; - return getItems().subList(index, Math.min(index + pageSize, getItems().size())); - }); - + setLoader(new SimpleLoader(this, itemsProperty())); + setLoadDelayInMillis(10); + setCommitLoadStatusDelay(400); loaderProperty().addListener(it -> { throw new UnsupportedOperationException("a custom loader can not be used for this list view"); }); - - totalItemCountProperty().addListener(it -> { - if (!internal) { - throw new UnsupportedOperationException("the total item count can not be explicitly changed for this list view"); - - } - }); - - items.addListener((Observable it) -> { - ObservableList list = getItems(); - if (list != null) { - internal = true; - try { - setTotalItemCount(list.size()); - setPage(Math.min(getTotalItemCount() / getPageSize(), getPage())); - } finally { - internal = false; - } - } - }); } /** @@ -87,4 +69,49 @@ public final ListProperty itemsProperty() { public final void setItems(ObservableList items) { this.items.set(items); } + + /* + * A convenience class to easily provide a loader for paging when the data is given as an + * observable list. + * + * @param the type of the items + */ + private final class SimpleLoader implements Callback> { + + private final ObservableList data; + private final PagingListView 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 listView, ListProperty data) { + this.listView = Objects.requireNonNull(listView); + this.data = Objects.requireNonNull(data); + this.listView.totalItemCountProperty().bind(Bindings.size(data)); + this.data.addListener((Observable it) -> { + ObservableList list = getData(); + listView.setPage(Math.min(listView.getTotalItemCount() / listView.getPageSize(), listView.getPage())); + listView.reload(); + }); + } + + @Override + public List 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 PagingListView getListView() { + return listView; + } + + public ObservableList getData() { + return data; + } + } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java index 0f66dd35..fd579528 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java @@ -43,6 +43,11 @@ public InnerListViewSkin(ListView control, PagingListView pagingListView) content.getStyleClass().add("inner-content"); loadingPane = new LoadingPane(content); + + /* + * Wait at least for the time the loading service is delayed times two. + */ + loadingPane.commitDelayProperty().bind(pagingListView.commitLoadStatusDelayProperty()); loadingPane.statusProperty().bind(pagingListView.loadingStatusProperty()); pagingListView.getProperties().addListener((MapChangeListener) change -> { diff --git a/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-controls.css b/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-controls.css index 131ca687..4cb498f1 100644 --- a/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-controls.css +++ b/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-controls.css @@ -56,7 +56,6 @@ -fx-background-insets: 0px; -fx-background-radius: 2px; -fx-max-height: infinity; - -fx-min-width: 1.5em; } .paging-controls > .pane > .page-buttons-container > .navigation-button:hover { @@ -75,7 +74,7 @@ .paging-controls > .pane > .page-buttons-container > .navigation-button { -fx-padding: 2px 5px; - -fx-graphic-text-gap: 10px; + -fx-graphic-text-gap: 5px; } .paging-controls > .pane > .page-buttons-container > .navigation-button.first-page-button {