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.hierarchy;
  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.target.AbstractDropTarget;
  37 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.AccessoryDropTarget;
  38 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.ContainerZDropTarget;
  39 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.RootDropTarget;
  40 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument;
  41 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  42 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  43 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask;
  44 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask.Accessory;
  45 import javafx.scene.control.TreeItem;
  46 import javafx.scene.input.DragEvent;
  47 
  48 /**
  49  * Controller for all drag and drop gestures in hierarchy panel. This class does
  50  * not depend on the TreeView or TreeTableView control and handles only
  51  * TreeItems.
  52  *
  53  * @treatAsPrivate
  54  */
  55 public class HierarchyDNDController {
  56 
  57     private final AbstractHierarchyPanelController panelController;
  58     private final HierarchyTaskScheduler scheduler;
  59 
  60     /**
  61      * Defines the mouse location within the cell when the dropping gesture
  62      * occurs.
  63      *
  64      * @treatAsPrivate
  65      */
  66     public enum DroppingMouseLocation {
  67 
  68         BOTTOM, CENTER, TOP
  69     }
  70 
  71     public HierarchyDNDController(final AbstractHierarchyPanelController panelController) {
  72         this.panelController = panelController;
  73         this.scheduler = new HierarchyTaskScheduler(panelController);
  74     }
  75 
  76     public HierarchyTaskScheduler getScheduler() {
  77         return scheduler;
  78     }
  79 
  80     /**
  81      * Called by the TreeCell/TreeTableRow event handler.
  82      *
  83      * @param treeItem the TreeItem
  84      * @param event the event
  85      */
  86     public void handleOnDragDropped(
  87             final TreeItem<HierarchyItem> treeItem,
  88             final DragEvent event) {
  89 
  90         // Cancel timer if any
  91         scheduler.cancelTimer();
  92     }
  93 
  94     /**
  95      * Called by the TreeCell/TreeTableRow event handler.
  96      *
  97      * @param treeItem the TreeItem
  98      * @param event the event
  99      */
 100     public void handleOnDragEntered(
 101             final TreeItem<HierarchyItem> treeItem,
 102             final DragEvent event) {
 103 
 104         // Cancel timer if any
 105         scheduler.cancelTimer();
 106 
 107         if (treeItem == null) {
 108             return;
 109         }
 110 
 111         // Auto scrolling timeline has been started :
 112         // do not schedule any other task
 113         if (panelController.isTimelineRunning()) {
 114             return;
 115         }
 116 
 117         // Schedule expanding job for collapsed TreeItems
 118         if (!treeItem.isExpanded() && !treeItem.isLeaf()) {
 119             scheduler.scheduleExpandTask(treeItem);
 120         }
 121     }
 122 
 123     /**
 124      * Called by the TreeCell/TreeTableRow event handler.
 125      *
 126      * @param treeItem the TreeItem
 127      * @param event the event
 128      * @param location the location
 129      */
 130     public void handleOnDragExited(
 131             final TreeItem<HierarchyItem> treeItem,
 132             final DragEvent event,
 133             final DroppingMouseLocation location) {
 134 
 135         // Cancel timer if any
 136         scheduler.cancelTimer();
 137 
 138         // Remove empty tree item graphic if previously added by the scheduler
 139         // when we exit the empty graphic TreeItem by the bottom
 140         if (treeItem != null) {
 141             final HierarchyItem item = treeItem.getValue();
 142             assert item != null;
 143             if (item instanceof HierarchyItemGraphic
 144                     && item.isEmpty()
 145                     && location == DroppingMouseLocation.BOTTOM) {
 146                 final TreeItem<HierarchyItem> parentTreeItem = treeItem.getParent();
 147                 parentTreeItem.getChildren().remove(treeItem);
 148             }
 149         }
 150     }
 151 
 152     /**
 153      * Called by the TreeCell/TreeTableRow event handler.
 154      *
 155      * @param treeItem the TreeItem
 156      * @param event the event
 157      * @param location the location
 158      */
 159     public void handleOnDragOver(
 160             final TreeItem<HierarchyItem> treeItem,
 161             final DragEvent event,
 162             final DroppingMouseLocation location) {
 163 
 164         // Remove empty tree item graphic if previously added by the scheduler
 165         // when we hover the empty graphic owner TreeItem on the top area
 166         if (treeItem != null) {
 167             final HierarchyItem item = treeItem.getValue();
 168             assert item != null;
 169             final TreeItem<HierarchyItem> graphicTreeItem = getEmptyGraphicTreeItemFor(treeItem);
 170             if (graphicTreeItem != null && location == DroppingMouseLocation.TOP) {
 171                 treeItem.getChildren().remove(graphicTreeItem);
 172             }
 173         }
 174 
 175         // First update drop target
 176         final DragController dragController
 177                 = panelController.getEditorController().getDragController();
 178         final AbstractDropTarget dropTarget = makeDropTarget(treeItem, location);
 179         dragController.setDropTarget(dropTarget);
 180 
 181         // Then update transfer mode
 182         event.acceptTransferModes(dragController.getAcceptedTransferModes());
 183 
 184         // Schedule adding empty graphic place holder job :
 185         // The drop target must be a GRAPHIC AccessoryDropTarget
 186         if (dragController.isDropAccepted()
 187                 && dropTarget instanceof AccessoryDropTarget
 188                 && ((AccessoryDropTarget) dropTarget).getAccessory() == Accessory.GRAPHIC) {
 189             // Retrieve the GRAPHIC accessory owner
 190             final TreeItem<HierarchyItem> graphicOwnerTreeItem;
 191             if (treeItem != null) {
 192                 if (treeItem.getValue().isEmpty() == false) {
 193                     graphicOwnerTreeItem = treeItem;
 194                 } else {
 195                     // Empty graphic place holder
 196                     // => the graphic owner is the parent
 197                     graphicOwnerTreeItem = treeItem.getParent();
 198                 }
 199             } else {
 200                 // TreeItem is null when dropping below the datas
 201                 // => the graphic owner is the root
 202                 graphicOwnerTreeItem = panelController.getRoot();
 203             }
 204             assert graphicOwnerTreeItem != null;
 205             assert graphicOwnerTreeItem.getValue().isEmpty() == false;
 206             // Schedule adding empty graphic place holder if :
 207             // - an empty graphic place holder has not yet been added
 208             // - an empty graphic place holder has not yet been scheduled
 209             if (getEmptyGraphicTreeItemFor(graphicOwnerTreeItem) == null
 210                     && scheduler.isAddEmptyGraphicTaskScheduled() == false) {
 211                 scheduler.scheduleAddEmptyGraphicTask(graphicOwnerTreeItem);
 212             }
 213         }
 214     }
 215 
 216     /**
 217      * Returns the empty graphic TreeItem (if any) of the specified TreeItem,
 218      * null otherwise.
 219      *
 220      * @param treeItem the TreeItem
 221      * @return the empty graphic TreeItem
 222      */
 223     public TreeItem<HierarchyItem> getEmptyGraphicTreeItemFor(final TreeItem<HierarchyItem> treeItem) {
 224         assert treeItem != null;
 225         for (TreeItem<HierarchyItem> childTreeItem : treeItem.getChildren()) {
 226             final HierarchyItem child = childTreeItem.getValue();
 227             if (child instanceof HierarchyItemGraphic && child.isEmpty()) {
 228                 return childTreeItem;
 229             }
 230         }
 231         return null;
 232     }
 233 
 234     private AbstractDropTarget makeDropTarget(
 235             final TreeItem<HierarchyItem> treeItem,
 236             final DroppingMouseLocation location) {
 237 
 238         assert location != null;
 239 
 240         final TreeItem<HierarchyItem> rootTreeItem = panelController.getRoot();
 241         final FXOMObject dropTargetObject;
 242         final AbstractDropTarget result;
 243         Accessory accessory = null; // Used if we insert as accessory (drop over a place holder)
 244         int targetIndex = -1; // Used if we insert as sub components
 245 
 246         final FXOMDocument document = panelController.getEditorController().getFxomDocument();
 247         if (document == null || document.getFxomRoot() == null) {
 248             return new RootDropTarget();
 249         }
 250         // TreeItem is null when dropping below the datas
 251         // => the drop target is the root
 252         if (treeItem == null) {
 253             dropTargetObject = rootTreeItem.getValue().getFxomObject();
 254 
 255         } else {
 256             final HierarchyItem item = treeItem.getValue();
 257             assert item != null;
 258 
 259             // When the TreeItem is a place holder :
 260             // - if the place holder is empty
 261             //      the drop target is the place holder parent
 262             //      the accessory is set to the place holder value
 263             // whatever the location value is.
 264             // - otherwise
 265             //      the drop target is the place holder item
 266             //      the accessory is set to null
 267             //      the target index is set depending on the location value
 268             //------------------------------------------------------------------
 269             if (item.isPlaceHolder()) { // (1)
 270 
 271                 assert treeItem != rootTreeItem;
 272                 assert item instanceof HierarchyItemBorderPane
 273                         || item instanceof HierarchyItemGraphic
 274                         || item instanceof HierarchyItemDialogPane;
 275 
 276                 if (item.isEmpty()) {
 277                     // Set the drop target
 278                     final TreeItem<HierarchyItem> parentTreeItem = treeItem.getParent();
 279                     assert parentTreeItem != null; // Because of (1)
 280                     dropTargetObject = parentTreeItem.getValue().getFxomObject();
 281                     // Set the accessory
 282                     if (item instanceof HierarchyItemBorderPane) {
 283                         accessory = ((HierarchyItemBorderPane) item).getPosition();
 284                     } else if (item instanceof HierarchyItemDialogPane) {
 285                         accessory = ((HierarchyItemDialogPane) item).getAccessory();
 286                     } else {
 287                         accessory = Accessory.GRAPHIC;
 288                     }
 289                 } else {
 290                     // Set the drop target
 291                     dropTargetObject = item.getFxomObject();
 292                     // Set the accessory
 293                     accessory = null;
 294                     // Set the target index
 295                     switch (location) {
 296                         case CENTER:
 297                         case TOP:
 298                             targetIndex = -1; // Insert at last position
 299                             break;
 300                         case BOTTOM:
 301                             if (treeItem.isLeaf() || !treeItem.isExpanded()) {
 302                                 targetIndex = -1; // Insert at last position
 303                             } else {
 304                                 targetIndex = 0; // Insert at first position
 305                             }
 306                             break;
 307                         default:
 308                             assert false;
 309                             break;
 310 
 311                     }
 312                 }
 313             } //
 314             // TreeItem is not a place holder:
 315             // we set the drop target, accessory and target index
 316             // depending on the mouse location value
 317             //------------------------------------------------------------------
 318             else {
 319                 switch (location) {
 320 
 321                     // REPARENTING
 322                     case CENTER:
 323                         dropTargetObject = item.getFxomObject();
 324                         targetIndex = -1; // Insert at last position
 325                         break;
 326 
 327                     // REORDERING ABOVE
 328                     case TOP:
 329                         // Dropping on TOP of the root TreeItem
 330                         if (treeItem == rootTreeItem) { // (2)
 331                             dropTargetObject = item.getFxomObject();
 332                             targetIndex = -1; // Insert at last position
 333                         } else {
 334                             // If the parent accepts sub components,
 335                             // this is a reordering gesture and the target is the parent
 336                             final DragController dragController
 337                                     = panelController.getEditorController().getDragController();
 338                             final AbstractDragSource dragSource = dragController.getDragSource();
 339                             final TreeItem<HierarchyItem> parentTreeItem = treeItem.getParent();
 340                             assert parentTreeItem != null; // Because of (2)
 341                             final FXOMObject parentObject = parentTreeItem.getValue().getFxomObject();
 342                             final DesignHierarchyMask parentMask = new DesignHierarchyMask(parentObject);
 343                             if (parentMask.isAcceptingSubComponent(dragSource.getDraggedObjects())) {
 344                                 dropTargetObject = parentTreeItem.getValue().getFxomObject();
 345                                 targetIndex = item.getFxomObject().getIndexInParentProperty();
 346                             } // Otherwise, attempt to set an accessory on the current TreeItem
 347                             else {
 348                                 dropTargetObject = item.getFxomObject();
 349                             }
 350                         }
 351                         break;
 352 
 353                     // REORDERING BELOW
 354                     case BOTTOM:
 355                         // Dropping on BOTTOM of the root TreeItem
 356                         if (treeItem == rootTreeItem) { // (3)
 357                             dropTargetObject = item.getFxomObject();
 358                             targetIndex = 0; // Insert at first position
 359                         } else {
 360                             if (treeItem.isLeaf() || !treeItem.isExpanded()) {
 361                                 // If the parent accepts sub components,
 362                                 // this is a reordering gesture and the target is the parent
 363                                 final DragController dragController
 364                                         = panelController.getEditorController().getDragController();
 365                                 final AbstractDragSource dragSource = dragController.getDragSource();
 366                                 final TreeItem<HierarchyItem> parentTreeItem = treeItem.getParent();
 367                                 assert parentTreeItem != null; // Because of (3)
 368                                 final FXOMObject parentObject = parentTreeItem.getValue().getFxomObject();
 369                                 final DesignHierarchyMask parentMask = new DesignHierarchyMask(parentObject);
 370                                 if (parentMask.isAcceptingSubComponent(dragSource.getDraggedObjects())) {
 371                                     dropTargetObject = parentTreeItem.getValue().getFxomObject();
 372                                     targetIndex = item.getFxomObject().getIndexInParentProperty() + 1;
 373                                 } // Otherwise, attempt to set an accessory on the current TreeItem
 374                                 else {
 375                                     dropTargetObject = item.getFxomObject();
 376                                 }
 377                             } else {
 378                                 dropTargetObject = item.getFxomObject();
 379                                 targetIndex = 0; // Insert at first position
 380                             }
 381                         }
 382                         break;
 383                     default:
 384                         assert false;
 385                         dropTargetObject = null;
 386                         break;
 387                 }
 388             }
 389         }
 390 
 391         result = makeDropTarget(dropTargetObject, accessory, targetIndex);
 392         return result;
 393     }
 394 
 395     private AbstractDropTarget makeDropTarget(
 396             final FXOMObject dropTargetObject,
 397             final Accessory accessory,
 398             int targetIndex) {
 399 
 400         AbstractDropTarget result = null;
 401 
 402         if (dropTargetObject instanceof FXOMInstance) {
 403             final DragController dragController
 404                     = panelController.getEditorController().getDragController();
 405             final AbstractDragSource dragSource = dragController.getDragSource();
 406             assert dragSource != null;
 407             final FXOMInstance dropTargetInstance = (FXOMInstance) dropTargetObject;
 408             if (accessory != null) {
 409                 result = new AccessoryDropTarget(dropTargetInstance, accessory);
 410             } else {
 411                 final DesignHierarchyMask dropTargetMask
 412                         = new DesignHierarchyMask(dropTargetInstance);
 413                 // Check if the drop target accepts sub components
 414                 if (dropTargetMask.isAcceptingSubComponent(dragSource.getDraggedObjects())) {
 415                     final FXOMObject beforeChild;
 416                     if (targetIndex == -1) {
 417                         beforeChild = null;
 418                     } else {
 419                         // targetIndex is the last sub component
 420                         if (targetIndex == dropTargetMask.getSubComponentCount()) {
 421                             beforeChild = null;
 422                         } else {
 423                             beforeChild = dropTargetMask.getSubComponentAtIndex(targetIndex);
 424                         }
 425                     }
 426                     result = new ContainerZDropTarget(dropTargetInstance, beforeChild);
 427                 } //
 428                 // Check if the drop target accepts accessories
 429                 else {
 430                     // Check if there is an accessory that can be accepted by the drop target.
 431                     // First we build the list of accessories that can be set by DND gesture.
 432                     final Accessory[] accessories = {
 433                         Accessory.TOP,
 434                         Accessory.LEFT,
 435                         Accessory.CENTER,
 436                         Accessory.RIGHT,
 437                         Accessory.BOTTOM,
 438                         Accessory.CONTENT,
 439                         Accessory.CONTEXT_MENU,
 440                         Accessory.GRAPHIC,
 441                         Accessory.TOOLTIP,
 442                         Accessory.HEADER,
 443                         Accessory.DP_GRAPHIC,
 444                         Accessory.DP_CONTENT,
 445                         Accessory.EXPANDABLE_CONTENT
 446                     };
 447                     for (Accessory a : accessories) {
 448                         final AccessoryDropTarget dropTarget
 449                                 = new AccessoryDropTarget(dropTargetInstance, a);
 450                         // If the accessory drop target accepts the dragged objects,
 451                         // we return this drop target.
 452                         // Otherwise, we look for the next accessory.
 453                         if (dropTarget.acceptDragSource(dragSource)) {
 454                             result = dropTarget;
 455                             break;
 456                         }
 457                     }
 458                 }
 459             }
 460         }
 461         return result;
 462     }
 463 }