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 }