1 /*
   2  * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.skin.Utils;
  29 import javafx.beans.property.DoubleProperty;
  30 import javafx.css.StyleableBooleanProperty;
  31 import javafx.css.StyleableDoubleProperty;
  32 import javafx.css.StyleableObjectProperty;
  33 import javafx.css.CssMetaData;
  34 
  35 import javafx.css.converter.BooleanConverter;
  36 import javafx.css.converter.EnumConverter;
  37 import javafx.css.converter.SizeConverter;
  38 import com.sun.javafx.scene.control.behavior.PaginationBehavior;
  39 
  40 import java.util.ArrayList;
  41 import java.util.Collections;
  42 import java.util.List;
  43 
  44 import javafx.animation.*;
  45 import javafx.application.Platform;
  46 import javafx.beans.property.BooleanProperty;
  47 import javafx.beans.property.ObjectProperty;
  48 import javafx.beans.value.ChangeListener;
  49 import javafx.beans.value.WritableValue;
  50 import javafx.collections.ListChangeListener;
  51 import javafx.css.Styleable;
  52 import javafx.css.StyleableProperty;
  53 import javafx.event.ActionEvent;
  54 import javafx.event.EventHandler;
  55 import javafx.geometry.HPos;
  56 import javafx.geometry.Insets;
  57 import javafx.geometry.Pos;
  58 import javafx.geometry.Side;
  59 import javafx.geometry.VPos;
  60 import javafx.scene.AccessibleAction;
  61 import javafx.scene.AccessibleAttribute;
  62 import javafx.scene.AccessibleRole;
  63 import javafx.scene.Node;
  64 import javafx.scene.control.*;
  65 import javafx.scene.input.MouseEvent;
  66 import javafx.scene.input.TouchEvent;
  67 import javafx.scene.layout.HBox;
  68 import javafx.scene.layout.StackPane;
  69 import javafx.scene.shape.Rectangle;
  70 import javafx.util.Duration;
  71 
  72 import static com.sun.javafx.scene.control.skin.resources.ControlResources.getString;
  73 
  74 /**
  75  * Default skin implementation for the {@link Pagination} control.
  76  *
  77  * @see Pagination
  78  * @since 9
  79  */
  80 public class PaginationSkin extends SkinBase<Pagination> {
  81 
  82     /***************************************************************************
  83      *                                                                         *
  84      * Static fields                                                           *
  85      *                                                                         *
  86      **************************************************************************/
  87 
  88     private static final Duration DURATION = new Duration(125.0);
  89     private static final double SWIPE_THRESHOLD = 0.30;
  90     private static final double TOUCH_THRESHOLD = 15;
  91     private static final Interpolator interpolator = Interpolator.SPLINE(0.4829, 0.5709, 0.6803, 0.9928);
  92 
  93 
  94 
  95     /***************************************************************************
  96      *                                                                         *
  97      * Private fields                                                          *
  98      *                                                                         *
  99      **************************************************************************/
 100 
 101     private Pagination pagination;
 102     private StackPane currentStackPane;
 103     private StackPane nextStackPane;
 104     private Timeline timeline;
 105     private Rectangle clipRect;
 106 
 107     private NavigationControl navigation;
 108     private int fromIndex;
 109     private int previousIndex;
 110     private int currentIndex;
 111     private int toIndex;
 112     private int pageCount;
 113     private int maxPageIndicatorCount;
 114 
 115     private double startTouchPos;
 116     private double lastTouchPos;
 117     private long startTouchTime;
 118     private long lastTouchTime;
 119     private double touchVelocity;
 120     private boolean touchThresholdBroken;
 121     private int touchEventId = -1;
 122     private boolean nextPageReached = false;
 123     private boolean setInitialDirection = false;
 124     private int direction;
 125 
 126     private int currentAnimatedIndex;
 127     private boolean hasPendingAnimation = false;
 128 
 129     private boolean animate = true;
 130 
 131     private final PaginationBehavior behavior;
 132 
 133 
 134 
 135     /***************************************************************************
 136      *                                                                         *
 137      * Listeners                                                               *
 138      *                                                                         *
 139      **************************************************************************/
 140 
 141     private EventHandler<ActionEvent> swipeAnimationEndEventHandler = new EventHandler<ActionEvent>() {
 142         @Override public void handle(ActionEvent t) {
 143             swapPanes();
 144             timeline = null;
 145 
 146             if (hasPendingAnimation) {
 147                 animateSwitchPage();
 148                 hasPendingAnimation = false;
 149             }
 150         }
 151     };
 152 
 153     private EventHandler<ActionEvent> clampAnimationEndEventHandler = new EventHandler<ActionEvent>() {
 154         @Override public void handle(ActionEvent t) {
 155             currentStackPane.setTranslateX(0);
 156             nextStackPane.setTranslateX(0);
 157             nextStackPane.setVisible(false);
 158             timeline = null;
 159         }
 160     };
 161 
 162 
 163 
 164     /***************************************************************************
 165      *                                                                         *
 166      * Constructors                                                            *
 167      *                                                                         *
 168      **************************************************************************/
 169 
 170     /**
 171      * Creates a new PaginationSkin instance, installing the necessary child
 172      * nodes into the Control {@link Control#getChildren() children} list, as
 173      * well as the necessary input mappings for handling key, mouse, etc events.
 174      *
 175      * @param control The control that this skin should be installed onto.
 176      */
 177     public PaginationSkin(final Pagination control) {
 178         super(control);
 179 
 180         // install default input map for the Pagination control
 181         behavior = new PaginationBehavior(control);
 182 //        control.setInputMap(behavior.getInputMap());
 183 
 184 //        setManaged(false);
 185         clipRect = new Rectangle();
 186         getSkinnable().setClip(clipRect);
 187 
 188         this.pagination = control;
 189 
 190         this.currentStackPane = new StackPane();
 191         currentStackPane.getStyleClass().add("page");
 192 
 193         this.nextStackPane = new StackPane();
 194         nextStackPane.getStyleClass().add("page");
 195         nextStackPane.setVisible(false);
 196 
 197         resetIndexes(true);
 198 
 199         this.navigation = new NavigationControl();
 200 
 201         getChildren().addAll(currentStackPane, nextStackPane, navigation);
 202 
 203         control.maxPageIndicatorCountProperty().addListener(o -> {
 204             resetIndiciesAndNav();
 205         });
 206 
 207         registerChangeListener(control.widthProperty(), e -> clipRect.setWidth(getSkinnable().getWidth()));
 208         registerChangeListener(control.heightProperty(), e -> clipRect.setHeight(getSkinnable().getHeight()));
 209         registerChangeListener(control.pageCountProperty(), e -> resetIndiciesAndNav());
 210         registerChangeListener(control.pageFactoryProperty(), e -> {
 211             if (animate && timeline != null) {
 212                 // If we are in the middle of a page animation.
 213                 // Speedup and finish the animation then update the page factory.
 214                 timeline.setRate(8);
 215                 timeline.setOnFinished(arg0 -> {
 216                     resetIndiciesAndNav();
 217                 });
 218                 return;
 219             }
 220             resetIndiciesAndNav();
 221         });
 222 
 223         initializeSwipeAndTouchHandlers();
 224     }
 225 
 226 
 227 
 228     /***************************************************************************
 229      *                                                                         *
 230      * Properties                                                              *
 231      *                                                                         *
 232      **************************************************************************/
 233 
 234     /** The size of the gap between number buttons and arrow buttons */
 235     private final DoubleProperty arrowButtonGap = new StyleableDoubleProperty(60.0) {
 236         @Override public Object getBean() {
 237             return PaginationSkin.this;
 238         }
 239         @Override public String getName() {
 240             return "arrowButtonGap";
 241         }
 242         @Override public CssMetaData<Pagination,Number> getCssMetaData() {
 243             return StyleableProperties.ARROW_BUTTON_GAP;
 244         }
 245     };
 246     private final DoubleProperty arrowButtonGapProperty() {
 247         return arrowButtonGap;
 248     }
 249     private final double getArrowButtonGap() {
 250         return arrowButtonGap.get();
 251     }
 252     private final void setArrowButtonGap(double value) {
 253         arrowButtonGap.set(value);
 254     }
 255 
 256     private BooleanProperty arrowsVisible;
 257     private final void setArrowsVisible(boolean value) { arrowsVisibleProperty().set(value); }
 258     private final boolean isArrowsVisible() { return arrowsVisible == null ? DEFAULT_ARROW_VISIBLE : arrowsVisible.get(); }
 259     private final BooleanProperty arrowsVisibleProperty() {
 260         if (arrowsVisible == null) {
 261             arrowsVisible = new StyleableBooleanProperty(DEFAULT_ARROW_VISIBLE) {
 262                 @Override
 263                 protected void invalidated() {
 264                     getSkinnable().requestLayout();
 265                 }
 266 
 267                 @Override
 268                 public CssMetaData<Pagination,Boolean> getCssMetaData() {
 269                     return StyleableProperties.ARROWS_VISIBLE;
 270                 }
 271 
 272                 @Override
 273                 public Object getBean() {
 274                     return PaginationSkin.this;
 275                 }
 276 
 277                 @Override
 278                 public String getName() {
 279                     return "arrowVisible";
 280                 }
 281             };
 282         }
 283         return arrowsVisible;
 284     }
 285 
 286     private BooleanProperty pageInformationVisible;
 287     private final void setPageInformationVisible(boolean value) { pageInformationVisibleProperty().set(value); }
 288     private final boolean isPageInformationVisible() { return pageInformationVisible == null ? DEFAULT_PAGE_INFORMATION_VISIBLE : pageInformationVisible.get(); }
 289     private final BooleanProperty pageInformationVisibleProperty() {
 290         if (pageInformationVisible == null) {
 291             pageInformationVisible = new StyleableBooleanProperty(DEFAULT_PAGE_INFORMATION_VISIBLE) {
 292                 @Override
 293                 protected void invalidated() {
 294                     getSkinnable().requestLayout();
 295                 }
 296 
 297                 @Override
 298                 public CssMetaData<Pagination,Boolean> getCssMetaData() {
 299                     return StyleableProperties.PAGE_INFORMATION_VISIBLE;
 300                 }
 301 
 302                 @Override
 303                 public Object getBean() {
 304                     return PaginationSkin.this;
 305                 }
 306 
 307                 @Override
 308                 public String getName() {
 309                     return "pageInformationVisible";
 310                 }
 311             };
 312         }
 313         return pageInformationVisible;
 314     }
 315 
 316     private ObjectProperty<Side> pageInformationAlignment;
 317     private final void setPageInformationAlignment(Side value) { pageInformationAlignmentProperty().set(value); }
 318     private final Side getPageInformationAlignment() { return pageInformationAlignment == null ? DEFAULT_PAGE_INFORMATION_ALIGNMENT : pageInformationAlignment.get(); }
 319     private final ObjectProperty<Side> pageInformationAlignmentProperty() {
 320         if (pageInformationAlignment == null) {
 321             pageInformationAlignment = new StyleableObjectProperty<Side>(Side.BOTTOM) {
 322                 @Override
 323                 protected void invalidated() {
 324                     getSkinnable().requestLayout();
 325                 }
 326 
 327                 @Override
 328                 public CssMetaData<Pagination,Side> getCssMetaData() {
 329                     return StyleableProperties.PAGE_INFORMATION_ALIGNMENT;
 330                 }
 331 
 332                 @Override
 333                 public Object getBean() {
 334                     return PaginationSkin.this;
 335                 }
 336 
 337                 @Override
 338                 public String getName() {
 339                     return "pageInformationAlignment";
 340                 }
 341             };
 342         }
 343         return pageInformationAlignment;
 344     }
 345 
 346     private BooleanProperty tooltipVisible;
 347     private final void setTooltipVisible(boolean value) { tooltipVisibleProperty().set(value); }
 348     private final boolean isTooltipVisible() { return tooltipVisible == null ? DEFAULT_TOOLTIP_VISIBLE : tooltipVisible.get(); }
 349     private final BooleanProperty tooltipVisibleProperty() {
 350         if (tooltipVisible == null) {
 351             tooltipVisible = new StyleableBooleanProperty(DEFAULT_TOOLTIP_VISIBLE) {
 352                 @Override
 353                 protected void invalidated() {
 354                     getSkinnable().requestLayout();
 355                 }
 356 
 357                 @Override
 358                 public CssMetaData<Pagination,Boolean> getCssMetaData() {
 359                     return StyleableProperties.TOOLTIP_VISIBLE;
 360                 }
 361 
 362                 @Override
 363                 public Object getBean() {
 364                     return PaginationSkin.this;
 365                 }
 366 
 367                 @Override
 368                 public String getName() {
 369                     return "tooltipVisible";
 370                 }
 371             };
 372         }
 373         return tooltipVisible;
 374     }
 375 
 376 
 377 
 378     /***************************************************************************
 379      *                                                                         *
 380      * Public API                                                              *
 381      *                                                                         *
 382      **************************************************************************/
 383 
 384     /** {@inheritDoc} */
 385     @Override public void dispose() {
 386         super.dispose();
 387 
 388         if (behavior != null) {
 389             behavior.dispose();
 390         }
 391     }
 392 
 393     /** {@inheritDoc} */
 394     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 395         double navigationWidth = navigation.isVisible() ? snapSize(navigation.minWidth(height)) : 0;
 396         return leftInset + Math.max(currentStackPane.minWidth(height), navigationWidth) + rightInset;
 397     }
 398 
 399     /** {@inheritDoc} */
 400     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 401         double navigationHeight = navigation.isVisible() ? snapSize(navigation.minHeight(width)) : 0;
 402         return topInset + currentStackPane.minHeight(width) + navigationHeight + bottomInset;
 403     }
 404 
 405     /** {@inheritDoc} */
 406     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 407         double navigationWidth = navigation.isVisible() ? snapSize(navigation.prefWidth(height)) : 0;
 408         return leftInset + Math.max(currentStackPane.prefWidth(height), navigationWidth) + rightInset;
 409     }
 410 
 411     /** {@inheritDoc} */
 412     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 413         double navigationHeight = navigation.isVisible() ? snapSize(navigation.prefHeight(width)) : 0;
 414         return topInset + currentStackPane.prefHeight(width) + navigationHeight + bottomInset;
 415     }
 416 
 417     /** {@inheritDoc} */
 418     @Override protected void layoutChildren(final double x, final double y,
 419                                             final double w, final double h) {
 420         double navigationHeight = navigation.isVisible() ? snapSize(navigation.prefHeight(-1)) : 0;
 421         double stackPaneHeight = snapSize(h - navigationHeight);
 422 
 423         layoutInArea(currentStackPane, x, y, w, stackPaneHeight, 0, HPos.CENTER, VPos.CENTER);
 424         layoutInArea(nextStackPane, x, y, w, stackPaneHeight, 0, HPos.CENTER, VPos.CENTER);
 425         layoutInArea(navigation, x, stackPaneHeight, w, navigationHeight, 0, HPos.CENTER, VPos.CENTER);
 426     }
 427 
 428     /** {@inheritDoc} */
 429     @Override protected Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 430         switch (attribute) {
 431             case FOCUS_ITEM: return navigation.indicatorButtons.getSelectedToggle();
 432             case ITEM_COUNT: return navigation.indicatorButtons.getToggles().size();
 433             case ITEM_AT_INDEX: {
 434                 Integer index = (Integer)parameters[0];
 435                 if (index == null) return null;
 436                 return navigation.indicatorButtons.getToggles().get(index);
 437             }
 438             default: return super.queryAccessibleAttribute(attribute, parameters);
 439         }
 440     }
 441 
 442 
 443 
 444     /***************************************************************************
 445      *                                                                         *
 446      * Private implementation                                                  *
 447      *                                                                         *
 448      **************************************************************************/
 449 
 450     private void selectNext() {
 451         if (getCurrentPageIndex() < getPageCount() - 1) {
 452             pagination.setCurrentPageIndex(getCurrentPageIndex() + 1);
 453         }
 454     }
 455 
 456     private void selectPrevious() {
 457         if (getCurrentPageIndex() > 0) {
 458             pagination.setCurrentPageIndex(getCurrentPageIndex() - 1);
 459         }
 460     }
 461 
 462     private void resetIndiciesAndNav() {
 463         resetIndexes(false);
 464         navigation.initializePageIndicators();
 465         navigation.updatePageIndicators();
 466     }
 467 
 468     private void initializeSwipeAndTouchHandlers() {
 469         final Pagination control = getSkinnable();
 470 
 471         getSkinnable().addEventHandler(TouchEvent.TOUCH_PRESSED, e -> {
 472             if (touchEventId == -1) {
 473                 touchEventId = e.getTouchPoint().getId();
 474             }
 475             if (touchEventId != e.getTouchPoint().getId()) {
 476                 return;
 477             }
 478             lastTouchPos = startTouchPos = e.getTouchPoint().getX();
 479             lastTouchTime = startTouchTime = System.currentTimeMillis();
 480             touchThresholdBroken = false;
 481             e.consume();
 482         });
 483 
 484         getSkinnable().addEventHandler(TouchEvent.TOUCH_MOVED, e -> {
 485             if (touchEventId != e.getTouchPoint().getId()) {
 486                 return;
 487             }
 488 
 489             double drag = e.getTouchPoint().getX() - lastTouchPos;
 490             long time = System.currentTimeMillis() - lastTouchTime;
 491             touchVelocity = drag/time;
 492             lastTouchPos = e.getTouchPoint().getX();
 493             lastTouchTime = System.currentTimeMillis();
 494             double delta = e.getTouchPoint().getX() - startTouchPos;
 495 
 496             if (!touchThresholdBroken && Math.abs(delta) > TOUCH_THRESHOLD) {
 497                 touchThresholdBroken = true;
 498             }
 499 
 500             if (touchThresholdBroken) {
 501                 double width = control.getWidth() - (snappedLeftInset() + snappedRightInset());
 502                 double currentPaneX;
 503                 double nextPaneX;
 504 
 505                 if (!setInitialDirection) {
 506                     // Remember the direction travelled so we can
 507                     // load the next or previous page if the touch is not released.
 508                     setInitialDirection = true;
 509                     direction = delta < 0 ? 1 : -1;
 510                 }
 511                 if (delta < 0) {
 512                     if (direction == -1) {
 513                         nextStackPane.getChildren().clear();
 514                         direction = 1;
 515                     }
 516                     // right to left
 517                     if (Math.abs(delta) <= width) {
 518                         currentPaneX = delta;
 519                         nextPaneX = width + delta;
 520                         nextPageReached = false;
 521                     } else {
 522                         currentPaneX = -width;
 523                         nextPaneX = 0;
 524                         nextPageReached = true;
 525                     }
 526                     currentStackPane.setTranslateX(currentPaneX);
 527                     if (getCurrentPageIndex() < getPageCount() - 1) {
 528                         createPage(nextStackPane, currentIndex + 1);
 529                         nextStackPane.setVisible(true);
 530                         nextStackPane.setTranslateX(nextPaneX);
 531                     } else {
 532                         currentStackPane.setTranslateX(0);
 533                     }
 534                 } else {
 535                     // left to right
 536                     if (direction == 1) {
 537                         nextStackPane.getChildren().clear();
 538                         direction = -1;
 539                     }
 540                     if (Math.abs(delta) <= width) {
 541                         currentPaneX = delta;
 542                         nextPaneX = -width + delta;
 543                         nextPageReached = false;
 544                     } else {
 545                         currentPaneX = width;
 546                         nextPaneX = 0;
 547                         nextPageReached = true;
 548                     }
 549                     currentStackPane.setTranslateX(currentPaneX);
 550                     if (getCurrentPageIndex() != 0) {
 551                         createPage(nextStackPane, currentIndex - 1);
 552                         nextStackPane.setVisible(true);
 553                         nextStackPane.setTranslateX(nextPaneX);
 554                     } else {
 555                         currentStackPane.setTranslateX(0);
 556                     }
 557                 }
 558             }
 559             e.consume();
 560         });
 561 
 562         getSkinnable().addEventHandler(TouchEvent.TOUCH_RELEASED, e -> {
 563             if (touchEventId != e.getTouchPoint().getId()) {
 564                 return;
 565             } else {
 566                 touchEventId = -1;
 567                 setInitialDirection = false;
 568             }
 569 
 570             if (touchThresholdBroken) {
 571                 // determin if click or swipe
 572                 final double drag = e.getTouchPoint().getX() - startTouchPos;
 573                 // calculate complete time from start to end of drag
 574                 final long time = System.currentTimeMillis() - startTouchTime;
 575                 // if time is less than 300ms then considered a quick swipe and whole time is used
 576                 final boolean quick = time < 300;
 577                 // calculate velocity
 578                 final double velocity = quick ? (double)drag / time : touchVelocity; // pixels/ms
 579                 // calculate distance we would travel at this speed for 500ms of travel
 580                 final double distance = (velocity * 500);
 581                 final double width = control.getWidth() - (snappedLeftInset() + snappedRightInset());
 582 
 583                 // The swipe distance travelled.
 584                 final double threshold = Math.abs(distance/width);
 585                 // The touch and dragged distance travelled.
 586                 final double delta = Math.abs(drag/width);
 587                 if (threshold > SWIPE_THRESHOLD || delta > SWIPE_THRESHOLD) {
 588                     if (startTouchPos > e.getTouchPoint().getX()) {
 589                         selectNext();
 590                     } else {
 591                         selectPrevious();
 592                     }
 593                 } else {
 594                     animateClamping(startTouchPos > e.getTouchPoint().getSceneX());
 595                 }
 596             }
 597             e.consume();
 598         });
 599     }
 600 
 601     private void resetIndexes(boolean usePageIndex) {
 602         maxPageIndicatorCount = getMaxPageIndicatorCount();
 603         // Used to indicate that we can change a set of pages.
 604         pageCount = getPageCount();
 605         if (pageCount > maxPageIndicatorCount) {
 606             pageCount = maxPageIndicatorCount;
 607         }
 608 
 609         fromIndex = 0;
 610         previousIndex = 0;
 611         currentIndex = usePageIndex ? getCurrentPageIndex() : 0;
 612         toIndex = pageCount - 1;
 613 
 614         if (pageCount == Pagination.INDETERMINATE && maxPageIndicatorCount == Pagination.INDETERMINATE) {
 615             // We do not know how many indicators  can fit.  Let the layout pass compute it.
 616             toIndex = 0;
 617         }
 618 
 619         boolean isAnimate = animate;
 620         if (isAnimate) {
 621             animate = false;
 622         }
 623 
 624         // Remove the children in the pane before we create a new page.
 625         currentStackPane.getChildren().clear();
 626         nextStackPane.getChildren().clear();
 627 
 628         pagination.setCurrentPageIndex(currentIndex);
 629         createPage(currentStackPane, currentIndex);
 630 
 631         if (isAnimate) {
 632             animate = true;
 633         }
 634     }
 635 
 636     private boolean createPage(StackPane pane, int index) {
 637         if (pagination.getPageFactory() != null && pane.getChildren().isEmpty()) {
 638             Node content = pagination.getPageFactory().call(index);
 639             // If the content is null we don't want to switch pages.
 640             if (content != null) {
 641                 pane.getChildren().setAll(content);
 642                 return true;
 643             } else {
 644                 // Disable animation if the new page does not exist.  It is strange to
 645                 // see the same page animated out then in.
 646                 boolean isAnimate = animate;
 647                 if (isAnimate) {
 648                     animate = false;
 649                 }
 650 
 651                 if (pagination.getPageFactory().call(previousIndex) != null) {
 652                     pagination.setCurrentPageIndex(previousIndex);
 653                 } else {
 654                     // Set the page index to 0 because both the current,
 655                     // and the previous pages have no content.
 656                     pagination.setCurrentPageIndex(0);
 657                 }
 658 
 659                 if (isAnimate) {
 660                     animate = true;
 661                 }
 662                 return false;
 663             }
 664         }
 665         return false;
 666     }
 667 
 668     private int getPageCount() {
 669         if (getSkinnable().getPageCount() < 1) {
 670             return 1;
 671         }
 672         return getSkinnable().getPageCount();
 673     }
 674 
 675     private int getMaxPageIndicatorCount() {
 676         return getSkinnable().getMaxPageIndicatorCount();
 677     }
 678 
 679     private int getCurrentPageIndex() {
 680         return getSkinnable().getCurrentPageIndex();
 681     }
 682 
 683     private void animateSwitchPage() {
 684         if (timeline != null) {
 685             timeline.setRate(8);
 686             hasPendingAnimation = true;
 687             return;
 688         }
 689 
 690         // We are handling a touch event if nextPane's page has already been
 691         // created and visible == true.
 692         if (!nextStackPane.isVisible()) {
 693             if (!createPage(nextStackPane, currentAnimatedIndex)) {
 694                 // The next page does not exist just return without starting
 695                 // any animation.
 696                 return;
 697             }
 698         }
 699         if (nextPageReached) {
 700             // No animation is needed when the next page is already showing
 701             // and in the correct position.  Just swap the panes and return
 702             swapPanes();
 703             nextPageReached = false;
 704             return;
 705         }
 706 
 707         nextStackPane.setCache(true);
 708         currentStackPane.setCache(true);
 709 
 710         // wait one pulse then animate
 711         Platform.runLater(() -> {
 712             // We are handling a touch event if nextPane's translateX is not 0
 713             boolean useTranslateX = nextStackPane.getTranslateX() != 0;
 714             if (currentAnimatedIndex > previousIndex) {  // animate right to left
 715                 if (!useTranslateX) {
 716                     nextStackPane.setTranslateX(currentStackPane.getWidth());
 717                 }
 718                 nextStackPane.setVisible(true);
 719                 timeline = new Timeline();
 720                 KeyFrame k1 =  new KeyFrame(Duration.millis(0),
 721                     new KeyValue(currentStackPane.translateXProperty(),
 722                         useTranslateX ? currentStackPane.getTranslateX() : 0,
 723                         interpolator),
 724                     new KeyValue(nextStackPane.translateXProperty(),
 725                         useTranslateX ?
 726                             nextStackPane.getTranslateX() : currentStackPane.getWidth(), interpolator));
 727                 KeyFrame k2 = new KeyFrame(DURATION,
 728                     swipeAnimationEndEventHandler,
 729                     new KeyValue(currentStackPane.translateXProperty(), -currentStackPane.getWidth(), interpolator),
 730                     new KeyValue(nextStackPane.translateXProperty(), 0, interpolator));
 731                 timeline.getKeyFrames().setAll(k1, k2);
 732                 timeline.play();
 733             } else { // animate left to right
 734                 if (!useTranslateX) {
 735                     nextStackPane.setTranslateX(-currentStackPane.getWidth());
 736                 }
 737                 nextStackPane.setVisible(true);
 738                 timeline = new Timeline();
 739                 KeyFrame k1 = new KeyFrame(Duration.millis(0),
 740                     new KeyValue(currentStackPane.translateXProperty(),
 741                         useTranslateX ? currentStackPane.getTranslateX() : 0,
 742                         interpolator),
 743                     new KeyValue(nextStackPane.translateXProperty(),
 744                         useTranslateX ? nextStackPane.getTranslateX() : -currentStackPane.getWidth(),
 745                         interpolator));
 746                 KeyFrame k2 = new KeyFrame(DURATION,
 747                     swipeAnimationEndEventHandler,
 748                     new KeyValue(currentStackPane.translateXProperty(), currentStackPane.getWidth(), interpolator),
 749                     new KeyValue(nextStackPane.translateXProperty(), 0, interpolator));
 750                 timeline.getKeyFrames().setAll(k1, k2);
 751                 timeline.play();
 752             }
 753         });
 754     }
 755 
 756     private void swapPanes() {
 757         StackPane temp = currentStackPane;
 758         currentStackPane = nextStackPane;
 759         nextStackPane = temp;
 760 
 761         currentStackPane.setTranslateX(0);
 762         currentStackPane.setCache(false);
 763 
 764         nextStackPane.setTranslateX(0);
 765         nextStackPane.setCache(false);
 766         nextStackPane.setVisible(false);
 767         nextStackPane.getChildren().clear();
 768     }
 769 
 770     // If the swipe hasn't reached the THRESHOLD we want to animate the clamping.
 771     private void animateClamping(boolean rightToLeft) {
 772         if (rightToLeft) {  // animate right to left
 773             timeline = new Timeline();
 774             KeyFrame k1 = new KeyFrame(Duration.millis(0),
 775                 new KeyValue(currentStackPane.translateXProperty(), currentStackPane.getTranslateX(), interpolator),
 776                 new KeyValue(nextStackPane.translateXProperty(), nextStackPane.getTranslateX(), interpolator));
 777             KeyFrame k2 = new KeyFrame(DURATION,
 778                 clampAnimationEndEventHandler,
 779                 new KeyValue(currentStackPane.translateXProperty(), 0, interpolator),
 780                 new KeyValue(nextStackPane.translateXProperty(), currentStackPane.getWidth(), interpolator));
 781             timeline.getKeyFrames().setAll(k1, k2);
 782             timeline.play();
 783         } else { // animate left to right
 784             timeline = new Timeline();
 785             KeyFrame k1 = new KeyFrame(Duration.millis(0),
 786                 new KeyValue(currentStackPane.translateXProperty(), currentStackPane.getTranslateX(), interpolator),
 787                 new KeyValue(nextStackPane.translateXProperty(), nextStackPane.getTranslateX(), interpolator));
 788             KeyFrame k2 = new KeyFrame(DURATION,
 789                 clampAnimationEndEventHandler,
 790                 new KeyValue(currentStackPane.translateXProperty(), 0, interpolator),
 791                 new KeyValue(nextStackPane.translateXProperty(), -currentStackPane.getWidth(), interpolator));
 792             timeline.getKeyFrames().setAll(k1, k2);
 793             timeline.play();
 794         }
 795     }
 796 
 797 
 798 
 799     /***************************************************************************
 800      *                                                                         *
 801      * Support classes                                                         *
 802      *                                                                         *
 803      **************************************************************************/
 804 
 805     class NavigationControl extends StackPane {
 806 
 807         private HBox controlBox;
 808         private Button leftArrowButton;
 809         private StackPane leftArrow;
 810         private Button rightArrowButton;
 811         private StackPane rightArrow;
 812         private ToggleGroup indicatorButtons;
 813         private Label pageInformation;
 814         private double minButtonSize = -1;
 815 
 816         public NavigationControl() {
 817             getStyleClass().setAll("pagination-control");
 818 
 819             // redirect mouse events to behavior
 820             addEventHandler(MouseEvent.MOUSE_PRESSED, behavior::mousePressed);
 821 
 822             controlBox = new HBox();
 823             controlBox.getStyleClass().add("control-box");
 824 
 825             leftArrowButton = new Button();
 826             leftArrowButton.setAccessibleText(getString("Accessibility.title.Pagination.PreviousButton"));
 827             minButtonSize = leftArrowButton.getFont().getSize() * 2;
 828             leftArrowButton.fontProperty().addListener((arg0, arg1, newFont) -> {
 829                 minButtonSize = newFont.getSize() * 2;
 830                 for(Node child: controlBox.getChildren()) {
 831                     ((Control)child).setMinSize(minButtonSize, minButtonSize);
 832                 }
 833                 // We want to relayout the indicator buttons because the size has changed.
 834                 requestLayout();
 835             });
 836             leftArrowButton.setMinSize(minButtonSize, minButtonSize);
 837             leftArrowButton.prefWidthProperty().bind(leftArrowButton.minWidthProperty());
 838             leftArrowButton.prefHeightProperty().bind(leftArrowButton.minHeightProperty());
 839             leftArrowButton.getStyleClass().add("left-arrow-button");
 840             leftArrowButton.setFocusTraversable(false);
 841             HBox.setMargin(leftArrowButton, new Insets(0, snapSize(arrowButtonGap.get()), 0, 0));
 842             leftArrow = new StackPane();
 843             leftArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
 844             leftArrowButton.setGraphic(leftArrow);
 845             leftArrow.getStyleClass().add("left-arrow");
 846 
 847             rightArrowButton = new Button();
 848             rightArrowButton.setAccessibleText(getString("Accessibility.title.Pagination.NextButton"));
 849             rightArrowButton.setMinSize(minButtonSize, minButtonSize);
 850             rightArrowButton.prefWidthProperty().bind(rightArrowButton.minWidthProperty());
 851             rightArrowButton.prefHeightProperty().bind(rightArrowButton.minHeightProperty());
 852             rightArrowButton.getStyleClass().add("right-arrow-button");
 853             rightArrowButton.setFocusTraversable(false);
 854             HBox.setMargin(rightArrowButton, new Insets(0, 0, 0, snapSize(arrowButtonGap.get())));
 855             rightArrow = new StackPane();
 856             rightArrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
 857             rightArrowButton.setGraphic(rightArrow);
 858             rightArrow.getStyleClass().add("right-arrow");
 859 
 860             indicatorButtons = new ToggleGroup();
 861 
 862             pageInformation = new Label();
 863             pageInformation.getStyleClass().add("page-information");
 864 
 865             getChildren().addAll(controlBox, pageInformation);
 866             initializeNavigationHandlers();
 867             initializePageIndicators();
 868             updatePageIndex();
 869 
 870             // listen to changes to arrowButtonGap and update margins
 871             arrowButtonGap.addListener((observable, oldValue, newValue) -> {
 872                 if (newValue.doubleValue() == 0) {
 873                     HBox.setMargin(leftArrowButton, null);
 874                     HBox.setMargin(rightArrowButton, null);
 875 
 876                 } else {
 877                     HBox.setMargin(leftArrowButton, new Insets(0, snapSize(newValue.doubleValue()), 0, 0));
 878                     HBox.setMargin(rightArrowButton, new Insets(0, 0, 0, snapSize(newValue.doubleValue())));
 879                 }
 880             });
 881         }
 882 
 883         private void initializeNavigationHandlers() {
 884             leftArrowButton.setOnAction(arg0 -> {
 885                 getNode().requestFocus();
 886                 selectPrevious();
 887                 requestLayout();
 888             });
 889 
 890             rightArrowButton.setOnAction(arg0 -> {
 891                 getNode().requestFocus();
 892                 selectNext();
 893                 requestLayout();
 894             });
 895 
 896             pagination.currentPageIndexProperty().addListener((arg0, arg1, arg2) -> {
 897                 previousIndex = arg1.intValue();
 898                 currentIndex = arg2.intValue();
 899                 updatePageIndex();
 900                 if (animate) {
 901                     currentAnimatedIndex = currentIndex;
 902                     animateSwitchPage();
 903                 } else {
 904                     createPage(currentStackPane, currentIndex);
 905                 }
 906             });
 907         }
 908 
 909         // Create the indicators using fromIndex and toIndex.
 910         private void initializePageIndicators() {
 911             previousIndicatorCount = 0;
 912             controlBox.getChildren().clear();
 913             clearIndicatorButtons();
 914 
 915             controlBox.getChildren().add(leftArrowButton);
 916             for (int i = fromIndex; i <= toIndex; i++) {
 917                 IndicatorButton ib = new IndicatorButton(i);
 918                 ib.setMinSize(minButtonSize, minButtonSize);
 919                 ib.setToggleGroup(indicatorButtons);
 920                 controlBox.getChildren().add(ib);
 921             }
 922             controlBox.getChildren().add(rightArrowButton);
 923         }
 924 
 925         private void clearIndicatorButtons() {
 926             for (Toggle toggle : indicatorButtons.getToggles()) {
 927                 if (toggle instanceof IndicatorButton) {
 928                     IndicatorButton indicatorButton = (IndicatorButton) toggle;
 929                     indicatorButton.release();
 930                 }
 931             }
 932             indicatorButtons.getToggles().clear();
 933         }
 934 
 935         // Finds and selects the IndicatorButton using the currentIndex.
 936          private void updatePageIndicators() {
 937             for (int i = 0; i < indicatorButtons.getToggles().size(); i++) {
 938                 IndicatorButton ib = (IndicatorButton)indicatorButtons.getToggles().get(i);
 939                 if (ib.getPageNumber() == currentIndex) {
 940                     ib.setSelected(true);
 941                     updatePageInformation();
 942                     break;
 943                 }
 944             }
 945             getSkinnable().notifyAccessibleAttributeChanged(AccessibleAttribute.FOCUS_ITEM);
 946         }
 947 
 948         // Update the page index using the currentIndex and updates the page set
 949         // if necessary.
 950         private void updatePageIndex() {
 951             //System.out.println("SELECT PROPERTY FROM " + fromIndex + " TO " + toIndex + " PREVIOUS " + previousIndex + " CURRENT "+ currentIndex + " PAGE COUNT " + pageCount + " MAX PAGE INDICATOR COUNT " + maxPageIndicatorCount);
 952             if (pageCount == maxPageIndicatorCount) {
 953                 if (changePageSet()) {
 954                     initializePageIndicators();
 955                 }
 956             }
 957             updatePageIndicators();
 958             requestLayout();
 959         }
 960 
 961         private void updatePageInformation() {
 962             String currentPageNumber = Integer.toString(currentIndex + 1);
 963             String lastPageNumber = getPageCount() == Pagination.INDETERMINATE ? "..." : Integer.toString(getPageCount());
 964             pageInformation.setText(currentPageNumber + "/" + lastPageNumber);
 965         }
 966 
 967         private int previousIndicatorCount = 0;
 968         // Layout the maximum number of page indicators we can fit within the width.
 969         // And always show the selected indicator.
 970         private void layoutPageIndicators() {
 971             final double left = snappedLeftInset();
 972             final double right = snappedRightInset();
 973             final double width = snapSize(getWidth()) - (left + right);
 974             final double controlBoxleft = controlBox.snappedLeftInset();
 975             final double controlBoxRight = controlBox.snappedRightInset();
 976             final double leftArrowWidth = snapSize(Utils.boundedSize(leftArrowButton.prefWidth(-1), leftArrowButton.minWidth(-1), leftArrowButton.maxWidth(-1)));
 977             final double rightArrowWidth = snapSize(Utils.boundedSize(rightArrowButton.prefWidth(-1), rightArrowButton.minWidth(-1), rightArrowButton.maxWidth(-1)));
 978             final double spacing = snapSize(controlBox.getSpacing());
 979             double w = width - (controlBoxleft + leftArrowWidth + 2* arrowButtonGap.get() + spacing + rightArrowWidth + controlBoxRight);
 980 
 981             if (isPageInformationVisible() &&
 982                     (Side.LEFT.equals(getPageInformationAlignment()) ||
 983                     Side.RIGHT.equals(getPageInformationAlignment()))) {
 984                 w -= snapSize(pageInformation.prefWidth(-1));
 985             }
 986 
 987             double x = 0;
 988             int indicatorCount = 0;
 989             for (int i = 0; i < getMaxPageIndicatorCount(); i++) {
 990                 int index = i < indicatorButtons.getToggles().size() ? i : indicatorButtons.getToggles().size() - 1;
 991                 double iw = minButtonSize;
 992                 if (index != -1) {
 993                     IndicatorButton ib = (IndicatorButton)indicatorButtons.getToggles().get(index);
 994                     iw = snapSize(Utils.boundedSize(ib.prefWidth(-1), ib.minWidth(-1), ib.maxWidth(-1)));
 995                 }
 996 
 997                 x += (iw + spacing);
 998                 if (x > w) {
 999                     break;
1000                 }
1001                 indicatorCount++;
1002             }
1003             if (indicatorCount == 0) {
1004                indicatorCount = 1; // The parent didn't respect the minSize of this Pagination.
1005                                    // We will show at least one indicator nonetheless.
1006             }
1007 
1008             if (indicatorCount != previousIndicatorCount) {
1009                 if (indicatorCount < getMaxPageIndicatorCount()) {
1010                     maxPageIndicatorCount = indicatorCount;
1011                 } else {
1012                     maxPageIndicatorCount = getMaxPageIndicatorCount();
1013                 }
1014 
1015                 int lastIndicatorButtonIndex;
1016                 if (pageCount > maxPageIndicatorCount) {
1017                     pageCount = maxPageIndicatorCount;
1018                     lastIndicatorButtonIndex = maxPageIndicatorCount - 1;
1019                  } else {
1020                     if (indicatorCount > getPageCount()) {
1021                         pageCount = getPageCount();
1022                         lastIndicatorButtonIndex = getPageCount() - 1;
1023                     } else {
1024                         pageCount = indicatorCount;
1025                         lastIndicatorButtonIndex = indicatorCount - 1;
1026                     }
1027                 }
1028 
1029                 if (currentIndex >= toIndex) {
1030                     // The current index has fallen off the right
1031                     toIndex = currentIndex;
1032                     fromIndex = toIndex - lastIndicatorButtonIndex;
1033                 } else if (currentIndex <= fromIndex) {
1034                     // The current index has fallen off the left
1035                     fromIndex = currentIndex;
1036                     toIndex = fromIndex + lastIndicatorButtonIndex;
1037                 } else {
1038                     toIndex = fromIndex + lastIndicatorButtonIndex;
1039                 }
1040 
1041                 if (toIndex > getPageCount() - 1) {
1042                     toIndex = getPageCount() - 1;
1043                     //fromIndex = toIndex - lastIndicatorButtonIndex;
1044                 }
1045 
1046                 if (fromIndex < 0) {
1047                     fromIndex = 0;
1048                     toIndex = fromIndex + lastIndicatorButtonIndex;
1049                 }
1050 
1051                 initializePageIndicators();
1052                 updatePageIndicators();
1053                 previousIndicatorCount = indicatorCount;
1054             }
1055         }
1056 
1057         // Only change to the next set when the current index is at the start or the end of the set.
1058         // Return true only if we have scrolled to the next/previous set.
1059         private boolean changePageSet() {
1060             int index = indexToIndicatorButtonsIndex(currentIndex);
1061             int lastIndicatorButtonIndex = maxPageIndicatorCount - 1;
1062             if (previousIndex < currentIndex &&
1063                     index == 0 &&
1064                     lastIndicatorButtonIndex != 0 &&
1065                     index % lastIndicatorButtonIndex == 0) {
1066                 // Get the right page set
1067                 fromIndex = currentIndex;
1068                 toIndex = fromIndex + lastIndicatorButtonIndex;
1069             } else if (currentIndex < previousIndex &&
1070                     index == lastIndicatorButtonIndex &&
1071                     lastIndicatorButtonIndex != 0 &&
1072                     index % lastIndicatorButtonIndex == 0) {
1073                 // Get the left page set
1074                 toIndex = currentIndex;
1075                 fromIndex = toIndex - lastIndicatorButtonIndex;
1076             } else {
1077                 // We need to get the new page set if the currentIndex is out of range.
1078                 // This can happen if setPageIndex() is called programatically.
1079                 if (currentIndex < fromIndex || currentIndex > toIndex) {
1080                     fromIndex = currentIndex - index;
1081                     toIndex = fromIndex + lastIndicatorButtonIndex;
1082                 } else {
1083                     return false;
1084                 }
1085             }
1086 
1087             // We have gone past the total number of pages
1088             if (toIndex > getPageCount() - 1) {
1089                 if (fromIndex > getPageCount() - 1) {
1090                     return false;
1091                 } else {
1092                   toIndex = getPageCount() - 1;
1093                   //fromIndex = toIndex - lastIndicatorButtonIndex;
1094                 }
1095             }
1096 
1097             // We have gone past the starting page
1098             if (fromIndex < 0) {
1099                 fromIndex = 0;
1100                 toIndex = fromIndex + lastIndicatorButtonIndex;
1101             }
1102             return true;
1103         }
1104 
1105         private int indexToIndicatorButtonsIndex(int index) {
1106             // This should be in the indicator buttons toggle list.
1107             if (index >= fromIndex && index <= toIndex) {
1108                 return index - fromIndex;
1109             }
1110             // The requested index is not in indicator buttons list we have to predict
1111             // where the index will be.
1112             int i = 0;
1113             int from = fromIndex;
1114             int to = toIndex;
1115             if (currentIndex > previousIndex) {
1116                 while(from < getPageCount() && to < getPageCount()) {
1117                     from += i;
1118                     to += i;
1119                     if (index >= from && index <= to) {
1120                         if (index == from) {
1121                             return 0;
1122                         } else if (index == to) {
1123                             return maxPageIndicatorCount - 1;
1124                         }
1125                         return index - from;
1126                     }
1127                     i += maxPageIndicatorCount;
1128                 }
1129             } else {
1130                 while (from > 0 && to > 0) {
1131                     from -= i;
1132                     to -= i;
1133                     if (index >= from && index <= to) {
1134                         if (index == from) {
1135                             return 0;
1136                         } else if (index == to) {
1137                             return maxPageIndicatorCount - 1;
1138                         }
1139                         return index - from;
1140                     }
1141                     i += maxPageIndicatorCount;
1142                 }
1143             }
1144             // We are on the last page set going back to the previous page set
1145             return maxPageIndicatorCount - 1;
1146         }
1147 
1148         private Pos sideToPos(Side s) {
1149             if (Side.TOP.equals(s)) {
1150                 return Pos.TOP_CENTER;
1151             } else if (Side.RIGHT.equals(s)) {
1152                 return Pos.CENTER_RIGHT;
1153             } else if (Side.BOTTOM.equals(s)) {
1154                 return Pos.BOTTOM_CENTER;
1155             }
1156             return Pos.CENTER_LEFT;
1157         }
1158 
1159         @Override protected double computeMinWidth(double height) {
1160             double left = snappedLeftInset();
1161             double right = snappedRightInset();
1162             double leftArrowWidth = snapSize(Utils.boundedSize(leftArrowButton.prefWidth(-1), leftArrowButton.minWidth(-1), leftArrowButton.maxWidth(-1)));
1163             double rightArrowWidth = snapSize(Utils.boundedSize(rightArrowButton.prefWidth(-1), rightArrowButton.minWidth(-1), rightArrowButton.maxWidth(-1)));
1164             double spacing = snapSize(controlBox.getSpacing());
1165             double pageInformationWidth = 0;
1166             Side side = getPageInformationAlignment();
1167             if (Side.LEFT.equals(side) || Side.RIGHT.equals(side)) {
1168                 pageInformationWidth = snapSize(pageInformation.prefWidth(-1));
1169             }
1170             double arrowGap = arrowButtonGap.get();
1171 
1172             return left + leftArrowWidth + 2 *arrowGap + minButtonSize /*at least one button*/
1173                     + 2 * spacing + rightArrowWidth + right + pageInformationWidth;
1174         }
1175 
1176         @Override protected double computeMinHeight(double width) {
1177             return computePrefHeight(width);
1178         }
1179 
1180         @Override protected double computePrefWidth(double height) {
1181             final double left = snappedLeftInset();
1182             final double right = snappedRightInset();
1183             final double controlBoxWidth = snapSize(controlBox.prefWidth(height));
1184             double pageInformationWidth = 0;
1185             Side side = getPageInformationAlignment();
1186             if (Side.LEFT.equals(side) || Side.RIGHT.equals(side)) {
1187                 pageInformationWidth = snapSize(pageInformation.prefWidth(-1));
1188             }
1189 
1190             return left + controlBoxWidth + right + pageInformationWidth;
1191         }
1192 
1193         @Override protected double computePrefHeight(double width) {
1194             final double top = snappedTopInset();
1195             final double bottom = snappedBottomInset();
1196             final double boxHeight = snapSize(controlBox.prefHeight(width));
1197             double pageInformationHeight = 0;
1198             Side side = getPageInformationAlignment();
1199             if (Side.TOP.equals(side) || Side.BOTTOM.equals(side)) {
1200                 pageInformationHeight = snapSize(pageInformation.prefHeight(-1));
1201             }
1202 
1203             return top + boxHeight + pageInformationHeight + bottom;
1204         }
1205 
1206         @Override protected void layoutChildren() {
1207             final double top = snappedTopInset();
1208             final double bottom = snappedBottomInset();
1209             final double left = snappedLeftInset();
1210             final double right = snappedRightInset();
1211             final double width = snapSize(getWidth()) - (left + right);
1212             final double height = snapSize(getHeight()) - (top + bottom);
1213             final double controlBoxWidth = snapSize(controlBox.prefWidth(-1));
1214             final double controlBoxHeight = snapSize(controlBox.prefHeight(-1));
1215             final double pageInformationWidth = snapSize(pageInformation.prefWidth(-1));
1216             final double pageInformationHeight = snapSize(pageInformation.prefHeight(-1));
1217 
1218             leftArrowButton.setDisable(false);
1219             rightArrowButton.setDisable(false);
1220 
1221             if (currentIndex == 0) {
1222                 // Grey out the left arrow if we are at the beginning.
1223                 leftArrowButton.setDisable(true);
1224             }
1225             if (currentIndex == (getPageCount() - 1)) {
1226                 // Grey out the right arrow if we have reached the end.
1227                 rightArrowButton.setDisable(true);
1228             }
1229             // Reapply CSS so the left and right arrow button's disable state is updated
1230             // immediately.
1231             applyCss();
1232 
1233             leftArrowButton.setVisible(isArrowsVisible());
1234             rightArrowButton.setVisible(isArrowsVisible());
1235             pageInformation.setVisible(isPageInformationVisible());
1236 
1237             // Determine the number of indicators we can fit within the pagination width.
1238             layoutPageIndicators();
1239 
1240             HPos controlBoxHPos = controlBox.getAlignment().getHpos();
1241             VPos controlBoxVPos = controlBox.getAlignment().getVpos();
1242             double controlBoxX = left + Utils.computeXOffset(width, controlBoxWidth, controlBoxHPos);
1243             double controlBoxY = top + Utils.computeYOffset(height, controlBoxHeight, controlBoxVPos);
1244 
1245             if (isPageInformationVisible()) {
1246                 Pos p = sideToPos(getPageInformationAlignment());
1247                 HPos pageInformationHPos = p.getHpos();
1248                 VPos pageInformationVPos = p.getVpos();
1249                 double pageInformationX = left + Utils.computeXOffset(width, pageInformationWidth, pageInformationHPos);
1250                 double pageInformationY = top + Utils.computeYOffset(height, pageInformationHeight, pageInformationVPos);
1251 
1252                 if (Side.TOP.equals(getPageInformationAlignment())) {
1253                     pageInformationY = top;
1254                     controlBoxY = top + pageInformationHeight;
1255                 } else if (Side.RIGHT.equals(getPageInformationAlignment())) {
1256                     pageInformationX = width - right - pageInformationWidth;
1257                 } else if (Side.BOTTOM.equals(getPageInformationAlignment())) {
1258                     controlBoxY = top;
1259                     pageInformationY = top + controlBoxHeight;
1260                 } else if (Side.LEFT.equals(getPageInformationAlignment())) {
1261                     pageInformationX = left;
1262                 }
1263                 layoutInArea(pageInformation, pageInformationX, pageInformationY, pageInformationWidth, pageInformationHeight, 0, pageInformationHPos, pageInformationVPos);
1264             }
1265 
1266             layoutInArea(controlBox, controlBoxX, controlBoxY, controlBoxWidth, controlBoxHeight, 0, controlBoxHPos, controlBoxVPos);
1267         }
1268     }
1269 
1270     class IndicatorButton extends ToggleButton {
1271         private final ListChangeListener<String> updateSkinIndicatorType =
1272                                                     c -> setIndicatorType();
1273 
1274         private final ChangeListener<Boolean> updateTooltipVisibility =
1275                        (ob, oldValue, newValue) -> setTooltipVisible(newValue);
1276 
1277         private int pageNumber;
1278 
1279         public IndicatorButton(int pageNumber) {
1280             this.pageNumber = pageNumber;
1281             setFocusTraversable(false);
1282             setIndicatorType();
1283             setTooltipVisible(isTooltipVisible());
1284 
1285             getSkinnable().getStyleClass().addListener(updateSkinIndicatorType);
1286 
1287             setOnAction(arg0 -> {
1288                     getNode().requestFocus();
1289                     int selected = getCurrentPageIndex();
1290                     // We do not need to update the selection if it has not changed.
1291                     if (selected != IndicatorButton.this.pageNumber) {
1292                         pagination.setCurrentPageIndex(IndicatorButton.this.pageNumber);
1293                         requestLayout();
1294                     }
1295             });
1296 
1297             tooltipVisibleProperty().addListener(updateTooltipVisibility);
1298 
1299             prefHeightProperty().bind(minHeightProperty());
1300             setAccessibleRole(AccessibleRole.PAGE_ITEM);
1301         }
1302 
1303         private void setIndicatorType() {
1304             if (getSkinnable().getStyleClass().contains(Pagination.STYLE_CLASS_BULLET)) {
1305                 getStyleClass().remove("number-button");
1306                 getStyleClass().add("bullet-button");
1307                 setText(null);
1308 
1309                 // Bind the width in addition to the height to ensure the region is square
1310                 prefWidthProperty().bind(minWidthProperty());
1311             } else {
1312                 getStyleClass().remove("bullet-button");
1313                 getStyleClass().add("number-button");
1314                 setText(Integer.toString(this.pageNumber + 1));
1315 
1316                 // Free the width to conform to the text content
1317                 prefWidthProperty().unbind();
1318             }
1319         }
1320 
1321         private void setTooltipVisible(boolean b) {
1322             if (b) {
1323                 setTooltip(new Tooltip(Integer.toString(IndicatorButton.this.pageNumber + 1)));
1324             } else {
1325                 setTooltip(null);
1326             }
1327         }
1328 
1329         public int getPageNumber() {
1330             return this.pageNumber;
1331         }
1332 
1333         @Override public void fire() {
1334             // we don't toggle from selected to not selected if part of a group
1335             if (getToggleGroup() == null || !isSelected()) {
1336                 super.fire();
1337             }
1338         }
1339 
1340         public void release() {
1341             getSkinnable().getStyleClass().removeListener(updateSkinIndicatorType);
1342             tooltipVisibleProperty().removeListener(updateTooltipVisibility);
1343         }
1344 
1345         @Override
1346         public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
1347             switch (attribute) {
1348                 case TEXT: return getText();
1349                 case SELECTED: return isSelected();
1350                 default: return super.queryAccessibleAttribute(attribute, parameters);
1351             }
1352         }
1353 
1354         @Override
1355         public void executeAccessibleAction(AccessibleAction action, Object... parameters) {
1356             switch (action) {
1357                 case REQUEST_FOCUS:
1358                     getSkinnable().setCurrentPageIndex(pageNumber);
1359                     break;
1360                 default: super.executeAccessibleAction(action);
1361             }
1362         }
1363     }
1364 
1365     /***************************************************************************
1366      *                                                                         *
1367      *                         Stylesheet Handling                             *
1368      *                                                                         *
1369      **************************************************************************/
1370 
1371     private static final Boolean DEFAULT_ARROW_VISIBLE = Boolean.FALSE;
1372     private static final Boolean DEFAULT_PAGE_INFORMATION_VISIBLE = Boolean.FALSE;
1373     private static final Side DEFAULT_PAGE_INFORMATION_ALIGNMENT = Side.BOTTOM;
1374     private static final Boolean DEFAULT_TOOLTIP_VISIBLE = Boolean.FALSE;
1375 
1376     private static class StyleableProperties {
1377         private static final CssMetaData<Pagination,Boolean> ARROWS_VISIBLE =
1378             new CssMetaData<Pagination,Boolean>("-fx-arrows-visible",
1379                 BooleanConverter.getInstance(), DEFAULT_ARROW_VISIBLE) {
1380 
1381             @Override
1382             public boolean isSettable(Pagination n) {
1383                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1384                 return skin.arrowsVisible == null || !skin.arrowsVisible.isBound();
1385             }
1386 
1387             @Override
1388             public StyleableProperty<Boolean> getStyleableProperty(Pagination n) {
1389                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1390                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)skin.arrowsVisibleProperty();
1391             }
1392         };
1393 
1394         private static final CssMetaData<Pagination,Boolean> PAGE_INFORMATION_VISIBLE =
1395             new CssMetaData<Pagination,Boolean>("-fx-page-information-visible",
1396                 BooleanConverter.getInstance(), DEFAULT_PAGE_INFORMATION_VISIBLE) {
1397 
1398             @Override
1399             public boolean isSettable(Pagination n) {
1400                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1401                 return skin.pageInformationVisible == null || !skin.pageInformationVisible.isBound();
1402             }
1403 
1404             @Override
1405             public StyleableProperty<Boolean> getStyleableProperty(Pagination n) {
1406                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1407                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)skin.pageInformationVisibleProperty();
1408             }
1409         };
1410 
1411         private static final CssMetaData<Pagination,Side> PAGE_INFORMATION_ALIGNMENT =
1412             new CssMetaData<Pagination,Side>("-fx-page-information-alignment",
1413                 new EnumConverter<Side>(Side.class), DEFAULT_PAGE_INFORMATION_ALIGNMENT) {
1414 
1415             @Override
1416             public boolean isSettable(Pagination n) {
1417                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1418                 return skin.pageInformationAlignment == null || !skin.pageInformationAlignment.isBound();
1419             }
1420 
1421             @Override
1422             public StyleableProperty<Side> getStyleableProperty(Pagination n) {
1423                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1424                 return (StyleableProperty<Side>)(WritableValue<Side>)skin.pageInformationAlignmentProperty();
1425             }
1426         };
1427 
1428         private static final CssMetaData<Pagination,Boolean> TOOLTIP_VISIBLE =
1429             new CssMetaData<Pagination,Boolean>("-fx-tooltip-visible",
1430                 BooleanConverter.getInstance(), DEFAULT_TOOLTIP_VISIBLE) {
1431 
1432             @Override
1433             public boolean isSettable(Pagination n) {
1434                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1435                 return skin.tooltipVisible == null || !skin.tooltipVisible.isBound();
1436             }
1437 
1438             @Override
1439             public StyleableProperty<Boolean> getStyleableProperty(Pagination n) {
1440                 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1441                 return (StyleableProperty<Boolean>)(WritableValue<Boolean>)skin.tooltipVisibleProperty();
1442             }
1443         };
1444         private static final CssMetaData<Pagination,Number> ARROW_BUTTON_GAP =
1445             new CssMetaData<Pagination,Number>("-fx-arrow-button-gap", SizeConverter.getInstance(), 4) {
1446                 @Override public boolean isSettable(Pagination n) {
1447                     final PaginationSkin skin = (PaginationSkin) n.getSkin();
1448                     return skin.arrowButtonGap == null ||
1449                             !skin.arrowButtonGap.isBound();
1450                 }
1451                 @Override public StyleableProperty<Number> getStyleableProperty(Pagination n) {
1452                     final PaginationSkin skin = (PaginationSkin) n.getSkin();
1453                     return (StyleableProperty<Number>)(WritableValue<Number>)skin.arrowButtonGapProperty();
1454                 }
1455             };
1456 
1457         private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1458         static {
1459             final List<CssMetaData<? extends Styleable, ?>> styleables =
1460                 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
1461             styleables.add(ARROWS_VISIBLE);
1462             styleables.add(PAGE_INFORMATION_VISIBLE);
1463             styleables.add(PAGE_INFORMATION_ALIGNMENT);
1464             styleables.add(TOOLTIP_VISIBLE);
1465             styleables.add(ARROW_BUTTON_GAP);
1466             STYLEABLES = Collections.unmodifiableList(styleables);
1467         }
1468     }
1469 
1470     /**
1471      * Returns the CssMetaData associated with this class, which may include the
1472      * CssMetaData of its super classes.
1473      */
1474     public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1475         return StyleableProperties.STYLEABLES;
1476     }
1477 
1478     /**
1479      * {@inheritDoc}
1480      */
1481     @Override
1482     public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1483         return getClassCssMetaData();
1484     }
1485 
1486 }