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 index 8cb2feae..b845054b 100644 --- a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsSettingsView.java +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsSettingsView.java @@ -39,7 +39,7 @@ public PagingControlsSettingsView(PagingControlBase pagingControls) { strategyChoiceBox.valueProperty().bindBidirectional(pagingControls.messageLabelStrategyProperty()); ChoiceBox maxPageIndicatorsBox = new ChoiceBox<>(); - List counts = new ArrayList<>(List.of(1, 2, 5, 10)); + List counts = new ArrayList<>(List.of(0, 1, 2, 5, 10)); if (!counts.contains(pagingControls.getMaxPageIndicatorsCount())) { counts.add(pagingControls.getMaxPageIndicatorsCount()); } 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 e8579b92..333f26d3 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 @@ -6,6 +6,7 @@ import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ToggleButton; import javafx.scene.layout.VBox; import javafx.stage.Stage; @@ -20,7 +21,7 @@ public class PagingListViewApp extends Application { public void start(Stage stage) { PagingListView pagingListView = new PagingListView<>(); pagingListView.setPrefWidth(400); - pagingListView.setTotalItemCount(300); + pagingListView.setTotalItemCount(305); pagingListView.setPageSize(15); pagingListView.setLoader(lv -> { if (Math.random() > .75) { @@ -33,7 +34,12 @@ public void start(Stage stage) { List data = new ArrayList<>(); int offset = lv.getPage() * lv.getPageSize(); for (int i = 0; i < lv.getPageSize(); i++) { - data.add("Item " + (offset + i + 1)); + int index = offset + i + 1; + if (index <= pagingListView.getTotalItemCount()) { + data.add("Item " + (offset + i + 1)); + } else { + break; + } } return data; }); @@ -41,7 +47,10 @@ public void start(Stage stage) { Button scenicView = new Button("Scenic View"); scenicView.setOnAction(evt -> ScenicView.show(scenicView.getScene())); - VBox box = new VBox(20, pagingListView, new PagingControlsSettingsView(pagingListView), scenicView); + CheckBox fillBox = new CheckBox("Fill last page"); + fillBox.selectedProperty().bindBidirectional(pagingListView.fillLastPageProperty()); + + VBox box = new VBox(20, pagingListView, fillBox, new PagingControlsSettingsView(pagingListView), scenicView); box.setPadding(new Insets(20)); Scene scene = new Scene(box); diff --git a/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-2.css b/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-2.css index d3afbfa5..eeb54e6d 100644 --- a/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-2.css +++ b/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-2.css @@ -22,4 +22,8 @@ -fx-background-insets: 0,1,1; -fx-text-fill: black; -fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.6) , 3, 0.0 , 0 , 1 ); +} + +.paging-controls > .pane > .page-buttons-container > .navigation-button { + -fx-content-display: graphic-only; } \ No newline at end of file diff --git a/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-9.css b/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-9.css index 53130a79..ac51bce7 100644 --- a/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-9.css +++ b/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/paging-controls-9.css @@ -3,4 +3,14 @@ -fx-background-radius: 30; -fx-background-insets: 0; -fx-text-fill: white; +} + +.paging-controls > .pane > .page-buttons-container > .button.current { + -fx-border-color: derive(red, -30%); + -fx-border-radius: 1em; +} + +.paging-controls > .pane > .page-buttons-container > .navigation-button { + -fx-content-display: text-only; + -fx-padding: 2px 10px; } \ No newline at end of file diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java index 228f8f7e..2903cf97 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControlBase.java @@ -9,6 +9,8 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.geometry.HPos; import javafx.scene.Node; import javafx.scene.control.Control; @@ -388,4 +390,80 @@ public final ObjectProperty alignmentProperty() { public final void setAlignment(HPos alignment) { this.alignment.set(alignment); } + + private final StringProperty firstPageText = new SimpleStringProperty(this, "firstPageText", "First"); + + public final String getFirstPageText() { + return firstPageText.get(); + } + + /** + * The text that will be shown by the "first page" button. + * + * @return the text of the "first page" button + */ + public final StringProperty firstPageTextProperty() { + return firstPageText; + } + + public final void setFirstPageText(String firstPageText) { + this.firstPageText.set(firstPageText); + } + + private final StringProperty lastPageText = new SimpleStringProperty(this, "lastPageText", "Last"); + + public final String getLastPageText() { + return lastPageText.get(); + } + + /** + * The text that will be shown by the "last page" button. + * + * @return the text of the "last page" button + */ + public final StringProperty lastPageTextProperty() { + return lastPageText; + } + + public final void setLastPageText(String lastPageText) { + this.lastPageText.set(lastPageText); + } + + private final StringProperty previousPageText = new SimpleStringProperty(this, "previousPageText", "Previous"); + + public final String getPreviousPageText() { + return previousPageText.get(); + } + + /** + * The text that will be shown by the "previous page" button. + * + * @return the text of the "previous page" button + */ + public final StringProperty previousPageTextProperty() { + return previousPageText; + } + + public final void setPreviousPageText(String previousPageText) { + this.previousPageText.set(previousPageText); + } + + private final StringProperty nextPageText = new SimpleStringProperty(this, "nextPageText", "Next"); + + public final String getNextPageText() { + return nextPageText.get(); + } + + /** + * The text that will be shown by the "next page" button. + * + * @return the text of the "next page" button + */ + public final StringProperty nextPageTextProperty() { + return nextPageText; + } + + public final void setNextPageText(String nextPageText) { + this.nextPageText.set(nextPageText); + } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java index 68ccdab0..649e80f6 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingListView.java @@ -35,7 +35,12 @@ public class PagingListView extends PagingControlBase { private final ObservableList unmodifiableItems = FXCollections.unmodifiableObservableList(items); - private final ListView listView = new ListView<>(items); + private final ListView listView = new ListView<>(items) { + @Override + protected Skin createDefaultSkin() { + return new InnerListViewSkin<>(this, PagingListView.this); + } + }; private final InvalidationListener updateListener = (Observable it) -> refresh(); @@ -47,7 +52,7 @@ public PagingListView() { listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); listView.cellFactoryProperty().bind(cellFactoryProperty()); - listView.setSkin( new InnerListViewSkin<>(listView, this)); + selectionModelProperty().bindBidirectional(listView.selectionModelProperty()); loadingService.setOnSucceeded(evt -> { loadingStatus.set(Status.OK); @@ -81,12 +86,12 @@ protected void updateItem(T item, boolean empty) { } }); - getUnmodifiableItems().addListener(weakUpdateListener); + unmodifiableItems.addListener(weakUpdateListener); + pageSizeProperty().addListener(weakUpdateListener); pageProperty().addListener(weakUpdateListener); cellFactoryProperty().addListener(weakUpdateListener); - - refresh(); + fillLastPageProperty().addListener(weakUpdateListener); } @Override @@ -103,6 +108,27 @@ public final ListView getListView() { return listView; } + private final BooleanProperty fillLastPage = new SimpleBooleanProperty(this, "fillLastPage", false); + + public final boolean isFillLastPage() { + return fillLastPage.get(); + } + + /** + * The list view might not have enough data to fill its last page with items / cells. This flag can be used + * to control whether we want the view to become smaller because of missing items or if we want the view to + * fill the page with empty cells. + * + * @return a flag used to control whether the last page will be filled with empty cells if needed + */ + public final BooleanProperty fillLastPageProperty() { + return fillLastPage; + } + + public final void setFillLastPage(boolean fillLastPage) { + this.fillLastPage.set(fillLastPage); + } + private final ObjectProperty loadingStatus = new SimpleObjectProperty<>(this, "loadingStatus", Status.OK); public final Status getLoadingStatus() { @@ -206,6 +232,7 @@ public final void setUsingScrollPane(boolean usingScrollPane) { * to configure it to only allow single selection (see * {@link MultipleSelectionModel#setSelectionMode(javafx.scene.control.SelectionMode)} * for more information). + * * @param value the MultipleSelectionModel to be used in this ListView */ public final void setSelectionModel(MultipleSelectionModel value) { @@ -214,6 +241,7 @@ public final void setSelectionModel(MultipleSelectionModel value) { /** * Returns the currently installed selection model. + * * @return the currently installed selection model */ public final MultipleSelectionModel getSelectionModel() { @@ -225,6 +253,7 @@ public final MultipleSelectionModel getSelectionModel() { * to select single or multiple items within a ListView, as well as inspect * which items have been selected by the user. Note that it has a generic * type that must match the type of the ListView itself. + * * @return the selectionModel property */ public final ObjectProperty> selectionModelProperty() { @@ -238,6 +267,7 @@ public final ObjectProperty> selectionModelProperty() * Sets a new cell factory to use in the ListView. This forces all old * {@link ListCell}'s to be thrown away, and new ListCell's created with * the new cell factory. + * * @param value cell factory to use in this ListView */ public final void setCellFactory(Callback, ListCell> value) { @@ -246,6 +276,7 @@ public final void setCellFactory(Callback, ListCell> value) { /** * Returns the current cell factory. + * * @return the current cell factory */ public final Callback, ListCell> getCellFactory() { @@ -260,6 +291,7 @@ public final Callback, ListCell> getCellFactory() { * which might be usable for representing any item in the ListView. * *

Refer to the {@link Cell} class documentation for more detail. + * * @return the cell factory property */ public final ObjectProperty, ListCell>> cellFactoryProperty() { @@ -270,6 +302,7 @@ public final ObjectProperty, ListCell>> cellFactoryPrope } public 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 5a66be36..109d56bc 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/SimplePagingListView.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/SimplePagingListView.java @@ -7,8 +7,9 @@ import javafx.collections.ObservableList; /** - * A simple version of the paging list view that is completely based on a list of tiems, just like a normal - * list view would be. + * A simple version of the paging list view that is completely based on a list of items, just like a normal + * list view would be. The view uses an internal data loader that accesses the list to retrieve the items of the + * current page. * * @param the type of items to show in the list view */ @@ -38,7 +39,7 @@ public SimplePagingListView() { }); items.addListener((Observable it) -> { - ObservableList list = getItems(); + ObservableList list = getItems(); if (list != null) { internal = true; try { @@ -51,12 +52,34 @@ public SimplePagingListView() { }); } + /** + * Ensures that the given item becomes visible within the list view. This method will only succeed if the + * given item is a member of the {@link #getItems()}. + * + * @param item the item to show + */ + public final void show(T item) { + ObservableList items = getItems(); + if (items != null) { + int index = items.indexOf(item); + if (index != -1) { + setPage(index / getPageSize()); + } + } + } + private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); public final ObservableList getItems() { return items.get(); } + /** + * Stores the data structure to be used by the list view. The internal data loader will simply retrieve the page + * items from this list. + * + * @return the data model feeding the list view + */ public final ListProperty itemsProperty() { return items; } 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 a074c012..f416ef4a 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/InnerListViewSkin.java @@ -2,7 +2,9 @@ import com.dlsc.gemsfx.LoadingPane; import com.dlsc.gemsfx.PagingListView; +import javafx.beans.Observable; import javafx.collections.MapChangeListener; +import javafx.collections.ObservableList; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.skin.ListViewSkin; @@ -36,13 +38,13 @@ public InnerListViewSkin(ListView control, PagingListView pagingListView) pagingListView.getProperties().addListener((MapChangeListener) change -> { if (change.wasAdded()) { if (change.getKey().equals("refresh-items")) { - pagingListView.getProperties().remove("refresh-items"); refresh(); } } }); getChildren().setAll(loadingPane); + refresh(); } @@ -57,7 +59,13 @@ public void refresh() { Callback, ListCell> cellFactory = pagingListView.getCellFactory(); if (cellFactory != null) { - for (int index = 0; index < pagingListView.getPageSize(); index++) { + int endIndex = pagingListView.getPageSize(); + if (!pagingListView.isFillLastPage()) { + // we do not want to fill the last page + endIndex = pagingListView.getUnmodifiableItems().size(); + } + + for (int index = 0; index < endIndex; index++) { ListCell cell = cellFactory.call(getSkinnable()); cell.setMaxWidth(Double.MAX_VALUE); diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingControlsSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingControlsSkin.java index 2c09633e..5e6f0d64 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingControlsSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingControlsSkin.java @@ -14,10 +14,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; -import org.kordamp.ikonli.javafx.FontIcon; -import org.kordamp.ikonli.materialdesign.MaterialDesign; public class PagingControlsSkin extends SkinBase { @@ -27,10 +24,10 @@ public class PagingControlsSkin extends SkinBase { private final HBox pageButtonsBox = new HBox(); - private StackPane lastPageButton; - private StackPane nextButton; - private StackPane previousButton; - private StackPane firstPageButton; + private Button lastPageButton; + private Button nextButton; + private Button previousButton; + private Button firstPageButton; private Label messageLabel; public PagingControlsSkin(PagingControls view) { @@ -96,7 +93,9 @@ private void createButtons() { Region nextPageRegion = new Region(); nextPageRegion.getStyleClass().add("icon"); - firstPageButton = new StackPane(firstPageButtonRegion); + firstPageButton = new Button(); + firstPageButton.textProperty().bind(view.firstPageTextProperty()); + firstPageButton.setGraphic(firstPageButtonRegion); firstPageButton.getStyleClass().addAll("element", "navigation-button", "first-page-button"); firstPageButton.setMinWidth(Region.USE_PREF_SIZE); firstPageButton.managedProperty().bind(firstPageButton.visibleProperty()); @@ -107,7 +106,9 @@ private void createButtons() { startPage.set(0); }); - previousButton = new StackPane(previousPageRegion); + previousButton = new Button(); + previousButton.textProperty().bind(view.previousPageTextProperty()); + previousButton.setGraphic(previousPageRegion); previousButton.getStyleClass().addAll("element", "navigation-button", "previous-page-button"); previousButton.setOnMouseClicked(evt -> view.setPage(Math.max(0, view.getPage() - 1))); previousButton.setMinWidth(Region.USE_PREF_SIZE); @@ -115,7 +116,9 @@ private void createButtons() { previousButton.managedProperty().bind(view.showPreviousNextPageButtonProperty()); previousButton.disableProperty().bind(view.pageProperty().greaterThan(0).not()); - nextButton = new StackPane(nextPageRegion); + nextButton = new Button(); + nextButton.textProperty().bind(view.nextPageTextProperty()); + nextButton.setGraphic(nextPageRegion); nextButton.getStyleClass().addAll("element", "navigation-button", "next-page-button"); nextButton.setOnMouseClicked(evt -> view.setPage(Math.min(view.getPageCount() - 1, view.getPage() + 1))); nextButton.setMinWidth(Region.USE_PREF_SIZE); @@ -123,7 +126,9 @@ private void createButtons() { nextButton.managedProperty().bind(view.showPreviousNextPageButtonProperty()); nextButton.disableProperty().bind(view.pageProperty().lessThan(view.pageCountProperty().subtract(1)).not()); - lastPageButton = new StackPane(lastPageButtonRegion); + lastPageButton = new Button(); + lastPageButton.textProperty().bind(view.lastPageTextProperty()); + lastPageButton.setGraphic(lastPageButtonRegion); lastPageButton.setMinWidth(Region.USE_PREF_SIZE); lastPageButton.getStyleClass().addAll("element", "navigation-button", "last-page-button"); lastPageButton.managedProperty().bind(lastPageButton.visibleProperty()); 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 348f2774..9e9c24ca 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingListViewSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingListViewSkin.java @@ -3,18 +3,13 @@ import com.dlsc.gemsfx.LoadingPane; import com.dlsc.gemsfx.PagingControls; import com.dlsc.gemsfx.PagingListView; -import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.beans.WeakInvalidationListener; import javafx.beans.binding.Bindings; import javafx.geometry.Orientation; import javafx.scene.Node; import javafx.scene.control.ListView; import javafx.scene.control.ScrollPane; -import javafx.scene.control.SelectionMode; import javafx.scene.control.SkinBase; -import javafx.scene.layout.Region; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; public class PagingListViewSkin extends SkinBase> { @@ -53,8 +48,6 @@ public PagingListViewSkin(PagingListView pagingListView) { innerListView = pagingListView.getListView(); - pagingListView.selectionModelProperty().bindBidirectional(innerListView.selectionModelProperty()); - pagingControls.pageProperty().bindBidirectional(pagingListView.pageProperty()); pagingControls.totalItemCountProperty().bindBidirectional(pagingListView.totalItemCountProperty()); pagingControls.pageSizeProperty().bindBidirectional(pagingListView.pageSizeProperty()); 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 ba1e472d..54f18912 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-color: transparent; -fx-background-insets: 0px; -fx-background-radius: 2px; - -fx-content-display: graphic-only; -fx-max-height: infinity; -fx-min-width: 1.5em; } @@ -76,9 +75,24 @@ } .paging-controls > .pane > .page-buttons-container > .navigation-button { - -fx-pref-width: 1em; - -fx-pref-height: 1em; -fx-padding: 2px; + -fx-graphic-text-gap: 10px; +} + +.paging-controls > .pane > .page-buttons-container > .navigation-button.first-page-button { + -fx-content-display: left; +} + +.paging-controls > .pane > .page-buttons-container > .navigation-button.previous-page-button { + -fx-content-display: left; +} + +.paging-controls > .pane > .page-buttons-container > .navigation-button.last-page-button { + -fx-content-display: right; +} + +.paging-controls > .pane > .page-buttons-container > .navigation-button.next-page-button { + -fx-content-display: right; } .paging-controls > .pane > .page-buttons-container > .navigation-button > .icon {