1 /* 2 * Copyright (c) 2008, 2014, Oracle and/or its affiliates. 3 * All rights reserved. Use is subject to license terms. 4 * 5 * This file is available and licensed under the following license: 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * - Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * - Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the distribution. 16 * - Neither the name of Oracle Corporation nor the names of its 17 * contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package ensemble.control; 33 34 import java.util.LinkedList; 35 36 import javafx.animation.Animation; 37 import javafx.animation.FadeTransition; 38 import javafx.animation.Interpolator; 39 import javafx.animation.KeyFrame; 40 import javafx.animation.KeyValue; 41 import javafx.animation.ParallelTransition; 42 import javafx.animation.ScaleTransition; 43 import javafx.animation.Timeline; 44 import javafx.beans.property.DoubleProperty; 45 import javafx.beans.property.SimpleDoubleProperty; 46 import javafx.event.ActionEvent; 47 import javafx.event.Event; 48 import javafx.event.EventHandler; 49 import javafx.geometry.Insets; 50 import javafx.geometry.Point2D; 51 import javafx.geometry.VPos; 52 import javafx.scene.Node; 53 import javafx.scene.Scene; 54 import javafx.scene.control.Button; 55 import javafx.scene.input.MouseEvent; 56 import javafx.scene.layout.Pane; 57 import javafx.scene.layout.Region; 58 import javafx.scene.shape.Rectangle; 59 import javafx.scene.text.Text; 60 import javafx.util.Duration; 61 62 /** 63 * A Popover is a mini-window that pops up and contains some application specific content. 64 * It's width is defined by the application, but defaults to a hard-coded pref width. 65 * The height will always be between a minimum height (determined by the application, but 66 * pre-set with a minimum value) and a maximum height (specified by the application, or 67 * based on the height of the scene). The value for the pref height is determined by 68 * inspecting the pref height of the current displayed page. At time this value is animated 69 * (when switching from page to page). 70 */ 71 public class Popover extends Region implements EventHandler<Event>{ 72 private static final int PAGE_GAP = 15; 73 74 /** 75 * The visual frame of the popover is defined as an addition region, rather than simply styling 76 * the popover itself as one might expect. The reason for this is that our frame is styled via 77 * a border image, and it has an inner shadow associated with it, and we want to be able to ensure 78 * that the shadow is on top of whatever content is drawn within the popover. In addition, the inner 79 * edge of the frame is rounded, and we want the content to slide under it, only to be clipped beneath 80 * the frame. So it works best for the frame to be overlaid on top, even though it is not intuitive. 81 */ 82 private final Region frameBorder = new Region(); 83 private final Button leftButton = new Button("Left"); 84 private final Button rightButton = new Button("Right"); 85 private final LinkedList<Page> pages = new LinkedList<Page>(); 86 private final Pane pagesPane = new Pane(); 87 private final Rectangle pagesClipRect = new Rectangle(); 88 private final Pane titlesPane = new Pane(); 89 private Text title; // the current title 90 private final Rectangle titlesClipRect = new Rectangle(); 91 // private final EventHandler<ScrollEvent> popoverScrollHandler; 92 private final EventHandler<MouseEvent> popoverHideHandler; 93 private Runnable onHideCallback = null; 94 private double maxPopupHeight = -1; 95 96 private DoubleProperty popoverHeight = new SimpleDoubleProperty(400) { 97 @Override protected void invalidated() { 98 requestLayout(); 99 } 100 }; 101 102 public Popover() { 103 // TODO Could pagesPane be a region instead? I need to draw some opaque background. Right now when 104 // TODO animating from one page to another you can see the background "shine through" because the 105 // TODO group background is transparent. That can't be good for performance either. 106 getStyleClass().setAll("popover"); 107 frameBorder.getStyleClass().setAll("popover-frame"); 108 frameBorder.setMouseTransparent(true); 109 // setup buttons 110 leftButton.setOnMouseClicked(this); 111 leftButton.getStyleClass().add("popover-left-button"); 112 leftButton.setMinWidth(USE_PREF_SIZE); 113 rightButton.setOnMouseClicked(this); 114 rightButton.getStyleClass().add("popover-right-button"); 115 rightButton.setMinWidth(USE_PREF_SIZE); 116 pagesClipRect.setSmooth(false); 117 pagesPane.setClip(pagesClipRect); 118 titlesClipRect.setSmooth(false); 119 titlesPane.setClip(titlesClipRect); 120 getChildren().addAll(pagesPane, frameBorder, titlesPane, leftButton, rightButton); 121 // always hide to start with 122 setVisible(false); 123 setOpacity(0); 124 setScaleX(.8); 125 setScaleY(.8); 126 // create handlers for auto hiding 127 popoverHideHandler = (MouseEvent t) -> { 128 // check if event is outside popup 129 Point2D mouseInFilterPane = sceneToLocal(t.getX(), t.getY()); 130 if (mouseInFilterPane.getX() < 0 || mouseInFilterPane.getX() > (getWidth()) || 131 mouseInFilterPane.getY() < 0 || mouseInFilterPane.getY() > (getHeight())) { 132 hide(); 133 t.consume(); 134 } 135 }; 136 // popoverScrollHandler = new EventHandler<ScrollEvent>() { 137 // @Override public void handle(ScrollEvent t) { 138 // t.consume(); // consume all scroll events 139 // } 140 // }; 141 } 142 143 /** 144 * Handle mouse clicks on the left and right buttons. 145 */ 146 @Override public void handle(Event event) { 147 if (event.getSource() == leftButton) { 148 pages.getFirst().handleLeftButton(); 149 } else if (event.getSource() == rightButton) { 150 pages.getFirst().handleRightButton(); 151 } 152 } 153 154 @Override protected double computeMinWidth(double height) { 155 Page page = pages.isEmpty() ? null : pages.getFirst(); 156 if (page != null) { 157 Node n = page.getPageNode(); 158 if (n != null) { 159 Insets insets = getInsets(); 160 return insets.getLeft() + n.minWidth(-1) + insets.getRight(); 161 } 162 } 163 return 200; 164 } 165 166 @Override protected double computeMinHeight(double width) { 167 Insets insets = getInsets(); 168 return insets.getLeft() + 100 + insets.getRight(); 169 } 170 171 @Override protected double computePrefWidth(double height) { 172 Page page = pages.isEmpty() ? null : pages.getFirst(); 173 if (page != null) { 174 Node n = page.getPageNode(); 175 if (n != null) { 176 Insets insets = getInsets(); 177 return insets.getLeft() + n.prefWidth(-1) + insets.getRight(); 178 } 179 } 180 return 400; 181 } 182 183 @Override protected double computePrefHeight(double width) { 184 double minHeight = minHeight(-1); 185 double maxHeight = maxHeight(-1); 186 double prefHeight = popoverHeight.get(); 187 if (prefHeight == -1) { 188 Page page = pages.getFirst(); 189 if (page != null) { 190 Insets inset = getInsets(); 191 if (width == -1) { 192 width = prefWidth(-1); 193 } 194 double contentWidth = width - inset.getLeft() - inset.getRight(); 195 double contentHeight = page.getPageNode().prefHeight(contentWidth); 196 prefHeight = inset.getTop() + contentHeight + inset.getBottom(); 197 popoverHeight.set(prefHeight); 198 } else { 199 prefHeight = minHeight; 200 } 201 } 202 return boundedSize(minHeight, prefHeight, maxHeight); 203 } 204 205 static double boundedSize(double min, double pref, double max) { 206 double a = pref >= min ? pref : min; 207 double b = min >= max ? min : max; 208 return a <= b ? a : b; 209 } 210 211 @Override protected double computeMaxWidth(double height) { 212 return Double.MAX_VALUE; 213 } 214 215 @Override protected double computeMaxHeight(double width) { 216 Scene scene = getScene(); 217 if (scene != null) { 218 return scene.getHeight() - 100; 219 } else { 220 return Double.MAX_VALUE; 221 } 222 } 223 224 @Override protected void layoutChildren() { 225 if (maxPopupHeight == -1) { 226 maxPopupHeight = (int)getScene().getHeight()-100; 227 } 228 final Insets insets = getInsets(); 229 final double width = getWidth(); 230 final double height = getHeight(); 231 final double top = insets.getTop(); 232 final double right = insets.getRight(); 233 final double bottom = insets.getBottom(); 234 final double left = insets.getLeft(); 235 236 double pageWidth = width - left - right; 237 double pageHeight = height - top - bottom; 238 239 frameBorder.resize(width, height); 240 241 pagesPane.resizeRelocate(left, top, pageWidth, pageHeight); 242 pagesClipRect.setWidth(pageWidth); 243 pagesClipRect.setHeight(pageHeight); 244 245 double pageX = 0; 246 for (Node page : pagesPane.getChildren()) { 247 page.resizeRelocate(pageX, 0, pageWidth, pageHeight); 248 pageX += pageWidth + PAGE_GAP; 249 } 250 251 double buttonHeight = leftButton.prefHeight(-1); 252 if (buttonHeight < 30) buttonHeight = 30; 253 final double buttonTop = (top-buttonHeight) / 2.0; 254 final double leftButtonWidth = snapSizeX(leftButton.prefWidth(-1)); 255 leftButton.resizeRelocate(left, buttonTop,leftButtonWidth,buttonHeight); 256 final double rightButtonWidth = snapSizeX(rightButton.prefWidth(-1)); 257 rightButton.resizeRelocate(width-right-rightButtonWidth, buttonTop,rightButtonWidth,buttonHeight); 258 259 final double leftButtonRight = leftButton.isVisible() ? (left + leftButtonWidth) : left; 260 final double rightButtonLeft = rightButton.isVisible() ? (right + rightButtonWidth) : right; 261 titlesClipRect.setX(leftButtonRight); 262 titlesClipRect.setWidth(pageWidth - leftButtonRight - rightButtonLeft); 263 titlesClipRect.setHeight(top); 264 265 if (title != null) { 266 title.setTranslateY((int) (top / 2d)); 267 } 268 } 269 270 public final void clearPages() { 271 while (!pages.isEmpty()) { 272 pages.pop().handleHidden(); 273 } 274 pagesPane.getChildren().clear(); 275 titlesPane.getChildren().clear(); 276 pagesClipRect.setX(0); 277 pagesClipRect.setWidth(400); 278 pagesClipRect.setHeight(400); 279 popoverHeight.set(400); 280 pagesPane.setTranslateX(0); 281 titlesPane.setTranslateX(0); 282 titlesClipRect.setTranslateX(0); 283 } 284 285 public final void popPage() { 286 Page oldPage = pages.pop(); 287 oldPage.handleHidden(); 288 oldPage.setPopover(null); 289 Page page = pages.getFirst(); 290 leftButton.setVisible(page.leftButtonText() != null); 291 leftButton.setText(page.leftButtonText()); 292 rightButton.setVisible(page.rightButtonText() != null); 293 rightButton.setText(page.rightButtonText()); 294 if (pages.size() > 0) { 295 final Insets insets = getInsets(); 296 final int width = (int)prefWidth(-1); 297 final int right = (int)insets.getRight(); 298 final int left = (int)insets.getLeft(); 299 int pageWidth = width - left - right; 300 final int newPageX = (pageWidth+PAGE_GAP) * (pages.size()-1); 301 new Timeline( 302 new KeyFrame(Duration.millis(350), (ActionEvent t) -> { 303 pagesPane.setCache(false); 304 pagesPane.getChildren().remove(pagesPane.getChildren().size()-1); 305 titlesPane.getChildren().remove(titlesPane.getChildren().size()-1); 306 resizePopoverToNewPage(pages.getFirst().getPageNode()); 307 }, 308 new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), 309 new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), 310 new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH), 311 new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH) 312 ) 313 ).play(); 314 } else { 315 hide(); 316 } 317 } 318 319 public final void pushPage(final Page page) { 320 final Node pageNode = page.getPageNode(); 321 pageNode.setManaged(false); 322 pagesPane.getChildren().add(pageNode); 323 final Insets insets = getInsets(); 324 final int pageWidth = (int)(prefWidth(-1) - insets.getLeft() - insets.getRight()); 325 final int newPageX = (pageWidth + PAGE_GAP) * pages.size(); 326 leftButton.setVisible(page.leftButtonText() != null); 327 leftButton.setText(page.leftButtonText()); 328 rightButton.setVisible(page.rightButtonText() != null); 329 rightButton.setText(page.rightButtonText()); 330 331 title = new Text(page.getPageTitle()); 332 title.getStyleClass().add("popover-title"); 333 //debtest title.setFill(Color.WHITE); 334 title.setTextOrigin(VPos.CENTER); 335 title.setTranslateX(newPageX + (int) ((pageWidth - title.getLayoutBounds().getWidth()) / 2d)); 336 titlesPane.getChildren().add(title); 337 338 if (!pages.isEmpty() && isVisible()) { 339 final Timeline timeline = new Timeline( 340 new KeyFrame(Duration.millis(350), (ActionEvent t) -> { 341 pagesPane.setCache(false); 342 resizePopoverToNewPage(pageNode); 343 }, 344 new KeyValue(pagesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), 345 new KeyValue(titlesPane.translateXProperty(), -newPageX, Interpolator.EASE_BOTH), 346 new KeyValue(pagesClipRect.xProperty(), newPageX, Interpolator.EASE_BOTH), 347 new KeyValue(titlesClipRect.translateXProperty(), newPageX, Interpolator.EASE_BOTH) 348 ) 349 ); 350 timeline.play(); 351 } 352 page.setPopover(this); 353 page.handleShown(); 354 pages.push(page); 355 } 356 357 private void resizePopoverToNewPage(final Node newPageNode) { 358 final Insets insets = getInsets(); 359 final double width = prefWidth(-1); 360 final double contentWidth = width - insets.getLeft() - insets.getRight(); 361 double h = newPageNode.prefHeight(contentWidth); 362 h += insets.getTop() + insets.getBottom(); 363 new Timeline( 364 new KeyFrame(Duration.millis(200), 365 new KeyValue(popoverHeight, h, Interpolator.EASE_BOTH) 366 ) 367 ).play(); 368 } 369 370 public void show(){ 371 show(null); 372 } 373 374 private Animation fadeAnimation = null; 375 376 public void show(Runnable onHideCallback){ 377 if (!isVisible() || fadeAnimation != null) { 378 this.onHideCallback = onHideCallback; 379 getScene().addEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler); 380 // getScene().addEventFilter(ScrollEvent.ANY,popoverScrollHandler); 381 382 if (fadeAnimation != null) { 383 fadeAnimation.stop(); 384 setVisible(true); // for good measure 385 } else { 386 popoverHeight.set(-1); 387 setVisible(true); 388 } 389 390 FadeTransition fade = new FadeTransition(Duration.seconds(.1), this); 391 fade.setToValue(1.0); 392 fade.setOnFinished((ActionEvent event) -> { 393 fadeAnimation = null; 394 }); 395 396 ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this); 397 scale.setToX(1); 398 scale.setToY(1); 399 400 ParallelTransition tx = new ParallelTransition(fade, scale); 401 fadeAnimation = tx; 402 tx.play(); 403 } 404 } 405 406 public void hide(){ 407 if (isVisible() || fadeAnimation != null) { 408 getScene().removeEventFilter(MouseEvent.MOUSE_CLICKED, popoverHideHandler); 409 // getScene().removeEventFilter(ScrollEvent.ANY,popoverScrollHandler); 410 411 if (fadeAnimation != null) { 412 fadeAnimation.stop(); 413 } 414 415 FadeTransition fade = new FadeTransition(Duration.seconds(.1), this); 416 fade.setToValue(0); 417 fade.setOnFinished((ActionEvent event) -> { 418 fadeAnimation = null; 419 setVisible(false); 420 clearPages(); 421 if (onHideCallback != null) onHideCallback.run(); 422 }); 423 424 ScaleTransition scale = new ScaleTransition(Duration.seconds(.1), this); 425 scale.setToX(.8); 426 scale.setToY(.8); 427 428 ParallelTransition tx = new ParallelTransition(fade, scale); 429 fadeAnimation = tx; 430 tx.play(); 431 } 432 } 433 434 /** 435 * Represents a page in a popover. 436 */ 437 public static interface Page { 438 public void setPopover(Popover popover); 439 public Popover getPopover(); 440 441 /** 442 * Get the node that represents the page. 443 * 444 * @return the page node. 445 */ 446 public Node getPageNode(); 447 448 /** 449 * Get the title to display for this page. 450 * 451 * @return The page title 452 */ 453 public String getPageTitle(); 454 455 /** 456 * The text for left button, if null then button will be hidden. 457 * @return The button text 458 */ 459 public String leftButtonText(); 460 461 /** 462 * Called on a click of the left button of the popover. 463 */ 464 public void handleLeftButton(); 465 466 /** 467 * The text for right button, if null then button will be hidden. 468 * @return The button text 469 */ 470 public String rightButtonText(); 471 472 /** 473 * Called on a click of the right button of the popover. 474 */ 475 public void handleRightButton(); 476 477 public void handleShown(); 478 public void handleHidden(); 479 } 480 }