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 }