diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsApp.java new file mode 100644 index 00000000..6727560a --- /dev/null +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/PagingControlsApp.java @@ -0,0 +1,90 @@ +package com.dlsc.gemsfx.demo; + +import com.dlsc.gemsfx.PagingControls; +import com.dlsc.gemsfx.Spacer; +import fr.brouillard.oss.cssfx.CSSFX; +import javafx.application.Application; +import javafx.beans.binding.Bindings; +import javafx.geometry.Insets; +import javafx.scene.Scene; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; + +import java.util.List; + +public class PagingControlsApp extends Application { + + @Override + public void start(Stage stage) { + VBox vBox1 = createSection(10, 221, false); + VBox vBox2 = createSection(15, 45, false); + VBox vBox3 = createSection(20, 1000, true); + VBox vBox4 = createSection(5, 5, false); + + VBox all = new VBox(20, vBox1, vBox2, vBox3, vBox4); + + StackPane stackPane = new StackPane(all); + stackPane.setPadding(new Insets(50, 50, 50, 50)); + + Scene scene = new Scene(stackPane); + stage.setScene(scene); + stage.centerOnScreen(); + stage.sizeToScene(); + stage.setTitle("Paging View"); + stage.show(); + + CSSFX.start(scene); + } + + private static VBox createSection(int pageSize, int itemCount, boolean showFirstLastButtons) { + PagingControls pagingControls = new PagingControls(); + + pagingControls.setTotalItemCount(itemCount); + pagingControls.setPageSize(pageSize); + + pagingControls.setShowGotoFirstPageButton(showFirstLastButtons); + pagingControls.setShowGotoLastPageButton(showFirstLastButtons); + + pagingControls.setStyle("-fx-border-color: black; -fx-padding: 20px"); + pagingControls.setPrefWidth(800); + + 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())); + + CheckBox showItemCounter = new CheckBox("Show item counter"); + showItemCounter.selectedProperty().bindBidirectional(pagingControls.showMessageLabelProperty()); + + CheckBox showGotoFirstPageButton = new CheckBox("First page button"); + showGotoFirstPageButton.selectedProperty().bindBidirectional(pagingControls.showGotoFirstPageButtonProperty()); + + CheckBox showGotoLastPageButton = new CheckBox("Last page button"); + showGotoLastPageButton.selectedProperty().bindBidirectional(pagingControls.showGotoLastPageButtonProperty()); + + CheckBox showMaxPage = new CheckBox("Show max page"); + showMaxPage.selectedProperty().bindBidirectional(pagingControls.showMaxPageProperty()); + + ChoiceBox maxPageIndicatorsBox = new ChoiceBox<>(); + maxPageIndicatorsBox.getItems().setAll(List.of(1, 2, 5, 10)); + maxPageIndicatorsBox.valueProperty().bindBidirectional(pagingControls.maxPageIndicatorsCountProperty().asObject()); + + HBox hbox = new HBox(20, pageLabel, pageCountLabel, new Spacer(), showGotoFirstPageButton, showGotoLastPageButton, showMaxPage, showItemCounter, new Label("# Indicators: "), maxPageIndicatorsBox); + + VBox vBox = new VBox(10, pagingControls, hbox); + vBox.setMaxHeight(Region.USE_PREF_SIZE); + + return vBox; + } + + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControls.java b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControls.java new file mode 100644 index 00000000..beaec34d --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/PagingControls.java @@ -0,0 +1,240 @@ +package com.dlsc.gemsfx; + +import com.dlsc.gemsfx.skins.PagingControlsSkin; +import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyIntegerProperty; +import javafx.beans.property.ReadOnlyIntegerWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.Node; +import javafx.scene.control.Control; +import javafx.scene.control.Label; +import javafx.scene.control.Skin; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.util.Callback; + +import java.util.Objects; + +public class PagingControls extends Control { + + private static final String DEFAULT_STYLE_CLASS = "paging-view"; + private static final String PAGE_BUTTON = "page-button"; + + private final IntegerProperty startPage = new SimpleIntegerProperty(); + + public PagingControls() { + getStyleClass().add(DEFAULT_STYLE_CLASS); + + setMessageLabelProvider(view -> { + int startIndex = (view.getPage() * getPageSize()) + 1; + int endIndex = startIndex + getPageSize() - 1; + + return "Showing items " + startIndex + " to " + endIndex + " of " + getTotalItemCount(); + }); + + addEventHandler(KeyEvent.KEY_PRESSED, evt -> { + if (Objects.equals(evt.getCode(), KeyCode.RIGHT)) { + nextPage(); + } else if (Objects.equals(evt.getCode(), KeyCode.LEFT)) { + previousPage(); + } else if (Objects.equals(evt.getCode(), KeyCode.HOME)) { + firstPage(); + } else if (Objects.equals(evt.getCode(), KeyCode.END)) { + lastPage(); + } + }); + + pageCount.bind(Bindings.createIntegerBinding(() -> { + int count = getTotalItemCount() / getPageSize(); + if (getTotalItemCount() % getPageSize() > 0) { + count++; + } + return count; + }, totalItemCountProperty(), pageSizeProperty())); + } + + @Override + protected Skin createDefaultSkin() { + return new PagingControlsSkin(this); + } + + @Override + public String getUserAgentStylesheet() { + return Objects.requireNonNull(PagingControls.class.getResource("paging-view.css")).toExternalForm(); + } + + private final BooleanProperty showMaxPage = new SimpleBooleanProperty(this, "showMaxButton"); + + public final boolean isShowMaxPage() { + return showMaxPage.get(); + } + + public final BooleanProperty showMaxPageProperty() { + return showMaxPage; + } + + public final void setShowMaxPage(boolean showMaxPage) { + this.showMaxPage.set(showMaxPage); + } + + private final ObjectProperty maxPageDividerNode = new SimpleObjectProperty<>(this, "maxPageDividerNode", new Label("...")); + + public final Node getMaxPageDividerNode() { + return maxPageDividerNode.get(); + } + + public final ObjectProperty maxPageDividerNodeProperty() { + return maxPageDividerNode; + } + + public final void setMaxPageDividerNode(Node maxPageDividerNode) { + this.maxPageDividerNode.set(maxPageDividerNode); + } + + private final ObjectProperty> messageLabelProvider = new SimpleObjectProperty<>(this, "messageLabelProvider"); + + public final Callback getMessageLabelProvider() { + return messageLabelProvider.get(); + } + + public final ObjectProperty> messageLabelProviderProperty() { + return messageLabelProvider; + } + + public final void setMessageLabelProvider(Callback messageLabelProvider) { + this.messageLabelProvider.set(messageLabelProvider); + } + + public void firstPage() { + setPage(0); + } + + public void lastPage() { + setPage(getPageCount() - 1); + } + + public void nextPage() { + setPage(Math.min(getPageCount() - 1, getPage() + 1)); + } + + public void previousPage() { + setPage(Math.max(0, getPage() - 1)); + } + + private final IntegerProperty totalItemCount = new SimpleIntegerProperty(this, "totalItemCount"); + + public final int getTotalItemCount() { + return totalItemCount.get(); + } + + public final IntegerProperty totalItemCountProperty() { + return totalItemCount; + } + + public final void setTotalItemCount(int totalItemCount) { + this.totalItemCount.set(totalItemCount); + } + + private final BooleanProperty showMessageLabel = new SimpleBooleanProperty(this, "showMessageLabel", true); + + public final boolean getShowMessageLabel() { + return showMessageLabel.get(); + } + + public final BooleanProperty showMessageLabelProperty() { + return showMessageLabel; + } + + public final void setShowMessageLabel(boolean showMessageLabel) { + this.showMessageLabel.set(showMessageLabel); + } + + private final ReadOnlyIntegerWrapper pageCount = new ReadOnlyIntegerWrapper(this, "pageCount", 0); + + public final int getPageCount() { + return pageCount.get(); + } + + public final ReadOnlyIntegerProperty pageCountProperty() { + return pageCount.getReadOnlyProperty(); + } + + private final IntegerProperty maxPageIndicatorsCount = new SimpleIntegerProperty(this, "maxPageIndicatorsCount", 5); + + public final int getMaxPageIndicatorsCount() { + return maxPageIndicatorsCount.get(); + } + + public final IntegerProperty maxPageIndicatorsCountProperty() { + return maxPageIndicatorsCount; + } + + public final void setMaxPageIndicatorsCount(int maxPageIndicatorsCount) { + this.maxPageIndicatorsCount.set(maxPageIndicatorsCount); + } + + private final IntegerProperty page = new SimpleIntegerProperty(this, "page"); + + public final int getPage() { + return page.get(); + } + + public final IntegerProperty pageProperty() { + return page; + } + + public final void setPage(int page) { + this.page.set(page); + } + + private final IntegerProperty pageSize = new SimpleIntegerProperty(this, "pageSize", 10); + + public final int getPageSize() { + return pageSize.get(); + } + + public final IntegerProperty pageSizeProperty() { + return pageSize; + } + + public final void setPageSize(int pageSize) { + this.pageSize.set(pageSize); + } + + public void refresh() { + startPage.set(0); + } + + private final BooleanProperty showGotoFirstPageButton = new SimpleBooleanProperty(this, "showGotoFirstPageButton", true); + + public final boolean isShowGotoFirstPageButton() { + return showGotoFirstPageButton.get(); + } + + public final BooleanProperty showGotoFirstPageButtonProperty() { + return showGotoFirstPageButton; + } + + public final void setShowGotoFirstPageButton(boolean showGotoFirstPageButton) { + this.showGotoFirstPageButton.set(showGotoFirstPageButton); + } + + private final BooleanProperty showGotoLastPageButton = new SimpleBooleanProperty(this, "showGotoLastPageButton", true); + + public final boolean isShowGotoLastPageButton() { + return showGotoLastPageButton.get(); + } + + public final BooleanProperty showGotoLastPageButtonProperty() { + return showGotoLastPageButton; + } + + public final void setShowGotoLastPageButton(boolean showGotoLastPageButton) { + this.showGotoLastPageButton.set(showGotoLastPageButton); + } +} diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingControlsSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingControlsSkin.java new file mode 100644 index 00000000..394e5744 --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/PagingControlsSkin.java @@ -0,0 +1,162 @@ +package com.dlsc.gemsfx.skins; + +import com.dlsc.gemsfx.PagingControls; +import com.dlsc.gemsfx.Spacer; +import javafx.beans.InvalidationListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.materialdesign.MaterialDesign; + +public class PagingControlsSkin extends SkinBase { + + private static final String PAGE_BUTTON = "page-button"; + + private final IntegerProperty startPage = new SimpleIntegerProperty(); + + private final HBox hBox = new HBox(); + private Button lastPageButton; + private Button nextButton; + private Button previousButton; + private Button firstPageButton; + private Label counterLabel; + + public PagingControlsSkin(PagingControls view) { + super(view); + + createButtons(); + + InvalidationListener buildViewListener = it -> updateView(); + + view.pageProperty().addListener(buildViewListener); + view.pageCountProperty().addListener(buildViewListener); + view.maxPageIndicatorsCountProperty().addListener(buildViewListener); + view.showMessageLabelProperty().addListener(buildViewListener); + view.showMaxPageProperty().addListener(buildViewListener); + startPage.addListener(buildViewListener); + + view.pageProperty().addListener((obs, oldPage, newPage) -> { + int startPage = this.startPage.get(); + int totalPages = view.getPageCount(); + int maxPageIndicatorCount = view.getMaxPageIndicatorsCount(); + + if (newPage.intValue() < startPage) { + this.startPage.set(Math.min(newPage.intValue(), Math.max(0, startPage - maxPageIndicatorCount))); + } else if (newPage.intValue() > startPage + maxPageIndicatorCount - 1) { + this.startPage.set(Math.max(newPage.intValue(), startPage + maxPageIndicatorCount)); + } + + updateView(); + }); + + updateView(); + + hBox.getStyleClass().add("hbox"); + getChildren().add(hBox); + } + + private void createButtons() { + PagingControls view = getSkinnable(); + + counterLabel = new Label(); + counterLabel.getStyleClass().add("counter-label"); + counterLabel.textProperty().bind(Bindings.createStringBinding(() -> view.getMessageLabelProvider().call(view), view.messageLabelProviderProperty(), view.totalItemCountProperty(), view.pageProperty(), view.pageSizeProperty(), view.pageCountProperty())); + counterLabel.visibleProperty().bind(view.showMessageLabelProperty()); + counterLabel.managedProperty().bind(view.showMessageLabelProperty()); + + firstPageButton = createFirstPageButton(); + firstPageButton.setGraphic(new FontIcon(MaterialDesign.MDI_PAGE_FIRST)); + firstPageButton.getStyleClass().addAll("nav-button", "first"); + firstPageButton.managedProperty().bind(firstPageButton.visibleProperty()); + firstPageButton.disableProperty().bind(startPage.greaterThan(0).not()); + firstPageButton.visibleProperty().bind(view.showGotoFirstPageButtonProperty()); + firstPageButton.setOnAction(evt -> { + view.setPage(0); + startPage.set(0); + }); + + previousButton = createPreviousPageButton(); + previousButton.setGraphic(new FontIcon(MaterialDesign.MDI_CHEVRON_LEFT)); + previousButton.getStyleClass().addAll("nav-button", "previous-button"); + previousButton.setOnAction(evt -> view.setPage(Math.max(0, view.getPage() - 1))); + previousButton.setMinWidth(Region.USE_PREF_SIZE); + previousButton.disableProperty().bind(view.pageProperty().greaterThan(0).not()); + + nextButton = createNextPageButton(); + nextButton.setGraphic(new FontIcon(MaterialDesign.MDI_CHEVRON_RIGHT)); + nextButton.getStyleClass().addAll("nav-button", "next-button"); + nextButton.setOnAction(evt -> view.setPage(Math.min(view.getPageCount() - 1, view.getPage() + 1))); + nextButton.setMinWidth(Region.USE_PREF_SIZE); + nextButton.disableProperty().bind(view.pageProperty().lessThan(view.getPageCount() - 1).not()); + + lastPageButton = createLastPageButton(); + lastPageButton.setGraphic(new FontIcon(MaterialDesign.MDI_PAGE_LAST)); + lastPageButton.getStyleClass().addAll("nav-button", "last"); + lastPageButton.managedProperty().bind(lastPageButton.visibleProperty()); + lastPageButton.disableProperty().bind(startPage.add(view.getMaxPageIndicatorsCount()).lessThan(view.getPageCount()).not()); + lastPageButton.visibleProperty().bind(view.showGotoLastPageButtonProperty()); + lastPageButton.setOnAction(evt -> view.setPage(view.getPageCount() - 1)); + } + + private void updateView() { + PagingControls view = getSkinnable(); + + hBox.getChildren().setAll(counterLabel, new Spacer(), firstPageButton, previousButton); + + int pageIndex; + int startIndex = startPage.get(); + int endIndex = Math.min(view.getPageCount(), startIndex + view.getMaxPageIndicatorsCount()); + + if (endIndex - startIndex < view.getMaxPageIndicatorsCount()) { + startIndex = Math.max(0, endIndex - view.getMaxPageIndicatorsCount()); + startPage.set(startIndex); + } + + for (pageIndex = startIndex; pageIndex < endIndex; pageIndex++) { + Button pageButton = createPageButton(pageIndex); + if (pageIndex == view.getPage()) { + pageButton.getStyleClass().add("current"); + } + hBox.getChildren().add(pageButton); + } + + if (view.isShowMaxPage() && endIndex < view.getPageCount()) { + // we need to show the "max page" button + Button pageButton = createPageButton(view.getPageCount() - 1); + hBox.getChildren().addAll(view.getMaxPageDividerNode(), pageButton); + } + + + hBox.getChildren().addAll(nextButton, lastPageButton); + } + + protected Button createFirstPageButton() { + return new Button(); + } + + protected Button createLastPageButton() { + return new Button(); + } + + protected Button createPreviousPageButton() { + return new Button(); + } + + protected Button createNextPageButton() { + return new Button(); + } + + protected Button createPageButton(int page) { + Button pageButton = new Button(Integer.toString(page + 1)); + pageButton.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + pageButton.getStyleClass().add(PAGE_BUTTON); + pageButton.setOnAction(evt -> getSkinnable().setPage(page)); + return pageButton; + } +} diff --git a/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-view.css b/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-view.css new file mode 100644 index 00000000..79797141 --- /dev/null +++ b/gemsfx/src/main/resources/com/dlsc/gemsfx/paging-view.css @@ -0,0 +1,57 @@ +.paging-view { + -fx-alignment: center-left; + -fx-padding: 5px 0px 0px 0px; + -fx-spacing: 2px; +} + +.paging-view > .hbox { + -fx-alignment: center-left; + -fx-spacing: 2px; +} + +.paging-view > .hbox .counter-label { +} + +.paging-view > .hbox .page-button { + -fx-padding: 5px; + -fx-background-color: transparent; + -fx-background-radius: 1000px; + -fx-border-radius: 1000px; + -fx-min-width: 30px; + -fx-min-height: 30px; + -fx-border-color: transparent; +} + +.paging-view .page-button:hover { + -fx-background: derive(-fx-selection-bar, +50%); + -fx-background-color: -fx-background; + -fx-text-fill: -fx-selection-bar-text; +} + +.paging-view .page-button.current { + -fx-background: -fx-selection-bar; + -fx-background-color: -fx-background; + -fx-text-fill: -fx-selection-bar-text; + -fx-padding: 5px; + -fx-background-radius: 1000px; + -fx-min-width: 30px; + -fx-min-height: 30px; +} + +.paging-view .spacer { +} + +.paging-view .nav-button { + -fx-padding: 5px; + -fx-background-color: transparent; + -fx-content-display: graphic-only; +} + +.paging-view .nav-button .ikonli-font-icon { + -fx-icon-size: 24px; + -fx-icon-color: -fx-text-background-color; +} + +.paging-view .nav-button:hover .ikonli-font-icon { + -fx-icon-color: -fx-text-background-color; +}