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 com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.handles;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.ContentPanelController;
  35 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.AbstractDriver;
  36 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.gesture.AbstractGesture;
  37 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.gesture.mouse.DiscardGesture;
  38 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.gesture.mouse.ResizeGesture;
  39 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.util.CardinalPoint;
  40 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  41 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  42 import com.oracle.javafx.scenebuilder.kit.util.MathUtils;
  43 import java.util.List;
  44 import javafx.geometry.BoundingBox;
  45 import javafx.geometry.Bounds;
  46 import javafx.geometry.Point2D;
  47 import javafx.scene.Cursor;
  48 import javafx.scene.Node;
  49 import javafx.scene.image.Image;
  50 import javafx.scene.image.ImageView;
  51 import javafx.scene.shape.ClosePath;
  52 import javafx.scene.shape.LineTo;
  53 import javafx.scene.shape.MoveTo;
  54 import javafx.scene.shape.Path;
  55 import javafx.scene.shape.PathElement;
  56 
  57 /**
  58  *
  59  *
  60  */
  61 public abstract class AbstractGenericHandles<T> extends AbstractHandles<T> {
  62 
  63     /*
  64      *
  65      *                      handleNN
  66      *    handleNW  o----------o----------o  handleNE
  67      *              |                     |
  68      *              |                     |
  69      *    handleWW  o                     o  handleEE
  70      *              |                     |
  71      *              |                     |
  72      *    handleSW  o----------o----------o  handleSE
  73      *                      handleSS
  74      *
  75      */
  76 
  77     private final ImageView handleNW = new ImageView();
  78     private final ImageView handleNE = new ImageView();
  79     private final ImageView handleSE = new ImageView();
  80     private final ImageView handleSW = new ImageView();
  81     private final ImageView handleNN = new ImageView();
  82     private final ImageView handleEE = new ImageView();
  83     private final ImageView handleSS = new ImageView();
  84     private final ImageView handleWW = new ImageView();
  85     private final MoveTo moveTo0 = new MoveTo();
  86     private final LineTo lineTo1 = new LineTo();
  87     private final LineTo lineTo2 = new LineTo();
  88     private final LineTo lineTo3 = new LineTo();
  89 
  90     public AbstractGenericHandles(ContentPanelController contentPanelController,
  91             FXOMObject fxomObject, Class<T> sceneGraphObjectClass) {
  92         super(contentPanelController, fxomObject, sceneGraphObjectClass);
  93 
  94         final Path shadow = new Path();
  95         final List<PathElement> shadowElements = shadow.getElements();
  96         shadowElements.add(moveTo0);
  97         shadowElements.add(lineTo1);
  98         shadowElements.add(lineTo2);
  99         shadowElements.add(lineTo3);
 100         shadowElements.add(new ClosePath());
 101         shadow.getStyleClass().add("selection-rect");
 102         shadow.setMouseTransparent(true);
 103 
 104         setupHandleImages();
 105 
 106         handleNW.setPickOnBounds(true);
 107         handleNE.setPickOnBounds(true);
 108         handleSE.setPickOnBounds(true);
 109         handleSW.setPickOnBounds(true);
 110 
 111         handleNN.setPickOnBounds(true);
 112         handleEE.setPickOnBounds(true);
 113         handleSS.setPickOnBounds(true);
 114         handleWW.setPickOnBounds(true);
 115 
 116         attachHandles(handleNW);
 117         attachHandles(handleNE);
 118         attachHandles(handleSE);
 119         attachHandles(handleSW);
 120 
 121         attachHandles(handleNN);
 122         attachHandles(handleEE);
 123         attachHandles(handleSS);
 124         attachHandles(handleWW);
 125 
 126         final List<Node> rootNodeChildren = getRootNode().getChildren();
 127         rootNodeChildren.add(shadow);
 128         rootNodeChildren.add(handleNW);
 129         rootNodeChildren.add(handleNE);
 130         rootNodeChildren.add(handleSE);
 131         rootNodeChildren.add(handleSW);
 132 
 133         rootNodeChildren.add(handleNN);
 134         rootNodeChildren.add(handleEE);
 135         rootNodeChildren.add(handleSS);
 136         rootNodeChildren.add(handleWW);
 137     }
 138 
 139     public Node getHandleNode(CardinalPoint cp) {
 140         final Node result;
 141 
 142         switch(cp) {
 143             case N:
 144                 result = handleNN;
 145                 break;
 146             case S:
 147                 result = handleSS;
 148                 break;
 149             case E:
 150                 result = handleEE;
 151                 break;
 152             case W:
 153                 result = handleWW;
 154                 break;
 155             case NW:
 156                 result = handleNW;
 157                 break;
 158             case NE:
 159                 result = handleNE;
 160                 break;
 161             case SW:
 162                 result = handleSW;
 163                 break;
 164             case SE:
 165                 result = handleSE;
 166                 break;
 167             default:
 168                 assert false;
 169                 result = null;
 170                 break;
 171         }
 172 
 173         return result;
 174     }
 175 
 176     /*
 177      * AbstractHandles
 178      */
 179 
 180     @Override
 181     protected void layoutDecoration() {
 182         final Bounds b = getSceneGraphObjectBounds();
 183 
 184         final double minX = b.getMinX();
 185         final double minY = b.getMinY();
 186         final double maxX = b.getMaxX();
 187         final double maxY = b.getMaxY();
 188         final double midX = (minX + maxX) / 2.0;
 189         final double midY = (minY + maxY) / 2.0;
 190 
 191         final boolean zeroWidth = MathUtils.equals(minX, maxX);
 192         final boolean zeroHeight = MathUtils.equals(minY, maxY);
 193 
 194         final boolean snapToPixel = true;
 195         final Point2D pNW, pNE, pSE, pSW;
 196         final Point2D pNN, pEE, pSS, pWW;
 197 
 198         if (zeroWidth && zeroHeight) {
 199             pNW = pNE = pSE = pSW =
 200             pNN = pEE = pSS = pWW =
 201                     sceneGraphObjectToDecoration(minX, minY, snapToPixel);
 202         } else if (zeroWidth) {
 203             pNW = pNN = pNE =
 204                     sceneGraphObjectToDecoration(minX, minY, snapToPixel);
 205             pSW = pSS = pSE =
 206                     sceneGraphObjectToDecoration(minX, maxY, snapToPixel);
 207             pEE = pWW =
 208                     sceneGraphObjectToDecoration(minX, midY, snapToPixel);
 209         } else if (b.getHeight() == 0) {
 210             pNW = pWW = pSW =
 211                     sceneGraphObjectToDecoration(minX, minY, snapToPixel);
 212             pNE = pEE = pSE =
 213                     sceneGraphObjectToDecoration(maxX, minY, snapToPixel);
 214             pNN = pSS =
 215                     sceneGraphObjectToDecoration(midX, minY, snapToPixel);
 216         } else {
 217             pNW = sceneGraphObjectToDecoration(minX, minY, snapToPixel);
 218             pNE = sceneGraphObjectToDecoration(maxX, minY, snapToPixel);
 219             pSE = sceneGraphObjectToDecoration(maxX, maxY, snapToPixel);
 220             pSW = sceneGraphObjectToDecoration(minX, maxY, snapToPixel);
 221 
 222             pNN = sceneGraphObjectToDecoration(midX, minY, snapToPixel);
 223             pEE = sceneGraphObjectToDecoration(maxX, midY, snapToPixel);
 224             pSS = sceneGraphObjectToDecoration(midX, maxY, snapToPixel);
 225             pWW = sceneGraphObjectToDecoration(minX, midY, snapToPixel);
 226         }
 227 
 228         moveTo0.setX(pNW.getX());
 229         moveTo0.setY(pNW.getY());
 230         lineTo1.setX(pNE.getX());
 231         lineTo1.setY(pNE.getY());
 232         lineTo2.setX(pSE.getX());
 233         lineTo2.setY(pSE.getY());
 234         lineTo3.setX(pSW.getX());
 235         lineTo3.setY(pSW.getY());
 236 
 237         handleNW.setLayoutX(pNW.getX());
 238         handleNW.setLayoutY(pNW.getY());
 239         handleNE.setLayoutX(pNE.getX());
 240         handleNE.setLayoutY(pNE.getY());
 241         handleSE.setLayoutX(pSE.getX());
 242         handleSE.setLayoutY(pSE.getY());
 243         handleSW.setLayoutX(pSW.getX());
 244         handleSW.setLayoutY(pSW.getY());
 245 
 246         handleNN.setLayoutX(pNN.getX());
 247         handleNN.setLayoutY(pNN.getY());
 248         handleEE.setLayoutX(pEE.getX());
 249         handleEE.setLayoutY(pEE.getY());
 250         handleSS.setLayoutX(pSS.getX());
 251         handleSS.setLayoutY(pSS.getY());
 252         handleWW.setLayoutX(pWW.getX());
 253         handleWW.setLayoutY(pWW.getY());
 254 
 255         final Bounds handlesBounds = computeBounds(pNW, pNE, pSE, pSW);
 256         final int rotation = computeNWHandleRotation(pNW, handlesBounds);
 257 
 258         setupCornerHandle(handleNW, rotation +   0);
 259         setupCornerHandle(handleNE, rotation +  90);
 260         setupCornerHandle(handleSE, rotation + 180);
 261         setupCornerHandle(handleSW, rotation + 270);
 262 
 263         setupSideHandle(handleNN, rotation +   0);
 264         setupSideHandle(handleEE, rotation +  90);
 265         setupSideHandle(handleSS, rotation + 180);
 266         setupSideHandle(handleWW, rotation + 270);
 267 
 268         showHideSideHandle(handleNN, pNW, pNE);
 269         showHideSideHandle(handleEE, pNE, pSE);
 270         showHideSideHandle(handleSS, pSW, pSE);
 271         showHideSideHandle(handleWW, pNW, pSW);
 272     }
 273 
 274 
 275     @Override
 276     public AbstractGesture findGesture(Node node) {
 277         final AbstractGesture result;
 278 
 279         if (isResizable() == false) {
 280             result = new DiscardGesture(getContentPanelController());
 281         } else {
 282             assert getFxomObject() instanceof FXOMInstance;
 283 
 284             final FXOMInstance fxomInstance = (FXOMInstance) getFxomObject();
 285 
 286             if (node == handleNW) {
 287                 result = new ResizeGesture(getContentPanelController(),
 288                         fxomInstance, CardinalPoint.NW);
 289             } else if (node == handleNE) {
 290                 result = new ResizeGesture(getContentPanelController(),
 291                         fxomInstance, CardinalPoint.NE);
 292             } else if (node == handleSE) {
 293                 result = new ResizeGesture(getContentPanelController(),
 294                         fxomInstance, CardinalPoint.SE);
 295             } else if (node == handleSW) {
 296                 result = new ResizeGesture(getContentPanelController(),
 297                         fxomInstance, CardinalPoint.SW);
 298             }  else if (node == handleNN) {
 299                 result = new ResizeGesture(getContentPanelController(),
 300                         fxomInstance, CardinalPoint.N);
 301             } else if (node == handleEE) {
 302                 result = new ResizeGesture(getContentPanelController(),
 303                         fxomInstance, CardinalPoint.E);
 304             } else if (node == handleSS) {
 305                 result = new ResizeGesture(getContentPanelController(),
 306                         fxomInstance, CardinalPoint.S);
 307             } else if (node == handleWW) {
 308                 result = new ResizeGesture(getContentPanelController(),
 309                         fxomInstance, CardinalPoint.W);
 310             } else {
 311                 result = null;
 312             }
 313         }
 314 
 315         return result;
 316     }
 317 
 318     @Override
 319     public void enabledDidChange() {
 320         setupHandleImages();
 321     }
 322 
 323     /*
 324      * Private
 325      */
 326 
 327     private Bounds computeBounds(Point2D p0, Point2D p1, Point2D p2, Point2D p3) {
 328         final double minX, minY, maxX, maxY;
 329 
 330         minX = Math.min(Math.min(p0.getX(), p1.getX()), Math.min(p2.getX(), p3.getX()));
 331         minY = Math.min(Math.min(p0.getY(), p1.getY()), Math.min(p2.getY(), p3.getY()));
 332         maxX = Math.max(Math.max(p0.getX(), p1.getX()), Math.max(p2.getX(), p3.getX()));
 333         maxY = Math.max(Math.max(p0.getY(), p1.getY()), Math.max(p2.getY(), p3.getY()));
 334 
 335         return new BoundingBox(minX, minY, maxX - minX, maxY - minY);
 336     }
 337 
 338     private void setupCornerHandle(ImageView handle, int rotation) {
 339 
 340         rotation = ((rotation % 360) + 360) % 360; // Clamp between 0 and 360
 341 
 342         final double dx, dy;
 343         final double handleWidth = handle.getLayoutBounds().getWidth();
 344         if (rotation == 0) {
 345             dx = +0.0;
 346             dy = +0.0;
 347         } else if (rotation == 90) {
 348             dx = -handleWidth;
 349             dy = +0.0;
 350         } else if (rotation == 180) {
 351             dx = -handleWidth;
 352             dy = -handleWidth;
 353         } else if (rotation == 270) {
 354             dx = +0.0;
 355             dy = -handleWidth;
 356         } else {
 357             assert false : "rotation=" + rotation;
 358             dx = +0.0;
 359             dy = +0.0;
 360         }
 361 
 362         handle.setRotate(rotation);
 363         handle.setTranslateX(dx);
 364         handle.setTranslateY(dy);
 365     }
 366 
 367     private void setupSideHandle(ImageView handle, int rotation) {
 368 
 369         rotation = ((rotation % 360) + 360) % 360; // Clamp between 0 and 360
 370 
 371         final double dx, dy;
 372         final double w = handle.getLayoutBounds().getWidth()  / 2.0;
 373         final double h = handle.getLayoutBounds().getHeight() / 2.0;
 374         final double k0 = 1.0; // Hugly trick to force pixel alignment :(
 375 
 376         if (rotation == 0) {
 377             dx = -w;
 378             dy = +0.0;
 379         } else if (rotation == 90) {
 380             dx = -w -h;
 381             dy = -h - k0;
 382         } else if (rotation == 180) {
 383             dx = -w + k0;
 384             dy = -h * 2;
 385         } else if (rotation == 270) {
 386             dx = -w + h;
 387             dy = -h;
 388         } else {
 389             assert false : "rotation=" + rotation;
 390             dx = +0.0;
 391             dy = +0.0;
 392         }
 393 
 394         handle.setRotate(rotation);
 395         handle.setTranslateX(dx);
 396         handle.setTranslateY(dy);
 397     }
 398 
 399     private int computeNWHandleRotation(Point2D handlePos, Bounds handlesBounds) {
 400         final int result;
 401 
 402         assert handlePos != null;
 403         assert handlesBounds != null;
 404         assert handlesBounds.contains(handlePos);
 405 
 406 
 407         if ((handlesBounds.getWidth() == 0) || (handlesBounds.getHeight() == 0)) {
 408             // scene graph object is zero sized
 409             result = +180;
 410         } else {
 411             /*
 412              *          x0        xm        x1
 413              *      y0  *---------*---------*
 414              *          |    tl   |   tr    |
 415              *          |         |         |
 416              *          |  +180°  |  +270°  |
 417              *          |         |         |
 418              *      ym  *---------*---------*
 419              *          |    bl   |   br    |
 420              *          |         |         |
 421              *          |   +90°  |    0°   |
 422              *          |         |         |
 423              *      y1  *---------*---------*
 424              */
 425 
 426             final double x0 = handlesBounds.getMinX();
 427             final double x1 = handlesBounds.getMaxX();
 428             final double xm = (x0 + x1) / 2.0;
 429             final double y0 = handlesBounds.getMinY();
 430             final double y1 = handlesBounds.getMaxY();
 431             final double ym = (y0 + y1) / 2.0;
 432 
 433             final double x = handlePos.getX();
 434             final double y = handlePos.getY();
 435 
 436             if (x <= xm) {
 437                 if (y <= ym) {
 438                     // (x, y) is in the top left quadrant
 439                     result = +180;
 440                 } else {
 441                     // (x, y) is in the bottom left quadrant
 442                     result = +90;
 443                 }
 444             } else {
 445                 if (y <= ym) {
 446                     // (x, y) is in the top right quadrant
 447                     result = +270;
 448                 } else {
 449                     // (x, y) is in the bottom right quadrant
 450                     result = +0;
 451                 }
 452             }
 453         }
 454 
 455         return result;
 456     }
 457 
 458 
 459     private void showHideSideHandle(ImageView handle, Point2D p0, Point2D p1) {
 460 
 461         final double dx = p1.getX() - p0.getX();
 462         final double dy = p1.getY() - p0.getY();
 463         final double d01 = Math.sqrt(dx * dx + dy * dy);
 464 
 465         final double sideHandleWidth = getSideHandleImage().getWidth();
 466         final double sideHandleHeight = getSideHandleImage().getHeight();
 467         final double sideHandleSize = Math.max(sideHandleWidth, sideHandleHeight);
 468 
 469         final boolean handleVisible = sideHandleSize < d01;
 470         handle.setVisible(handleVisible);
 471         handle.setMouseTransparent(! handleVisible);
 472     }
 473 
 474 
 475     private void setupHandleImages() {
 476         final Image handleImage, sideHandleImage;
 477         if (isEnabled() && isResizable()) {
 478 
 479             handleNW.setCursor(Cursor.NW_RESIZE);
 480             handleNE.setCursor(Cursor.NE_RESIZE);
 481             handleSE.setCursor(Cursor.SE_RESIZE);
 482             handleSW.setCursor(Cursor.SW_RESIZE);
 483 
 484             handleNN.setCursor(Cursor.N_RESIZE);
 485             handleEE.setCursor(Cursor.E_RESIZE);
 486             handleSS.setCursor(Cursor.S_RESIZE);
 487             handleWW.setCursor(Cursor.W_RESIZE);
 488 
 489             handleImage = getCornerHandleImage();
 490             sideHandleImage = getSideHandleImage();
 491 
 492         } else {
 493 
 494             handleNW.setCursor(Cursor.DEFAULT);
 495             handleNE.setCursor(Cursor.DEFAULT);
 496             handleSE.setCursor(Cursor.DEFAULT);
 497             handleSW.setCursor(Cursor.DEFAULT);
 498 
 499             handleNN.setCursor(Cursor.DEFAULT);
 500             handleEE.setCursor(Cursor.DEFAULT);
 501             handleSS.setCursor(Cursor.DEFAULT);
 502             handleWW.setCursor(Cursor.DEFAULT);
 503 
 504             handleImage = getCornerHandleDimImage();
 505             sideHandleImage = getSideHandleDimImage();
 506         }
 507 
 508         handleNW.setImage(handleImage);
 509         handleNE.setImage(handleImage);
 510         handleSE.setImage(handleImage);
 511         handleSW.setImage(handleImage);
 512 
 513         handleNN.setImage(sideHandleImage);
 514         handleEE.setImage(sideHandleImage);
 515         handleSS.setImage(sideHandleImage);
 516         handleWW.setImage(sideHandleImage);
 517 
 518     }
 519 
 520 
 521     private boolean isResizable() {
 522         final AbstractDriver driver
 523                 = getContentPanelController().lookupDriver(getFxomObject());
 524         return driver.makeResizer(getFxomObject()) != null;
 525     }
 526 
 527     /*
 528      * Wraper to avoid the 'leaking this in constructor' warning emitted by NB.
 529      */
 530     private void attachHandles(Node node) {
 531         attachHandles(node, this);
 532     }
 533 }