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;
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();
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),
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);
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 }
|
1 /*
2 * Copyright (c) 2012, 2015, 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;
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();
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),
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 previousWidth = -1;
815 private double minButtonSize = -1;
816
817 public NavigationControl() {
818 getStyleClass().setAll("pagination-control");
819
820 // redirect mouse events to behavior
821 addEventHandler(MouseEvent.MOUSE_PRESSED, behavior::mousePressed);
822
823 controlBox = new HBox();
824 controlBox.getStyleClass().add("control-box");
825
826 leftArrowButton = new Button();
827 leftArrowButton.setAccessibleText(getString("Accessibility.title.Pagination.PreviousButton"));
828 minButtonSize = leftArrowButton.getFont().getSize() * 2;
829 leftArrowButton.fontProperty().addListener((arg0, arg1, newFont) -> {
830 minButtonSize = newFont.getSize() * 2;
831 for(Node child: controlBox.getChildren()) {
832 ((Control)child).setMinSize(minButtonSize, minButtonSize);
833 }
834 // We want to relayout the indicator buttons because the size has changed.
835 requestLayout();
836 });
837 leftArrowButton.setMinSize(minButtonSize, minButtonSize);
838 leftArrowButton.prefWidthProperty().bind(leftArrowButton.minWidthProperty());
839 leftArrowButton.prefHeightProperty().bind(leftArrowButton.minHeightProperty());
840 leftArrowButton.getStyleClass().add("left-arrow-button");
841 leftArrowButton.setFocusTraversable(false);
1454 @Override public StyleableProperty<Number> getStyleableProperty(Pagination n) {
1455 final PaginationSkin skin = (PaginationSkin) n.getSkin();
1456 return (StyleableProperty<Number>)(WritableValue<Number>)skin.arrowButtonGapProperty();
1457 }
1458 };
1459
1460 private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
1461 static {
1462 final List<CssMetaData<? extends Styleable, ?>> styleables =
1463 new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
1464 styleables.add(ARROWS_VISIBLE);
1465 styleables.add(PAGE_INFORMATION_VISIBLE);
1466 styleables.add(PAGE_INFORMATION_ALIGNMENT);
1467 styleables.add(TOOLTIP_VISIBLE);
1468 styleables.add(ARROW_BUTTON_GAP);
1469 STYLEABLES = Collections.unmodifiableList(styleables);
1470 }
1471 }
1472
1473 /**
1474 * Returns the CssMetaData associated with this class, which may include the
1475 * CssMetaData of its super classes.
1476 */
1477 public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
1478 return StyleableProperties.STYLEABLES;
1479 }
1480
1481 /**
1482 * {@inheritDoc}
1483 */
1484 @Override
1485 public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
1486 return getClassCssMetaData();
1487 }
1488
1489 }
|