From 73c7e03358186c87a6228ea25e616452b05c6e1f Mon Sep 17 00:00:00 2001 From: Dirk Lemmermann Date: Tue, 3 Dec 2024 15:38:07 +0100 Subject: [PATCH] Add paging controls settings and improve loading visualization Introduced a new class, `PagingControlsSettingsView`, to enhance paging controls configurability. Updated loading behavior and added new 'loading' status to `PagingListView`, improving user feedback during data fetch operations. Refined CSS and bindings to better manage layout and styling of pagination elements. --- .../demo/PagingControlsSettingsView.java | 61 +++++++++++++++++++ .../dlsc/gemsfx/demo/PagingListViewApp.java | 15 +++-- .../com/dlsc/gemsfx/PagingControlBase.java | 57 +++++++++++++++++ .../java/com/dlsc/gemsfx/PagingControls.java | 54 ---------------- .../java/com/dlsc/gemsfx/PagingListView.java | 26 +++++++- .../dlsc/gemsfx/skins/InnerListViewSkin.java | 32 ++++++---- .../dlsc/gemsfx/skins/PagingListViewSkin.java | 12 ++-- .../com/dlsc/gemsfx/paging-list-view.css | 4 +- 8 files changed, 184 insertions(+), 77 deletions(-) create mode 100644 gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsSettingsView.java diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsSettingsView.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsSettingsView.java new file mode 100644 index 00000000..c1a7a8af --- /dev/null +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsSettingsView.java @@ -0,0 +1,61 @@ +package com.dlsc.gemsfx.demo; + +import com.dlsc.gemsfx.PagingControlBase; +import com.dlsc.gemsfx.PagingControls; +import com.dlsc.gemsfx.Spacer; +import javafx.beans.binding.Bindings; +import javafx.geometry.Pos; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +import java.util.List; + +public class PagingControlsSettingsView extends VBox { + + public PagingControlsSettingsView(PagingControlBase pagingControls) { + setSpacing(10); + + Label pageLabel = new Label(); + pageLabel.textProperty().bind(Bindings.createStringBinding(() -> "Page Index: " + pagingControls.getPage(), pagingControls.pageProperty())); + + Label pageCountLabel = new Label(); + pageCountLabel.textProperty().bind(Bindings.createStringBinding(() -> "Page count: " + pagingControls.getPageCount(), pagingControls.pageCountProperty())); + + ChoiceBox displayModeChoiceBox = new ChoiceBox<>(); + displayModeChoiceBox.getItems().setAll(PagingControls.FirstLastPageDisplayMode.values()); + displayModeChoiceBox.valueProperty().bindBidirectional(pagingControls.firstLastPageDisplayModeProperty()); + + CheckBox showPreviousNextButton = new CheckBox("Show prev / next buttons"); + showPreviousNextButton.selectedProperty().bindBidirectional(pagingControls.showPreviousNextPageButtonProperty()); + + ChoiceBox strategyChoiceBox = new ChoiceBox<>(); + strategyChoiceBox.getItems().addAll(PagingControlBase.MessageLabelStrategy.values()); + strategyChoiceBox.valueProperty().bindBidirectional(pagingControls.messageLabelStrategyProperty()); + + ChoiceBox maxPageIndicatorsBox = new ChoiceBox<>(); + maxPageIndicatorsBox.getItems().setAll(List.of(1, 2, 5, 10)); + maxPageIndicatorsBox.valueProperty().bindBidirectional(pagingControls.maxPageIndicatorsCountProperty().asObject()); + + HBox displayModeBox = new HBox(5, new Label("Display mode: "), displayModeChoiceBox); + displayModeBox.setAlignment(Pos.CENTER_LEFT); + + HBox strategyBox = new HBox(5, new Label("Label strategy: "), strategyChoiceBox); + strategyBox.setAlignment(Pos.CENTER_LEFT); + + HBox indicatorBox = new HBox(5, new Label("# Indicators: "), maxPageIndicatorsBox); + indicatorBox.setAlignment(Pos.CENTER_LEFT); + + FlowPane flowPane = new FlowPane(pageLabel, pageCountLabel, new Spacer(), showPreviousNextButton, displayModeBox, strategyBox, indicatorBox); + flowPane.setVgap(10); + flowPane.setHgap(20); + + setMaxHeight(Region.USE_PREF_SIZE); + + getChildren().setAll(flowPane); + } +} diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingListViewApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingListViewApp.java index caba3237..42489ceb 100644 --- a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingListViewApp.java +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingListViewApp.java @@ -20,13 +20,20 @@ public class PagingListViewApp extends Application { public void start(Stage stage) { PagingListView pagingListView = new PagingListView<>(); pagingListView.setPrefWidth(400); - pagingListView.setTotalItemCount(30); - pagingListView.setPageSize(10); + pagingListView.setTotalItemCount(300); + pagingListView.setPageSize(15); pagingListView.setLoader(lv -> { + if (Math.random() > .75) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } List data = new ArrayList<>(); int offset = lv.getPage() * lv.getPageSize(); for (int i = 0; i < lv.getPageSize(); i++) { - data.add("Item " + (offset + i)); + data.add("Item " + (offset + i + 1)); } return data; }); @@ -34,7 +41,7 @@ public void start(Stage stage) { Button scenicView = new Button("Scenic View"); scenicView.setOnAction(evt -> ScenicView.show(scenicView.getScene())); - VBox box = new VBox(20, pagingListView, scenicView); + VBox box = new VBox(20, pagingListView, new PagingControlsSettingsView(pagingListView), scenicView); box.setPadding(new Insets(20)); Scene scene = new Scene(box); diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java index 2b2e5fba..04c203c0 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java @@ -1,5 +1,6 @@ package com.dlsc.gemsfx; +import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; @@ -8,12 +9,48 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.HPos; import javafx.scene.Node; import javafx.scene.control.Control; +import javafx.scene.control.Label; import javafx.util.Callback; public abstract class PagingControlBase extends Control { + public PagingControlBase() { + setMessageLabelProvider(view -> { + if (getPageCount() == 0) { + return "No items"; + } + + if (getPageCount() == 1) { + return "Showing all items"; + } + + int startIndex = (view.getPage() * getPageSize()) + 1; + int endIndex = startIndex + getPageSize() - 1; + + endIndex = Math.min(endIndex, getTotalItemCount()); + return "Showing items " + startIndex + " to " + endIndex + " of " + getTotalItemCount(); + }); + + pageCount.bind(Bindings.createIntegerBinding(() -> { + int count = getTotalItemCount() / getPageSize(); + if (getTotalItemCount() % getPageSize() > 0) { + count++; + } + return count; + }, totalItemCountProperty(), pageSizeProperty())); + + Label firstPageDivider = new Label("..."); + firstPageDivider.getStyleClass().addAll("page-divider", "first-page-divider"); + setFirstPageDivider(firstPageDivider); + + Label lastPageDivider = new Label("..."); + lastPageDivider.getStyleClass().addAll("page-divider", "first-page-divider"); + setLastPageDivider(lastPageDivider); + } + private final BooleanProperty showPreviousNextPageButton = new SimpleBooleanProperty(this, "showPreviousNextButton", true); public final boolean getShowPreviousNextPageButton() { @@ -306,4 +343,24 @@ public final IntegerProperty pageSizeProperty() { public final void setPageSize(int pageSize) { this.pageSize.set(pageSize); } + + private final ObjectProperty alignment = new SimpleObjectProperty<>(this, "alignment", HPos.RIGHT); + + public final HPos getAlignment() { + return alignment.get(); + } + + /** + * The alignment property controls where in the view the paging buttons will appear: left, + * center, middle. + * + * @return the alignment / the position of the paging buttons + */ + public final ObjectProperty alignmentProperty() { + return alignment; + } + + public final void setAlignment(HPos alignment) { + this.alignment.set(alignment); + } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControls.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControls.java index d4c5bd8d..7c2c4b65 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControls.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControls.java @@ -2,9 +2,6 @@ import com.dlsc.gemsfx.skins.PagingControlsSkin; import javafx.beans.binding.Bindings; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.geometry.HPos; import javafx.scene.control.Label; import javafx.scene.control.Skin; import javafx.scene.input.KeyCode; @@ -27,21 +24,6 @@ public PagingControls() { getStyleClass().add(DEFAULT_STYLE_CLASS); addEventFilter(MouseEvent.MOUSE_PRESSED, evt -> requestFocus()); - setMessageLabelProvider(view -> { - if (getPageCount() == 0) { - return "No items"; - } - - if (getPageCount() == 1) { - return "Showing all items"; - } - - int startIndex = (view.getPage() * getPageSize()) + 1; - int endIndex = startIndex + getPageSize() - 1; - - endIndex = Math.min(endIndex, getTotalItemCount()); - return "Showing items " + startIndex + " to " + endIndex + " of " + getTotalItemCount(); - }); addEventHandler(KeyEvent.KEY_PRESSED, evt -> { if (Objects.equals(evt.getCode(), KeyCode.RIGHT)) { @@ -54,22 +36,6 @@ public PagingControls() { lastPage(); } }); - - pageCount.bind(Bindings.createIntegerBinding(() -> { - int count = getTotalItemCount() / getPageSize(); - if (getTotalItemCount() % getPageSize() > 0) { - count++; - } - return count; - }, totalItemCountProperty(), pageSizeProperty())); - - Label firstPageDivider = new Label("..."); - firstPageDivider.getStyleClass().addAll("page-divider", "first-page-divider"); - setFirstPageDivider(firstPageDivider); - - Label lastPageDivider = new Label("..."); - lastPageDivider.getStyleClass().addAll("page-divider", "first-page-divider"); - setLastPageDivider(lastPageDivider); } @Override @@ -81,24 +47,4 @@ protected Skin createDefaultSkin() { public String getUserAgentStylesheet() { return Objects.requireNonNull(PagingControls.class.getResource("paging-controls.css")).toExternalForm(); } - - private final ObjectProperty alignment = new SimpleObjectProperty<>(this, "alignment", HPos.RIGHT); - - public final HPos getAlignment() { - return alignment.get(); - } - - /** - * The alignment property controls where in the view the paging buttons will appear: left, - * center, middle. - * - * @return the alignment / the position of the paging buttons - */ - public final ObjectProperty alignmentProperty() { - return alignment; - } - - public final void setAlignment(HPos alignment) { - this.alignment.set(alignment); - } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java index 0accbfe7..d1da4b1d 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java @@ -1,5 +1,6 @@ package com.dlsc.gemsfx; +import com.dlsc.gemsfx.LoadingPane.Status; import com.dlsc.gemsfx.skins.InnerListViewSkin; import com.dlsc.gemsfx.skins.PagingListViewSkin; import javafx.beans.InvalidationListener; @@ -13,6 +14,7 @@ import javafx.collections.ObservableList; import javafx.concurrent.Service; import javafx.concurrent.Task; +import javafx.geometry.HPos; import javafx.scene.Node; import javafx.scene.control.Cell; import javafx.scene.control.ListCell; @@ -52,6 +54,7 @@ public PagingListView() { listView.setSkin(innerListViewSkin); loadingService.setOnSucceeded(evt -> { + loadingStatus.set(Status.OK); List newList = loadingService.getValue(); if (newList != null) { items.setAll(newList); @@ -60,6 +63,9 @@ public PagingListView() { } }); + loadingService.setOnRunning(evt -> loadingStatus.set(Status.LOADING)); + loadingService.setOnFailed(evt -> loadingStatus.set(Status.ERROR)); + InvalidationListener loadListener = it -> loadingService.restart(); pageProperty().addListener(loadListener); @@ -101,6 +107,20 @@ public final ListView getListView() { return listView; } + private final ObjectProperty loadingStatus = new SimpleObjectProperty<>(this, "loadingStatus", Status.OK); + + public final Status getLoadingStatus() { + return loadingStatus.get(); + } + + public final ObjectProperty loadingStatusProperty() { + return loadingStatus; + } + + public final void setLoadingStatus(Status loadingStatus) { + this.loadingStatus.set(loadingStatus); + } + private class LoadingService extends Service> { @Override @@ -109,7 +129,11 @@ protected Task> createTask() { @Override protected List call() { if (!isCancelled()) { - return loader.get().call(PagingListView.this); + Callback, List> loader = PagingListView.this.loader.get(); + if (loader == null) { + throw new IllegalArgumentException("data loader can not be null"); + } + return loader.call(PagingListView.this); } return Collections.emptyList(); } 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 f1f757a4..b4e2d83d 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java @@ -1,36 +1,44 @@ package com.dlsc.gemsfx.skins; +import com.dlsc.gemsfx.LoadingPane; import com.dlsc.gemsfx.PagingListView; -import javafx.geometry.Orientation; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.skin.ListViewSkin; -import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import javafx.util.Callback; +import java.util.Objects; + public class InnerListViewSkin extends ListViewSkin { private final VBox content = new VBox() { @Override - public Orientation getContentBias() { - return Orientation.HORIZONTAL; + public String getUserAgentStylesheet() { + return Objects.requireNonNull(PagingListView.class.getResource("paging-list-view.css")).toExternalForm(); } }; private final PagingListView pagingListView; + private final LoadingPane loadingPane; public InnerListViewSkin(ListView control, PagingListView pagingListView) { super(control); + this.pagingListView = pagingListView; + content.getStyleClass().add("content"); - getChildren().setAll(content); + + loadingPane = new LoadingPane(content); + loadingPane.statusProperty().bind(pagingListView.loadingStatusProperty()); + + getChildren().setAll(loadingPane); updateItems(); } @Override protected void layoutChildren(double x, double y, double w, double h) { - content.resizeRelocate(x, y, w, h); + loadingPane.resizeRelocate(x, y, w, h); } public void updateItems() { @@ -61,31 +69,31 @@ public void updateItems() { @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return content.minHeight(width) + topInset + bottomInset; + return loadingPane.minHeight(width) + topInset + bottomInset; } @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return content.prefHeight(width) + topInset + bottomInset; + return loadingPane.prefHeight(width) + topInset + bottomInset; } @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return content.maxHeight(width) + topInset + bottomInset; + return loadingPane.maxHeight(width) + topInset + bottomInset; } @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - return content.minWidth(height) + leftInset + rightInset; + return loadingPane.minWidth(height) + leftInset + rightInset; } @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - return content.prefWidth(height) + leftInset + rightInset; + return loadingPane.prefWidth(height) + leftInset + rightInset; } @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - return content.maxWidth(height) + leftInset + rightInset; + return loadingPane.maxWidth(height) + leftInset + rightInset; } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingListViewSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingListViewSkin.java index 8ade3398..3af3198a 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingListViewSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingListViewSkin.java @@ -1,5 +1,6 @@ package com.dlsc.gemsfx.skins; +import com.dlsc.gemsfx.LoadingPane; import com.dlsc.gemsfx.PagingControls; import com.dlsc.gemsfx.PagingListView; import javafx.beans.InvalidationListener; @@ -33,7 +34,7 @@ public Orientation getContentBias() { private final PagingControls pagingControls = new PagingControls(); - private final StackPane stackPane = new StackPane() { + private final LoadingPane stackPane = new LoadingPane() { /* * Very important or the layout inside PaginationListView will not work due * to text wrapping inside AdvancedItemView. @@ -58,9 +59,12 @@ public PagingListViewSkin(PagingListView pagingListView) { pagingControls.totalItemCountProperty().bindBidirectional(pagingListView.totalItemCountProperty()); pagingControls.pageSizeProperty().bind(pagingListView.pageSizeProperty()); pagingControls.maxPageIndicatorsCountProperty().bindBidirectional(pagingListView.maxPageIndicatorsCountProperty()); - pagingControls.messageLabelStrategyProperty().bind(pagingListView.messageLabelStrategyProperty()); - pagingControls.setShowPreviousNextPageButton(true); - pagingControls.setFirstLastPageDisplayMode(PagingControls.FirstLastPageDisplayMode.SHOW_PAGE_BUTTONS); + pagingControls.messageLabelStrategyProperty().bind(pagingListView.messageLabelStrategyProperty());; + pagingControls.showPreviousNextPageButtonProperty().bind(pagingListView.showPreviousNextPageButtonProperty()); + pagingControls.alignmentProperty().bind(pagingListView.alignmentProperty()); + pagingControls.firstLastPageDisplayModeProperty().bind(pagingListView.firstLastPageDisplayModeProperty()); + pagingControls.firstPageDividerProperty().bind(pagingListView.firstPageDividerProperty()); + pagingControls.maxPageIndicatorsCountProperty().bind(pagingListView.maxPageIndicatorsCountProperty()); content.getStyleClass().add("content"); diff --git a/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-list-view.css b/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-list-view.css index 68399347..d42270a4 100644 --- a/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-list-view.css +++ b/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-list-view.css @@ -1,5 +1,5 @@ -.paging-list-view .list-view .content > .list-cell:filled:selected, -.paging-list-view .list-view .content > .list-cell:filled:selected:hover { +.paging-list-view .list-view .loading-pane .content > .list-cell:filled:selected, +.paging-list-view .list-view .loading-pane .content > .list-cell:filled:selected:hover { -fx-background: -fx-accent; -fx-background-color: -fx-background, -fx-cell-focus-inner-border, -fx-background; -fx-background-insets: 0, 1, 2;