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.gesture;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.drag.DragController;
  35 import com.oracle.javafx.scenebuilder.kit.editor.drag.source.AbstractDragSource;
  36 import com.oracle.javafx.scenebuilder.kit.editor.drag.source.ExternalDragSource;
  37 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.AbstractDropTarget;
  38 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.AccessoryDropTarget;
  39 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.ContainerXYDropTarget;
  40 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.ImageViewDropTarget;
  41 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.RootDropTarget;
  42 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.ContentPanelController;
  43 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.AbstractDriver;
  44 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.driver.BorderPaneDriver;
  45 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.guides.MovingGuideController;
  46 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.util.BoundsUtils;
  47 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument;
  48 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  49 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  50 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask;
  51 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask.Accessory;
  52 import com.oracle.javafx.scenebuilder.kit.util.MathUtils;
  53 
  54 import java.util.HashSet;
  55 import java.util.Set;
  56 import java.util.logging.Level;
  57 import java.util.logging.Logger;
  58 
  59 import javafx.event.EventType;
  60 import javafx.geometry.Bounds;
  61 import javafx.geometry.Point2D;
  62 import javafx.scene.Group;
  63 import javafx.scene.Node;
  64 import javafx.scene.image.ImageView;
  65 import javafx.scene.input.DragEvent;
  66 import javafx.scene.input.InputEvent;
  67 import javafx.scene.input.KeyCode;
  68 import javafx.scene.input.KeyEvent;
  69 import javafx.scene.layout.BorderPane;
  70 import javafx.stage.Window;
  71 
  72 /**
  73  *
  74  *
  75  */
  76 public class DragGesture extends AbstractGesture {
  77 
  78     private static final Logger LOG = Logger.getLogger(DragGesture.class.getName());
  79 
  80     private final double MARGIN = 14.0;
  81 
  82     private final DragController dragController;
  83     private final Set<FXOMObject> pickExcludes = new HashSet<>();
  84     private DragEvent dragEnteredEvent;
  85     private DragEvent lastDragEvent;
  86     private Observer observer;
  87     private boolean willReceiveDragDone;
  88     private boolean shouldInvokeEnd;
  89     private FXOMObject hitParent;
  90     private DesignHierarchyMask hitParentMask;
  91     private MovingGuideController movingGuideController;
  92     private boolean guidesDisabled;
  93     private Node shadow;
  94 
  95     public DragGesture(ContentPanelController contentPanelController) {
  96         super(contentPanelController);
  97         this.dragController = contentPanelController.getEditorController().getDragController();
  98     }
  99 
 100     /*
 101      * AbstractDragGesture
 102      */
 103 
 104     @Override
 105     public void start(InputEvent e, Observer observer) {
 106         assert e != null;
 107         assert e instanceof DragEvent;
 108         assert e.getEventType() == DragEvent.DRAG_ENTERED;
 109 
 110         final Node glassLayer = contentPanelController.getGlassLayer();
 111         assert glassLayer.getOnDragEntered()== null;
 112         assert glassLayer.getOnDragOver()== null;
 113         assert glassLayer.getOnDragExited()== null;
 114         assert glassLayer.getOnDragDropped()== null;
 115         assert glassLayer.getOnDragDone()== null;
 116         assert glassLayer.getOnKeyPressed()== null;
 117 
 118         glassLayer.setOnDragEntered(e1 -> {
 119             lastDragEvent = e1;
 120             dragEnteredGlassLayer();
 121         });
 122         glassLayer.setOnDragOver(e1 -> {
 123             lastDragEvent = e1;
 124             dragOverGlassLayer();
 125         });
 126         glassLayer.setOnDragExited(e1 -> {
 127             lastDragEvent = e1;
 128             dragExitedGlassLayer();
 129         });
 130         glassLayer.setOnDragDropped(e1 -> {
 131             lastDragEvent = e1;
 132             dragDroppedOnGlassLayer();
 133             e1.consume();
 134             // On Linux, "drag over" is randomly called before "drag done".
 135             // It's unclear whether it's an FX bug or feature.
 136             // To make things unambiguous, we clear the "drag over" callback.
 137             // See DTL-5956.
 138             glassLayer.setOnDragOver(null);
 139         });
 140         glassLayer.setOnDragDone(e1 -> {
 141             lastDragEvent = e1;
 142             dragDoneOnGlassLayer();
 143             e1.getDragboard().clear();
 144             e1.consume();
 145         });
 146         glassLayer.setOnKeyPressed(e1 -> handleKeyPressed(e1));
 147 
 148         this.dragEnteredEvent = (DragEvent) e;
 149         this.lastDragEvent = this.dragEnteredEvent;
 150         this.observer = observer;
 151         this.willReceiveDragDone = this.dragEnteredEvent.getGestureSource() == glassLayer;
 152         this.shouldInvokeEnd = willReceiveDragDone;
 153         assert this.hitParent == null;
 154         assert this.hitParentMask == null;
 155         assert this.shadow == null;
 156 
 157         setupMovingGuideController();
 158 
 159         dragEnteredGlassLayer();
 160     }
 161 
 162 
 163     /*
 164      * Private
 165      */
 166 
 167     private void dragEnteredGlassLayer() {
 168         if (dragController.getDragSource() == null) { // Drag started externally
 169             final FXOMDocument fxomDocument
 170                     = contentPanelController.getEditorController().getFxomDocument();
 171             final Window ownerWindow
 172                     = contentPanelController.getPanelRoot().getScene().getWindow();
 173             final ExternalDragSource dragSource = new ExternalDragSource(
 174                     lastDragEvent.getDragboard(), fxomDocument, ownerWindow);
 175             assert dragSource.isAcceptable();
 176             dragController.begin(dragSource);
 177             shouldInvokeEnd = true;
 178         }
 179 
 180         // Objects being dragged should be excluded from the pick.
 181         // We create the exclude list here once.
 182         pickExcludes.clear();
 183         pickExcludes.addAll(dragController.getDragSource().getDraggedObjects());
 184 
 185         // We show the shadow
 186         showShadow();
 187 
 188         // Now same logic as dragOver
 189         dragOverGlassLayer();
 190     }
 191 
 192     private void dragOverGlassLayer() {
 193         /*
 194          * On Linux, Node.onDragOver() is sometimes called *after*
 195          * Node.onDragDropped() : see RT-34537.
 196          * We detect those illegal invocations here and ignore them.
 197          */
 198 
 199         if (lastDragEvent.isDropCompleted()) {
 200             LOG.log(Level.WARNING, "Ignored dragOver() after dragDropped()"); //NOI18N
 201         } else {
 202             dragOverGlassLayerBis();
 203         }
 204     }
 205 
 206     private void dragOverGlassLayerBis() {
 207 
 208         // Let's set what is below the mouse
 209         final double hitX = lastDragEvent.getSceneX();
 210         final double hitY = lastDragEvent.getSceneY();
 211         FXOMObject hitObject = contentPanelController.pick(hitX, hitY, pickExcludes);
 212         if (hitObject == null) {
 213             final FXOMDocument fxomDocument
 214                 = contentPanelController.getEditorController().getFxomDocument();
 215             hitObject = fxomDocument.getFxomRoot();
 216         }
 217 
 218         if (hitObject == null) {
 219             // FXOM document is empty
 220             dragOverEmptyDocument();
 221         } else {
 222             dragOverHitObject(hitObject);
 223         }
 224     }
 225 
 226     private void dragOverEmptyDocument() {
 227         dragController.setDropTarget(new RootDropTarget());
 228         lastDragEvent.acceptTransferModes(dragController.getAcceptedTransferModes());
 229         updateShadow(lastDragEvent.getSceneX(), lastDragEvent.getSceneY());
 230     }
 231 
 232     private void dragOverHitObject(FXOMObject hitObject) {
 233         assert hitObject != null;
 234 
 235         final FXOMDocument fxomDocument
 236                 = contentPanelController.getEditorController().getFxomDocument();
 237         final AbstractDragSource dragSource
 238                 = dragController.getDragSource();
 239         final DesignHierarchyMask m
 240                 = new DesignHierarchyMask(hitObject);
 241         final double hitX
 242                 = lastDragEvent.getSceneX();
 243         final double hitY
 244                 = lastDragEvent.getSceneY();
 245 
 246         assert fxomDocument != null;
 247         assert dragSource != null;
 248 
 249         AbstractDropTarget dropTarget = null;
 250         FXOMObject newHitParent = null;
 251         DesignHierarchyMask newHitParentMask = null;
 252 
 253         // dragSource is a single ImageView ?
 254         final boolean hitImageView = hitObject.getSceneGraphObject() instanceof ImageView;
 255         final boolean externalDragSource = dragSource instanceof ExternalDragSource;
 256 
 257         if (dragSource.isSingleImageViewOnly() && hitImageView && externalDragSource) {
 258             dropTarget = new ImageViewDropTarget(hitObject);
 259             newHitParent = hitObject;
 260             newHitParentMask = m;
 261         }
 262 
 263         // dragSource is a single Tooltip ?
 264         if (dropTarget == null) {
 265             if (dragSource.isSingleTooltipOnly()) {
 266                 assert hitObject instanceof FXOMInstance;
 267                 dropTarget = new AccessoryDropTarget((FXOMInstance)hitObject, Accessory.TOOLTIP);
 268                 newHitParent = hitObject;
 269                 newHitParentMask = m;
 270             }
 271         }
 272 
 273         // dragSource is a single ContextMenu ?
 274         if (dropTarget == null) {
 275             if (dragSource.isSingleContextMenuOnly()) {
 276                 assert hitObject instanceof FXOMInstance;
 277                 dropTarget = new AccessoryDropTarget((FXOMInstance)hitObject, Accessory.CONTEXT_MENU);
 278                 newHitParent = hitObject;
 279                 newHitParentMask = m;
 280             }
 281         }
 282 
 283         // hitObject is BorderPane ?
 284         if (dropTarget == null) {
 285             if (hitObject.getSceneGraphObject() instanceof BorderPane) {
 286                 final AbstractDriver driver = contentPanelController.lookupDriver(hitObject);
 287                 assert driver instanceof BorderPaneDriver;
 288                 dropTarget = driver.makeDropTarget(hitObject, hitX, hitY);
 289                 newHitParent = hitObject;
 290                 newHitParentMask = m;
 291             }
 292         }
 293 
 294         // hitObject has sub-components (ie it is a container)
 295         if (dropTarget == null) {
 296             if (m.isAcceptingSubComponent()) {
 297                 final AbstractDriver driver = contentPanelController.lookupDriver(hitObject);
 298                 dropTarget = driver.makeDropTarget(hitObject, hitX, hitY);
 299                 newHitParent = hitObject;
 300                 newHitParentMask = m;
 301             }
 302         }
 303 
 304         // hitObject accepts Accessory.CONTENT
 305         if (dropTarget == null) {
 306             if (m.isAcceptingAccessory(Accessory.CONTENT)) {
 307                 assert hitObject instanceof FXOMInstance;
 308                 dropTarget = new AccessoryDropTarget((FXOMInstance)hitObject, Accessory.CONTENT);
 309                 newHitParent = hitObject;
 310                 newHitParentMask = m;
 311             }
 312         }
 313 
 314         // hitObject parent is a container ?
 315         if (dropTarget == null) {
 316             final FXOMObject hitObjectParent = hitObject.getParentObject();
 317             if (hitObjectParent != null) {
 318                 final DesignHierarchyMask mp = new DesignHierarchyMask(hitObjectParent);
 319                 if (mp.isAcceptingSubComponent()) {
 320                     final AbstractDriver driver = contentPanelController.lookupDriver(hitObjectParent);
 321                     dropTarget = driver.makeDropTarget(hitObjectParent, hitX, hitY);
 322                     newHitParent = hitObjectParent;
 323                     newHitParentMask = mp;
 324                 }
 325             }
 326         }
 327 
 328         // Update movingGuideController
 329         if (newHitParent != hitParent) {
 330             hitParent = newHitParent;
 331             hitParentMask = newHitParentMask;
 332             if (hitParent == null) {
 333                 assert hitParentMask == null;
 334                 movingGuideController.clearSampleBounds();
 335             } else {
 336                 assert hitParentMask != null;
 337                 if (hitParentMask.isFreeChildPositioning() && dragSource.isNodeOnly()) {
 338                     populateMovingGuideController();
 339                 } else {
 340                     movingGuideController.clearSampleBounds();
 341                 }
 342             }
 343         }
 344 
 345         final double guidedX, guidedY;
 346         if (movingGuideController.hasSampleBounds() && (guidesDisabled == false)) {
 347             updateShadow(hitX, hitY);
 348             final Bounds shadowBounds = shadow.getLayoutBounds();
 349             final Bounds shadowBoundsInScene = shadow.localToScene(shadowBounds, true /* rootScene */);
 350             movingGuideController.match(shadowBoundsInScene);
 351 
 352             guidedX = hitX + movingGuideController.getSuggestedDX();
 353             guidedY = hitY + movingGuideController.getSuggestedDY();
 354         } else {
 355             guidedX = hitX;
 356             guidedY = hitY;
 357         }
 358 
 359         updateShadow(guidedX, guidedY);
 360 
 361         if (!MathUtils.equals(guidedX , hitX) || !MathUtils.equals(guidedY, hitY)) {
 362             assert dropTarget != null;
 363             assert dropTarget instanceof ContainerXYDropTarget;
 364             final AbstractDriver driver = contentPanelController.lookupDriver(dropTarget.getTargetObject());
 365             dropTarget = driver.makeDropTarget(hitParent, guidedX, guidedY);
 366             assert dropTarget instanceof ContainerXYDropTarget;
 367         }
 368 
 369         dragController.setDropTarget(dropTarget);
 370         lastDragEvent.acceptTransferModes(dragController.getAcceptedTransferModes());
 371 
 372     }
 373 
 374     private void dragExitedGlassLayer() {
 375 
 376         dragController.setDropTarget(null);
 377         hideShadow();
 378         movingGuideController.clearSampleBounds();
 379 
 380         if (willReceiveDragDone == false) {
 381             dragDoneOnGlassLayer();
 382         }
 383     }
 384 
 385     private void dragDroppedOnGlassLayer() {
 386         lastDragEvent.setDropCompleted(true);
 387         dragController.commit();
 388         contentPanelController.getGlassLayer().requestFocus();
 389     }
 390 
 391     private void dragDoneOnGlassLayer() {
 392         if (shouldInvokeEnd) {
 393             dragController.end();
 394         }
 395         performTermination();
 396     }
 397 
 398     private void handleKeyPressed(KeyEvent e) {
 399         if (e.getCode() == KeyCode.ESCAPE) {
 400             dragExitedGlassLayer();
 401             if (willReceiveDragDone) {
 402                 // dragDone will not arrive but
 403                 // we need to execute the corresponding logic
 404                 dragDoneOnGlassLayer();
 405             }
 406         } else if (e.getCode() == KeyCode.ALT) {
 407             final EventType<KeyEvent> eventType = e.getEventType();
 408             if (eventType == KeyEvent.KEY_PRESSED) {
 409                 guidesDisabled = true;
 410             } else if (eventType == KeyEvent.KEY_RELEASED) {
 411                 guidesDisabled = false;
 412             }
 413             dragOverGlassLayer();
 414         }
 415     }
 416 
 417 
 418     private void performTermination() {
 419         final Node glassLayer = contentPanelController.getGlassLayer();
 420         glassLayer.setOnDragEntered(null);
 421         glassLayer.setOnDragOver(null);
 422         glassLayer.setOnDragExited(null);
 423         glassLayer.setOnDragDropped(null);
 424         glassLayer.setOnDragDone(null);
 425         glassLayer.setOnKeyPressed(null);
 426 
 427         dismantleMovingGuideController();
 428 
 429         observer.gestureDidTerminate(this);
 430         observer = null;
 431 
 432         dragEnteredEvent = null;
 433         lastDragEvent = null;
 434         shouldInvokeEnd = false;
 435         hitParent = null;
 436         hitParentMask = null;
 437         assert shadow == null; // Because dragExitedGlassLayer() called hideShadow()
 438     }
 439 
 440     /*
 441      * Shadow
 442      */
 443 
 444     private void showShadow() {
 445         assert shadow == null;
 446 
 447         shadow = dragController.getDragSource().makeShadow();
 448         shadow.setMouseTransparent(true);
 449         contentPanelController.getRudderLayer().getChildren().add(shadow);
 450 
 451         updateShadow(0.0, 0.0);
 452     }
 453 
 454     private void updateShadow(double hitX, double hitY) {
 455         assert shadow != null;
 456 
 457         final Group rudderLayer = contentPanelController.getRudderLayer();
 458         final Point2D p = rudderLayer.sceneToLocal(hitX, hitY, true /* rootScene */);
 459         shadow.setLayoutX(p.getX());
 460         shadow.setLayoutY(p.getY());
 461     }
 462 
 463     private void hideShadow() {
 464         assert shadow != null;
 465         contentPanelController.getRudderLayer().getChildren().remove(shadow);
 466         shadow = null;
 467     }
 468 
 469     /*
 470      * MovingGuideController
 471      */
 472 
 473     private void setupMovingGuideController() {
 474         final Bounds scope = contentPanelController.getWorkspacePane().getLayoutBounds();
 475         final Bounds scopeInScene = contentPanelController.getWorkspacePane().localToScene(scope, true /* rootScene */);
 476         this.movingGuideController = new MovingGuideController(
 477                 contentPanelController.getGuidesColor(), scopeInScene);
 478         final Group rudderLayer = contentPanelController.getRudderLayer();
 479         final Group guideGroup = movingGuideController.getGuideGroup();
 480         assert guideGroup.isMouseTransparent();
 481         rudderLayer.getChildren().add(guideGroup);
 482     }
 483 
 484 
 485     private void populateMovingGuideController() {
 486         assert hitParentMask != null;
 487         assert hitParentMask.isFreeChildPositioning(); // (1)
 488 
 489         movingGuideController.clearSampleBounds();
 490 
 491         // Adds N, S, E, W and center lines for each child of the hitParent
 492         for (int i = 0, c = hitParentMask.getSubComponentCount(); i < c; i++) {
 493             final FXOMObject child = hitParentMask.getSubComponentAtIndex(i);
 494             final boolean isNode = child.getSceneGraphObject() instanceof Node;
 495             if ((pickExcludes.contains(child) == false) && isNode) {
 496                 final Node childNode = (Node) child.getSceneGraphObject();
 497                 movingGuideController.addSampleBounds(childNode);
 498             }
 499         }
 500 
 501         // Adds N, S, E, W and center lines of the hitParent itself
 502         assert hitParent.getSceneGraphObject() instanceof Node; // Because (1)
 503         final Node hitParentNode = (Node) hitParent.getSceneGraphObject();
 504         movingGuideController.addSampleBounds(hitParentNode);
 505 
 506         // If bounds of hitParent are larger enough then adds the margin boundaries
 507         final Bounds hitParentBounds = hitParentNode.getLayoutBounds();
 508         final Bounds insetBounds = BoundsUtils.inset(hitParentBounds, MARGIN, MARGIN);
 509         if (insetBounds.isEmpty() == false) {
 510             final Bounds insetBoundsInScene = hitParentNode.localToScene(insetBounds, true /* rootScene */);
 511             movingGuideController.addSampleBounds(insetBoundsInScene, false /* addMiddle */);
 512         }
 513     }
 514 
 515 
 516     private void dismantleMovingGuideController() {
 517         assert movingGuideController != null;
 518         final Group guideGroup = movingGuideController.getGuideGroup();
 519         final Group rudderLayer = contentPanelController.getRudderLayer();
 520         assert rudderLayer.getChildren().contains(guideGroup);
 521         rudderLayer.getChildren().remove(guideGroup);
 522         movingGuideController = null;
 523     }
 524 }