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 }