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 }