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.EditorController;
  35 import com.oracle.javafx.scenebuilder.kit.editor.drag.DragController;
  36 import com.oracle.javafx.scenebuilder.kit.editor.drag.source.DocumentDragSource;
  37 import com.oracle.javafx.scenebuilder.kit.editor.drag.source.ExternalDragSource;
  38 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N;
  39 import static com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.treeview.HierarchyTreeCell.HIERARCHY_FIRST_CELL;
  40 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.AbstractFxmlPanelController;
  41 import com.oracle.javafx.scenebuilder.kit.editor.selection.GridSelectionGroup;
  42 import com.oracle.javafx.scenebuilder.kit.editor.selection.ObjectSelectionGroup;
  43 import com.oracle.javafx.scenebuilder.kit.editor.selection.Selection;
  44 import com.oracle.javafx.scenebuilder.kit.editor.util.ContextMenuController;
  45 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument;
  46 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  47 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask;
  48 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask.Accessory;
  49 
  50 import java.net.URL;
  51 import java.util.ArrayList;
  52 import java.util.HashMap;
  53 import java.util.HashSet;
  54 import java.util.Iterator;
  55 import java.util.List;
  56 import java.util.Map;
  57 import java.util.Set;
  58 
  59 import javafx.beans.property.ObjectProperty;
  60 import javafx.beans.property.SimpleObjectProperty;
  61 import javafx.beans.value.ObservableValue;
  62 import javafx.collections.ListChangeListener;
  63 import javafx.collections.ObservableList;
  64 import javafx.event.EventTarget;
  65 import javafx.geometry.Insets;
  66 import javafx.geometry.Orientation;
  67 import javafx.scene.Node;
  68 import javafx.scene.Parent;
  69 import javafx.scene.control.Cell;
  70 import javafx.scene.control.Control;
  71 import javafx.scene.control.Label;
  72 import javafx.scene.control.ScrollBar;
  73 import javafx.scene.control.Skin;
  74 import javafx.scene.control.SkinBase;
  75 import javafx.scene.control.TreeItem;
  76 import javafx.scene.input.DragEvent;
  77 import javafx.scene.input.Dragboard;
  78 import javafx.scene.input.KeyEvent;
  79 import javafx.scene.input.MouseButton;
  80 import javafx.scene.input.MouseEvent;
  81 import javafx.scene.input.TransferMode;
  82 import javafx.scene.layout.Border;
  83 import javafx.scene.layout.BorderStroke;
  84 import javafx.scene.layout.BorderStrokeStyle;
  85 import javafx.scene.layout.BorderWidths;
  86 import javafx.scene.layout.CornerRadii;
  87 import javafx.scene.layout.Pane;
  88 import javafx.scene.paint.Color;
  89 import javafx.scene.paint.Paint;
  90 import javafx.stage.Window;
  91 
  92 /**
  93  * This class creates and controls the <b>Hierarchy Panel</b> of Scene Builder
  94  * Kit.
  95  *
  96  *
  97  */
  98 public abstract class AbstractHierarchyPanelController extends AbstractFxmlPanelController {
  99 
 100     public enum BorderSide {
 101 
 102         BOTTOM, RIGHT_BOTTOM_LEFT, RIGHT_LEFT, TOP_RIGHT_BOTTOM_LEFT, TOP_RIGHT_LEFT
 103     }
 104 
 105     private final HierarchyDNDController dndController = new HierarchyDNDController(this);
 106     private final HierarchyAnimationScheduler animationScheduler = new HierarchyAnimationScheduler();
 107     private final ObjectProperty<DisplayOption> displayOptionProperty
 108             = new SimpleObjectProperty<>(DisplayOption.INFO);
 109     /**
 110      * @treatAsPrivate
 111      */
 112     protected TreeItem<HierarchyItem> rootTreeItem;
 113     private boolean parentRingEnabled = true;
 114     private Paint parentRingColor;
 115     private final Map<FXOMObject, Boolean> treeItemsExpandedMapProperty = new HashMap<>();
 116     private boolean shouldEndOnExit;
 117     private Label promptLabel;
 118 
 119     // When DND few pixels of the top or bottom of the Hierarchy
 120     // the user can cause it to auto-scroll until the desired target node
 121     private static final double AUTO_SCROLLING_ZONE_HEIGHT = 40.0;
 122 
 123     private Border bottomBorder;
 124     private Border rightBottomLeftBorder;
 125     private Border rightLeftBorder;
 126     private Border topRightBottomLeftBorder;
 127     private Border topRightLeftBorder;
 128     // First cell : top insets = 0 instead of -2
 129     private Border firstCellBottomBorder;
 130     private Border firstCellRightBottomLeftBorder;
 131     private Border firstCellRightLeftBorder;
 132     private Border firstCellTopRightBottomLeftBorder;
 133     private Border firstCellTopRightLeftBorder;
 134 
 135     private final Border transparentBorder;
 136     private final Border firstCellTransparentBorder;
 137 
 138     private final BorderWidths cellBorderWidths = new BorderWidths(2);
 139     private final Insets cellInsets = new Insets(-2, 0, -2, 0);
 140     private final Insets firstCellInsets = new Insets(0, 0, -2, 0);
 141 
 142     private static final Color DEFAULT_PARENT_RING_COLOR = Color.rgb(238, 168, 47);
 143 
 144     /**
 145      * @treatAsPrivate
 146      */
 147     protected final ListChangeListener<TreeItem<HierarchyItem>> treeItemSelectionListener = change -> treeItemSelectionDidChange();
 148 
 149     /**
 150      * Used to define the type of information displayed in the hierarchy.
 151      */
 152     public enum DisplayOption {
 153 
 154         INFO {
 155 
 156                     @Override
 157                     public String toString() {
 158                         return I18N.getString("hierarchy.displayoption.info");
 159                     }
 160                 },
 161         FXID {
 162 
 163                     @Override
 164                     public String toString() {
 165                         return I18N.getString("hierarchy.displayoption.fxid");
 166                     }
 167                 },
 168         NODEID {
 169 
 170                     @Override
 171                     public String toString() {
 172                         return I18N.getString("hierarchy.displayoption.nodeid");
 173                     }
 174                 }
 175     }
 176 
 177     /*
 178      * Public
 179      */
 180     public AbstractHierarchyPanelController(URL fxmlURL, EditorController editorController) {
 181         super(fxmlURL, I18N.getBundle(), editorController);
 182 
 183         final BorderStroke bs = new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.SOLID,
 184                 CornerRadii.EMPTY, cellBorderWidths, cellInsets);
 185         transparentBorder = new Border(bs);
 186         final BorderStroke firstbs = new BorderStroke(Color.TRANSPARENT, BorderStrokeStyle.SOLID,
 187                 CornerRadii.EMPTY, cellBorderWidths, firstCellInsets);
 188         firstCellTransparentBorder = new Border(firstbs);
 189     }
 190 
 191     private Label getPromptLabel() {
 192         if (promptLabel == null) {
 193             promptLabel = new Label();
 194             promptLabel.getStyleClass().add("hierarchy-prompt-label");
 195             promptLabel.setMouseTransparent(true);
 196         }
 197         return promptLabel;
 198     }
 199 
 200     /**
 201      * Returns the root TreeItem.
 202      *
 203      * @return the root TreeItem.
 204      */
 205     public final TreeItem<HierarchyItem> getRoot() {
 206         return rootTreeItem;
 207     }
 208 
 209     /**
 210      * Returns the display option property.
 211      *
 212      * @return the display option property.
 213      */
 214     public ObservableValue<DisplayOption> displayOptionProperty() {
 215         return displayOptionProperty;
 216     }
 217 
 218     /**
 219      * Returns the type of information displayed in the hierarchy.
 220      *
 221      * @return the type of information displayed in the hierarchy.
 222      */
 223     public final DisplayOption getDisplayOption() {
 224         return displayOptionProperty.getValue();
 225     }
 226 
 227     /**
 228      * Sets the type of information displayed in the hierarchy.
 229      *
 230      * @param displayOption the type of information displayed in the hierarchy.
 231      */
 232     public final void setDisplayOption(DisplayOption displayOption) {
 233         displayOptionProperty.setValue(displayOption);
 234     }
 235 
 236     /**
 237      * @return the DND controller
 238      * @treatAsPrivate
 239      */
 240     public final HierarchyDNDController getDNDController() {
 241         return dndController;
 242     }
 243 
 244     /**
 245      * @return true if the timeline is running
 246      * @treatAsPrivate
 247      */
 248     public final boolean isTimelineRunning() {
 249         return animationScheduler.isTimelineRunning();
 250     }
 251 
 252     /**
 253      * @return true if the parent ring is enabled
 254      * @treatAsPrivate
 255      */
 256     public boolean isParentRingEnabled() {
 257         return parentRingEnabled;
 258     }
 259 
 260     /**
 261      * @param enabled the enabled value
 262      * @treatAsPrivate
 263      */
 264     public void setParentRingEnabled(boolean enabled) {
 265         parentRingEnabled = enabled;
 266     }
 267 
 268     public Paint getParentRingColor() {
 269         return parentRingColor;
 270     }
 271 
 272     public void setParentRingColor(Paint value) {
 273         parentRingColor = value;
 274         updateParentRingColor();
 275     }
 276 
 277     public void setBorder(Cell<?> cell, BorderSide side) {
 278         assert cell != null;
 279         boolean isFirstCell = cell.getStyleClass() != null
 280                 && cell.getStyleClass().contains(HIERARCHY_FIRST_CELL);
 281         final Border border;
 282         switch (side) {
 283             case BOTTOM:
 284                 border = isFirstCell ? firstCellBottomBorder : bottomBorder;
 285                 break;
 286             case RIGHT_BOTTOM_LEFT:
 287                 border = isFirstCell ? firstCellRightBottomLeftBorder : rightBottomLeftBorder;
 288                 break;
 289             case RIGHT_LEFT:
 290                 border = isFirstCell ? firstCellRightLeftBorder : rightLeftBorder;
 291                 break;
 292             case TOP_RIGHT_BOTTOM_LEFT:
 293                 border = isFirstCell ? firstCellTopRightBottomLeftBorder : topRightBottomLeftBorder;
 294                 break;
 295             case TOP_RIGHT_LEFT:
 296                 border = isFirstCell ? firstCellTopRightLeftBorder : topRightLeftBorder;
 297                 break;
 298             default:
 299                 border = null;
 300                 assert false;
 301                 break;
 302         }
 303         assert border != null;
 304         cell.setBorder(border);
 305     }
 306 
 307     private void setTransparentBorder(Cell<?> cell) {
 308         assert cell != null;
 309         boolean isFirstCell = cell.getStyleClass() != null
 310                 && cell.getStyleClass().contains(HIERARCHY_FIRST_CELL);
 311         final Border border = isFirstCell ? firstCellTransparentBorder : transparentBorder;
 312         cell.setBorder(border);
 313     }
 314 
 315     /**
 316      * Returns the control used to represent the hierarchy. Either a TreeView or
 317      * a TreeTableView.
 318      *
 319      * @return the control used to represent the hierarchy.
 320      */
 321     public abstract Control getPanelControl();
 322 
 323     /**
 324      * Returns the selected TreeItems.
 325      *
 326      * @return the selected TreeItems.
 327      */
 328     public abstract ObservableList<TreeItem<HierarchyItem>> getSelectedItems();
 329 
 330     /**
 331      * Returns the panel control scrollbar for the specified orientation.
 332      *
 333      * @param orientation the scrollbar orientation
 334      * @return the panel control scrollbar for the specified orientation
 335      * @treatAsPrivate
 336      */
 337     public ScrollBar getScrollBar(final Orientation orientation) {
 338         final Control panelControl = getPanelControl();
 339         final Set<Node> scrollBars = panelControl.lookupAll(".scroll-bar"); //NOI18N
 340         for (Node node : scrollBars) {
 341             if (node instanceof ScrollBar) {
 342                 final ScrollBar scrollBar = (ScrollBar) node;
 343                 if (scrollBar.getOrientation() == orientation) {
 344                     return scrollBar;
 345                 }
 346             }
 347         }
 348         return null;
 349     }
 350 
 351     /**
 352      * @treatAsPrivate
 353      */
 354     protected abstract void startEditingDisplayInfo();
 355 
 356     /**
 357      * @treatAsPrivate
 358      */
 359     protected abstract void updatePanel();
 360 
 361     /**
 362      * @treatAsPrivate
 363      */
 364     protected abstract void clearSelection();
 365 
 366     /**
 367      * @param treeItem the TreeItem
 368      * @treatAsPrivate
 369      */
 370     protected abstract void select(final TreeItem<HierarchyItem> treeItem);
 371 
 372     /**
 373      * @param treeItems the TreeItems
 374      * @treatAsPrivate
 375      */
 376     protected void select(final List<TreeItem<HierarchyItem>> treeItems) {
 377         for (TreeItem<HierarchyItem> treeItem : treeItems) {
 378             select(treeItem);
 379         }
 380     }
 381 
 382     /**
 383      * @param treeItem the TreeItem
 384      * @treatAsPrivate
 385      */
 386     public abstract void scrollTo(final TreeItem<HierarchyItem> treeItem);
 387 
 388     /**
 389      * @param treeItem the TreeItem
 390      * @return true if visible
 391      * @treatAsPrivate
 392      */
 393     protected boolean isVisible(final TreeItem<HierarchyItem> treeItem) {
 394         final Cell<?> cell = getCell(treeItem);
 395         return (cell == null ? false : cell.isVisible());
 396     }
 397 
 398     /**
 399      * @param treeItem the TreeItem
 400      * @return the cell corresponding to the specified TreeItem
 401      * @treatAsPrivate
 402      */
 403     public abstract Cell<?> getCell(final TreeItem<?> treeItem);
 404 
 405     /**
 406      * Returns the Y coordinate of the panel content TOP. Used to define the
 407      * zone for auto scrolling.
 408      *
 409      * @return the Y coordinate of the panel content TOP
 410      * @treatAsPrivate
 411      */
 412     public abstract double getContentTopY();
 413 
 414     /**
 415      * Returns the Y coordinate of the panel content BOTTOM. Used to define the
 416      * zone for auto scrolling.
 417      *
 418      * @return the Y coordinate of the panel content BOTTOM
 419      * @treatAsPrivate
 420      */
 421     public abstract double getContentBottomY();
 422 
 423     /**
 424      * @treatAsPrivate
 425      */
 426     public abstract void updateParentRing();
 427 
 428     /**
 429      * @treatAsPrivate
 430      */
 431     public abstract void updatePlaceHolder();
 432 
 433     /**
 434      * @treatAsPrivate
 435      */
 436     public abstract void clearBorderColor();
 437 
 438     /**
 439      * @param cell the cell
 440      * @treatAsPrivate
 441      */
 442     public void clearBorderColor(final Cell<?> cell) {
 443         assert cell != null;
 444         setTransparentBorder(cell);
 445     }
 446 
 447     /**
 448      * @param node the node
 449      * @treatAsPrivate
 450      */
 451     public void addToPanelControlSkin(final Node node) {
 452         final Skin<?> skin = getPanelControl().getSkin();
 453         assert skin instanceof SkinBase;
 454         final SkinBase<?> skinbase = (SkinBase<?>) skin;
 455         skinbase.getChildren().add(node);
 456     }
 457 
 458     /**
 459      * @param node the node
 460      * @treatAsPrivate
 461      */
 462     public void removeFromPanelControlSkin(final Node node) {
 463         final Skin<?> skin = getPanelControl().getSkin();
 464         assert skin instanceof SkinBase;
 465         final SkinBase<?> skinbase = (SkinBase<?>) skin;
 466         skinbase.getChildren().remove(node);
 467     }
 468 
 469     /**
 470      * @treatAsPrivate
 471      */
 472     protected abstract void startListeningToTreeItemSelection();
 473 
 474     /**
 475      * @treatAsPrivate
 476      */
 477     protected abstract void stopListeningToTreeItemSelection();
 478 
 479     /*
 480      * AbstractPanelController
 481      * @treatAsPrivate
 482      */
 483     @Override
 484     protected void fxomDocumentDidChange(FXOMDocument oldDocument) {
 485         // Clear the map containing the TreeItems expanded property values
 486         treeItemsExpandedMapProperty.clear();
 487         updatePanel();
 488     }
 489 
 490     /**
 491      * @treatAsPrivate
 492      */
 493     @Override
 494     protected void sceneGraphRevisionDidChange() {
 495         if (getPanelControl() != null) {
 496             // Update the map containing the TreeItems expanded property values
 497             // This map will be used after rebuilding the tree,
 498             // in order to update the TreeItems expanded property to their previous value
 499             if (rootTreeItem != null) { // Root TreeItem may be null
 500                 updateTreeItemsExpandedMap(rootTreeItem);
 501             }
 502             // FXOM document has rebuilt the scene graph. Tree items must all
 503             // be updated because:
 504             //  - classes of scene graph objects may have mutated
 505             //  - infos displayed in the tree items may be obsoletes
 506             updatePanel();
 507             editorSelectionDidChange();
 508         }
 509     }
 510 
 511     /**
 512      * @treatAsPrivate
 513      */
 514     @Override
 515     protected void cssRevisionDidChange() {
 516         sceneGraphRevisionDidChange();
 517     }
 518 
 519     private void updateTreeItemsExpandedMap(TreeItem<HierarchyItem> treeItem) {
 520         assert treeItem != null;
 521         final HierarchyItem item = treeItem.getValue();
 522         if (!item.isEmpty()) {
 523             final FXOMObject fxomObject = item.getFxomObject();
 524             assert fxomObject != null;
 525             treeItemsExpandedMapProperty.put(fxomObject, treeItem.isExpanded());
 526             // Inspect TreeItem chidren
 527             for (TreeItem<HierarchyItem> treeItemChild : treeItem.getChildren()) {
 528                 updateTreeItemsExpandedMap(treeItemChild);
 529             }
 530         }
 531     }
 532 
 533     /**
 534      * @treatAsPrivate
 535      */
 536     @Override
 537     protected void jobManagerRevisionDidChange() {
 538         // FXOMDocument has been modified by a job.
 539         // Tree items must all be updated.
 540         sceneGraphRevisionDidChange();
 541     }
 542 
 543     /**
 544      * @treatAsPrivate
 545      */
 546     @Override
 547     protected void controllerDidLoadFxml() {
 548         assert getPanelControl() != null;
 549 
 550         // Initialize and configure the hierarchy panel
 551         initializePanel();
 552 
 553         // Add listener on the selection
 554         // Used to update global selection + update parent ring
 555         startListeningToTreeItemSelection();
 556 
 557         // Populate panel
 558         updatePanel();
 559     }
 560 
 561     /**
 562      * @treatAsPrivate
 563      */
 564     @Override
 565     protected void editorSelectionDidChange() {
 566         final Selection selection = getEditorController().getSelection();
 567         final List<FXOMObject> selectedFxomObjects = new ArrayList<>();
 568 
 569         if (getPanelControl() != null) {
 570             if (selection.getGroup() instanceof ObjectSelectionGroup) {
 571                 final ObjectSelectionGroup osg = (ObjectSelectionGroup) selection.getGroup();
 572                 selectedFxomObjects.addAll(osg.getItems());
 573             } else if (selection.getGroup() instanceof GridSelectionGroup) {
 574                 final GridSelectionGroup gsg = (GridSelectionGroup) selection.getGroup();
 575                 selectedFxomObjects.add(gsg.getParentObject());
 576             }
 577 
 578             // Update selected items
 579             stopListeningToTreeItemSelection();
 580             clearSelection();
 581             // Root TreeItem may be null
 582             if (getRoot() != null && selectedFxomObjects.isEmpty() == false) {
 583                 final List<TreeItem<HierarchyItem>> selectedTreeItems
 584                         = lookupTreeItem(selectedFxomObjects);
 585                 if (selectedTreeItems.isEmpty() == false) {
 586                     select(selectedTreeItems);
 587                     // Scroll to the last TreeItem
 588                     final TreeItem<HierarchyItem> lastTreeItem
 589                             = selectedTreeItems.get(selectedTreeItems.size() - 1);
 590                     // Call scrollTo only if the item is not visible.
 591                     // This avoid unexpected scrolling to occur in the hierarchy
 592                     // TreeView / TreeTableView while changing some property in the inspector.
 593                     if (isVisible(lastTreeItem) == false) {
 594                         scrollTo(lastTreeItem);
 595                     }
 596                 }
 597             }
 598             startListeningToTreeItemSelection();
 599 
 600             // Update parent ring when selection did change
 601             updateParentRing();
 602         }
 603     }
 604 
 605     private void treeItemSelectionDidChange() {
 606 
 607         /*
 608          * Before updating the selection, we test if a text session is on-going
 609          * and can be completed cleanly. If not, we do not update the selection.
 610          */
 611         if (getEditorController().canGetFxmlText()) {
 612             final Set<FXOMObject> selectedFxomObjects = new HashSet<>();
 613             for (TreeItem<HierarchyItem> selectedItem : getSelectedItems()) {
 614                 // TreeItems may be null when selection is updating
 615                 if (selectedItem != null) {
 616                     final FXOMObject fxomObject = selectedItem.getValue().getFxomObject();
 617                     // Placeholders may have a null fxom object
 618                     if (fxomObject != null) {
 619                         selectedFxomObjects.add(fxomObject);
 620                     }
 621                 }
 622             }
 623 
 624             // Update selection
 625             stopListeningToEditorSelection();
 626             getEditorController().getSelection().select(selectedFxomObjects);
 627             startListeningToEditorSelection();
 628 
 629             // Update parent ring when selection did change
 630             updateParentRing();
 631         } /*
 632          * If a text session is on-going and cannot be completed cleanly,
 633          * we go back to previous TreeItem selection.
 634          */ else {
 635             editorSelectionDidChange();
 636         }
 637     }
 638 
 639     private TreeItem<HierarchyItem> makeTreeItem(final FXOMObject fxomObject) {
 640         final HierarchyItem item = new HierarchyItem(fxomObject);
 641         final TreeItem<HierarchyItem> treeItem = new TreeItem<>(item);
 642         // Set back the TreeItem expanded property if any
 643         Boolean expanded = treeItemsExpandedMapProperty.get(fxomObject);
 644         if (expanded != null) {
 645             treeItem.setExpanded(expanded);
 646         }
 647         updateTreeItem(treeItem);
 648         return treeItem;
 649     }
 650 
 651     private TreeItem<HierarchyItem> makeTreeItemBorderPane(
 652             final DesignHierarchyMask owner,
 653             final FXOMObject fxomObject,
 654             final Accessory accessory) {
 655         final HierarchyItemBorderPane item
 656                 = new HierarchyItemBorderPane(owner, fxomObject, accessory);
 657         final TreeItem<HierarchyItem> treeItem = new TreeItem<>(item);
 658         // Set back the TreeItem expanded property if any
 659         Boolean expanded = treeItemsExpandedMapProperty.get(fxomObject);
 660         if (expanded != null) {
 661             treeItem.setExpanded(expanded);
 662         }
 663         // Mask may be null for empty place holder
 664         if (item.getMask() != null) {
 665             updateTreeItem(treeItem);
 666         }
 667         return treeItem;
 668     }
 669 
 670     private TreeItem<HierarchyItem> makeTreeItemDialogPane(
 671             final DesignHierarchyMask owner,
 672             final FXOMObject fxomObject,
 673             final Accessory accessory) {
 674         final HierarchyItemDialogPane item
 675                 = new HierarchyItemDialogPane(owner, fxomObject, accessory);
 676         final TreeItem<HierarchyItem> treeItem = new TreeItem<>(item);
 677         // Set back the TreeItem expanded property if any
 678         Boolean expanded = treeItemsExpandedMapProperty.get(fxomObject);
 679         if (expanded != null) {
 680             treeItem.setExpanded(expanded);
 681         }
 682         // Mask may be null for empty place holder
 683         if (item.getMask() != null) {
 684             updateTreeItem(treeItem);
 685         }
 686         return treeItem;
 687     }
 688 
 689     /**
 690      * @param owner the mask owner
 691      * @param fxomObject the FXOMObject
 692      * @return the new TreeItem
 693      * @treatAsPrivate
 694      */
 695     protected TreeItem<HierarchyItem> makeTreeItemGraphic(
 696             final DesignHierarchyMask owner,
 697             final FXOMObject fxomObject) {
 698         final HierarchyItemGraphic item
 699                 = new HierarchyItemGraphic(owner, fxomObject);
 700         final TreeItem<HierarchyItem> treeItem = new TreeItem<>(item);
 701         Boolean expanded = treeItemsExpandedMapProperty.get(fxomObject);
 702         if (expanded != null) {
 703             treeItem.setExpanded(expanded);
 704         }
 705         // Mask may be null for empty place holder
 706         if (item.getMask() != null) {
 707             updateTreeItem(treeItem);
 708         }
 709         return treeItem;
 710     }
 711 
 712     protected void updateTreeItems() {
 713         assert getPanelControl() != null;
 714         final Parent parent = getPanelControl().getParent();
 715         assert parent instanceof Pane;
 716         final Pane pane = (Pane) parent;
 717         final FXOMDocument fxomDocument = getEditorController().getFxomDocument();
 718 
 719         final Label label = getPromptLabel();
 720         if (fxomDocument == null || fxomDocument.getFxomRoot() == null) {
 721             rootTreeItem = null;
 722             // Add place holder to the parent
 723             if (fxomDocument == null) {
 724                 label.setText(I18N.getString("contant.label.status.fxomdocument.null"));
 725             } else {
 726                 label.setText(I18N.getString("content.label.status.invitation"));
 727             }
 728             if (pane.getChildren().contains(label) == false) {
 729                 // This may occur when closing en empty document
 730                 // => we switch from null FXOM root to null FXOM document
 731                 pane.getChildren().add(label);
 732             }
 733         } else {
 734             rootTreeItem = makeTreeItem(fxomDocument.getFxomRoot());
 735             rootTreeItem.setExpanded(true);
 736             // Remove place holder from the parent
 737             ((Pane) parent).getChildren().remove(label);
 738         }
 739     }
 740 
 741     protected void updateParentRingColor() {
 742         // Update border items used to build the hierarchy parent ring
 743         BorderStroke bs;
 744         // bottom border
 745         bs = new BorderStroke(Color.TRANSPARENT, Color.TRANSPARENT, parentRingColor, Color.TRANSPARENT,
 746                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 747                 CornerRadii.EMPTY, cellBorderWidths, cellInsets);
 748         bottomBorder = new Border(bs);
 749         bs = new BorderStroke(Color.TRANSPARENT, Color.TRANSPARENT, parentRingColor, Color.TRANSPARENT,
 750                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 751                 CornerRadii.EMPTY, cellBorderWidths, firstCellInsets);
 752         firstCellBottomBorder = new Border(bs);
 753         // right bottom and left border
 754         bs = new BorderStroke(Color.TRANSPARENT, parentRingColor, parentRingColor, parentRingColor,
 755                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 756                 CornerRadii.EMPTY, cellBorderWidths, cellInsets);
 757         rightBottomLeftBorder = new Border(bs);
 758         bs = new BorderStroke(Color.TRANSPARENT, parentRingColor, parentRingColor, parentRingColor,
 759                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 760                 CornerRadii.EMPTY, cellBorderWidths, firstCellInsets);
 761         firstCellRightBottomLeftBorder = new Border(bs);
 762         // right and left border
 763         bs = new BorderStroke(Color.TRANSPARENT, parentRingColor, Color.TRANSPARENT, parentRingColor,
 764                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 765                 CornerRadii.EMPTY, cellBorderWidths, cellInsets);
 766         rightLeftBorder = new Border(bs);
 767         bs = new BorderStroke(Color.TRANSPARENT, parentRingColor, Color.TRANSPARENT, parentRingColor,
 768                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 769                 CornerRadii.EMPTY, cellBorderWidths, firstCellInsets);
 770         firstCellRightLeftBorder = new Border(bs);
 771         // top right bottom and left border
 772         bs = new BorderStroke(parentRingColor, BorderStrokeStyle.SOLID,
 773                 CornerRadii.EMPTY, cellBorderWidths, cellInsets);
 774         topRightBottomLeftBorder = new Border(bs);
 775         bs = new BorderStroke(parentRingColor, BorderStrokeStyle.SOLID,
 776                 CornerRadii.EMPTY, cellBorderWidths, firstCellInsets);
 777         firstCellTopRightBottomLeftBorder = new Border(bs);
 778         // top right and left border
 779         bs = new BorderStroke(parentRingColor, parentRingColor, Color.TRANSPARENT, parentRingColor,
 780                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 781                 CornerRadii.EMPTY, cellBorderWidths, cellInsets);
 782         topRightLeftBorder = new Border(bs);
 783         bs = new BorderStroke(parentRingColor, parentRingColor, Color.TRANSPARENT, parentRingColor,
 784                 BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID, BorderStrokeStyle.SOLID,
 785                 CornerRadii.EMPTY, cellBorderWidths, firstCellInsets);
 786         firstCellTopRightLeftBorder = new Border(bs);
 787 
 788         updateParentRing();
 789         updatePlaceHolder();
 790     }
 791 
 792     private void updateTreeItem(final TreeItem<HierarchyItem> treeItem) {
 793 
 794         final DesignHierarchyMask mask = treeItem.getValue().getMask();
 795         assert mask != null;
 796 
 797         // Graphic (displayed at first position)
 798         //---------------------------------
 799         if (mask.isAcceptingAccessory(Accessory.GRAPHIC)) {
 800             final FXOMObject value = mask.getAccessory(Accessory.GRAPHIC);
 801             if (value != null) {
 802                 treeItem.getChildren().add(makeTreeItemGraphic(mask, value));
 803             }
 804         }
 805 
 806         // Tooltip (displayed at second position)
 807         //---------------------------------
 808         if (mask.isAcceptingAccessory(Accessory.TOOLTIP)) {
 809             final FXOMObject value = mask.getAccessory(Accessory.TOOLTIP);
 810             if (value != null) {
 811                 treeItem.getChildren().add(makeTreeItem(value));
 812             }
 813         }
 814 
 815         // Context menu (displayed at third position)
 816         //---------------------------------
 817         if (mask.isAcceptingAccessory(Accessory.CONTEXT_MENU)) {
 818             final FXOMObject value = mask.getAccessory(Accessory.CONTEXT_MENU);
 819             if (value != null) {
 820                 treeItem.getChildren().add(makeTreeItem(value));
 821             }
 822         }
 823 
 824         // Axis (chart)
 825         //---------------------------------
 826         if (mask.isAcceptingAccessory(Accessory.XAXIS)) {
 827             final FXOMObject value = mask.getAccessory(Accessory.XAXIS);
 828             if (value != null) {
 829                 treeItem.getChildren().add(makeTreeItem(value));
 830             }
 831         }
 832         if (mask.isAcceptingAccessory(Accessory.YAXIS)) {
 833             final FXOMObject value = mask.getAccessory(Accessory.YAXIS);
 834             if (value != null) {
 835                 treeItem.getChildren().add(makeTreeItem(value));
 836             }
 837         }
 838 
 839         // Content (ScrollPane, Tab...)
 840         //---------------------------------
 841         if (mask.isAcceptingAccessory(Accessory.CONTENT)) {
 842             final FXOMObject value = mask.getAccessory(Accessory.CONTENT);
 843             if (value != null) {
 844                 treeItem.getChildren().add(makeTreeItem(value));
 845             }
 846         }
 847 
 848         // Positionning
 849         //---------------------------------
 850         for (Accessory accessory : new Accessory[]{
 851             Accessory.TOP,
 852             Accessory.LEFT,
 853             Accessory.CENTER,
 854             Accessory.RIGHT,
 855             Accessory.BOTTOM}) {
 856             if (mask.isAcceptingAccessory(accessory)) {
 857                 final FXOMObject value = mask.getAccessory(accessory);
 858                 treeItem.getChildren().add(makeTreeItemBorderPane(mask, value, accessory));
 859             }
 860         }
 861 
 862         // DialogPane
 863         //---------------------------------
 864         for (Accessory accessory : new Accessory[]{
 865             Accessory.HEADER,
 866             Accessory.DP_GRAPHIC,
 867             Accessory.DP_CONTENT,
 868             Accessory.EXPANDABLE_CONTENT}) {
 869             if (mask.isAcceptingAccessory(accessory)) {
 870                 final FXOMObject value = mask.getAccessory(accessory);
 871                 treeItem.getChildren().add(makeTreeItemDialogPane(mask, value, accessory));
 872             }
 873         }
 874 
 875         // Sub components
 876         //---------------------------------
 877         if (mask.isAcceptingSubComponent()) {
 878             for (int i = 0, count = mask.getSubComponentCount(); i < count; i++) {
 879                 final FXOMObject value = mask.getSubComponentAtIndex(i);
 880                 treeItem.getChildren().add(makeTreeItem(value));
 881             }
 882         }
 883     }
 884 
 885     private List<TreeItem<HierarchyItem>> lookupTreeItem(List<FXOMObject> fxomObjects) {
 886         final List<TreeItem<HierarchyItem>> result = new ArrayList<>();
 887         for (FXOMObject fxomObject : fxomObjects) {
 888             final TreeItem<HierarchyItem> treeItem = lookupTreeItem(fxomObject);
 889             // TreeItem may be null when selecting a GridPane column/row
 890             // constraint in content panel
 891             if (treeItem != null) {
 892                 result.add(treeItem);
 893             }
 894         }
 895         return result;
 896     }
 897 
 898     /**
 899      * @param fxomObject the FXOMObject
 900      * @return the TreeItem corresponding to the specified FXOMObject
 901      * @treatAsPrivate
 902      */
 903     public TreeItem<HierarchyItem> lookupTreeItem(FXOMObject fxomObject) {
 904         return lookupTreeItem(fxomObject, getRoot());
 905     }
 906 
 907     private TreeItem<HierarchyItem> lookupTreeItem(FXOMObject fxomObject, TreeItem<HierarchyItem> fromTreeItem) {
 908         TreeItem<HierarchyItem> result;
 909         assert fxomObject != null;
 910 
 911         // ROOT TreeItem may be null when no document is loaded
 912         if (fromTreeItem != null) {
 913             assert fromTreeItem.getValue() != null;
 914             if (fromTreeItem.getValue().getFxomObject() == fxomObject) {
 915                 result = fromTreeItem;
 916             } else {
 917                 Iterator<TreeItem<HierarchyItem>> it = fromTreeItem.getChildren().iterator();
 918                 result = null;
 919                 while ((result == null) && it.hasNext()) {
 920                     TreeItem<HierarchyItem> childItem = it.next();
 921                     result = lookupTreeItem(fxomObject, childItem);
 922                 }
 923             }
 924         } else {
 925             result = null;
 926         }
 927         return result;
 928     }
 929 
 930     /**
 931      * Returns the list of all descendant from the specified parent TreeItem.
 932      * The specified parent TreeItem is excluded from the returned list.
 933      *
 934      * @param <T> type
 935      * @param parentTreeItem the parent TreeItem
 936      * @return the list of all descendant
 937      */
 938     private <T> List<TreeItem<T>> getAllTreeItems(final TreeItem<T> parentTreeItem) {
 939         assert parentTreeItem != null;
 940         final List<TreeItem<T>> treeItems = new ArrayList<>();
 941         for (TreeItem<T> child : parentTreeItem.getChildren()) {
 942             treeItems.add(child);
 943             treeItems.addAll(getAllTreeItems(child));
 944         }
 945         return treeItems;
 946     }
 947 
 948     /**
 949      * Returns the last visible TreeItem descendant of the specified parent
 950      * TreeItem.
 951      *
 952      * @param <T> type
 953      * @param parentTreeItem the parent TreeItem
 954      * @return the last visible TreeItem
 955      * @treatAsPrivate
 956      */
 957     public <T> TreeItem<T> getLastVisibleTreeItem(final TreeItem<T> parentTreeItem) {
 958         assert parentTreeItem != null;
 959         TreeItem<T> result = parentTreeItem;
 960         int size = result.getChildren().size();
 961         while (size != 0) {
 962             if (result.isExpanded()) {
 963                 result = result.getChildren().get(size - 1);
 964                 size = result.getChildren().size();
 965             } else {
 966                 size = 0;
 967             }
 968         }
 969         return result;
 970     }
 971 
 972     /**
 973      * Returns the next visible TreeItem of the specified TreeItem.
 974      *
 975      * @param <T> type
 976      * @param treeItem the TreeItem
 977      * @return the next visible TreeItem
 978      * @treatAsPrivate
 979      */
 980     public <T> TreeItem<T> getNextVisibleTreeItem(final TreeItem<T> treeItem) {
 981         assert treeItem != null;
 982         if (treeItem == getRoot()) {
 983             // Root TreeItem has no next TreeItem
 984             return null;
 985         } else if (treeItem.isExpanded() && !treeItem.getChildren().isEmpty()) {
 986             // Return first child
 987             return treeItem.getChildren().get(0);
 988         } else {
 989             TreeItem<T> parentTreeItem = treeItem.getParent();
 990             TreeItem<T> result = treeItem.nextSibling();
 991             while (result == null && parentTreeItem != getRoot()) {
 992                 result = parentTreeItem.nextSibling();
 993                 parentTreeItem = parentTreeItem.getParent();
 994             }
 995             return result;
 996         }
 997     }
 998 
 999     /**
1000      * Returns the previous visible TreeItem of the specified TreeItem.
1001      *
1002      * @param <T> type
1003      * @param treeItem the TreeItem
1004      * @return the previous visible TreeItem
1005      * @treatAsPrivate
1006      */
1007     public <T> TreeItem<T> getPreviousVisibleTreeItem(final TreeItem<T> treeItem) {
1008         assert treeItem != null;
1009         if (treeItem == getRoot()) {
1010             // Root TreeItem has no previous TreeItem
1011             return null;
1012         } else {
1013             TreeItem<T> parentTreeItem = treeItem.getParent();
1014             TreeItem<T> result = treeItem.previousSibling();
1015             while (result == null && parentTreeItem != getRoot()) {
1016                 result = parentTreeItem.previousSibling();
1017                 parentTreeItem = parentTreeItem.getParent();
1018             }
1019             return result;
1020         }
1021     }
1022 
1023     /**
1024      * @treatAsPrivate
1025      */
1026     protected void initializePanel() {
1027         // Panel may be either a TreeView or a TreeTableView
1028         assert getPanelControl() != null;
1029 
1030         // Drag events
1031         //----------------------------------------------------------------------
1032         // DRAG_DONE event received when drag gesture
1033         // started from the hierarchy panel ends
1034         getPanelControl().setOnDragDone(event -> handleOnDragDone(event));
1035         getPanelControl().setOnDragDropped(event -> handleOnDragDropped(event));
1036         getPanelControl().setOnDragEntered(event -> handleOnDragEntered(event));
1037         getPanelControl().setOnDragExited(event -> handleOnDragExited(event));
1038         getPanelControl().setOnDragOver(event -> handleOnDragOver(event));
1039 
1040         // Key events
1041         //----------------------------------------------------------------------
1042         getPanelControl().setOnKeyPressed(event -> handleOnKeyPressed(event));
1043 
1044         // Mouse events
1045         //----------------------------------------------------------------------
1046         // DRAG_DETECTED event received when drag gesture
1047         // starts from the hierarchy panel
1048         getPanelControl().setOnDragDetected(event -> handleOnDragDetected(event));
1049         getPanelControl().setOnMousePressed(event -> handleOnMousePressed(event));
1050 
1051         // Setup the context menu
1052         final ContextMenuController contextMenuController
1053                 = getEditorController().getContextMenuController();
1054         getPanelControl().setContextMenu(contextMenuController.getContextMenu());
1055 
1056         // Set default parent ring color
1057         setParentRingColor(DEFAULT_PARENT_RING_COLOR);
1058     }
1059 
1060     private void handleOnDragDetected(final MouseEvent event) {
1061         final ObservableList<TreeItem<HierarchyItem>> selectedTreeItems = getSelectedItems();
1062 
1063         // Do not start a DND gesture if there is an editing session on-going
1064         if (!getEditorController().canGetFxmlText()) {
1065             return;
1066         }
1067 
1068         final Selection selection = getEditorController().getSelection();
1069         if (selection.isEmpty() == false) { // (1)
1070             if (selection.getGroup() instanceof ObjectSelectionGroup) {
1071                 // A set of regular component (ie fxom objects) are selected
1072                 final ObjectSelectionGroup osg = (ObjectSelectionGroup) selection.getGroup();
1073 
1074                 // Abort dragging an empty place holder
1075                 for (TreeItem<HierarchyItem> selectedTreeItem : selectedTreeItems) {
1076                     final HierarchyItem item = selectedTreeItem.getValue();
1077                     if (item.isEmpty()) {
1078                         return;
1079                     }
1080                 }
1081                 // Retrieve the hit object
1082                 final Cell<?> cell = lookupCell(event.getTarget());
1083                 final Object item = cell.getItem();
1084                 assert item instanceof HierarchyItem;
1085                 final HierarchyItem hierarchyItem = (HierarchyItem) item;
1086                 final FXOMObject hitObject = hierarchyItem.getFxomObject();
1087                 assert (hitObject != null); // Because we cannot drag placeholders
1088                 // Build drag source
1089                 final Window ownerWindow = getPanelRoot().getScene().getWindow();
1090                 final DocumentDragSource dragSource = new DocumentDragSource(
1091                         osg.getSortedItems(), hitObject, ownerWindow);
1092                 if (dragSource.isAcceptable()) {
1093                     // Start drag and drop
1094                     final Dragboard db = getPanelControl().startDragAndDrop(TransferMode.COPY_OR_MOVE);
1095                     db.setContent(dragSource.makeClipboardContent());
1096                     db.setDragView(dragSource.makeDragView());
1097                     // DragController.begin
1098                     assert getEditorController().getDragController().getDragSource() == null;
1099                     getEditorController().getDragController().begin(dragSource);
1100                 }
1101 
1102             } else {
1103                 // Emergency code : a new type of AbstractSelectionGroup
1104                 // exists but is not managed by this code yet.
1105                 assert false : "Add implementation for " + selection.getGroup().getClass();
1106             }
1107         }
1108     }
1109 
1110     private void handleOnDragDone(final DragEvent event) {
1111         // DragController update
1112         final DragController dragController
1113                 = getEditorController().getDragController();
1114         assert shouldEndOnExit == false;
1115         dragController.end();
1116         event.getDragboard().clear();
1117     }
1118 
1119     private void handleOnDragDropped(final DragEvent event) {
1120         // If there is no document loaded
1121         // Should we allow to start with empty document in SB 2.0 ?
1122         if (getEditorController().getFxomDocument() == null) {
1123             return;
1124         }
1125 
1126         // DragController update
1127         final DragController dragController
1128                 = getEditorController().getDragController();
1129         dragController.commit();
1130         // Do not invoke dragController.end here because we always receive a
1131         // DRAG_EXITED event which will perform the termination
1132         event.setDropCompleted(true);
1133 
1134         // Give the focus to the hierarchy
1135         getPanelControl().requestFocus();
1136     }
1137 
1138     private void handleOnDragEntered(final DragEvent event) {
1139         // CSS
1140         clearBorderColor();
1141         // When starting a DND gesture, disable parent ring updates
1142         setParentRingEnabled(false);
1143 
1144         // DragController update
1145         // The drag source is null if the drag gesture
1146         // has been started from outside (from the explorer / finder)
1147         final DragController dragController
1148                 = getEditorController().getDragController();
1149         if (dragController.getDragSource() == null) { // Drag started externally
1150             final FXOMDocument fxomDocument
1151                     = getEditorController().getFxomDocument();
1152             // Build drag source
1153             final Window ownerWindow = getPanelRoot().getScene().getWindow();
1154             final ExternalDragSource dragSource = new ExternalDragSource(
1155                     event.getDragboard(), fxomDocument, ownerWindow);
1156             assert dragSource.isAcceptable();
1157             dragController.begin(dragSource);
1158             shouldEndOnExit = true;
1159         }
1160     }
1161 
1162     private void handleOnDragExited(final DragEvent event) {
1163         // CSS
1164         clearBorderColor();
1165         // When ending a DND gesture, enable parent ring updates
1166         setParentRingEnabled(true);
1167 
1168         // Cancel timeline animation if any
1169         animationScheduler.stopTimeline();
1170 
1171         // Retrieve the vertical scroll bar value before updating the TreeItems
1172         double verticalScrollBarValue = 0.0;
1173         final ScrollBar scrollBar = getScrollBar(Orientation.VERTICAL);
1174         if (scrollBar != null) {
1175             verticalScrollBarValue = scrollBar.getValue();
1176         }
1177 
1178         // DragController update
1179         final DragController dragController
1180                 = getEditorController().getDragController();
1181         dragController.setDropTarget(null);
1182         if (shouldEndOnExit) {
1183             dragController.end();
1184             shouldEndOnExit = false;
1185         }
1186         // Set back the vertical scroll bar value after the TreeItems have been updated
1187         if (scrollBar != null) {
1188             scrollBar.setValue(verticalScrollBarValue);
1189         }
1190     }
1191 
1192     private void handleOnDragOver(final DragEvent event) {
1193         final ScrollBar verticalScrollBar = getScrollBar(Orientation.VERTICAL);
1194 
1195         // By dragging and hovering the cell within a few pixels
1196         // of the top or bottom of the Hierarchy,
1197         // the user can cause it to auto-scroll until the desired cell is in view.
1198         if (verticalScrollBar != null && verticalScrollBar.isVisible()) {
1199             final double eventY = event.getY();
1200             final double topY = getContentTopY();
1201             final double bottomY = getContentBottomY();
1202 
1203             // TOP auto scrolling zone
1204             if (topY <= eventY && eventY < topY + AUTO_SCROLLING_ZONE_HEIGHT) {
1205                 // Start the timeline if not already playing
1206                 if (!animationScheduler.isTimelineRunning()) {
1207                     animationScheduler.playDecrementAnimation(verticalScrollBar);
1208                 }
1209             } // BOTTOM auto scrolling zone
1210             else if (bottomY >= eventY && eventY > bottomY - AUTO_SCROLLING_ZONE_HEIGHT) {
1211                 // Start the timeline if not already playing
1212                 if (!animationScheduler.isTimelineRunning()) {
1213                     animationScheduler.playIncrementAnimation(verticalScrollBar);
1214                 }
1215             } else if (animationScheduler.isTimelineRunning()) {
1216                 animationScheduler.stopTimeline();
1217             }
1218         }
1219     }
1220 
1221     private void handleOnKeyPressed(final KeyEvent event) {
1222         switch (event.getCode()) {
1223 
1224             // Handle Inline editing
1225             case ENTER:
1226                 startEditingDisplayInfo();
1227                 break;
1228 
1229             // Handle collapse all
1230             case LEFT:
1231                 if (event.isAltDown()) {
1232                     final List<TreeItem<HierarchyItem>> treeItems = getSelectedItems();
1233                     if (!treeItems.isEmpty()) {
1234                         for (TreeItem<HierarchyItem> treeItem : treeItems) {
1235                             collapseAllTreeItems(treeItem);
1236                         }
1237                     }
1238                 }
1239                 break;
1240 
1241             // Handle expand all
1242             case RIGHT:
1243                 if (event.isAltDown()) {
1244                     final List<TreeItem<HierarchyItem>> treeItems = getSelectedItems();
1245                     if (!treeItems.isEmpty()) {
1246                         for (TreeItem<HierarchyItem> treeItem : treeItems) {
1247                             expandAllTreeItems(treeItem);
1248                         }
1249                     }
1250                 }
1251                 break;
1252 
1253             default:
1254                 break;
1255         }
1256     }
1257 
1258     private void handleOnMousePressed(final MouseEvent event) {
1259 
1260         if (event.getButton() == MouseButton.SECONDARY) {
1261             final ContextMenuController contextMenuController
1262                     = getEditorController().getContextMenuController();
1263             // The context menu items depend on the selection so
1264             // we need to rebuild it each time it is invoked.
1265             contextMenuController.updateContextMenuItems();
1266         }
1267     }
1268 
1269     private <T> void expandAllTreeItems(final TreeItem<T> parentTreeItem) {
1270         assert parentTreeItem != null;
1271         parentTreeItem.setExpanded(true);
1272         final List<TreeItem<T>> treeItems = getAllTreeItems(parentTreeItem);
1273         assert treeItems != null;
1274         for (TreeItem<T> treeItem : treeItems) {
1275             treeItem.setExpanded(true);
1276         }
1277     }
1278 
1279     private <T> void collapseAllTreeItems(final TreeItem<T> parentTreeItem) {
1280         assert parentTreeItem != null;
1281         parentTreeItem.setExpanded(false);
1282         final List<TreeItem<T>> treeItems = getAllTreeItems(parentTreeItem);
1283         assert treeItems != null;
1284         for (TreeItem<T> treeItem : treeItems) {
1285             treeItem.setExpanded(false);
1286         }
1287     }
1288 
1289     /**
1290      * Returns the cell ancestor of the specified event target. Indeed,
1291      * depending on the mouse click position, the event target may be the cell
1292      * node itself, the cell graphic or the cell labeled text.
1293      *
1294      * @param target
1295      * @return
1296      */
1297     private Cell<?> lookupCell(EventTarget target) {
1298         assert target instanceof Node;
1299         Node node = (Node) target;
1300         while ((node instanceof Cell) == false) {
1301             node = node.getParent();
1302         }
1303         return (Cell<?>) node;
1304     }
1305 }