/* * Copyright (c) 2008, 2014, Oracle and/or its affiliates. * All rights reserved. Use is subject to license terms. * * This file is available and licensed under the following license: * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the distribution. * - Neither the name of Oracle Corporation nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package ctbrec.ui.controls; import java.util.LinkedList; import javafx.animation.Animation; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ParallelTransition; import javafx.animation.ScaleTransition; import javafx.animation.Timeline; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.event.ActionEvent; import javafx.event.Event; import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.input.MouseEvent; import javafx.scene.layout.Pane; import javafx.scene.layout.Region; import javafx.scene.shape.Rectangle; import javafx.scene.text.TextAlignment; import javafx.util.Duration; /** * A Popover is a mini-window that pops up and contains some application specific content. * It's width is defined by the application, but defaults to a hard-coded pref width. * The height will always be between a minimum height (determined by the application, but * pre-set with a minimum value) and a maximum height (specified by the application, or * based on the height of the scene). The value for the pref height is determined by * inspecting the pref height of the current displayed page. At time this value is animated * (when switching from page to page). */ public class Popover extends Region implements EventHandler{ private static final int PAGE_GAP = 15; /** * The visual frame of the popover is defined as an addition region, rather than simply styling * the popover itself as one might expect. The reason for this is that our frame is styled via * a border image, and it has an inner shadow associated with it, and we want to be able to ensure * that the shadow is on top of whatever content is drawn within the popover. In addition, the inner * edge of the frame is rounded, and we want the content to slide under it, only to be clipped beneath * the frame. So it works best for the frame to be overlaid on top, even though it is not intuitive. */ private final Region frameBorder = new Region(); private final Button leftButton = new Button("Left"); private final Button rightButton = new Button("Right"); private final LinkedList pages = new LinkedList(); private final Pane pagesPane = new Pane(); private final Rectangle pagesClipRect = new Rectangle(); private final Pane titlesPane = new Pane(); private Label title; // the current title private final EventHandler popoverHideHandler; private Runnable onHideCallback = null; private int maxPopupHeight = -1; private DoubleProperty popoverHeight = new SimpleDoubleProperty(400) { @Override protected void invalidated() { requestLayout(); } }; public Popover() { getStyleClass().setAll("popover"); frameBorder.getStyleClass().setAll("popover-frame"); frameBorder.setMouseTransparent(true); // setup buttons leftButton.setOnMouseClicked(this); leftButton.getStyleClass().add("popover-left-button"); leftButton.setMinWidth(USE_PREF_SIZE); rightButton.setOnMouseClicked(this); rightButton.getStyleClass().add("popover-right-button"); rightButton.setMinWidth(USE_PREF_SIZE); pagesClipRect.setSmooth(false); pagesClipRect.setArcHeight(10); pagesClipRect.arcWidthProperty().bind(pagesClipRect.arcHeightProperty()); pagesPane.setClip(pagesClipRect); getChildren().addAll(frameBorder, titlesPane, leftButton, rightButton, pagesPane); // always hide to start with setVisible(false); setOpacity(0); setScaleX(.8); setScaleY(.8); // create handlers for auto hiding popoverHideHandler = (MouseEvent t) -> { // check if event is outside popup Point2D mouseInFilterPane = sceneToLocal(t.getX(), t.getY()); if (mouseInFilterPane.getX() < 0 || mouseInFilterPane.getX() > (getWidth()) || mouseInFilterPane.getY() < 0 || mouseInFilterPane.getY() > (getHeight())) { hide(); t.consume(); } }; // popoverScrollHandler = new EventHandler() { // @Override public void handle(ScrollEvent t) { // t.consume(); // consume all scroll events // } // }; } /** * Handle mouse clicks on the left and right buttons. */ @Override public void handle(Event event) { if (event.getSource() == leftButton) { pages.getFirst().handleLeftButton(); } else if (event.getSource() == rightButton) { pages.getFirst().handleRightButton(); } } @Override protected double computeMinWidth(double height) { Page page = pages.isEmpty() ? null : pages.getFirst(); if (page != null) { Node n = page.getPageNode(); if (n != null) { Insets insets = getInsets(); return insets.getLeft() + n.minWidth(-1) + insets.getRight(); } } return 200; } @Override protected double computeMinHeight(double width) { Insets insets = getInsets(); return insets.getLeft() + 100 + insets.getRight(); } @Override protected double computePrefWidth(double height) { Page page = pages.isEmpty() ? null : pages.getFirst(); if (page != null) { Node n = page.getPageNode(); if (n != null) { Insets insets = getInsets(); return insets.getLeft() + n.prefWidth(-1) + insets.getRight(); } } return 400; } @Override protected double computePrefHeight(double width) { double minHeight = minHeight(-1); double maxHeight = maxHeight(-1); double prefHeight = popoverHeight.get(); if (prefHeight == -1) { Page page = pages.getFirst(); if (page != null) { Insets inset = getInsets(); if (width == -1) { width = prefWidth(-1); } double contentWidth = width - inset.getLeft() - inset.getRight(); double contentHeight = page.getPageNode().prefHeight(contentWidth); prefHeight = inset.getTop() + contentHeight + inset.getBottom(); popoverHeight.set(prefHeight); } else { prefHeight = minHeight; } } return boundedSize(minHeight, prefHeight, maxHeight); } static double boundedSize(double min, double pref, double max) { double a = pref >= min ? pref : min; double b = min >= max ? min : max; return a <= b ? a : b; } @Override protected double computeMaxWidth(double height) { return Double.MAX_VALUE; } @Override protected double computeMaxHeight(double width) { Scene scene = getScene(); if (scene != null) { return scene.getHeight() - 100; } else { return Double.MAX_VALUE; } } @Override protected void layoutChildren() { if (maxPopupHeight == -1) { maxPopupHeight = (int)getScene().getHeight()-100; } final Insets insets = getInsets(); final int width = (int)getWidth(); final int height = (int)getHeight(); final int top = (int)insets.getTop() + 40; final int right = (int)insets.getRight(); final int bottom = (int)insets.getBottom(); final int left = (int)insets.getLeft(); final int offset = 18; int pageWidth = width - left - right; int pageHeight = height - top - bottom; frameBorder.resize(width, height); pagesPane.resizeRelocate(left, top, pageWidth, pageHeight); pagesClipRect.setWidth(pageWidth); pagesClipRect.setHeight(pageHeight); int pageX = 0; for (Node page : pagesPane.getChildren()) { page.resizeRelocate(pageX, 0, pageWidth, pageHeight); pageX += pageWidth + PAGE_GAP; } int buttonHeight = (int)(leftButton.prefHeight(-1)); if (buttonHeight < 30) buttonHeight = 30; final int buttonTop = (int)((top-buttonHeight)/2d); final int leftButtonWidth = (int)snapSizeX(leftButton.prefWidth(-1)); leftButton.resizeRelocate(left, buttonTop + offset,leftButtonWidth,buttonHeight); final int rightButtonWidth = (int)snapSizeX(rightButton.prefWidth(-1)); rightButton.resizeRelocate(width-right-rightButtonWidth, buttonTop + offset,rightButtonWidth,buttonHeight); if (title != null) { double tw = title.getWidth(); double th = title.getHeight(); title.setTranslateX((width - tw) / 2); title.setTranslateY((top - th) / 2 + offset); } } public final void clearPages() { while (!pages.isEmpty()) { pages.pop().handleHidden(); } pagesPane.getChildren().clear(); titlesPane.getChildren().clear(); pagesClipRect.setX(0); pagesClipRect.setWidth(400); pagesClipRect.setHeight(400); popoverHeight.set(400); pagesPane.setTranslateX(0); titlesPane.setTranslateX(0); } public final void popPage() { Page oldPage = pages.pop(); oldPage.handleHidden(); oldPage.setPopover(null); Page page = pages.getFirst(); leftButton.setVisible(page.leftButtonText() != null); leftButton.setText(page.leftButtonText()); rightButton.setVisible(page.rightButtonText() != null); rightButton.setText(page.rightButtonText()); if (pages.size() > 0) { final Insets insets = getInsets(); final int width = (int)prefWidth(-1); final int right = (int)insets.getRight(); final int left = (int)insets.getLeft(); int pageWidth = width - left - right; final int newPageX = (pageWidth+PAGE_GAP) * (pages.size()-1); new Timeline( new KeyFrame(Duration.millis(350), (ActionEvent t) -> { pagesPane.setCache(false); pagesPane.getChildren().remove(pagesPane.getChildren().size()-1); titlesPane.getChildren().remove(titlesPane.getChildren().size()-1); resizePopoverToNewPage(pages.getFirst().getPageNode()); }, new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH) ) ).play(); } else { hide(); } } public final void pushPage(final Page page) { final Node pageNode = page.getPageNode(); pageNode.setManaged(false); pagesPane.getChildren().add(pageNode); final Insets insets = getInsets(); final int pageWidth = (int)(prefWidth(-1) - insets.getLeft() - insets.getRight()); final int newPageX = (pageWidth + PAGE_GAP) * pages.size(); leftButton.setVisible(page.leftButtonText() != null); leftButton.setText(page.leftButtonText()); rightButton.setVisible(page.rightButtonText() != null); rightButton.setText(page.rightButtonText()); title = new Label(page.getPageTitle()); title.getStyleClass().add("popover-title"); title.setTextAlignment(TextAlignment.CENTER); title.setTranslateX(newPageX + (int) ((pageWidth - title.getLayoutBounds().getWidth()) / 2d)); titlesPane.getChildren().add(title); if (!pages.isEmpty() && isVisible()) { final Timeline timeline = new Timeline( new KeyFrame(Duration.millis(350), (ActionEvent t) -> { pagesPane.setCache(false); resizePopoverToNewPage(pageNode); }, new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH) ) ); timeline.play(); } page.setPopover(this); page.handleShown(); pages.push(page); } private void resizePopoverToNewPage(final Node newPageNode) { final Insets insets = getInsets(); final double width = prefWidth(-1); final double contentWidth = width - insets.getLeft() - insets.getRight(); double h = newPageNode.prefHeight(contentWidth); h += insets.getTop() + insets.getBottom(); new Timeline( new KeyFrame(Duration.millis(200), new KeyValue(popoverHeight, h, Interpolator.EASE_BOTH) ) ).play(); } public void show(){ show(null); } private Animation fadeAnimation = null; public void show(Runnable onHideCallback){ if (!isVisible() || fadeAnimation != null) { this.onHideCallback = onHideCallback; getScene().addEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler); // getScene().addEventFilter(ScrollEvent.ANY,popoverScrollHandler); if (fadeAnimation != null) { fadeAnimation.stop(); setVisible(true); // for good measure } else { popoverHeight.set(-1); setVisible(true); } FadeTransition fade = new FadeTransition(Duration.seconds(.1), this); fade.setToValue(1.0); fade.setOnFinished((ActionEvent event) -> { fadeAnimation = null; }); ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this); scale.setToX(1); scale.setToY(1); ParallelTransition tx = new ParallelTransition(fade, scale); fadeAnimation = tx; tx.play(); } } public void hide(){ if (isVisible() || fadeAnimation != null) { getScene().removeEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler); // getScene().removeEventFilter(ScrollEvent.ANY,popoverScrollHandler); if (fadeAnimation != null) { fadeAnimation.stop(); } FadeTransition fade = new FadeTransition(Duration.seconds(.1), this); fade.setToValue(0); fade.setOnFinished((ActionEvent event) -> { fadeAnimation = null; setVisible(false); //clearPages(); if (onHideCallback != null) onHideCallback.run(); }); ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this); scale.setToX(.8); scale.setToY(.8); ParallelTransition tx = new ParallelTransition(fade, scale); fadeAnimation = tx; tx.play(); } } /** * Represents a page in a popover. */ public static interface Page { public void setPopover(Popover popover); public Popover getPopover(); /** * Get the node that represents the page. * * @return the page node. */ public Node getPageNode(); /** * Get the title to display for this page. * * @return The page title */ public String getPageTitle(); /** * The text for left button, if null then button will be hidden. * @return The button text */ public String leftButtonText(); /** * Called on a click of the left button of the popover. */ public void handleLeftButton(); /** * The text for right button, if null then button will be hidden. * @return The button text */ public String rightButtonText(); /** * Called on a click of the right button of the popover. */ public void handleRightButton(); public void handleShown(); public void handleHidden(); } }