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