Skip to content

Instantly share code, notes, and snippets.

@gferon
Created January 24, 2013 19:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gferon/4626632 to your computer and use it in GitHub Desktop.
Save gferon/4626632 to your computer and use it in GitHub Desktop.
The PaginationSkin.java class from OpenJFX 2.2.2 controls/rt without the page flip animation. It's quite barbaric right now, but I needed a quick fix. I'll refactor the class to remove everything dealing with the page flip animation later.
/*
* Copyright (c) 2012, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
* CA 95054 USA or visit www.sun.com if you need additional information or
* have any questions.
*/
package com.lepape.javafx.scene.control.skin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Pagination;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.input.TouchEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.util.Duration;
import com.sun.javafx.css.StyleableBooleanProperty;
import com.sun.javafx.css.StyleableObjectProperty;
import com.sun.javafx.css.StyleableProperty;
import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.EnumConverter;
import com.sun.javafx.scene.control.behavior.PaginationBehavior;
import com.sun.javafx.scene.control.skin.SkinBase;
public class PaginationSkin extends SkinBase<Pagination, PaginationBehavior> {
private static final Duration DURATION = new Duration(125.0);
private static final double SWIPE_THRESHOLD = 0.30;
private static final double TOUCH_THRESHOLD = 15;
private Pagination pagination;
private StackPane currentStackPane;
private StackPane nextStackPane;
private Timeline timeline;
private Rectangle clipRect;
private NavigationControl navigation;
private int fromIndex;
private int previousIndex;
private int currentIndex;
private int toIndex;
private int pageCount;
private int maxPageIndicatorCount;
private boolean animate = true;
public PaginationSkin(final Pagination pagination) {
super(pagination, new PaginationBehavior(pagination));
setManaged(false);
clipRect = new Rectangle();
setClip(clipRect);
this.pagination = pagination;
this.currentStackPane = new StackPane();
currentStackPane.getStyleClass().add("page");
this.nextStackPane = new StackPane();
nextStackPane.getStyleClass().add("page");
nextStackPane.setVisible(false);
resetIndexes(true);
this.navigation = new NavigationControl();
getChildren().addAll(currentStackPane, nextStackPane, navigation);
pagination.maxPageIndicatorCountProperty().addListener(new InvalidationListener() {
@Override
public void invalidated(Observable o) {
resetIndexes(false);
navigation.initializePageIndicators();
navigation.updatePageIndicators();
}
});
registerChangeListener(pagination.pageCountProperty(), "PAGE_COUNT");
registerChangeListener(pagination.pageFactoryProperty(), "PAGE_FACTORY");
initializeSwipeAndTouchHandlers();
}
public void selectNext() {
if (getCurrentPageIndex() < getPageCount() - 1) {
pagination.setCurrentPageIndex(getCurrentPageIndex() + 1);
}
}
public void selectPrevious() {
if (getCurrentPageIndex() > 0) {
pagination.setCurrentPageIndex(getCurrentPageIndex() - 1);
}
}
private double startTouchPos;
private double lastTouchPos;
private long startTouchTime;
private long lastTouchTime;
private double touchVelocity;
private boolean touchThresholdBroken;
private int touchEventId = -1;
private boolean nextPageReached = false;
private boolean setInitialDirection = false;
private int direction;
private void initializeSwipeAndTouchHandlers() {
setOnTouchPressed(new EventHandler<TouchEvent>() {
@Override public void handle(TouchEvent e) {
if (touchEventId == -1) {
touchEventId = e.getTouchPoint().getId();
}
if (touchEventId != e.getTouchPoint().getId()) {
return;
}
lastTouchPos = startTouchPos = e.getTouchPoint().getX();
lastTouchTime = startTouchTime = System.currentTimeMillis();
touchThresholdBroken = false;
e.consume();
}
});
setOnTouchMoved(new EventHandler<TouchEvent>() {
@Override public void handle(TouchEvent e) {
if (touchEventId != e.getTouchPoint().getId()) {
return;
}
double drag = e.getTouchPoint().getX() - lastTouchPos;
long time = System.currentTimeMillis() - lastTouchTime;
touchVelocity = drag/time;
lastTouchPos = e.getTouchPoint().getX();
lastTouchTime = System.currentTimeMillis();
double delta = e.getTouchPoint().getX() - startTouchPos;
if (!touchThresholdBroken && Math.abs(delta) > TOUCH_THRESHOLD) {
touchThresholdBroken = true;
}
if (touchThresholdBroken) {
double width = getWidth() - (getInsets().getLeft() + getInsets().getRight());
double currentPaneX;
double nextPaneX;
if (!setInitialDirection) {
// Remember the direction travelled so we can
// load the next or previous page if the touch is not released.
setInitialDirection = true;
direction = delta < 0 ? 1 : -1;
}
if (delta < 0) {
if (direction == -1) {
nextStackPane.getChildren().clear();
direction = 1;
}
// right to left
if (Math.abs(delta) <= width) {
currentPaneX = delta;
nextPaneX = width + delta;
nextPageReached = false;
} else {
currentPaneX = -width;
nextPaneX = 0;
nextPageReached = true;
}
currentStackPane.setTranslateX(currentPaneX);
if (getCurrentPageIndex() < getPageCount() - 1) {
createPage(nextStackPane, currentIndex + 1);
nextStackPane.setVisible(true);
nextStackPane.setTranslateX(nextPaneX);
} else {
currentStackPane.setTranslateX(0);
}
} else {
// left to right
if (direction == 1) {
nextStackPane.getChildren().clear();
direction = -1;
}
if (Math.abs(delta) <= width) {
currentPaneX = delta;
nextPaneX = -width + delta;
nextPageReached = false;
} else {
currentPaneX = width;
nextPaneX = 0;
nextPageReached = true;
}
currentStackPane.setTranslateX(currentPaneX);
if (getCurrentPageIndex() != 0) {
createPage(nextStackPane, currentIndex - 1);
nextStackPane.setVisible(true);
nextStackPane.setTranslateX(nextPaneX);
} else {
currentStackPane.setTranslateX(0);
}
}
}
e.consume();
}
});
setOnTouchReleased(new EventHandler<TouchEvent>() {
@Override public void handle(TouchEvent e) {
if (touchEventId != e.getTouchPoint().getId()) {
return;
} else {
touchEventId = -1;
setInitialDirection = false;
}
if (touchThresholdBroken) {
// determin if click or swipe
final double drag = e.getTouchPoint().getX() - startTouchPos;
// calculate complete time from start to end of drag
final long time = System.currentTimeMillis() - startTouchTime;
// if time is less than 300ms then considered a quick swipe and whole time is used
final boolean quick = time < 300;
// calculate velocity
final double velocity = quick ? (double)drag / time : touchVelocity; // pixels/ms
// calculate distance we would travel at this speed for 500ms of travel
final double distance = (velocity * 500);
final double width = getWidth() - (getInsets().getLeft() + getInsets().getRight());
// The swipe distance travelled.
final double threshold = Math.abs(distance/width);
// The touch and dragged distance travelled.
final double delta = Math.abs(drag/width);
if (threshold > SWIPE_THRESHOLD || delta > SWIPE_THRESHOLD) {
if (startTouchPos > e.getTouchPoint().getX()) {
selectNext();
} else {
selectPrevious();
}
} else {
animateClamping(startTouchPos > e.getTouchPoint().getSceneX());
}
}
e.consume();
}
});
}
private void resetIndexes(boolean usePageIndex) {
maxPageIndicatorCount = getMaxPageIndicatorCount();
// Used to indicate that we can change a set of pages.
pageCount = getPageCount();
if (pageCount > maxPageIndicatorCount) {
pageCount = maxPageIndicatorCount;
}
fromIndex = 0;
previousIndex = 0;
currentIndex = usePageIndex ? getCurrentPageIndex() : 0;
toIndex = fromIndex + (pageCount - 1);
if (pageCount == Pagination.INDETERMINATE && maxPageIndicatorCount == Pagination.INDETERMINATE) {
// We do not know how many indicators can fit. Let the layout pass compute it.
toIndex = 0;
}
boolean isAnimate = animate;
if (isAnimate) {
animate = false;
}
// Remove the children in the pane before we create a new page.
currentStackPane.getChildren().clear();
nextStackPane.getChildren().clear();
pagination.setCurrentPageIndex(currentIndex);
createPage(currentStackPane, currentIndex);
if (isAnimate) {
animate = true;
}
}
private boolean createPage(StackPane pane, int index) {
if (pagination.getPageFactory() != null && pane.getChildren().isEmpty()) {
Node content = pagination.getPageFactory().call(index);
// If the content is null we don't want to switch pages.
if (content != null) {
pane.getChildren().setAll(content);
return true;
} else {
// Disable animation if the new page does not exist. It is strange to
// see the same page animated out then in.
boolean isAnimate = animate;
if (isAnimate) {
animate = false;
}
if (pagination.getPageFactory().call(previousIndex) != null) {
pagination.setCurrentPageIndex(previousIndex);
} else {
// Set the page index to 0 because both the current,
// and the previous pages have no content.
pagination.setCurrentPageIndex(0);
}
if (isAnimate) {
animate = true;
}
return false;
}
}
return false;
}
private int getPageCount() {
if (getSkinnable().getPageCount() < 1) {
return 1;
}
return getSkinnable().getPageCount();
}
private int getMaxPageIndicatorCount() {
return getSkinnable().getMaxPageIndicatorCount();
}
private int getCurrentPageIndex() {
return getSkinnable().getCurrentPageIndex();
}
private static final Interpolator interpolator = Interpolator.SPLINE(0.4829, 0.5709, 0.6803, 0.9928);
private int currentAnimatedIndex;
private int previousAnimatedIndex;
private boolean hasPendingAnimation = false;
private void animateSwitchPage() {
if (timeline != null) {
timeline.setRate(8);
hasPendingAnimation = true;
return;
}
// We are handling a touch event if nextPane's page has already been
// created and visible == true.
if (!nextStackPane.isVisible()) {
if (!createPage(nextStackPane, currentAnimatedIndex)) {
// The next page does not exist just return without starting
// any animation.
return;
}
}
if (nextPageReached) {
// No animation is needed when the next page is already showing
// and in the correct position. Just swap the panes and return
swapPanes();
nextPageReached = false;
return;
}
nextStackPane.setCache(true);
currentStackPane.setCache(true);
nextStackPane.setVisible(true);
swapPanes();
// wait one pulse then animate
/*
Platform.runLater(new Runnable() {
@Override public void run() {
// We are handling a touch event if nextPane's translateX is not 0
boolean useTranslateX = nextStackPane.getTranslateX() != 0;
if (currentAnimatedIndex > previousIndex) { // animate right to left
if (!useTranslateX) {
nextStackPane.setTranslateX(currentStackPane.getWidth());
}
nextStackPane.setVisible(true);
timeline = new Timeline();
KeyFrame k1 = new KeyFrame(Duration.millis(0),
new KeyValue(currentStackPane.translateXProperty(),
useTranslateX ? currentStackPane.getTranslateX() : 0,
interpolator),
new KeyValue(nextStackPane.translateXProperty(),
useTranslateX ?
nextStackPane.getTranslateX() : currentStackPane.getWidth(), interpolator));
KeyFrame k2 = new KeyFrame(DURATION,
swipeAnimationEndEventHandler,
new KeyValue(currentStackPane.translateXProperty(), -currentStackPane.getWidth(), interpolator),
new KeyValue(nextStackPane.translateXProperty(), 0, interpolator));
timeline.getKeyFrames().setAll(k1, k2);
timeline.play();
} else { // animate left to right
if (!useTranslateX) {
nextStackPane.setTranslateX(-currentStackPane.getWidth());
}
nextStackPane.setVisible(true);
timeline = new Timeline();
KeyFrame k1 = new KeyFrame(Duration.millis(0),
new KeyValue(currentStackPane.translateXProperty(),
useTranslateX ? currentStackPane.getTranslateX() : 0,
interpolator),
new KeyValue(nextStackPane.translateXProperty(),
useTranslateX ? nextStackPane.getTranslateX() : -currentStackPane.getWidth(),
interpolator));
KeyFrame k2 = new KeyFrame(DURATION,
swipeAnimationEndEventHandler,
new KeyValue(currentStackPane.translateXProperty(), currentStackPane.getWidth(), interpolator),
new KeyValue(nextStackPane.translateXProperty(), 0, interpolator));
timeline.getKeyFrames().setAll(k1, k2);
timeline.play();
}
}
});
/*/
}
private EventHandler<ActionEvent> swipeAnimationEndEventHandler = new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent t) {
swapPanes();
timeline = null;
if (hasPendingAnimation) {
animateSwitchPage();
hasPendingAnimation = false;
}
}
};
private void swapPanes() {
StackPane temp = currentStackPane;
currentStackPane = nextStackPane;
nextStackPane = temp;
currentStackPane.setTranslateX(0);
currentStackPane.setCache(false);
nextStackPane.setTranslateX(0);
nextStackPane.setCache(false);
nextStackPane.setVisible(false);
nextStackPane.getChildren().clear();
}
// If the swipe hasn't reached the THRESHOLD we want to animate the clamping.
private void animateClamping(boolean rightToLeft) {
if (rightToLeft) { // animate right to left
timeline = new Timeline();
KeyFrame k1 = new KeyFrame(Duration.millis(0),
new KeyValue(currentStackPane.translateXProperty(), currentStackPane.getTranslateX(), interpolator),
new KeyValue(nextStackPane.translateXProperty(), nextStackPane.getTranslateX(), interpolator));
KeyFrame k2 = new KeyFrame(DURATION,
clampAnimationEndEventHandler,
new KeyValue(currentStackPane.translateXProperty(), 0, interpolator),
new KeyValue(nextStackPane.translateXProperty(), currentStackPane.getWidth(), interpolator));
timeline.getKeyFrames().setAll(k1, k2);
timeline.play();
} else { // animate left to right
timeline = new Timeline();
KeyFrame k1 = new KeyFrame(Duration.millis(0),
new KeyValue(currentStackPane.translateXProperty(), currentStackPane.getTranslateX(), interpolator),
new KeyValue(nextStackPane.translateXProperty(), nextStackPane.getTranslateX(), interpolator));
KeyFrame k2 = new KeyFrame(DURATION,
clampAnimationEndEventHandler,
new KeyValue(currentStackPane.translateXProperty(), 0, interpolator),
new KeyValue(nextStackPane.translateXProperty(), -currentStackPane.getWidth(), interpolator));
timeline.getKeyFrames().setAll(k1, k2);
timeline.play();
}
}
private EventHandler<ActionEvent> clampAnimationEndEventHandler = new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent t) {
currentStackPane.setTranslateX(0);
nextStackPane.setTranslateX(0);
nextStackPane.setVisible(false);
timeline = null;
}
};
private BooleanProperty arrowsVisible;
public final void setArrowsVisible(boolean value) { arrowsVisibleProperty().set(value); }
public final boolean isArrowsVisible() { return arrowsVisible == null ? DEFAULT_ARROW_VISIBLE : arrowsVisible.get(); }
public final BooleanProperty arrowsVisibleProperty() {
if (arrowsVisible == null) {
arrowsVisible = new StyleableBooleanProperty(DEFAULT_ARROW_VISIBLE) {
@Override
protected void invalidated() {
requestLayout();
}
@Override
public StyleableProperty getStyleableProperty() {
return StyleableProperties.ARROWS_VISIBLE;
}
@Override
public Object getBean() {
return PaginationSkin.this;
}
@Override
public String getName() {
return "arrowVisible";
}
};
}
return arrowsVisible;
}
private BooleanProperty pageInformationVisible;
public final void setPageInformationVisible(boolean value) { pageInformationVisibleProperty().set(value); }
public final boolean isPageInformationVisible() { return pageInformationVisible == null ? DEFAULT_PAGE_INFORMATION_VISIBLE : pageInformationVisible.get(); }
public final BooleanProperty pageInformationVisibleProperty() {
if (pageInformationVisible == null) {
pageInformationVisible = new StyleableBooleanProperty(DEFAULT_PAGE_INFORMATION_VISIBLE) {
@Override
protected void invalidated() {
requestLayout();
}
@Override
public StyleableProperty getStyleableProperty() {
return StyleableProperties.PAGE_INFORMATION_VISIBLE;
}
@Override
public Object getBean() {
return PaginationSkin.this;
}
@Override
public String getName() {
return "pageInformationVisible";
}
};
}
return pageInformationVisible;
}
private ObjectProperty<Side> pageInformationAlignment;
public final void setPageInformationAlignment(Side value) { pageInformationAlignmentProperty().set(value); }
public final Side getPageInformationAlignment() { return pageInformationAlignment == null ? DEFAULT_PAGE_INFORMATION_ALIGNMENT : pageInformationAlignment.get(); }
public final ObjectProperty<Side> pageInformationAlignmentProperty() {
if (pageInformationAlignment == null) {
pageInformationAlignment = new StyleableObjectProperty<Side>(Side.BOTTOM) {
@Override
protected void invalidated() {
requestLayout();
}
@Override
public StyleableProperty getStyleableProperty() {
return StyleableProperties.PAGE_INFORMATION_ALIGNMENT;
}
@Override
public Object getBean() {
return PaginationSkin.this;
}
@Override
public String getName() {
return "pageInformationAlignment";
}
};
}
return pageInformationAlignment;
}
private BooleanProperty tooltipVisible;
public final void setTooltipVisible(boolean value) { tooltipVisibleProperty().set(value); }
public final boolean isTooltipVisible() { return tooltipVisible == null ? DEFAULT_TOOLTIP_VISIBLE : tooltipVisible.get(); }
public final BooleanProperty tooltipVisibleProperty() {
if (tooltipVisible == null) {
tooltipVisible = new StyleableBooleanProperty(DEFAULT_TOOLTIP_VISIBLE) {
@Override
protected void invalidated() {
requestLayout();
}
@Override
public StyleableProperty getStyleableProperty() {
return StyleableProperties.TOOLTIP_VISIBLE;
}
@Override
public Object getBean() {
return PaginationSkin.this;
}
@Override
public String getName() {
return "tooltipVisible";
}
};
}
return tooltipVisible;
}
@Override protected void handleControlPropertyChanged(String p) {
super.handleControlPropertyChanged(p);
if (p == "PAGE_FACTORY") {
if (animate && timeline != null) {
// If we are in the middle of a page animation.
// Speedup and finish the animation then update the page factory.
timeline.setRate(8);
timeline.setOnFinished(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent arg0) {
resetIndexes(false);
navigation.initializePageIndicators();
navigation.updatePageIndicators();
}
});
return;
}
resetIndexes(false);
navigation.initializePageIndicators();
navigation.updatePageIndicators();
} else if (p == "PAGE_COUNT") {
resetIndexes(false);
navigation.initializePageIndicators();
navigation.updatePageIndicators();
}
requestLayout();
}
@Override protected void setWidth(double value) {
super.setWidth(value);
clipRect.setWidth(value);
}
@Override protected void setHeight(double value) {
super.setHeight(value);
clipRect.setHeight(value);
}
@Override protected double computeMinWidth(double height) {
double left = snapSpace(getInsets().getLeft());
double right = snapSpace(getInsets().getRight());
double navigationWidth = navigation.isVisible() ? snapSize(navigation.minWidth(height)) : 0;
return left + Math.max(currentStackPane.minWidth(height), navigationWidth) + right;
}
@Override protected double computeMinHeight(double width) {
double top = snapSpace(getInsets().getTop());
double bottom = snapSpace(getInsets().getBottom());
double navigationHeight = navigation.isVisible() ? snapSize(navigation.minHeight(width)) : 0;
return top + currentStackPane.minHeight(width) + navigationHeight + bottom;
}
@Override protected double computePrefWidth(double height) {
double left = snapSpace(getInsets().getLeft());
double right = snapSpace(getInsets().getRight());
double navigationWidth = navigation.isVisible() ? snapSize(navigation.prefWidth(height)) : 0;
return left + Math.max(currentStackPane.prefWidth(height), navigationWidth) + right;
}
@Override protected double computePrefHeight(double width) {
double top = snapSpace(getInsets().getTop());
double bottom = snapSpace(getInsets().getBottom());
double navigationHeight = navigation.isVisible() ? snapSize(navigation.prefHeight(width)) : 0;
return top + currentStackPane.prefHeight(width) + navigationHeight + bottom;
}
@Override protected void layoutChildren() {
double left = snapSpace(getInsets().getLeft());
double right = snapSpace(getInsets().getRight());
double top = snapSpace(getInsets().getTop());
double bottom = snapSpace(getInsets().getBottom());
double width = snapSize(getWidth() - (left + right));
double height = snapSize(getHeight() - (top + bottom));
double navigationHeight = navigation.isVisible() ? snapSize(navigation.prefHeight(-1)) : 0;
double stackPaneHeight = snapSize(height - navigationHeight);
layoutInArea(currentStackPane, left, top, width, stackPaneHeight, 0, HPos.CENTER, VPos.CENTER);
layoutInArea(nextStackPane, left, top, width, stackPaneHeight, 0, HPos.CENTER, VPos.CENTER);
layoutInArea(navigation, left, stackPaneHeight, width, navigationHeight, 0, HPos.CENTER, VPos.CENTER);
}
class NavigationControl extends StackPane {
private HBox controlBox;
private Button leftArrowButton;
private StackPane leftArrow;
private Button rightArrowButton;
private StackPane rightArrow;
private ToggleGroup indicatorButtons;
private Label pageInformation;
private double previousWidth = -1;
private double minButtonSize = -1;
public NavigationControl() {
getStyleClass().setAll("pagination-control");
controlBox = new HBox();
controlBox.getStyleClass().add("control-box");
leftArrowButton = new Button();
minButtonSize = leftArrowButton.getFont().getSize() * 2;
leftArrowButton.fontProperty().addListener(new ChangeListener<Font>() {
@Override public void changed(ObservableValue<? extends Font> arg0, Font arg1, Font newFont) {
minButtonSize = newFont.getSize() * 2;
for(Node child: controlBox.getChildren()) {
((Control)child).setMinSize(minButtonSize, minButtonSize);
}
// We want to relayout the indicator buttons because the size has changed.
requestLayout();
}
});
leftArrowButton.setMinSize(minButtonSize, minButtonSize);
leftArrowButton.getStyleClass().add("left-arrow-button");
leftArrowButton.setFocusTraversable(false);
HBox.setMargin(leftArrowButton, new Insets(0, 4, 0, 0));
leftArrow = new StackPane();
leftArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
leftArrowButton.setGraphic(leftArrow);
leftArrow.getStyleClass().add("left-arrow");
rightArrowButton = new Button();
rightArrowButton.setMinSize(minButtonSize, minButtonSize);
rightArrowButton.getStyleClass().add("right-arrow-button");
rightArrowButton.setFocusTraversable(false);
HBox.setMargin(rightArrowButton, new Insets(0, 0, 0, 4));
rightArrow = new StackPane();
rightArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
rightArrowButton.setGraphic(rightArrow);
rightArrow.getStyleClass().add("right-arrow");
indicatorButtons = new ToggleGroup();
pageInformation = new Label();
pageInformation.getStyleClass().add("page-information");
getChildren().addAll(controlBox, pageInformation);
initializeNavigationHandlers();
initializePageIndicators();
updatePageIndex();
}
private void initializeNavigationHandlers() {
leftArrowButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent arg0) {
selectPrevious();
requestLayout();
}
});
rightArrowButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent arg0) {
selectNext();
requestLayout();
}
});
pagination.currentPageIndexProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number arg2) {
previousIndex = arg1.intValue();
currentIndex = arg2.intValue();
updatePageIndex();
if (animate) {
currentAnimatedIndex = currentIndex;
animateSwitchPage();
} else {
createPage(currentStackPane, currentIndex);
}
}
});
}
// Create the indicators using fromIndex and toIndex.
private void initializePageIndicators() {
previousIndicatorCount = 0;
controlBox.getChildren().clear();
indicatorButtons.getToggles().clear();
controlBox.getChildren().add(leftArrowButton);
for (int i = fromIndex; i <= toIndex; i++) {
IndicatorButton ib = new IndicatorButton(i);
ib.setMinSize(minButtonSize, minButtonSize);
ib.setToggleGroup(indicatorButtons);
controlBox.getChildren().add(ib);
}
controlBox.getChildren().add(rightArrowButton);
}
// Finds and selects the IndicatorButton using the currentIndex.
private void updatePageIndicators() {
for (int i = 0; i < indicatorButtons.getToggles().size(); i++) {
IndicatorButton ib = (IndicatorButton)indicatorButtons.getToggles().get(i);
if (ib.getPageNumber() == currentIndex) {
ib.setSelected(true);
updatePageInformation();
break;
}
}
}
// Update the page index using the currentIndex and updates the page set
// if necessary.
private void updatePageIndex() {
//System.out.println("SELECT PROPERTY FROM " + fromIndex + " TO " + toIndex + " PREVIOUS " + previousIndex + " CURRENT "+ currentIndex + " PAGE COUNT " + pageCount + " MAX PAGE INDICATOR COUNT " + maxPageIndicatorCount);
if (pageCount == maxPageIndicatorCount) {
if (changePageSet()) {
initializePageIndicators();
}
}
updatePageIndicators();
requestLayout();
}
private void updatePageInformation() {
String currentPageNumber = Integer.toString(currentIndex + 1);
String lastPageNumber = getPageCount() == Pagination.INDETERMINATE ? "..." : Integer.toString(getPageCount());
pageInformation.setText(currentPageNumber + "/" + lastPageNumber);
}
private int previousIndicatorCount = 0;
// Layout the maximum number of page indicators we can fit within the width.
// And always show the selected indicator.
private void layoutPageIndicators() {
double left = snapSpace(getInsets().getLeft());
double right = snapSpace(getInsets().getRight());
double width = snapSize(getWidth()) - (left + right);
double controlBoxleft = snapSpace(controlBox.getInsets().getLeft());
double controlBoxRight = snapSpace(controlBox.getInsets().getRight());
double leftArrowWidth = snapSize(Utils.boundedSize(leftArrowButton.prefWidth(-1), leftArrowButton.minWidth(-1), leftArrowButton.maxWidth(-1)));
double rightArrowWidth = snapSize(Utils.boundedSize(rightArrowButton.prefWidth(-1), rightArrowButton.minWidth(-1), rightArrowButton.maxWidth(-1)));
double spacing = snapSize(controlBox.getSpacing());
double w = width - (controlBoxleft + leftArrowWidth + spacing + rightArrowWidth + controlBoxRight);
if (isPageInformationVisible() &&
(Side.LEFT.equals(getPageInformationAlignment()) ||
Side.RIGHT.equals(getPageInformationAlignment()))) {
w -= snapSize(pageInformation.prefWidth(-1));
}
double x = 0;
int indicatorCount = 0;
for (int i = 0; i < getMaxPageIndicatorCount(); i++) {
int index = i < indicatorButtons.getToggles().size() ? i : indicatorButtons.getToggles().size() - 1;
double iw = minButtonSize;
if (index != -1) {
IndicatorButton ib = (IndicatorButton)indicatorButtons.getToggles().get(index);
iw = snapSize(Utils.boundedSize(ib.prefWidth(-1), ib.minWidth(-1), ib.maxWidth(-1)));
}
x += (iw + controlBox.getSpacing());
if (x >= w) {
break;
}
indicatorCount++;
}
if (indicatorCount != previousIndicatorCount) {
if (indicatorCount < getMaxPageIndicatorCount()) {
maxPageIndicatorCount = indicatorCount;
} else if (indicatorCount >= getMaxPageIndicatorCount()) {
maxPageIndicatorCount = getMaxPageIndicatorCount();
} else {
maxPageIndicatorCount = toIndex - fromIndex;
}
int lastIndicatorButtonIndex;
if (pageCount > maxPageIndicatorCount) {
pageCount = maxPageIndicatorCount;
lastIndicatorButtonIndex = maxPageIndicatorCount - 1;
} else {
if (indicatorCount > getPageCount()) {
pageCount = getPageCount();
lastIndicatorButtonIndex = getPageCount() - 1;
} else {
pageCount = indicatorCount;
lastIndicatorButtonIndex = indicatorCount - 1;
}
}
if (currentIndex >= toIndex) {
// The current index has fallen off the right
toIndex = currentIndex;
fromIndex = toIndex - lastIndicatorButtonIndex;
} else if (currentIndex <= fromIndex) {
// The current index has fallen off the left
fromIndex = currentIndex;
toIndex = fromIndex + lastIndicatorButtonIndex;
} else {
toIndex = fromIndex + lastIndicatorButtonIndex;
}
if (toIndex > getPageCount() - 1) {
toIndex = getPageCount() - 1;
//fromIndex = toIndex - lastIndicatorButtonIndex;
}
if (fromIndex < 0) {
fromIndex = 0;
toIndex = fromIndex + lastIndicatorButtonIndex;
}
initializePageIndicators();
updatePageIndicators();
previousIndicatorCount = indicatorCount;
}
}
// Only change to the next set when the current index is at the start or the end of the set.
// Return true only if we have scrolled to the next/previous set.
private boolean changePageSet() {
int index = indexToIndicatorButtonsIndex(currentIndex);
int lastIndicatorButtonIndex = maxPageIndicatorCount - 1;
if (previousIndex < currentIndex &&
index == 0 &&
lastIndicatorButtonIndex != 0 &&
index % lastIndicatorButtonIndex == 0) {
// Get the right page set
fromIndex = currentIndex;
toIndex = fromIndex + lastIndicatorButtonIndex;
} else if (currentIndex < previousIndex &&
index == lastIndicatorButtonIndex &&
lastIndicatorButtonIndex != 0 &&
index % lastIndicatorButtonIndex == 0) {
// Get the left page set
toIndex = currentIndex;
fromIndex = toIndex - lastIndicatorButtonIndex;
} else {
// We need to get the new page set if the currentIndex is out of range.
// This can happen if setPageIndex() is called programatically.
if (currentIndex < fromIndex || currentIndex > toIndex) {
fromIndex = currentIndex - index;
toIndex = fromIndex + lastIndicatorButtonIndex;
} else {
return false;
}
}
// We have gone past the total number of pages
if (toIndex > getPageCount() - 1) {
if (fromIndex > getPageCount() - 1) {
return false;
} else {
toIndex = getPageCount() - 1;
//fromIndex = toIndex - lastIndicatorButtonIndex;
}
}
// We have gone past the starting page
if (fromIndex < 0) {
fromIndex = 0;
toIndex = fromIndex + lastIndicatorButtonIndex;
}
return true;
}
private int indexToIndicatorButtonsIndex(int index) {
// This should be in the indicator buttons toggle list.
if (index >= fromIndex && index <= toIndex) {
return index - fromIndex;
}
// The requested index is not in indicator buttons list we have to predict
// where the index will be.
int i = 0;
int from = fromIndex;
int to = toIndex;
if (currentIndex > previousIndex) {
while(from < getPageCount() && to < getPageCount()) {
from += i;
to += i;
if (index >= from && index <= to) {
if (index == from) {
return 0;
} else if (index == to) {
return maxPageIndicatorCount - 1;
}
return index - from;
}
i += maxPageIndicatorCount;
}
} else {
while (from > 0 && to > 0) {
from -= i;
to -= i;
if (index >= from && index <= to) {
if (index == from) {
return 0;
} else if (index == to) {
return maxPageIndicatorCount - 1;
}
return index - from;
}
i += maxPageIndicatorCount;
}
}
// We are on the last page set going back to the previous page set
return maxPageIndicatorCount - 1;
}
private Pos sideToPos(Side s) {
if (Side.TOP.equals(s)) {
return Pos.TOP_CENTER;
} else if (Side.RIGHT.equals(s)) {
return Pos.CENTER_RIGHT;
} else if (Side.BOTTOM.equals(s)) {
return Pos.BOTTOM_CENTER;
}
return Pos.CENTER_LEFT;
}
@Override protected double computeMinWidth(double height) {
double left = snapSpace(getInsets().getLeft());
double right = snapSpace(getInsets().getRight());
double leftArrowWidth = snapSize(Utils.boundedSize(leftArrowButton.prefWidth(-1), leftArrowButton.minWidth(-1), leftArrowButton.maxWidth(-1)));
double rightArrowWidth = snapSize(Utils.boundedSize(rightArrowButton.prefWidth(-1), rightArrowButton.minWidth(-1), rightArrowButton.maxWidth(-1)));
double spacing = snapSize(controlBox.getSpacing());
double pageInformationWidth = 0;
Side side = getPageInformationAlignment();
if (Side.LEFT.equals(side) || Side.RIGHT.equals(side)) {
pageInformationWidth = snapSize(pageInformation.prefWidth(-1));
}
return left + leftArrowWidth + spacing + rightArrowWidth + right + pageInformationWidth;
}
@Override protected double computeMinHeight(double width) {
return computePrefHeight(width);
}
@Override protected double computePrefWidth(double height) {
double left = snapSpace(getInsets().getLeft());
double right = snapSpace(getInsets().getRight());
double controlBoxWidth = snapSize(controlBox.prefWidth(height));
double pageInformationWidth = 0;
Side side = getPageInformationAlignment();
if (Side.LEFT.equals(side) || Side.RIGHT.equals(side)) {
pageInformationWidth = snapSize(pageInformation.prefWidth(-1));
}
return left + controlBoxWidth + right + pageInformationWidth;
}
@Override protected double computePrefHeight(double width) {
double top = snapSpace(getInsets().getTop());
double bottom = snapSpace(getInsets().getBottom());
double boxHeight = snapSize(controlBox.prefHeight(width));
double pageInformationHeight = 0;
Side side = getPageInformationAlignment();
if (Side.TOP.equals(side) || Side.BOTTOM.equals(side)) {
pageInformationHeight = snapSize(pageInformation.prefHeight(-1));
}
return top + boxHeight + pageInformationHeight + bottom;
}
@Override protected void layoutChildren() {
double top = snapSpace(getInsets().getTop());
double bottom = snapSpace(getInsets().getBottom());
double left = snapSpace(getInsets().getLeft());
double right = snapSpace(getInsets().getRight());
double width = snapSize(getWidth()) - (left + right);
double height = snapSize(getHeight()) - (top + bottom);
double controlBoxWidth = snapSize(controlBox.prefWidth(-1));
double controlBoxHeight = snapSize(controlBox.prefHeight(-1));
double pageInformationWidth = snapSize(pageInformation.prefWidth(-1));
double pageInformationHeight = snapSize(pageInformation.prefHeight(-1));
leftArrowButton.setDisable(false);
rightArrowButton.setDisable(false);
if (currentIndex == 0) {
// Grey out the left arrow if we are at the beginning.
leftArrowButton.setDisable(true);
}
if (currentIndex == (getPageCount() - 1)) {
// Grey out the right arrow if we have reached the end.
rightArrowButton.setDisable(true);
}
// Reapply CSS so the left and right arrow button's disable state is updated
// immediately.
impl_reapplyCSS();
leftArrowButton.setVisible(isArrowsVisible());
rightArrowButton.setVisible(isArrowsVisible());
pageInformation.setVisible(isPageInformationVisible());
// Determine the number of indicators we can fit within the pagination width.
// if (snapSize(getWidth()) != previousWidth) {
layoutPageIndicators();
// }
previousWidth = getWidth();
HPos controlBoxHPos = controlBox.getAlignment().getHpos();
VPos controlBoxVPos = controlBox.getAlignment().getVpos();
double controlBoxX = left + Utils.computeXOffset(width, controlBoxWidth, controlBoxHPos);
double controlBoxY = top + Utils.computeYOffset(height, controlBoxHeight, controlBoxVPos);
if (isPageInformationVisible()) {
Pos p = sideToPos(getPageInformationAlignment());
HPos pageInformationHPos = p.getHpos();
VPos pageInformationVPos = p.getVpos();
double pageInformationX = left + Utils.computeXOffset(width, pageInformationWidth, pageInformationHPos);
double pageInformationY = top + Utils.computeYOffset(height, pageInformationHeight, pageInformationVPos);
if (Side.TOP.equals(getPageInformationAlignment())) {
pageInformationY = top;
controlBoxY = top + pageInformationHeight;
} else if (Side.RIGHT.equals(getPageInformationAlignment())) {
pageInformationX = width - right - pageInformationWidth;
} else if (Side.BOTTOM.equals(getPageInformationAlignment())) {
controlBoxY = top;
pageInformationY = top + controlBoxHeight;
} else if (Side.LEFT.equals(getPageInformationAlignment())) {
pageInformationX = left;
}
layoutInArea(pageInformation, pageInformationX, pageInformationY, pageInformationWidth, pageInformationHeight, 0, pageInformationHPos, pageInformationVPos);
}
layoutInArea(controlBox, controlBoxX, controlBoxY, controlBoxWidth, controlBoxHeight, 0, controlBoxHPos, controlBoxVPos);
}
}
class IndicatorButton extends ToggleButton {
private int pageNumber;
public IndicatorButton(int pageNumber) {
this.pageNumber = pageNumber;
setFocusTraversable(false);
setIndicatorType();
setTooltipVisible(isTooltipVisible());
getSkinnable().getStyleClass().addListener(new ListChangeListener<String>() {
@Override
public void onChanged(Change<? extends String> change) {
setIndicatorType();
}
});
setOnAction(new EventHandler<ActionEvent>() {
@Override public void handle(ActionEvent arg0) {
int selected = getCurrentPageIndex();
// We do not need to update the selection if it has not changed.
if (selected != IndicatorButton.this.pageNumber) {
pagination.setCurrentPageIndex(IndicatorButton.this.pageNumber);
requestLayout();
}
}
});
tooltipVisibleProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> ov, Boolean oldValue, Boolean newValue) {
setTooltipVisible(newValue);
}
});
}
private void setIndicatorType() {
if (getSkinnable().getStyleClass().contains(Pagination.STYLE_CLASS_BULLET)) {
getStyleClass().remove("number-button");
getStyleClass().add("bullet-button");
setText(null);
} else {
getStyleClass().remove("bullet-button");
getStyleClass().add("number-button");
setText(Integer.toString(this.pageNumber + 1));
}
}
private void setTooltipVisible(boolean b) {
if (b) {
setTooltip(new Tooltip(Integer.toString(IndicatorButton.this.pageNumber + 1)));
} else {
setTooltip(null);
}
}
public int getPageNumber() {
return this.pageNumber;
}
@Override public void fire() {
// we don't toggle from selected to not selected if part of a group
if (getToggleGroup() == null || !isSelected()) {
super.fire();
}
}
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
private static final Boolean DEFAULT_ARROW_VISIBLE = Boolean.FALSE;
private static final Boolean DEFAULT_PAGE_INFORMATION_VISIBLE = Boolean.FALSE;
private static final Side DEFAULT_PAGE_INFORMATION_ALIGNMENT = Side.BOTTOM;
private static final Boolean DEFAULT_TOOLTIP_VISIBLE = Boolean.FALSE;
private static class StyleableProperties {
private static final StyleableProperty<PaginationSkin,Boolean> ARROWS_VISIBLE =
new StyleableProperty<PaginationSkin,Boolean>("-fx-arrows-visible",
BooleanConverter.getInstance(), DEFAULT_ARROW_VISIBLE) {
@Override
public boolean isSettable(PaginationSkin n) {
return n.arrowsVisible == null || !n.arrowsVisible.isBound();
}
@Override
public WritableValue<Boolean> getWritableValue(PaginationSkin n) {
return n.arrowsVisibleProperty();
}
};
private static final StyleableProperty<PaginationSkin,Boolean> PAGE_INFORMATION_VISIBLE =
new StyleableProperty<PaginationSkin,Boolean>("-fx-page-information-visible",
BooleanConverter.getInstance(), DEFAULT_PAGE_INFORMATION_VISIBLE) {
@Override
public boolean isSettable(PaginationSkin n) {
return n.pageInformationVisible == null || !n.pageInformationVisible.isBound();
}
@Override
public WritableValue<Boolean> getWritableValue(PaginationSkin n) {
return n.pageInformationVisibleProperty();
}
};
private static final StyleableProperty<PaginationSkin,Side> PAGE_INFORMATION_ALIGNMENT =
new StyleableProperty<PaginationSkin,Side>("-fx-page-information-alignment",
new EnumConverter<Side>(Side.class), DEFAULT_PAGE_INFORMATION_ALIGNMENT) {
@Override
public boolean isSettable(PaginationSkin n) {
return n.pageInformationAlignment == null || !n.pageInformationAlignment.isBound();
}
@Override
public WritableValue<Side> getWritableValue(PaginationSkin n) {
return n.pageInformationAlignmentProperty();
}
};
private static final StyleableProperty<PaginationSkin,Boolean> TOOLTIP_VISIBLE =
new StyleableProperty<PaginationSkin,Boolean>("-fx-tooltip-visible",
BooleanConverter.getInstance(), DEFAULT_TOOLTIP_VISIBLE) {
@Override
public boolean isSettable(PaginationSkin n) {
return n.tooltipVisible == null || !n.tooltipVisible.isBound();
}
@Override
public WritableValue<Boolean> getWritableValue(PaginationSkin n) {
return n.tooltipVisibleProperty();
}
};
private static final List<StyleableProperty> STYLEABLES;
static {
final List<StyleableProperty> styleables =
new ArrayList<StyleableProperty>(SkinBase.impl_CSS_STYLEABLES());
Collections.addAll(styleables,
ARROWS_VISIBLE,
PAGE_INFORMATION_VISIBLE,
PAGE_INFORMATION_ALIGNMENT,
TOOLTIP_VISIBLE
);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
/**
* @treatAsPrivate implementation detail
* @deprecated This is an internal API that is not intended for use and will be removed in the next version
*/
@Deprecated
public static List<StyleableProperty> impl_CSS_STYLEABLES() {
return StyleableProperties.STYLEABLES;
};
/**
* RT-19263
* @treatAsPrivate implementation detail
* @deprecated This is an experimental API that is not intended for general use and is subject to change in future versions
*/
@Deprecated
public List<StyleableProperty> impl_getStyleableProperties() {
return impl_CSS_STYLEABLES();
}
}
@jewelsea
Copy link

Solution to StackOverflow question: [JavaFX pagination animation (disabling)](JavaFX pagination animation)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment