1 /*
   2  * Copyright (c) 2012, 2015, 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 
  35 import javafx.animation.*;
  36 import javafx.beans.property.DoubleProperty;
  37 import javafx.beans.property.ObjectProperty;
  38 import javafx.beans.property.SimpleDoubleProperty;
  39 import javafx.beans.property.SimpleObjectProperty;
  40 import javafx.beans.value.ObservableValue;
  41 import javafx.geometry.HPos;
  42 import javafx.geometry.Point2D;
  43 import javafx.geometry.VPos;
  44 import javafx.scene.Node;
  45 import javafx.scene.input.MouseEvent;
  46 import javafx.scene.layout.Region;
  47 import javafx.scene.shape.Path;
  48 import javafx.util.Duration;
  49 import static ensemble.control.BendingPages.State.*;
  50 import static ensemble.control.BendingPages.AnimationState.*;
  51 import javafx.event.ActionEvent;
  52 import javafx.geometry.Bounds;
  53 import javafx.scene.paint.Color;
  54 
  55 public class BendingPages extends Region {
  56 
  57     private BookBend bookBend;
  58     private Path frontPageBack = new Path();
  59 
  60     private Path shadow = new Path();
  61 
  62     private ObjectProperty<Node> frontPage = new SimpleObjectProperty<Node>(new Region());
  63     
  64     public ObjectProperty<Node> frontPageProperty() {
  65         return frontPage;
  66     }
  67     
  68     public Node getFrontPage() {
  69         return frontPage.get();
  70     }
  71 
  72     public void setFrontPage(Node frontPage) {
  73         this.frontPage.set(frontPage);
  74     }
  75 
  76     private ObjectProperty<Node> backPage = new SimpleObjectProperty<Node>(new Region());
  77 
  78     public ObjectProperty<Node> backPageProperty() {
  79         return backPage;
  80     }
  81 
  82     public Node getBackPage() {
  83         return backPage.get();
  84     }
  85     
  86     public void setBackPage(Node backPage) {
  87         this.backPage.set(backPage);
  88     }
  89 
  90     public void reset() {
  91         if (animation != null) {
  92             animation.stop();
  93         }
  94         state = State.CLOSED;
  95         setTarget();
  96         update();
  97         fixMouseTransparency();
  98     }
  99 
 100     @Override
 101     protected void layoutChildren() {
 102         super.layoutChildren();
 103         layoutInArea(frontPage.get(), 0, 0, getWidth(), getHeight(), 0, HPos.LEFT, VPos.TOP);
 104         layoutInArea(backPage.get(), 0, 0, getWidth(), getHeight(), 0, HPos.LEFT, VPos.TOP);
 105     }
 106     
 107     private ObjectProperty<Point2D> closedOffset = new SimpleObjectProperty<>(new Point2D(50, 100));
 108     
 109     /**
 110      * Offset is subtracted from right bottom corner. (0, 0) is fully closed
 111      * (100, 200) is opened by (-100, -200).
 112      * @return offset point property
 113      */
 114     public ObjectProperty<Point2D> closedOffsetProperty() {
 115         return closedOffset;
 116     }
 117     
 118     public Point2D getClosedOffset() {
 119         return closedOffset.get();
 120     }
 121     
 122     public void setClosedOffset(Point2D from) {
 123         this.closedOffset.set(from);
 124     }
 125     
 126     private ObjectProperty<Point2D> openedOffset = new SimpleObjectProperty<>(new Point2D(250, 250));
 127     
 128     /**
 129      * Offset is added to top left corner of maximum opened corner. (0, 0) is
 130      * fully opened. (2 * width - 100, 2 * height - 200) is opened by (-100, -200).
 131      * @return offset point property
 132      */
 133     public ObjectProperty<Point2D> openedOffsetProperty() {
 134         return openedOffset;
 135     }
 136     
 137     public Point2D getOpenedOffset() {
 138         return openedOffset.get();
 139     }
 140     
 141     public void setOpenedOffset(Point2D openedOffset) {
 142         this.openedOffset.set(openedOffset);
 143     }
 144     
 145     private DoubleProperty gripSize = new SimpleDoubleProperty(100);
 146     
 147     public DoubleProperty gripSizeProperty() {
 148         return gripSize;
 149     }
 150     
 151     public double getGripSize() {
 152         return gripSize.get();
 153     }
 154     
 155     public void setGripSize(double gripSize) {
 156         this.gripSize.set(gripSize);
 157     }
 158 
 159     private void fixMouseTransparency() {
 160         if (state == State.OPENED) {
 161             frontPage.get().setMouseTransparent(true);
 162         } else if (state == State.CLOSED) {
 163             frontPage.get().setMouseTransparent(false);
 164         }
 165     }
 166     
 167     static enum AnimationState { NO_ANIMATION, FOLLOWING_MOVING_MOUSE, 
 168             FOLLOWING_DRAGGING_MOUSE, ANIMATION 
 169     };
 170     static enum State { CLOSED, OPENED };
 171     
 172     private State state = CLOSED;
 173     private AnimationState animState = NO_ANIMATION;
 174     
 175     private Timeline animation;
 176 
 177     public BendingPages() {
 178         frontPageBack.setStroke(null);
 179         frontPageBack.setId("frontPageBack");
 180         shadow.setStroke(null);
 181         shadow.setId("frontPageShadow");
 182         shadow.setMouseTransparent(true);
 183         getChildren().setAll(backPage.get(), frontPage.get(), frontPageBack, shadow);
 184         
 185         backPage.addListener((ObservableValue<? extends Node> arg0, Node arg1, Node arg2) -> {
 186             getChildren().set(0, arg2);
 187         });
 188         
 189         frontPage.addListener((ObservableValue<? extends Node> arg0, Node oldPage, Node newPage) -> {
 190             if (bookBend != null) {
 191                 bookBend.detach();
 192             }
 193             getChildren().set(1, newPage);
 194             bookBend = new BookBend(newPage, frontPageBack, shadow);
 195             setTarget();
 196         });
 197         
 198         addEventFilter(MouseEvent.MOUSE_MOVED, (MouseEvent me) -> {
 199             if (withinGrip(me)) {
 200                 animState = FOLLOWING_MOVING_MOUSE;
 201                 setTarget(me);
 202                 update();
 203             } else if (animState == FOLLOWING_MOVING_MOUSE) {
 204                 endFollowingMouse();
 205             }
 206         });
 207         
 208         setOnMousePressed((MouseEvent me) -> {
 209             if (withinGrip(me)) {
 210                 animState = FOLLOWING_DRAGGING_MOUSE;
 211                 me.consume();
 212             } else if (withinPath(me)) {
 213                 animState = FOLLOWING_DRAGGING_MOUSE;
 214                 me.consume();
 215             }
 216         });
 217         
 218         setOnMouseDragged((MouseEvent me) -> {
 219             if (animState == FOLLOWING_DRAGGING_MOUSE) {
 220                 setTarget(me);
 221                 deltaX = targetX - bookBend.getTargetX();
 222                 deltaY = targetY - bookBend.getTargetY();
 223                 update();
 224                 me.consume();
 225             }
 226         });
 227         
 228         setOnMouseExited((MouseEvent me) -> {
 229             if (animState == FOLLOWING_MOVING_MOUSE) {
 230                 endFollowingMouse();
 231                 me.consume();
 232             }
 233         });
 234         
 235         setOnMouseReleased((MouseEvent me) -> {
 236             if (animState == FOLLOWING_DRAGGING_MOUSE && !me.isStillSincePress()) {
 237                 endFollowingMouse();
 238                 me.consume();
 239             }
 240         });
 241         
 242         setOnMouseClicked((MouseEvent me) -> {
 243             if (me.isStillSincePress() && (withinGrip(me) || withinPath(me))) {
 244                 if (state == OPENED) {
 245                     state = CLOSED;
 246                 } else {
 247                     state = OPENED;
 248                 }
 249                 setTarget();
 250                 animateTo();
 251                 me.consume();
 252             }
 253         });
 254         
 255         layoutBoundsProperty().addListener((ObservableValue<? extends Bounds> arg0, Bounds arg1, Bounds arg2) -> {
 256             if (state == State.CLOSED) {
 257                 setTarget();
 258                 bookBend.update(targetX, targetY);
 259             } else if (state == State.OPENED) {
 260                 setTarget();
 261                 bookBend.update(targetX, targetY);
 262             }
 263         });
 264     }
 265     
 266     private boolean withinGrip(MouseEvent me) {
 267         if (state == CLOSED) {
 268             return getWidth() - me.getX() <= gripSize.doubleValue() 
 269                     && getHeight() - me.getY() <= gripSize.doubleValue();
 270         } else {
 271             return me.getX() <= gripSize.doubleValue() 
 272                     && me.getY() <= gripSize.doubleValue();
 273         }
 274     }
 275 
 276     private boolean withinPath(MouseEvent me) {
 277         boolean contains = frontPageBack.contains(me.getX(), me.getY());
 278         return contains;
 279     }
 280     
 281     private void endFollowingMouse() {
 282         if (animState != FOLLOWING_MOVING_MOUSE) { 
 283             if (deltaX >= 0 && deltaY >= 0) {
 284                 state = CLOSED;
 285             } else {
 286                 state = OPENED;
 287             }
 288         }
 289         setTarget();
 290         animateTo();
 291     }
 292     
 293     private double targetX, targetY;
 294     private double deltaX, deltaY;
 295 
 296     private void setTarget(MouseEvent me) {
 297         if (state == OPENED) {
 298             targetX = me.getX() - (getWidth() - me.getX()) * 0.8;
 299             targetY = me.getY() - (getHeight() - me.getY()) * 0.8;
 300         } else {
 301             targetX = me.getX();
 302             targetY = me.getY();
 303         }
 304     }
 305     
 306     private void setTarget() {
 307         if (state == State.CLOSED) {
 308             targetX = getWidth() - closedOffset.get().getX();
 309             targetY = getHeight() - closedOffset.get().getY();
 310         } else if (state == State.OPENED) {
 311             targetX = -getWidth() + openedOffset.get().getX();
 312             targetY = -getHeight() + openedOffset.get().getY();
 313         }
 314     }
 315     
 316     private void update() {
 317         bookBend.update(targetX, targetY);
 318     }
 319     
 320     private void animateTo() {
 321         final double fx = bookBend.getTargetX();
 322         final double fy = bookBend.getTargetY();
 323         if (animation != null) {
 324             animation.stop();
 325         }
 326         DoubleProperty t = new SimpleDoubleProperty();
 327         t.addListener((ObservableValue<? extends Number> arg0, Number arg1, Number t1) -> {
 328             bookBend.update(fx + (targetX - fx) * t1.doubleValue(), fy + (targetY - fy) * t1.doubleValue());
 329         });
 330 
 331         Timeline animation = new Timeline(
 332                 new KeyFrame(Duration.millis(200), (ActionEvent arg0) -> { animState = NO_ANIMATION; },
 333                 new KeyValue(t, 1, Interpolator.EASE_OUT))
 334         );
 335 
 336         animation.play();
 337         animState = ANIMATION;
 338         fixMouseTransparency();
 339     }
 340     
 341     /**
 342      * Sets colors for path gradient. Values are used on next update().
 343      * @param pathColor Color for path
 344      * @param bendStartColor Start color for path
 345      * @param bendEndColor  End color for path
 346      */
 347     public void setColors(Color pathColor, Color bendStartColor, Color bendEndColor) {
 348         bookBend.setColors(pathColor, bendStartColor, bendEndColor);
 349     }
 350 }