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.treeview;
  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.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.GridPaneDropTarget;
  40 import com.oracle.javafx.scenebuilder.kit.editor.drag.target.RootDropTarget;
  41 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N;
  42 import com.oracle.javafx.scenebuilder.kit.editor.images.ImageUtils;
  43 import com.oracle.javafx.scenebuilder.kit.editor.job.atomic.ModifyFxIdJob;
  44 import com.oracle.javafx.scenebuilder.kit.editor.job.atomic.ModifyObjectJob;
  45 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.HierarchyDNDController;
  46 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.HierarchyItem;
  47 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController;
  48 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController.BorderSide;
  49 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController.DisplayOption;
  50 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.HierarchyDNDController.DroppingMouseLocation;
  51 import com.oracle.javafx.scenebuilder.kit.editor.report.CSSParsingReport;
  52 import com.oracle.javafx.scenebuilder.kit.editor.util.InlineEditController;
  53 import com.oracle.javafx.scenebuilder.kit.editor.util.InlineEditController.Type;
  54 import com.oracle.javafx.scenebuilder.kit.editor.report.ErrorReport;
  55 import com.oracle.javafx.scenebuilder.kit.editor.report.ErrorReportEntry;
  56 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument;
  57 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  58 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMIntrinsic;
  59 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMNode;
  60 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  61 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMPropertyT;
  62 import com.oracle.javafx.scenebuilder.kit.glossary.Glossary;
  63 import com.oracle.javafx.scenebuilder.kit.metadata.Metadata;
  64 import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata;
  65 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask;
  66 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask.Accessory;
  67 import com.oracle.javafx.scenebuilder.kit.metadata.util.PropertyName;
  68 
  69 import java.net.URL;
  70 import java.util.List;
  71 import java.util.Set;
  72 
  73 import javafx.beans.value.ChangeListener;
  74 import javafx.beans.value.WeakChangeListener;
  75 import javafx.collections.ObservableList;
  76 import javafx.css.CssParser;
  77 import javafx.event.EventHandler;
  78 import javafx.scene.control.TreeCell;
  79 import javafx.scene.control.TreeItem;
  80 import javafx.scene.input.DragEvent;
  81 import javafx.scene.input.MouseEvent;
  82 import javafx.geometry.Bounds;
  83 import javafx.geometry.Point2D;
  84 import javafx.scene.Cursor;
  85 import javafx.scene.Node;
  86 import javafx.scene.Scene;
  87 import javafx.scene.control.Control;
  88 import javafx.scene.control.Label;
  89 import javafx.scene.control.TextArea;
  90 import javafx.scene.control.TextInputControl;
  91 import javafx.scene.control.Tooltip;
  92 import javafx.scene.image.Image;
  93 import javafx.scene.image.ImageView;
  94 import javafx.scene.input.KeyEvent;
  95 import javafx.scene.input.MouseButton;
  96 import javafx.scene.layout.Border;
  97 import javafx.scene.layout.BorderStroke;
  98 import javafx.scene.layout.BorderStrokeStyle;
  99 import javafx.scene.layout.BorderWidths;
 100 import javafx.scene.layout.CornerRadii;
 101 import javafx.scene.layout.HBox;
 102 import javafx.scene.layout.Priority;
 103 import javafx.scene.layout.StackPane;
 104 import javafx.scene.paint.Paint;
 105 import javafx.scene.shape.Line;
 106 import javafx.util.Callback;
 107 
 108 /**
 109  * TreeCells used by the hierarchy TreeView.
 110  *
 111  * p
 112  *
 113  * @param <T>
 114  */
 115 public class HierarchyTreeCell<T extends HierarchyItem> extends TreeCell<HierarchyItem> {
 116 
 117     private final AbstractHierarchyPanelController panelController;
 118 
 119     static final String TREE_CELL_GRAPHIC = "tree-cell-graphic";
 120     public static final String HIERARCHY_FIRST_CELL = "hierarchy-first-cell";
 121     static final String HIERARCHY_PLACE_HOLDER_LABEL = "hierarchy-place-holder-label";
 122     static final String HIERARCHY_READWRITE_LABEL = "hierarchy-readwrite-label";
 123     // Style class used for lookup
 124     static final String HIERARCHY_TREE_CELL = "hierarchy-tree-cell";
 125 
 126     private final HBox graphic = new HBox();
 127     private final Label placeHolderLabel = new Label();
 128     private final Label classNameInfoLabel = new Label();
 129     private final Label displayInfoLabel = new Label();
 130     private final ImageView placeHolderImageView = new ImageView();
 131     private final ImageView classNameImageView = new ImageView();
 132     private final ImageView warningBadgeImageView = new ImageView();
 133     private final ImageView includedFileImageView = new ImageView();
 134     // Stack used to add badges over the top of the node icon
 135     private final StackPane iconsStack = new StackPane();
 136     // We use a label to set a tooltip over the node icon 
 137     // (StackPane does not allow to set tooltips)
 138     private final Label iconsLabel = new Label();
 139     private final Tooltip warningBadgeTooltip = new Tooltip();
 140 
 141     // Vertical line used when inserting an item in order to indicate 
 142     // the parent into which the item will be inserted.
 143     // Horizontal lines are handled directly by the cell and are built using CSS only.
 144     //
 145     // This line will be added to / removed from the skin of the panel control
 146     // during DND gestures.
 147     private final Line insertLineIndicator = new Line();
 148 
 149     // Listener for the display option used to update the display info label
 150     final ChangeListener<DisplayOption> displayOptionListener = (ov, t, t1) -> {
 151         // Update display info for non empty cells
 152         if (!isEmpty() && getItem() != null && !getItem().isEmpty()) {
 153             final String displayInfo = getItem().getDisplayInfo(t1);
 154             displayInfoLabel.setText(displayInfo);
 155             displayInfoLabel.setManaged(getItem().hasDisplayInfo(t1));
 156             displayInfoLabel.setVisible(getItem().hasDisplayInfo(t1));
 157         }
 158     };
 159 
 160     public HierarchyTreeCell(final AbstractHierarchyPanelController c) {
 161         super();
 162         this.panelController = c;
 163 
 164         iconsStack.getChildren().setAll(
 165                 classNameImageView,
 166                 warningBadgeImageView);
 167         iconsLabel.setGraphic(iconsStack);
 168         // RT-31645 : we cannot dynamically update the HBox graphic children 
 169         // in the cell.updateItem method.
 170         // We set once the graphic children, then we update the managed property
 171         // of the children depending on the cell item. 
 172         graphic.getChildren().setAll(
 173                 includedFileImageView,
 174                 placeHolderImageView,
 175                 iconsLabel,
 176                 placeHolderLabel,
 177                 classNameInfoLabel,
 178                 displayInfoLabel);
 179 
 180         // Add style class used when invoking lookupAll
 181         this.getStyleClass().add(HIERARCHY_TREE_CELL);
 182 
 183         // Update vertical insert line indicator stroke width
 184         insertLineIndicator.setStrokeWidth(2.0);
 185 
 186         // CSS
 187         graphic.getStyleClass().add(TREE_CELL_GRAPHIC);
 188         updatePlaceHolder();
 189         displayInfoLabel.getStyleClass().add(HIERARCHY_READWRITE_LABEL);
 190         placeHolderLabel.getStyleClass().add(HIERARCHY_PLACE_HOLDER_LABEL);
 191         // Layout
 192         classNameInfoLabel.setMinWidth(Control.USE_PREF_SIZE);
 193         displayInfoLabel.setMaxWidth(Double.MAX_VALUE);
 194         HBox.setHgrow(displayInfoLabel, Priority.ALWAYS);
 195 
 196         panelController.displayOptionProperty().addListener(
 197                 new WeakChangeListener<>(displayOptionListener));
 198 
 199         // Key events
 200         //----------------------------------------------------------------------
 201         final EventHandler<KeyEvent> keyEventHandler = e -> filterKeyEvent(e);
 202         this.addEventFilter(KeyEvent.ANY, keyEventHandler);
 203 
 204         // Mouse events
 205         //----------------------------------------------------------------------
 206         final EventHandler<MouseEvent> mouseEventHandler = e -> filterMouseEvent(e);
 207         this.addEventFilter(MouseEvent.ANY, mouseEventHandler);
 208 
 209         // Drag events
 210         //----------------------------------------------------------------------
 211         final HierarchyDNDController dndController = panelController.getDNDController();
 212 
 213         setOnDragDropped(event -> {
 214             final TreeItem<HierarchyItem> treeItem
 215                     = HierarchyTreeCell.this.getTreeItem();
 216             // Forward to the DND controller
 217             dndController.handleOnDragDropped(treeItem, event);
 218 
 219             // CSS
 220             panelController.clearBorderColor(HierarchyTreeCell.this);
 221             // Remove insert line indicator
 222             panelController.removeFromPanelControlSkin(insertLineIndicator);
 223         });
 224         setOnDragEntered(event -> {
 225             final TreeItem<HierarchyItem> treeItem
 226                     = HierarchyTreeCell.this.getTreeItem();
 227             // Forward to the DND controller
 228             dndController.handleOnDragEntered(treeItem, event);
 229         });
 230         setOnDragExited(event -> {
 231             final TreeItem<HierarchyItem> treeItem
 232                     = HierarchyTreeCell.this.getTreeItem();
 233             final Bounds bounds = HierarchyTreeCell.this.getLayoutBounds();
 234             final Point2D point = HierarchyTreeCell.this.localToScene(bounds.getMinX(), bounds.getMinY(), true /* rootScene */);
 235             final DroppingMouseLocation location;
 236             if (event.getSceneY() <= point.getY()) {
 237                 location = DroppingMouseLocation.TOP;
 238             } else {
 239                 location = DroppingMouseLocation.BOTTOM;
 240             }
 241 
 242             // Forward to the DND controller
 243             dndController.handleOnDragExited(treeItem, event, location);
 244 
 245             // CSS
 246             panelController.clearBorderColor(HierarchyTreeCell.this);
 247             // Remove insert line indicator
 248             panelController.removeFromPanelControlSkin(insertLineIndicator);
 249         });
 250         setOnDragOver(event -> {
 251             final TreeItem<HierarchyItem> treeItem
 252                     = HierarchyTreeCell.this.getTreeItem();
 253             final DragController dragController
 254                     = panelController.getEditorController().getDragController();
 255             final DroppingMouseLocation location = getDroppingMouseLocation(event);
 256 
 257             // Forward to the DND controller
 258             dndController.handleOnDragOver(treeItem, event, location); // (1)
 259 
 260             panelController.clearBorderColor();
 261             // Update vertical insert line indicator stroke color
 262             final Paint paint = panelController.getParentRingColor();
 263             insertLineIndicator.setStroke(paint);
 264             // Remove insert line indicator
 265             panelController.removeFromPanelControlSkin(insertLineIndicator);
 266 
 267             // If an animation timeline is running 
 268             // (auto-scroll when DND to the top or bottom of the Hierarchy),
 269             // we do not display insert indicators.
 270             if (panelController.isTimelineRunning()) {
 271                 return;
 272             }
 273 
 274             // Drop target has been updated because of (1)
 275             if (dragController.isDropAccepted()) {
 276 
 277                 final AbstractDropTarget dropTarget = dragController.getDropTarget();
 278                 final FXOMObject dropTargetObject = dropTarget.getTargetObject();
 279                 final TreeItem<?> rootTreeItem = getTreeView().getRoot();
 280 
 281                 if (dropTarget instanceof RootDropTarget) {
 282                     // No visual feedback in case of dropping the root node
 283                     return;
 284                 }
 285 
 286                 //==========================================================
 287                 // ACCESSORIES :
 288                 //
 289                 // No need to handle the insert line indicator.
 290                 // Border is set either on the accessory place holder cell
 291                 // or on the accessory owner cell.
 292                 //==========================================================
 293                 if (dropTarget instanceof AccessoryDropTarget) {
 294 
 295                     final AccessoryDropTarget accessoryDropTarget = (AccessoryDropTarget) dropTarget;
 296                     final TreeCell<?> cell;
 297 
 298                     // TreeItem is null when dropping below the datas
 299                     // => the drop target is the root
 300                     if (treeItem == null) {
 301                         cell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), rootTreeItem);
 302                     } else {
 303                         final HierarchyItem item = treeItem.getValue();
 304                         assert item != null;
 305 
 306                         if (item.isPlaceHolder()) {
 307                             cell = HierarchyTreeCell.this;
 308                         } else if (accessoryDropTarget.getAccessory() == Accessory.GRAPHIC) {
 309                             // Check if an empty graphic TreeItem has been added
 310                             final TreeItem<HierarchyItem> graphicTreeItem
 311                                     = dndController.getEmptyGraphicTreeItemFor(treeItem);
 312                             if (graphicTreeItem != null) {
 313                                 cell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), graphicTreeItem);
 314                             } else {
 315                                 final TreeItem<HierarchyItem> accessoryOwnerTreeItem1
 316                                         = panelController.lookupTreeItem(dropTargetObject);
 317                                 cell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), accessoryOwnerTreeItem1);
 318                             }
 319                         } else {
 320                             final TreeItem<HierarchyItem> accessoryOwnerTreeItem2
 321                                     = panelController.lookupTreeItem(dropTargetObject);
 322                             cell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), accessoryOwnerTreeItem2);
 323                         }
 324                     }
 325 
 326                     panelController.setBorder(cell, BorderSide.TOP_RIGHT_BOTTOM_LEFT);
 327                 }//
 328                 //==========================================================
 329                 // SUB COMPONENTS :
 330                 //
 331                 // Need to handle the insert line indicator.
 332                 //==========================================================
 333                 else {
 334                     assert dropTarget instanceof ContainerZDropTarget
 335                             || dropTarget instanceof GridPaneDropTarget;
 336                     TreeItem<?> startTreeItem;
 337                     TreeCell<?> startCell, stopCell;
 338 
 339                     // TreeItem is null when dropping below the datas
 340                     // => the drop target is the root
 341                     if (treeItem == null) {
 342                         if (rootTreeItem.isLeaf() || !rootTreeItem.isExpanded()) {
 343                             final TreeCell<?> rootCell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), 0);
 344                             panelController.setBorder(rootCell, BorderSide.TOP_RIGHT_BOTTOM_LEFT);
 345                         } else {
 346                             final TreeItem<?> lastTreeItem = panelController.getLastVisibleTreeItem(rootTreeItem);
 347                             final TreeCell<?> lastCell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), lastTreeItem);
 348                             // As we are dropping below the datas, the last cell is visible
 349                             assert lastCell != null;
 350                             panelController.setBorder(lastCell, BorderSide.BOTTOM);
 351 
 352                             // Update vertical insert line
 353                             startTreeItem = rootTreeItem;
 354                             startCell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), startTreeItem);
 355                             stopCell = lastCell;
 356                             updateInsertLineIndicator(startCell, stopCell);
 357                             panelController.addToPanelControlSkin(insertLineIndicator);
 358                         }
 359 
 360                     } else {
 361                         final HierarchyItem item = treeItem.getValue();
 362                         assert item != null;
 363 
 364                         if (item.isPlaceHolder() || item.getFxomObject() == dropTargetObject) {
 365                             // The place holder item is filled with a container
 366                             // accepting sub components
 367                             panelController.setBorder(HierarchyTreeCell.this, BorderSide.TOP_RIGHT_BOTTOM_LEFT);
 368                         } else {
 369                             // REORDERING :
 370                             // To avoid visual movement of the horizontal border when
 371                             // dragging from one cell to another,
 372                             // we always set the border on the cell bottom location :
 373                             // - if we handle REORDER BELOW gesture, just set the bottom 
 374                             // border on the current cell
 375                             // - if we handle REORDER ABOVE gesture, we set the bottom 
 376                             // border on the previous cell
 377                             //
 378                             switch (location) {
 379 
 380                                 // REORDER ABOVE gesture
 381                                 case TOP:
 382                                     if (treeItem == rootTreeItem) {
 383                                         panelController.setBorder(HierarchyTreeCell.this, BorderSide.TOP_RIGHT_BOTTOM_LEFT);
 384                                     } else {
 385                                         final int index = getIndex();
 386                                         // Retrieve the previous cell
 387                                         // Note : we set the border on the bottom of the previous cell 
 388                                         // instead of using the top of the current cell in order to avoid
 389                                         // visual gap when DND from one cell to another
 390                                         final TreeCell<?> previousCell
 391                                                 = HierarchyTreeViewUtils.getTreeCell(getTreeView(), index - 1);
 392                                         // The previous cell is null when the item is not visible
 393                                         if (previousCell != null) {
 394                                             panelController.setBorder(previousCell, BorderSide.BOTTOM);
 395                                         }
 396 
 397                                         // Update vertical insert line
 398                                         startTreeItem = panelController.lookupTreeItem(dropTarget.getTargetObject());
 399                                         startCell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), startTreeItem);
 400                                         stopCell = previousCell;
 401                                         updateInsertLineIndicator(startCell, stopCell);
 402                                         panelController.addToPanelControlSkin(insertLineIndicator);
 403                                     }
 404                                     break;
 405 
 406                                 // REPARENT gesture
 407                                 case CENTER:
 408                                     if (treeItem.isLeaf() || !treeItem.isExpanded()) {
 409                                         panelController.setBorder(HierarchyTreeCell.this, BorderSide.TOP_RIGHT_BOTTOM_LEFT);
 410                                     } else {
 411                                         // Reparent to the treeItem as last child
 412                                         final TreeItem<?> lastTreeItem = panelController.getLastVisibleTreeItem(treeItem);
 413                                         final TreeCell<?> lastCell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), lastTreeItem);
 414                                         // Last cell is null when the item is not visible
 415                                         if (lastCell != null) {
 416                                             panelController.setBorder(lastCell, BorderSide.BOTTOM);
 417                                         }
 418 
 419                                         // Update vertical insert line
 420                                         startTreeItem = getTreeItem();
 421                                         startCell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), startTreeItem);
 422                                         stopCell = lastCell;
 423                                         updateInsertLineIndicator(startCell, stopCell);
 424                                         panelController.addToPanelControlSkin(insertLineIndicator);
 425                                     }
 426                                     break;
 427 
 428                                 // REORDER BELOW gesture
 429                                 case BOTTOM:
 430                                     if (treeItem == rootTreeItem
 431                                             && (treeItem.isLeaf() || !treeItem.isExpanded())) {
 432                                         panelController.setBorder(HierarchyTreeCell.this, BorderSide.TOP_RIGHT_BOTTOM_LEFT);
 433                                     } else {
 434                                         // Reparent to the treeItem as first child
 435                                         panelController.setBorder(HierarchyTreeCell.this, BorderSide.BOTTOM);
 436 
 437                                         // Update vertical insert line
 438                                         startTreeItem = panelController.lookupTreeItem(dropTarget.getTargetObject());
 439                                         startCell = HierarchyTreeViewUtils.getTreeCell(getTreeView(), startTreeItem);
 440                                         stopCell = HierarchyTreeCell.this;
 441                                         updateInsertLineIndicator(startCell, stopCell);
 442                                         panelController.addToPanelControlSkin(insertLineIndicator);
 443                                     }
 444                                     break;
 445 
 446                                 default:
 447                                     assert false;
 448                                     break;
 449                             }
 450                         }
 451                     }
 452                 }
 453             }
 454         });
 455     }
 456 
 457     @Override
 458     public void updateItem(HierarchyItem item, boolean empty) {
 459         super.updateItem(item, empty);
 460 
 461         // The cell is not empty (TreeItem is not null) 
 462         // AND the TreeItem value is not null
 463         if (!empty && item != null) {
 464             updateLayout(item);
 465             setGraphic(graphic);
 466             setText(null);
 467             // Update parent ring when scrolling / resizing vertically / expanding and collapsing
 468             panelController.updateParentRing();
 469         } else {
 470             assert item == null;
 471             setGraphic(null);
 472             setText(null);
 473             // Clear CSS for empty cells
 474             panelController.clearBorderColor(this);
 475         }
 476     }
 477 
 478     public final void updatePlaceHolder() {
 479         final Paint paint = panelController.getParentRingColor();
 480         placeHolderLabel.setTextFill(paint);
 481         final BorderWidths bw = new BorderWidths(1);
 482         final BorderStroke bs = new BorderStroke(paint, BorderStrokeStyle.SOLID,
 483                 CornerRadii.EMPTY, bw);
 484         final Border b = new Border(bs);
 485         placeHolderLabel.setBorder(b);
 486     }
 487 
 488     private void filterKeyEvent(final KeyEvent ke) {
 489         // empty
 490     }
 491 
 492     private void filterMouseEvent(final MouseEvent me) {
 493 
 494         if (me.getEventType() == MouseEvent.MOUSE_PRESSED
 495                 && me.getButton() == MouseButton.PRIMARY) {
 496 
 497             // Mouse pressed on a non empty cell :
 498             // => we may start inline editing
 499             if (isEmpty() == false) { // (1)
 500                 if (me.getClickCount() >= 2) {
 501                     // Start inline editing the display info on double click OVER the display info label
 502                     // Double click over the class name label will end up with the native expand/collapse behavior
 503                     final HierarchyItem item = getItem();
 504                     assert item != null; // Because of (1)
 505                     final DisplayOption option = panelController.getDisplayOption();
 506                     if (item.hasDisplayInfo(option)
 507                             && item.isResourceKey(option) == false // Do not allow inline editing of the I18N value
 508                             && displayInfoLabel.isHover()) {
 509                         startEditingDisplayInfo();
 510                         // Consume the event so the native expand/collapse behavior is not performed
 511                         me.consume();
 512                     }
 513                 }
 514             } //
 515             // Mouse pressed on an empty cell
 516             // => we perform select none
 517             else {
 518                 // We clear the TreeView selection.
 519                 // Note that this is not the same as invoking selection.clear().
 520                 // Indeed, when empty BorderPane place holders are selected,
 521                 // the SB selection is empty whereas the TreeView selection is not.
 522                 getTreeView().getSelectionModel().clearSelection();
 523             }
 524         }
 525         updateCursor(me);
 526     }
 527 
 528     private void updateCursor(final MouseEvent me) {
 529         final Scene scene = getScene();
 530 
 531         if (scene == null) {
 532             // scene may be null when tree view is collapsed
 533             return;
 534         }
 535         // When another window is focused (just like the preview window), 
 536         // we use default cursor
 537         if (!getScene().getWindow().isFocused()) {
 538             scene.setCursor(Cursor.DEFAULT);
 539             return;
 540         }
 541         if (isEmpty()) {
 542             scene.setCursor(Cursor.DEFAULT);
 543         } else {
 544             final TreeItem<HierarchyItem> rootTreeItem = getTreeView().getRoot();
 545             final HierarchyItem item = getTreeItem().getValue();
 546             assert item != null;
 547             boolean isRoot = getTreeItem() == rootTreeItem;
 548             boolean isEmpty = item.isEmpty();
 549 
 550             if (me.getEventType() == MouseEvent.MOUSE_ENTERED) {
 551                 if (!me.isPrimaryButtonDown()) {
 552                     // Cannot DND root or place holder items
 553                     if (isRoot || isEmpty) {
 554                         setCursor(Cursor.DEFAULT);
 555                     } else {
 556                         setCursor(Cursor.OPEN_HAND);
 557                     }
 558                 }
 559             } else if (me.getEventType() == MouseEvent.MOUSE_PRESSED) {
 560                 // Cannot DND root or place holder items
 561                 if (isRoot || isEmpty) {
 562                     setCursor(Cursor.DEFAULT);
 563                 } else {
 564                     setCursor(Cursor.CLOSED_HAND);
 565                 }
 566             } else if (me.getEventType() == MouseEvent.MOUSE_RELEASED) {
 567                 // Cannot DND root or place holder items
 568                 if (isRoot || isEmpty) {
 569                     setCursor(Cursor.DEFAULT);
 570                 } else {
 571                     setCursor(Cursor.OPEN_HAND);
 572                 }
 573             } else if (me.getEventType() == MouseEvent.MOUSE_EXITED) {
 574                 setCursor(Cursor.DEFAULT);
 575             }
 576         }
 577     }
 578 
 579     /**
 580      * *************************************************************************
 581      * Inline editing
 582      *
 583      * We cannot use the FX inline editing because it occurs on selection +
 584      * simple mouse click
 585      * *************************************************************************
 586      */
 587     public void startEditingDisplayInfo() {
 588         assert getItem().hasDisplayInfo(panelController.getDisplayOption());
 589         final InlineEditController inlineEditController
 590                 = panelController.getEditorController().getInlineEditController();
 591         final TextInputControl editor;
 592         final Type type;
 593         final String initialValue;
 594 
 595         // 1) Build the TextInputControl used to inline edit
 596         //----------------------------------------------------------------------
 597         // INFO display option may use either a TextField or a TextArea
 598         if (panelController.getDisplayOption() == DisplayOption.INFO) {
 599             final String info = getItem().getDescription();
 600             final Object sceneGraphObject = getItem().getFxomObject().getSceneGraphObject();
 601             if (sceneGraphObject instanceof TextArea || DesignHierarchyMask.containsLineFeed(info)) {
 602                 type = Type.TEXT_AREA;
 603             } else {
 604                 type = Type.TEXT_FIELD;
 605             }
 606             // displayInfoLabel.getText() may be truncated to a single line description
 607             // We set the initial value with the complete description value
 608             initialValue = getItem().getDescription();
 609         } //
 610         // FXID and NODEID options use a TextField
 611         else {
 612             type = Type.TEXT_FIELD;
 613             initialValue = displayInfoLabel.getText();
 614         }
 615         editor = inlineEditController.createTextInputControl(type, displayInfoLabel, initialValue);
 616         // CSS
 617         final ObservableList<String> styleSheets
 618                 = panelController.getPanelRoot().getStylesheets();
 619         editor.getStylesheets().addAll(styleSheets);
 620         editor.getStyleClass().add("theme-presets"); //NOI18N
 621         editor.getStyleClass().add(InlineEditController.INLINE_EDITOR);
 622 
 623         // 2) Build the COMMIT Callback
 624         // This callback will be invoked to commit the new value
 625         // It returns true if the value is unchanged or if the commit succeeded, 
 626         // false otherwise
 627         //----------------------------------------------------------------------
 628         final Callback<String, Boolean> requestCommit = newValue -> {
 629             // 1) Check the input value is valid
 630             // 2) If valid, commit the new value and return true
 631             // 3) Otherwise, return false
 632             final HierarchyItem item = getItem();
 633             // Item may be null when invoking UNDO while inline editing session is on going
 634             if (item != null) {
 635                 final FXOMObject fxomObject = item.getFxomObject();
 636                 final DisplayOption option = panelController.getDisplayOption();
 637                 final EditorController editorController = panelController.getEditorController();
 638                 switch (option) {
 639                     case INFO:
 640                     case NODEID:
 641                         if (fxomObject instanceof FXOMInstance) {
 642                             final FXOMInstance fxomInstance = (FXOMInstance) fxomObject;
 643                             final PropertyName propertyName = item.getPropertyNameForDisplayInfo(option);
 644                             assert propertyName != null;
 645                             final ValuePropertyMetadata vpm
 646                                     = Metadata.getMetadata().queryValueProperty(fxomInstance, propertyName);
 647                             final ModifyObjectJob job1
 648                                     = new ModifyObjectJob(fxomInstance, vpm, newValue, editorController);
 649                             if (job1.isExecutable()) {
 650                                 editorController.getJobManager().push(job1);
 651                             }
 652                         }
 653                         break;
 654                     case FXID:
 655                         assert newValue != null;
 656                         final String fxId = newValue.isEmpty() ? null : newValue;
 657                         final ModifyFxIdJob job2
 658                                 = new ModifyFxIdJob(fxomObject, fxId, editorController);
 659                         if (job2.isExecutable()) {
 660 
 661                             // If a controller class has been defined, 
 662                             // check if the fx id is an injectable field
 663                             final String controllerClass
 664                                     = editorController.getFxomDocument().getFxomRoot().getFxController();
 665                             if (controllerClass != null && fxId != null) {
 666                                 final URL location = editorController.getFxmlLocation();
 667                                 final Class<?> clazz = fxomObject.getSceneGraphObject() == null ? null
 668                                         : fxomObject.getSceneGraphObject().getClass();
 669                                 final Glossary glossary = editorController.getGlossary();
 670                                 final List<String> fxIds1 = glossary.queryFxIds(location, controllerClass, clazz);
 671                                 if (fxIds1.contains(fxId) == false) {
 672                                     editorController.getMessageLog().logWarningMessage(
 673                                             "log.warning.no.injectable.fxid", fxId);
 674                                 }
 675                             }
 676 
 677                             // Check duplicared fx ids
 678                             final FXOMDocument fxomDocument = editorController.getFxomDocument();
 679                             final Set<String> fxIds2 = fxomDocument.collectFxIds().keySet();
 680                             if (fxIds2.contains(fxId)) {
 681                                 editorController.getMessageLog().logWarningMessage(
 682                                         "log.warning.duplicate.fxid", fxId);
 683                             }
 684                             
 685                             editorController.getJobManager().push(job2);
 686 
 687                         } else if (fxId != null) {
 688                             editorController.getMessageLog().logWarningMessage(
 689                                     "log.warning.invalid.fxid", fxId);
 690                         }
 691                         break;
 692                     default:
 693                         assert false;
 694                         return false;
 695                 }
 696             }
 697             return true;
 698         };
 699         inlineEditController.startEditingSession(editor, displayInfoLabel, requestCommit, null);
 700     }
 701 
 702     private void updateLayout(HierarchyItem item) {
 703         assert item != null;
 704         final FXOMObject fxomObject = item.getFxomObject();
 705 
 706         // Update styling
 707         this.getStyleClass().removeAll(HIERARCHY_FIRST_CELL);
 708         if (fxomObject != null && fxomObject.getParentObject() == null) {
 709             this.getStyleClass().add(HIERARCHY_FIRST_CELL);
 710         }
 711 
 712         // Update ImageViews
 713         final Image placeHolderImage = item.getPlaceHolderImage();
 714         placeHolderImageView.setImage(placeHolderImage);
 715         placeHolderImageView.setManaged(placeHolderImage != null);
 716 
 717         final Image classNameImage = item.getClassNameIcon();
 718         classNameImageView.setImage(classNameImage);
 719         classNameImageView.setManaged(classNameImage != null);
 720 
 721         // Included file
 722         if (fxomObject instanceof FXOMIntrinsic
 723                 && ((FXOMIntrinsic) fxomObject).getType() == FXOMIntrinsic.Type.FX_INCLUDE) {
 724             final URL resource = ImageUtils.getNodeIconURL("Included.png"); //NOI18N
 725             includedFileImageView.setImage(ImageUtils.getImage(resource));
 726             includedFileImageView.setManaged(true);
 727         } else {
 728             includedFileImageView.setImage(null);
 729             includedFileImageView.setManaged(false);
 730         }
 731 
 732         final List<ErrorReportEntry> entries = getErrorReportEntries(item);
 733         if (entries != null) {
 734             assert !entries.isEmpty();
 735             // Update tooltip with the first entry
 736             final ErrorReportEntry entry = entries.get(0);
 737             warningBadgeTooltip.setText(getErrorReport(entry));
 738             warningBadgeImageView.setImage(ImageUtils.getWarningBadgeImage());
 739             warningBadgeImageView.setManaged(true);
 740             iconsLabel.setTooltip(warningBadgeTooltip);
 741         } else {
 742             warningBadgeTooltip.setText(null);
 743             warningBadgeImageView.setImage(null);
 744             warningBadgeImageView.setManaged(false);
 745             iconsLabel.setTooltip(null);
 746         }
 747 
 748         // Update Labels
 749         final String placeHolderInfo = item.getPlaceHolderInfo();
 750         placeHolderLabel.setText(placeHolderInfo);
 751         placeHolderLabel.setManaged(item.isEmpty());
 752         placeHolderLabel.setVisible(item.isEmpty());
 753 
 754         final String classNameInfo = item.getClassNameInfo();
 755         classNameInfoLabel.setText(classNameInfo);
 756         classNameInfoLabel.setManaged(classNameInfo != null);
 757         classNameInfoLabel.setVisible(classNameInfo != null);
 758 
 759         final DisplayOption option = panelController.getDisplayOption();
 760         final String displayInfo = item.getDisplayInfo(option);
 761         // Do not allow inline editing of the I18N value
 762         if (item.isResourceKey(option)) {
 763             displayInfoLabel.getStyleClass().removeAll(HIERARCHY_READWRITE_LABEL);
 764         } else {
 765             if (displayInfoLabel.getStyleClass().contains(HIERARCHY_READWRITE_LABEL) == false) {
 766                 displayInfoLabel.getStyleClass().add(HIERARCHY_READWRITE_LABEL);
 767             }
 768         }
 769         displayInfoLabel.setText(displayInfo);
 770         displayInfoLabel.setManaged(item.hasDisplayInfo(option));
 771         displayInfoLabel.setVisible(item.hasDisplayInfo(option));
 772     }
 773 
 774     private List<ErrorReportEntry> getErrorReportEntries(HierarchyItem item) {
 775         if (item == null || item.isEmpty()) {
 776             return null;
 777         }
 778         final EditorController editorController = panelController.getEditorController();
 779         final ErrorReport errorReport = editorController.getErrorReport();
 780         final FXOMObject fxomObject = item.getFxomObject();
 781         assert fxomObject != null;
 782         return errorReport.query(fxomObject, !getTreeItem().isExpanded());
 783     }
 784     
 785     public String getErrorReport(final ErrorReportEntry entry) {
 786 
 787         final StringBuilder result = new StringBuilder();
 788 
 789         final FXOMNode fxomNode = entry.getFxomNode();
 790 
 791         switch (entry.getType()) {
 792             case UNRESOLVED_CLASS:
 793                 result.append(I18N.getString("hierarchy.unresolved.class"));
 794                 break;
 795             case UNRESOLVED_LOCATION:
 796                 result.append(I18N.getString("hierarchy.unresolved.location"));
 797                 break;
 798             case UNRESOLVED_RESOURCE:
 799                 result.append(I18N.getString("hierarchy.unresolved.resource"));
 800                 break;
 801             case INVALID_CSS_CONTENT:
 802                 assert entry.getCssParsingReport() != null;
 803                 result.append(makeCssParsingErrorString(entry.getCssParsingReport()));
 804                 break;
 805             case UNSUPPORTED_EXPRESSION:
 806                 result.append(I18N.getString("hierarchy.unsupported.expression"));
 807                 break;
 808         }
 809         result.append(" "); //NOI18N
 810         if (fxomNode instanceof FXOMPropertyT) {
 811             final FXOMPropertyT fxomProperty = (FXOMPropertyT) fxomNode;
 812             result.append(fxomProperty.getValue());
 813         } else if (fxomNode instanceof FXOMIntrinsic) {
 814             final FXOMIntrinsic fxomIntrinsic = (FXOMIntrinsic) fxomNode;
 815             result.append(fxomIntrinsic.getSource());
 816         } else if (fxomNode instanceof FXOMObject) {
 817             final FXOMObject fxomObject = (FXOMObject) fxomNode;
 818             final DesignHierarchyMask mask = new DesignHierarchyMask(fxomObject);
 819             result.append(mask.getClassNameInfo());
 820         }
 821 
 822         return result.toString();
 823     }
 824     
 825     private void updateInsertLineIndicator(
 826             final TreeCell<?> startTreeCell,
 827             final TreeCell<?> stopTreeCell) {
 828 
 829         //----------------------------------------------------------------------
 830         // START POINT CALCULATION
 831         //----------------------------------------------------------------------
 832         // Retrieve the disclosure node from which the vertical line will start
 833         double startX, startY;
 834         if (startTreeCell != null) {
 835             final Node disclosureNode = startTreeCell.getDisclosureNode();
 836             final Bounds startBounds = startTreeCell.getLayoutBounds();
 837             final Point2D startCellPoint = startTreeCell.localToParent(
 838                     startBounds.getMinX(), startBounds.getMinY());
 839 
 840             final Bounds disclosureNodeBounds = disclosureNode.getLayoutBounds();
 841             final Point2D disclosureNodePoint = disclosureNode.localToParent(
 842                     disclosureNodeBounds.getMinX(), disclosureNodeBounds.getMinY());
 843 
 844             // Initialize start point to the disclosure node of the start cell
 845             startX = startCellPoint.getX()
 846                     + disclosureNodePoint.getX()
 847                     + disclosureNodeBounds.getWidth() / 2 + 1; // +1 px tuning
 848             startY = startCellPoint.getY()
 849                     + disclosureNodePoint.getY()
 850                     + disclosureNodeBounds.getHeight() - 6; // -6 px tuning
 851         } else {
 852             // The start cell is not visible :
 853             // x is set to the current cell graphic
 854             // y is set to the top of the TreeView / TreeTableView
 855             final Bounds graphicBounds = getGraphic().getLayoutBounds();
 856             final Point2D graphicPoint = getGraphic().localToParent(
 857                     graphicBounds.getMinX(), graphicBounds.getMinY());
 858 
 859             startX = graphicPoint.getX();
 860             startY = panelController.getContentTopY();
 861         }
 862 
 863         //----------------------------------------------------------------------
 864         // END POINT CALCULATION
 865         //----------------------------------------------------------------------
 866         double endX, endY;
 867         endX = startX;
 868         if (stopTreeCell != null) {
 869             final Bounds stopBounds = stopTreeCell.getLayoutBounds();
 870             final Point2D stopCellPoint = stopTreeCell.localToParent(
 871                     stopBounds.getMinX(), stopBounds.getMinY());
 872 
 873             // Initialize end point to the end cell
 874             endY = stopCellPoint.getY()
 875                     + stopBounds.getHeight() // Add the stop cell height
 876                     - 1; // -1 px tuning
 877         } else {
 878             // The stop cell is not visisble :
 879             // y is set to the bottom of the TreeView / TreeTableView
 880             endY = panelController.getContentBottomY();
 881         }
 882 
 883         insertLineIndicator.setStartX(startX);
 884         insertLineIndicator.setStartY(startY);
 885         insertLineIndicator.setEndX(endX);
 886         insertLineIndicator.setEndY(endY);
 887     }
 888 
 889     private DroppingMouseLocation getDroppingMouseLocation(final DragEvent event) {
 890         final DroppingMouseLocation location;
 891         if (this.getTreeItem() != null) {
 892             if ((getHeight() * 0.25) > event.getY()) {
 893                 location = DroppingMouseLocation.TOP;
 894             } else if ((getHeight() * 0.75) < event.getY()) {
 895                 location = DroppingMouseLocation.BOTTOM;
 896             } else {
 897                 location = DroppingMouseLocation.CENTER;
 898             }
 899         } else {
 900             location = DroppingMouseLocation.BOTTOM;
 901         }
 902         return location;
 903     }
 904     
 905     private String makeCssParsingErrorString(CSSParsingReport r) {
 906         final StringBuilder result = new StringBuilder();
 907         
 908         if (r.getIOException() != null) {
 909             result.append(r.getIOException());
 910         } else {
 911             assert r.getParseErrors().isEmpty() == false;
 912             int errorCount = 0;
 913             for (CssParser.ParseError e : r.getParseErrors()) {
 914                 result.append(e.getMessage());
 915                 errorCount++;
 916                 if (errorCount < 5) {
 917                     result.append('\n');
 918                 } else {
 919                     result.append("...");
 920                     break;
 921                 }
 922             }
 923         }
 924         
 925         return result.toString();
 926     }
 927     
 928 }