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 }