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