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;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform.Theme;
  35 import com.oracle.javafx.scenebuilder.kit.editor.drag.DragController;
  36 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N;
  37 import com.oracle.javafx.scenebuilder.kit.editor.job.AddContextMenuToSelectionJob;
  38 import com.oracle.javafx.scenebuilder.kit.editor.job.AddTooltipToSelectionJob;
  39 import com.oracle.javafx.scenebuilder.kit.editor.job.ModifySelectionJob;
  40 import com.oracle.javafx.scenebuilder.kit.editor.job.BringForwardJob;
  41 import com.oracle.javafx.scenebuilder.kit.editor.job.BringToFrontJob;
  42 import com.oracle.javafx.scenebuilder.kit.editor.job.CutSelectionJob;
  43 import com.oracle.javafx.scenebuilder.kit.editor.job.DeleteSelectionJob;
  44 import com.oracle.javafx.scenebuilder.kit.editor.job.DuplicateSelectionJob;
  45 import com.oracle.javafx.scenebuilder.kit.editor.job.FitToParentSelectionJob;
  46 import com.oracle.javafx.scenebuilder.kit.editor.job.ImportFileJob;
  47 import com.oracle.javafx.scenebuilder.kit.editor.job.IncludeFileJob;
  48 import com.oracle.javafx.scenebuilder.kit.editor.job.InsertAsSubComponentJob;
  49 import com.oracle.javafx.scenebuilder.kit.editor.job.Job;
  50 import com.oracle.javafx.scenebuilder.kit.editor.job.PasteIntoJob;
  51 import com.oracle.javafx.scenebuilder.kit.editor.job.PasteJob;
  52 import com.oracle.javafx.scenebuilder.kit.editor.job.SendBackwardJob;
  53 import com.oracle.javafx.scenebuilder.kit.editor.job.SendToBackJob;
  54 import com.oracle.javafx.scenebuilder.kit.editor.job.SetDocumentRootJob;
  55 import com.oracle.javafx.scenebuilder.kit.editor.job.TrimSelectionJob;
  56 import com.oracle.javafx.scenebuilder.kit.editor.job.UseComputedSizesSelectionJob;
  57 import com.oracle.javafx.scenebuilder.kit.editor.job.UsePredefinedSizeJob;
  58 import com.oracle.javafx.scenebuilder.kit.editor.job.gridpane.AddColumnJob;
  59 import com.oracle.javafx.scenebuilder.kit.editor.job.gridpane.AddRowJob;
  60 import com.oracle.javafx.scenebuilder.kit.editor.job.gridpane.GridPaneJobUtils.Position;
  61 import com.oracle.javafx.scenebuilder.kit.editor.job.gridpane.MoveColumnJob;
  62 import com.oracle.javafx.scenebuilder.kit.editor.job.gridpane.MoveRowJob;
  63 import com.oracle.javafx.scenebuilder.kit.editor.job.gridpane.v2.SpanJob;
  64 import com.oracle.javafx.scenebuilder.kit.editor.job.wrap.AbstractWrapInJob;
  65 import com.oracle.javafx.scenebuilder.kit.editor.job.wrap.UnwrapJob;
  66 import com.oracle.javafx.scenebuilder.kit.editor.messagelog.MessageLog;
  67 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.ErrorDialog;
  68 import com.oracle.javafx.scenebuilder.kit.editor.util.InlineEditController;
  69 import com.oracle.javafx.scenebuilder.kit.editor.report.ErrorReport;
  70 import com.oracle.javafx.scenebuilder.kit.editor.selection.AbstractSelectionGroup;
  71 import com.oracle.javafx.scenebuilder.kit.editor.selection.GridSelectionGroup;
  72 import com.oracle.javafx.scenebuilder.kit.editor.selection.ObjectSelectionGroup;
  73 import com.oracle.javafx.scenebuilder.kit.editor.selection.Selection;
  74 import com.oracle.javafx.scenebuilder.kit.editor.util.ContextMenuController;
  75 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument;
  76 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  77 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMIntrinsic;
  78 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  79 import com.oracle.javafx.scenebuilder.kit.glossary.Glossary;
  80 import com.oracle.javafx.scenebuilder.kit.glossary.BuiltinGlossary;
  81 import com.oracle.javafx.scenebuilder.kit.library.BuiltinLibrary;
  82 import com.oracle.javafx.scenebuilder.kit.library.Library;
  83 import com.oracle.javafx.scenebuilder.kit.library.LibraryItem;
  84 import com.oracle.javafx.scenebuilder.kit.metadata.Metadata;
  85 import com.oracle.javafx.scenebuilder.kit.metadata.property.PropertyMetadata;
  86 import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata;
  87 import com.oracle.javafx.scenebuilder.kit.metadata.util.ClipboardEncoder;
  88 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask;
  89 import com.oracle.javafx.scenebuilder.kit.metadata.util.DesignHierarchyMask.Accessory;
  90 import com.oracle.javafx.scenebuilder.kit.metadata.util.PrefixedValue;
  91 import com.oracle.javafx.scenebuilder.kit.metadata.util.PropertyName;
  92 import com.oracle.javafx.scenebuilder.kit.util.control.effectpicker.Utils;
  93 
  94 import java.io.File;
  95 import java.io.IOException;
  96 import java.net.URISyntaxException;
  97 import java.net.URL;
  98 import java.nio.file.Path;
  99 import java.util.ArrayList;
 100 import java.util.Collection;
 101 import java.util.Collections;
 102 import java.util.HashSet;
 103 import java.util.List;
 104 import java.util.ResourceBundle;
 105 import java.util.Set;
 106 
 107 import javafx.beans.property.BooleanProperty;
 108 import javafx.beans.property.ListProperty;
 109 import javafx.beans.property.ObjectProperty;
 110 import javafx.beans.property.SimpleBooleanProperty;
 111 import javafx.beans.property.SimpleListProperty;
 112 import javafx.beans.property.SimpleObjectProperty;
 113 import javafx.beans.property.SimpleStringProperty;
 114 import javafx.beans.value.ChangeListener;
 115 import javafx.beans.value.ObservableListValue;
 116 import javafx.beans.value.ObservableValue;
 117 import javafx.collections.ObservableList;
 118 import javafx.geometry.Bounds;
 119 import javafx.scene.Node;
 120 import javafx.scene.Parent;
 121 import javafx.scene.control.Control;
 122 import javafx.scene.effect.Effect;
 123 import javafx.scene.input.Clipboard;
 124 import javafx.scene.layout.BorderPane;
 125 import javafx.util.Callback;
 126 
 127 /**
 128  * An editor controller is the central object which coordinates the editing
 129  * of an FXML document across the different panels (hierarchy, content,
 130  * inspector...).
 131  * <p>
 132  * An editor controller is associated to an FXML document. It can perform
 133  * editing and control actions on this document. It also maintains the list of
 134  * objects selected by the user.
 135  * <p>
 136  * Some panel controllers can be attached to an editor controller. They listen
 137  * to the editor and update their content accordingly.
 138  */
 139 public class EditorController {
 140 
 141     /**
 142      * An 'edit' action is an action which modifies the document associated
 143      * to this editor. It makes the document dirty and pushes a
 144      * new item on the undo/redo stack.
 145      */
 146     public enum EditAction {
 147         // Candidates for Edit menu
 148         CUT,
 149         PASTE,
 150         PASTE_INTO,
 151         DUPLICATE,
 152         DELETE,
 153         TRIM,
 154         TOGGLE_FX_ROOT,
 155         // Candidates for Modify menu
 156         FIT_TO_PARENT,
 157         USE_COMPUTED_SIZES,
 158         ADD_CONTEXT_MENU,
 159         ADD_TOOLTIP,
 160         SET_SIZE_320x240,
 161         SET_SIZE_640x480,
 162         SET_SIZE_1280x800,
 163         SET_SIZE_1920x1080,
 164         // Candidates for Modify/GridPane menu
 165         MOVE_ROW_ABOVE,
 166         MOVE_ROW_BELOW,
 167         MOVE_COLUMN_BEFORE,
 168         MOVE_COLUMN_AFTER,
 169         ADD_ROW_ABOVE,
 170         ADD_ROW_BELOW,
 171         ADD_COLUMN_BEFORE,
 172         ADD_COLUMN_AFTER,
 173         INCREASE_ROW_SPAN,
 174         DECREASE_ROW_SPAN,
 175         INCREASE_COLUMN_SPAN,
 176         DECREASE_COLUMN_SPAN,
 177         // Candidates for Arrange menu
 178         BRING_TO_FRONT,
 179         SEND_TO_BACK,
 180         BRING_FORWARD,
 181         SEND_BACKWARD,
 182         UNWRAP,
 183         WRAP_IN_ANCHOR_PANE,
 184         WRAP_IN_BORDER_PANE,
 185         WRAP_IN_BUTTON_BAR,
 186         WRAP_IN_DIALOG_PANE,
 187         WRAP_IN_FLOW_PANE,
 188         WRAP_IN_GRID_PANE,
 189         WRAP_IN_GROUP,
 190         WRAP_IN_HBOX,
 191         WRAP_IN_PANE,
 192         WRAP_IN_SCROLL_PANE,
 193         WRAP_IN_SPLIT_PANE,
 194         WRAP_IN_STACK_PANE,
 195         WRAP_IN_TAB_PANE,
 196         WRAP_IN_TEXT_FLOW,
 197         WRAP_IN_TILE_PANE,
 198         WRAP_IN_TITLED_PANE,
 199         WRAP_IN_TOOL_BAR,
 200         WRAP_IN_VBOX
 201     }
 202 
 203     /**
 204      * A 'control' action does not modify the document. It only changes a
 205      * state or a mode in this editor.
 206      */
 207     public enum ControlAction {
 208         // Candidates for Edit menu
 209         COPY,
 210         SELECT_ALL,
 211         SELECT_NONE,
 212         SELECT_PARENT,
 213         SELECT_NEXT,
 214         SELECT_PREVIOUS,
 215         EDIT_INCLUDED_FILE,
 216         REVEAL_INCLUDED_FILE,
 217         TOGGLE_CSS_SELECTION,
 218         TOGGLE_SAMPLE_DATA
 219     }
 220 
 221     /**
 222      * Predefined sizes (width x height).
 223      * Preferred one refers to the one explicitly set by the user: it is for
 224      * use for previewing only.
 225      * Default one is the one stored as a global preference: its value is
 226      * under user control.
 227      */
 228     public enum Size {
 229         SIZE_320x240,
 230         SIZE_640x480,
 231         SIZE_1280x800,
 232         SIZE_1920x1080,
 233         SIZE_PREFERRED,
 234         SIZE_DEFAULT
 235     }
 236 
 237     private final Selection selection = new Selection();
 238     private final JobManager jobManager = new JobManager(this, 50);
 239     private final MessageLog messageLog = new MessageLog();
 240     private final ErrorReport errorReport = new ErrorReport();
 241     private final DragController dragController = new DragController(this);
 242     private final InlineEditController inlineEditController = new InlineEditController(this);
 243     private final ContextMenuController contextMenuController = new ContextMenuController(this);
 244     private final WatchingController watchingController = new WatchingController(this);
 245 
 246     // At start-up the setter for the two variables below might be called by the
 247     // Preferences controller.
 248     private double defaultRootContainerWidth = 600;
 249     private double defaultRootContainerHeight = 400;
 250 
 251     private final ObjectProperty<FXOMDocument> fxomDocumentProperty
 252             = new SimpleObjectProperty<>();
 253     private final ObjectProperty<URL> fxmlLocationProperty
 254             = new SimpleObjectProperty<>();
 255     private final ObjectProperty<Library> libraryProperty
 256             = new SimpleObjectProperty<>(BuiltinLibrary.getLibrary());
 257     private final ObjectProperty<Glossary> glossaryProperty
 258             = new SimpleObjectProperty<>(new BuiltinGlossary());
 259     private final ObjectProperty<ResourceBundle> resourcesProperty
 260             = new SimpleObjectProperty<>(null);
 261     private final ObjectProperty<Theme> themeProperty
 262             = new SimpleObjectProperty<>(Theme.MODENA);
 263     private final ListProperty<File> sceneStyleSheetProperty
 264             = new SimpleListProperty<>();
 265     private final BooleanProperty pickModeEnabledProperty
 266             = new SimpleBooleanProperty(false);
 267     private final BooleanProperty sampleDataEnabledProperty
 268             = new SimpleBooleanProperty(false);
 269     private final SimpleStringProperty toolStylesheetProperty
 270             = new SimpleStringProperty(getBuiltinToolStylesheet());
 271 
 272     private Callback<Void, Boolean> requestTextEditingSessionEnd;
 273 
 274     private static String builtinToolStylesheet;
 275     private static File nextInitialDirectory = new File(System.getProperty("user.home")); //NOI18N
 276 
 277 
 278     /**
 279      * Creates an empty editor controller (ie it has no associated fxom document).
 280      */
 281     public EditorController() {
 282         jobManager.revisionProperty().addListener((ChangeListener<Number>) (ov, t, t1) -> jobManagerRevisionDidChange());
 283     }
 284 
 285     /**
 286      * Get the width to use by default for the root container.
 287      *
 288      * @return default width for the root container.
 289      */
 290     public double getDefaultRootContainerWidth() {
 291         return defaultRootContainerWidth;
 292     }
 293 
 294     /**
 295      * Set the width to use by default for the root container.
 296      *
 297      * @param defaultRootContainerWidth the new root container's default width.
 298      */
 299     public void setDefaultRootContainerWidth(double defaultRootContainerWidth) {
 300         this.defaultRootContainerWidth = defaultRootContainerWidth;
 301     }
 302 
 303     /**
 304      * Get the height to use by default for the root container.
 305      *
 306      * @return default height for the root container.
 307      */
 308     public double getDefaultRootContainerHeight() {
 309         return defaultRootContainerHeight;
 310     }
 311 
 312     /**
 313      * Set the height to use by default for the root container.
 314      *
 315      * @param defaultRootContainerHeight the new root container's default height.
 316      */
 317     public void setDefaultRootContainerHeight(double defaultRootContainerHeight) {
 318         this.defaultRootContainerHeight = defaultRootContainerHeight;
 319     }
 320 
 321     /**
 322      * Sets the fxml content to be edited by this editor.
 323      * A null value makes this editor empty.
 324      *
 325      * @param fxmlText null or the fxml text to be edited
 326      * @throws IOException if fxml text cannot be parsed and loaded correctly.
 327      */
 328     public void setFxmlText(String fxmlText) throws IOException {
 329         setFxmlTextAndLocation(fxmlText, getFxmlLocation());
 330     }
 331 
 332     /**
 333      * Returns null or the fxml content being edited by this editor.
 334      *
 335      * @return null or the fxml content being edited by this editor.
 336      */
 337     public String getFxmlText() {
 338         final String result;
 339 
 340         final FXOMDocument fxomDocument = getFxomDocument();
 341         if (fxomDocument == null) {
 342             result = null;
 343         } else {
 344             final boolean sampleDataEnabled = fxomDocument.isSampleDataEnabled();
 345             if (sampleDataEnabled) {
 346                 fxomDocument.setSampleDataEnabled(false);
 347             }
 348             result = fxomDocument.getFxmlText();
 349             if (sampleDataEnabled) {
 350                 fxomDocument.setSampleDataEnabled(true);
 351             }
 352         }
 353 
 354         return result;
 355     }
 356 
 357     /**
 358      * Returns true if fxml content being edited can be returned safely.
 359      * This method will return false if there is a text editing session on-going.
 360      *
 361      * @return true if fxml content being edited can be returned safely.
 362      */
 363     public boolean canGetFxmlText() {
 364         final boolean result;
 365 
 366         if (requestTextEditingSessionEnd == null) {
 367             result = true;
 368         } else {
 369             result = requestTextEditingSessionEnd.call(null);
 370             // If the callback returns true, then it should have call
 371             // textEditingSessionDidEnd()
 372             // => requestTextEditingSessionEnd should be null
 373             assert (requestTextEditingSessionEnd == null) || (result == false);
 374         }
 375 
 376         return result;
 377     }
 378 
 379     /**
 380      * Tells this editor that a text editing session has started.
 381      * The editor controller may invoke the requestSessionEnd() callback
 382      * if it needs the text editing session to stop. The callback should;
 383      *   - either stop the text editing session, invoke textEditingSessionDidEnd()
 384      *     and return true
 385      *   - either keep the text editing session on-going and return false
 386      *
 387      * @param requestSessionEnd Callback that should end the text editing session or return false
 388      */
 389     public void textEditingSessionDidBegin(Callback<Void, Boolean> requestSessionEnd) {
 390         assert requestTextEditingSessionEnd == null;
 391         requestTextEditingSessionEnd = requestSessionEnd;
 392     }
 393 
 394 
 395     /**
 396      * Tells this editor that the text editing session has ended.
 397      */
 398     public void textEditingSessionDidEnd() {
 399         assert requestTextEditingSessionEnd != null;
 400         requestTextEditingSessionEnd = null;
 401     }
 402 
 403     /*
 404      * Returns true if a text editing session is currently on going.
 405      */
 406     public boolean isTextEditingSessionOnGoing() {
 407         return requestTextEditingSessionEnd != null;
 408     }
 409 
 410     /**
 411      * The property holding the fxml location associated to this editor.
 412      * @return the property holding the fxml location associated to this editor.
 413      */
 414     public ObservableValue<URL> fxmlLocationProperty() {
 415         return fxmlLocationProperty;
 416     }
 417 
 418     /**
 419      * Sets the location of the fxml being edited.
 420      * If null value is passed, fxml text is being interpreted with any location
 421      * (ie some references may be broken).
 422      *
 423      * @param fxmlLocation null or the location of the fxml being edited.
 424      */
 425     public void setFxmlLocation(URL fxmlLocation) {
 426         fxmlLocationProperty.setValue(fxmlLocation);
 427         if (getFxomDocument() != null) {
 428             getFxomDocument().setLocation(fxmlLocation);
 429             clearUndoRedo(); // Because FXOMDocument.setLocation() mutates the document
 430         }
 431         if (fxmlLocation != null) {
 432             final File newInitialDirectory = new File(fxmlLocation.getPath());
 433             EditorController.updateNextInitialDirectory(newInitialDirectory);
 434         }
 435     }
 436 
 437     /**
 438      * Returns the library used by this editor.
 439      *
 440      * @return the library used by this editor (never null).
 441      */
 442     public Library getLibrary() {
 443         return libraryProperty.getValue();
 444     }
 445 
 446     /**
 447      * Sets the library used by this editor.
 448      * When this method is called, user scene graph is fully rebuilt using
 449      * the new library and all panel refresh their contents.
 450      *
 451      * @param library the library to be used by this editor (never null).
 452      */
 453     public void setLibrary(Library library) {
 454         assert library != null;
 455         libraryProperty.getValue().classLoaderProperty().removeListener(libraryClassLoaderListener);
 456         libraryProperty.setValue(library);
 457         libraryProperty.getValue().classLoaderProperty().addListener(libraryClassLoaderListener);
 458         libraryClassLoaderDidChange();
 459     }
 460 
 461     /**
 462      * The property holding the library used by this editor.
 463      *
 464      * @return the property holding the library used by this editor (never null).
 465      */
 466     public ObservableValue<Library> libraryProperty() {
 467         return libraryProperty;
 468     }
 469 
 470     /**
 471      * Returns the glossary used by this editor.
 472      *
 473      * @return the glossary used by this editor (never null).
 474      */
 475     public Glossary getGlossary() {
 476         return glossaryProperty.getValue();
 477     }
 478 
 479     /**
 480      * Sets the glossary used by this editor.
 481      * The Inspector panel(s) connected to this editor will update
 482      * their suggested lists in Code section.
 483      *
 484      * @param glossary the glossary to be used by this editor (never null).
 485      */
 486     public void setLibrary(Glossary glossary) {
 487         assert glossary != null;
 488         glossaryProperty.setValue(glossary);
 489     }
 490 
 491     /**
 492      * The property holding the glossary used by this editor.
 493      *
 494      * @return the property holding the glossary used by this editor (never null).
 495      */
 496     public ObservableValue<Glossary> glossaryProperty() {
 497         return glossaryProperty;
 498     }
 499 
 500     /**
 501      * Returns the resource bundle used by this editor.
 502      *
 503      * @return  the resource bundle used by this editor.
 504      */
 505     public ResourceBundle getResources() {
 506         return resourcesProperty.getValue();
 507     }
 508 
 509     /**
 510      * Sets the resource bundle used by this editor.
 511      * Content and Preview panels sharing this editor will update
 512      * their content to use this new theme.
 513      *
 514      * @param resources null of the resource bundle to be used by this editor.
 515      */
 516     public void setResources(ResourceBundle resources) {
 517         resourcesProperty.setValue(resources);
 518         resourcesDidChange();
 519     }
 520 
 521     /**
 522      * The property holding the resource bundle used by this editor.
 523      *
 524      * @return the property holding the resource bundle used by this editor (never null).
 525      */
 526     public ObservableValue<ResourceBundle> resourcesProperty() {
 527         return resourcesProperty;
 528     }
 529 
 530     /**
 531      * Returns the theme used by this editor.
 532      *
 533      * @return the theme used by this editor.
 534      */
 535     public Theme getTheme() {
 536         return themeProperty.getValue();
 537     }
 538 
 539     /**
 540      * Sets the theme used by this editor.
 541      * Content and Preview panels sharing this editor will update
 542      * their content to use this new theme.
 543      *
 544      * @param theme the theme to be used by this editor
 545      */
 546     public void setTheme(Theme theme) {
 547         themeProperty.setValue(theme);
 548     }
 549 
 550     /**
 551      * The property holding the theme used by this editor.
 552      *
 553      * @return the property holding the theme associated to the editor (never null).
 554      */
 555     public ObservableValue<Theme> themeProperty() {
 556         return themeProperty;
 557     }
 558 
 559     /**
 560      *
 561      * @return the list of scene style sheet used by this editor
 562      */
 563     public ObservableList<File> getSceneStyleSheets() {
 564         return sceneStyleSheetProperty.getValue();
 565     }
 566 
 567     /**
 568      *
 569      * @param styleSheets the list of scene style sheet to be used by this editor
 570      */
 571     public void setSceneStyleSheets(ObservableList<File> styleSheets) {
 572         sceneStyleSheetProperty.setValue(styleSheets);
 573     }
 574 
 575     /**
 576      * The property holding the list of scene style sheet used by this editor.
 577      *
 578      * @return the property holding the set of scene style sheet used by the editor,
 579      * or null if has not been set.
 580      */
 581     public ObservableListValue<File> sceneStyleSheetProperty() {
 582         return sceneStyleSheetProperty;
 583     }
 584 
 585     /**
 586      * Returns true if 'pick mode' is enabled for this editor.
 587      * @return true if 'pick mode' is enabled for this editor.
 588      */
 589     public boolean isPickModeEnabled() {
 590         return pickModeEnabledProperty.getValue();
 591     }
 592 
 593     /**
 594      * Enables or disables 'pick mode' on this editor.
 595      *
 596      * @param pickModeEnabled true if 'pick mode' should be enabled.
 597      */
 598     public void setPickModeEnabled(boolean pickModeEnabled) {
 599         pickModeEnabledProperty.setValue(pickModeEnabled);
 600     }
 601 
 602     /**
 603      * The property indicating if 'pick mode' is enabled or not.
 604      *
 605      * @return the property indicating if 'pick mode' is enabled or not.
 606      */
 607     public ObservableValue<Boolean> pickModeEnabledProperty() {
 608         return pickModeEnabledProperty;
 609     }
 610 
 611     /**
 612      * Returns true if content and preview panels attached to this editor
 613      * should display sample data.
 614      *
 615      * @return true if content and preview panels should display sample data.
 616      */
 617     public boolean isSampleDataEnabled() {
 618         return sampleDataEnabledProperty.getValue();
 619     }
 620 
 621     /**
 622      * Enables or disables display of sample data in content and preview panels
 623      * attached to this editor.
 624      *
 625      * @param sampleDataEnabled true if sample data should be displayed
 626      */
 627     public void setSampleDataEnabled(boolean sampleDataEnabled) {
 628         setPickModeEnabled(false);
 629         sampleDataEnabledProperty.setValue(sampleDataEnabled);
 630         if (getFxomDocument() != null) {
 631             getFxomDocument().setSampleDataEnabled(isSampleDataEnabled());
 632         }
 633     }
 634 
 635     /**
 636      * The property indicating if sample data should be displayed or not.
 637      *
 638      * @return the property indicating if sample data should be displayed or not.
 639      */
 640     public ObservableValue<Boolean> sampleDataEnabledProperty() {
 641         return sampleDataEnabledProperty;
 642     }
 643 
 644 
 645     /**
 646      * Returns null or the location of the fxml being edited.
 647      *
 648      * @return null or the location of the fxml being edited.
 649      */
 650     public URL getFxmlLocation() {
 651         return fxmlLocationProperty.getValue();
 652     }
 653 
 654     /**
 655      * Sets both fxml text and location to be edited by this editor.
 656      * Performs setFxmlText() and setFxmlLocation() but in a optimized manner
 657      * (it avoids an extra scene graph refresh).
 658      *
 659      * @param fxmlText null or the fxml text to be edited
 660      * @param fxmlLocation null or the location of the fxml text being edited
 661      * @throws IOException if fxml text cannot be parsed and loaded correctly.
 662      */
 663     public void setFxmlTextAndLocation(String fxmlText, URL fxmlLocation) throws IOException {
 664         updateFxomDocument(fxmlText, fxmlLocation, getResources());
 665         this.fxmlLocationProperty.setValue(fxmlLocation);
 666     }
 667 
 668     /**
 669      * Sets fxml text, location and resources to be edited by this editor.
 670      * Performs setFxmlText(), setFxmlLocation() and setResources() but in an
 671      * optimized manner (it avoids extra scene graph refresh).
 672      *
 673      * @param fxmlText null or the fxml text to be edited
 674      * @param fxmlLocation null or the location of the fxml text being edited
 675      * @param resources null or the resource bundle used to load the fxml text
 676      * @throws IOException if fxml text cannot be parsed and loaded correctly.
 677      */
 678     public void setFxmlTextLocationAndResources(String fxmlText, URL fxmlLocation,
 679             ResourceBundle resources) throws IOException {
 680         updateFxomDocument(fxmlText, fxmlLocation, resources);
 681         this.fxmlLocationProperty.setValue(fxmlLocation);
 682     }
 683 
 684     /**
 685      * The property holding the document associated to this editor.
 686      * @return the property holding the document associated to this editor.
 687      */
 688     public ObservableValue<FXOMDocument> fxomDocumentProperty() {
 689         return fxomDocumentProperty;
 690     }
 691 
 692     /**
 693      * Returns the document associated to this editor.
 694      *
 695      * @return the document associated to this editor.
 696      */
 697     public FXOMDocument getFxomDocument() {
 698         return fxomDocumentProperty.getValue();
 699     }
 700 
 701     /**
 702      * Returns the tool stylesheet associated to this editor controller.
 703      * Its default value equals to getBuiltinToolStylesheet().
 704      *
 705      * @return the tool stylesheet associated to this editor controller (never null)
 706      */
 707     public String getToolStylesheet() {
 708         return toolStylesheetProperty.getValue();
 709     }
 710 
 711     /**
 712      * Sets the tool stylesheet associated to this editor controller.
 713      * Each panel connected to this editor controller will install this style
 714      * sheet in its root object.
 715      *
 716      * @param stylesheet the tool stylesheet associated to this editor controller (never null)
 717      */
 718     public void setToolStylesheet(String stylesheet) {
 719         assert stylesheet != null;
 720         toolStylesheetProperty.setValue(stylesheet);
 721     }
 722 
 723     /**
 724      * The property holding tool stylesheet associated to this editor controller.
 725      * @return the property holding tool stylesheet associated to this editor controller.
 726      */
 727     public ObservableValue<String> toolStylesheetProperty() {
 728         return toolStylesheetProperty;
 729     }
 730 
 731     /**
 732      * Returns the builtin tool stylesheet.
 733      * This is the default value for EditorController#toolStylesheet property.
 734      *
 735      * @return the builtin tool stylesheet.
 736      */
 737     public static synchronized String getBuiltinToolStylesheet() {
 738         if (builtinToolStylesheet == null) {
 739             final URL url = EditorController.class.getResource("css/Theme.css"); //NOI18N
 740             assert url != null;
 741             builtinToolStylesheet = url.toExternalForm();
 742         }
 743         return builtinToolStylesheet;
 744     }
 745 
 746     /**
 747      * Starts file watching on this editor.
 748      * This editor will now monitor the files referenced by the FXML text
 749      * (like images, medias, stylesheets, included fxmls...) and automatically
 750      * request attached panels to update themselves.
 751      */
 752     public void startFileWatching() {
 753         watchingController.start();
 754     }
 755 
 756     /**
 757      * Stops file watching on this editor.
 758      */
 759     public void stopFileWatching() {
 760         watchingController.stop();
 761     }
 762 
 763     /**
 764      * Returns true if file watching is started on this editor.
 765      *
 766      * @return true if file watching is started on this editor.
 767      */
 768     public boolean isFileWatchingStarted() {
 769         return watchingController.isStarted();
 770     }
 771 
 772     /**
 773      * @treatAsPrivate Returns the selection associated to this editor.
 774      *
 775      * @return  the selection associated to this editor.
 776      */
 777     public Selection getSelection() {
 778         return selection;
 779     }
 780 
 781 
 782     /**
 783      * @treatAsPrivate Returns the job manager associated to this editor.
 784      *
 785      * @return  the job manager associated to this editor.
 786      */
 787     public JobManager getJobManager() {
 788         return jobManager;
 789     }
 790 
 791     /**
 792      * @treatAsPrivate Returns the message log associated to this editor.
 793      *
 794      * @return  the message log associated to this editor.
 795      */
 796     public MessageLog getMessageLog() {
 797         return messageLog;
 798     }
 799 
 800     /**
 801      * @treatAsPrivate Returns the error report associated to this editor.
 802      *
 803      * @return  the error report associated to this editor.
 804      */
 805     public ErrorReport getErrorReport() {
 806         return errorReport;
 807     }
 808 
 809     /**
 810      * @treatAsPrivate Returns the drag controller associated to this editor.
 811      *
 812      * @return the drag controller associated to this editor.
 813      */
 814     public DragController getDragController() {
 815         return dragController;
 816     }
 817 
 818     /**
 819      * @treatAsPrivate Returns the inline edit controller associated to this editor.
 820      *
 821      * @return the inline edit controller associated to this editor.
 822      */
 823     public InlineEditController getInlineEditController() {
 824         return inlineEditController;
 825     }
 826 
 827     /**
 828      * @treatAsPrivate Returns the context menu controller associated to this editor.
 829      *
 830      * @return the context menu controller associated to this editor.
 831      */
 832     public ContextMenuController getContextMenuController() {
 833         return contextMenuController;
 834     }
 835 
 836     /**
 837      * Returns true if the undo action is permitted (ie there is something
 838      * to be undone).
 839      *
 840      * @return true if the undo action is permitted.
 841      */
 842     public boolean canUndo() {
 843         return jobManager.canUndo();
 844     }
 845 
 846     /**
 847      * Returns null or the description of the action to be undone.
 848      *
 849      * @return null or the description of the action to be undone.
 850      */
 851     public String getUndoDescription() {
 852         return jobManager.getUndoDescription();
 853     }
 854 
 855     /**
 856      * Performs the undo action.
 857      */
 858     public void undo() {
 859         jobManager.undo();
 860         assert getFxomDocument().isUpdateOnGoing() == false;
 861     }
 862 
 863     /**
 864      * Returns true if the redo action is permitted (ie there is something
 865      * to be redone).
 866      *
 867      * @return true if the redo action is permitted.
 868      */
 869     public boolean canRedo() {
 870         return jobManager.canRedo();
 871     }
 872 
 873     /**
 874      * Returns null or the description of the action to be redone.
 875      *
 876      * @return null or the description of the action to be redone.
 877      */
 878     public String getRedoDescription() {
 879         return jobManager.getRedoDescription();
 880     }
 881 
 882     /**
 883      * Performs the redo action.
 884      */
 885     public void redo() {
 886         jobManager.redo();
 887         assert getFxomDocument().isUpdateOnGoing() == false;
 888     }
 889 
 890     /**
 891      * Clears the undo/redo stack of this editor controller.
 892      */
 893     public void clearUndoRedo() {
 894         jobManager.clear();
 895     }
 896 
 897     /**
 898      * Performs an edit action.
 899      *
 900      * @param editAction the edit action to be performed.
 901      */
 902     public void performEditAction(EditAction editAction) {
 903         switch(editAction) {
 904             case ADD_CONTEXT_MENU: {
 905                 performAddContextMenu();
 906                 break;
 907             }
 908             case ADD_TOOLTIP: {
 909                 performAddTooltip();
 910                 break;
 911             }
 912             case ADD_COLUMN_BEFORE: {
 913                 final AddColumnJob job = new AddColumnJob(this, Position.BEFORE);
 914                 jobManager.push(job);
 915                 break;
 916             }
 917             case ADD_COLUMN_AFTER: {
 918                 final AddColumnJob job = new AddColumnJob(this, Position.AFTER);
 919                 jobManager.push(job);
 920                 break;
 921             }
 922             case ADD_ROW_ABOVE: {
 923                 final AddRowJob job = new AddRowJob(this, Position.ABOVE);
 924                 jobManager.push(job);
 925                 break;
 926             }
 927             case ADD_ROW_BELOW: {
 928                 final AddRowJob job = new AddRowJob(this, Position.BELOW);
 929                 jobManager.push(job);
 930                 break;
 931             }
 932             case BRING_FORWARD: {
 933                 final BringForwardJob job = new BringForwardJob(this);
 934                 jobManager.push(job);
 935                 break;
 936             }
 937             case BRING_TO_FRONT: {
 938                 final BringToFrontJob job = new BringToFrontJob(this);
 939                 jobManager.push(job);
 940                 break;
 941             }
 942             case CUT: {
 943                 final CutSelectionJob job = new CutSelectionJob(this);
 944                 jobManager.push(job);
 945                 break;
 946             }
 947             case DECREASE_COLUMN_SPAN: {
 948                 final SpanJob job = new SpanJob(this, EditAction.DECREASE_COLUMN_SPAN);
 949                 jobManager.push(job);
 950                 break;
 951             }
 952             case DECREASE_ROW_SPAN: {
 953                 final SpanJob job = new SpanJob(this, EditAction.DECREASE_ROW_SPAN);
 954                 jobManager.push(job);
 955                 break;
 956             }
 957             case DELETE: {
 958                 final DeleteSelectionJob job = new DeleteSelectionJob(this);
 959                 jobManager.push(job);
 960                 break;
 961             }
 962             case DUPLICATE: {
 963                 final DuplicateSelectionJob job = new DuplicateSelectionJob(this);
 964                 jobManager.push(job);
 965                 break;
 966             }
 967             case FIT_TO_PARENT: {
 968                 final FitToParentSelectionJob job
 969                         = new FitToParentSelectionJob(this);
 970                 jobManager.push(job);
 971                 break;
 972             }
 973             case INCREASE_COLUMN_SPAN: {
 974                 final SpanJob job = new SpanJob(this, EditAction.INCREASE_COLUMN_SPAN);
 975                 jobManager.push(job);
 976                 break;
 977             }
 978             case INCREASE_ROW_SPAN: {
 979                 final SpanJob job = new SpanJob(this, EditAction.INCREASE_ROW_SPAN);
 980                 jobManager.push(job);
 981                 break;
 982             }
 983             case MOVE_COLUMN_BEFORE: {
 984                 final MoveColumnJob job = new MoveColumnJob(this, Position.BEFORE);
 985                 jobManager.push(job);
 986                 break;
 987             }
 988             case MOVE_COLUMN_AFTER: {
 989                 final MoveColumnJob job = new MoveColumnJob(this, Position.AFTER);
 990                 jobManager.push(job);
 991                 break;
 992             }
 993             case MOVE_ROW_ABOVE: {
 994                 final MoveRowJob job = new MoveRowJob(this, Position.ABOVE);
 995                 jobManager.push(job);
 996                 break;
 997             }
 998             case MOVE_ROW_BELOW: {
 999                 final MoveRowJob job = new MoveRowJob(this, Position.BELOW);
1000                 jobManager.push(job);
1001                 break;
1002             }
1003             case PASTE: {
1004                 final PasteJob job = new PasteJob(this);
1005                 jobManager.push(job);
1006                 break;
1007             }
1008             case PASTE_INTO: {
1009                 final PasteIntoJob job = new PasteIntoJob(this);
1010                 jobManager.push(job);
1011                 break;
1012             }
1013             case SEND_BACKWARD: {
1014                 final SendBackwardJob job = new SendBackwardJob(this);
1015                 jobManager.push(job);
1016                 break;
1017             }
1018             case SEND_TO_BACK: {
1019                 final SendToBackJob job = new SendToBackJob(this);
1020                 jobManager.push(job);
1021                 break;
1022             }
1023             case SET_SIZE_320x240: {
1024                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_320x240);
1025                 jobManager.push(job);
1026                 break;
1027             }
1028             case SET_SIZE_640x480: {
1029                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_640x480);
1030                 jobManager.push(job);
1031                 break;
1032             }
1033             case SET_SIZE_1280x800: {
1034                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_1280x800);
1035                 jobManager.push(job);
1036                 break;
1037             }
1038             case SET_SIZE_1920x1080: {
1039                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_1920x1080);
1040                 jobManager.push(job);
1041                 break;
1042             }
1043             case TRIM: {
1044                 final TrimSelectionJob job = new TrimSelectionJob(this);
1045                 jobManager.push(job);
1046                 break;
1047             }
1048             case UNWRAP: {
1049                 final UnwrapJob job = new UnwrapJob(this);
1050                 jobManager.push(job);
1051                 break;
1052             }
1053             case USE_COMPUTED_SIZES: {
1054                 final UseComputedSizesSelectionJob job
1055                         = new UseComputedSizesSelectionJob(this);
1056                 jobManager.push(job);
1057                 break;
1058             }
1059             case WRAP_IN_ANCHOR_PANE: {
1060                 performWrap(javafx.scene.layout.AnchorPane.class);
1061                 break;
1062             }
1063             case WRAP_IN_BORDER_PANE: {
1064                 performWrap(javafx.scene.layout.BorderPane.class);
1065                 break;
1066             }
1067             case WRAP_IN_BUTTON_BAR: {
1068                 performWrap(javafx.scene.control.ButtonBar.class);
1069                 break;
1070             }
1071             case WRAP_IN_DIALOG_PANE: {
1072                 performWrap(javafx.scene.control.DialogPane.class);
1073                 break;
1074             }
1075             case WRAP_IN_FLOW_PANE: {
1076                 performWrap(javafx.scene.layout.FlowPane.class);
1077                 break;
1078             }
1079             case WRAP_IN_GRID_PANE: {
1080                 performWrap(javafx.scene.layout.GridPane.class);
1081                 break;
1082             }
1083             case WRAP_IN_GROUP: {
1084                 performWrap(javafx.scene.Group.class);
1085                 break;
1086             }
1087             case WRAP_IN_HBOX: {
1088                 performWrap(javafx.scene.layout.HBox.class);
1089                 break;
1090             }
1091             case WRAP_IN_PANE: {
1092                 performWrap(javafx.scene.layout.Pane.class);
1093                 break;
1094             }
1095             case WRAP_IN_SCROLL_PANE: {
1096                 performWrap(javafx.scene.control.ScrollPane.class);
1097                 break;
1098             }
1099             case WRAP_IN_SPLIT_PANE: {
1100                 performWrap(javafx.scene.control.SplitPane.class);
1101                 break;
1102             }
1103             case WRAP_IN_STACK_PANE: {
1104                 performWrap(javafx.scene.layout.StackPane.class);
1105                 break;
1106             }
1107             case WRAP_IN_TAB_PANE: {
1108                 performWrap(javafx.scene.control.TabPane.class);
1109                 break;
1110             }
1111             case WRAP_IN_TEXT_FLOW: {
1112                 performWrap(javafx.scene.text.TextFlow.class);
1113                 break;
1114             }
1115             case WRAP_IN_TILE_PANE: {
1116                 performWrap(javafx.scene.layout.TilePane.class);
1117                 break;
1118             }
1119             case WRAP_IN_TITLED_PANE: {
1120                 performWrap(javafx.scene.control.TitledPane.class);
1121                 break;
1122             }
1123             case WRAP_IN_TOOL_BAR: {
1124                 performWrap(javafx.scene.control.ToolBar.class);
1125                 break;
1126             }
1127             case WRAP_IN_VBOX: {
1128                 performWrap(javafx.scene.layout.VBox.class);
1129                 break;
1130             }
1131             default:
1132                 throw new UnsupportedOperationException("Not yet implemented"); //NOI18N
1133         }
1134         assert getFxomDocument().isUpdateOnGoing() == false;
1135     }
1136 
1137     /**
1138      * Returns true if the specified edit action is permitted.
1139      *
1140      * @param editAction the edit action to be tested.
1141      * @return true if the specified edit action is permitted.
1142      */
1143     public boolean canPerformEditAction(EditAction editAction) {
1144         final boolean result;
1145         switch(editAction) {
1146             case ADD_CONTEXT_MENU: {
1147                 result = canPerformAddContextMenu();
1148                 break;
1149             }
1150             case ADD_TOOLTIP: {
1151                 result = canPerformAddTooltip();
1152                 break;
1153             }
1154             case ADD_COLUMN_BEFORE: {
1155                 final AddColumnJob job = new AddColumnJob(this, Position.BEFORE);
1156                 result = job.isExecutable();
1157                 break;
1158             }
1159             case ADD_COLUMN_AFTER: {
1160                 final AddColumnJob job = new AddColumnJob(this, Position.AFTER);
1161                 result = job.isExecutable();
1162                 break;
1163             }
1164             case ADD_ROW_ABOVE: {
1165                 final AddRowJob job = new AddRowJob(this, Position.ABOVE);
1166                 result = job.isExecutable();
1167                 break;
1168             }
1169             case ADD_ROW_BELOW: {
1170                 final AddRowJob job = new AddRowJob(this, Position.BELOW);
1171                 result = job.isExecutable();
1172                 break;
1173             }
1174             case BRING_FORWARD: {
1175                 final BringForwardJob job = new BringForwardJob(this);
1176                 result = job.isExecutable();
1177                 break;
1178             }
1179             case BRING_TO_FRONT: {
1180                 final BringToFrontJob job = new BringToFrontJob(this);
1181                 result = job.isExecutable();
1182                 break;
1183             }
1184             case CUT: {
1185                 final CutSelectionJob job = new CutSelectionJob(this);
1186                 result = job.isExecutable();
1187                 break;
1188             }
1189             case DECREASE_COLUMN_SPAN: {
1190                 final SpanJob job = new SpanJob(this, EditAction.DECREASE_COLUMN_SPAN);
1191                 result = job.isExecutable();
1192                 break;
1193             }
1194             case DECREASE_ROW_SPAN: {
1195                 final SpanJob job = new SpanJob(this, EditAction.DECREASE_ROW_SPAN);
1196                 result = job.isExecutable();
1197                 break;
1198             }
1199             case DELETE: {
1200                 final DeleteSelectionJob job = new DeleteSelectionJob(this);
1201                 result = job.isExecutable();
1202                 break;
1203             }
1204             case DUPLICATE: {
1205                 final DuplicateSelectionJob job = new DuplicateSelectionJob(this);
1206                 result = job.isExecutable();
1207                 break;
1208             }
1209             case FIT_TO_PARENT: {
1210                 final FitToParentSelectionJob job
1211                         = new FitToParentSelectionJob(this);
1212                 result = job.isExecutable();
1213                 break;
1214             }
1215             case INCREASE_COLUMN_SPAN: {
1216                 final SpanJob job = new SpanJob(this, EditAction.INCREASE_COLUMN_SPAN);
1217                 result = job.isExecutable();
1218                 break;
1219             }
1220             case INCREASE_ROW_SPAN: {
1221                 final SpanJob job = new SpanJob(this, EditAction.INCREASE_ROW_SPAN);
1222                 result = job.isExecutable();
1223                 break;
1224             }
1225             case MOVE_COLUMN_BEFORE: {
1226                 final MoveColumnJob job = new MoveColumnJob(this, Position.BEFORE);
1227                 result = job.isExecutable();
1228                 break;
1229             }
1230             case MOVE_COLUMN_AFTER: {
1231                 final MoveColumnJob job = new MoveColumnJob(this, Position.AFTER);
1232                 result = job.isExecutable();
1233                 break;
1234             }
1235             case MOVE_ROW_ABOVE: {
1236                 final MoveRowJob job = new MoveRowJob(this, Position.ABOVE);
1237                 result = job.isExecutable();
1238                 break;
1239             }
1240             case MOVE_ROW_BELOW: {
1241                 final MoveRowJob job = new MoveRowJob(this, Position.BELOW);
1242                 result = job.isExecutable();
1243                 break;
1244             }
1245             case PASTE: {
1246                 final PasteJob job = new PasteJob(this);
1247                 result = job.isExecutable();
1248                 break;
1249             }
1250             case PASTE_INTO: {
1251                 final PasteIntoJob job = new PasteIntoJob(this);
1252                 result = job.isExecutable();
1253                 break;
1254             }
1255             case SEND_BACKWARD: {
1256                 final SendBackwardJob job = new SendBackwardJob(this);
1257                 result = job.isExecutable();
1258                 break;
1259             }
1260             case SEND_TO_BACK: {
1261                 final SendToBackJob job = new SendToBackJob(this);
1262                 result = job.isExecutable();
1263                 break;
1264             }
1265             case SET_SIZE_320x240: {
1266                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_320x240);
1267                 result = job.isExecutable();
1268                 break;
1269             }
1270             case SET_SIZE_640x480: {
1271                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_640x480);
1272                 result = job.isExecutable();
1273                 break;
1274             }
1275             case SET_SIZE_1280x800: {
1276                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_1280x800);
1277                 result = job.isExecutable();
1278                 break;
1279             }
1280             case SET_SIZE_1920x1080: {
1281                 final UsePredefinedSizeJob job = new UsePredefinedSizeJob(this, Size.SIZE_1920x1080);
1282                 result = job.isExecutable();
1283                 break;
1284             }
1285             case TRIM: {
1286                 final TrimSelectionJob job = new TrimSelectionJob(this);
1287                 result = job.isExecutable();
1288                 break;
1289             }
1290             case UNWRAP: {
1291                 final UnwrapJob job = new UnwrapJob(this);
1292                 result = job.isExecutable();
1293                 break;
1294             }
1295             case USE_COMPUTED_SIZES: {
1296                 final UseComputedSizesSelectionJob job
1297                         = new UseComputedSizesSelectionJob(this);
1298                 result = job.isExecutable();
1299                 break;
1300             }
1301             case WRAP_IN_ANCHOR_PANE: {
1302                 result = canPerformWrap(javafx.scene.layout.AnchorPane.class);
1303                 break;
1304             }
1305             case WRAP_IN_BORDER_PANE: {
1306                 result = canPerformWrap(javafx.scene.layout.BorderPane.class);
1307                 break;
1308             }
1309             case WRAP_IN_BUTTON_BAR: {
1310                 result = canPerformWrap(javafx.scene.control.ButtonBar.class);
1311                 break;
1312             }
1313             case WRAP_IN_DIALOG_PANE: {
1314                 result = canPerformWrap(javafx.scene.control.DialogPane.class);
1315                 break;
1316             }
1317             case WRAP_IN_FLOW_PANE: {
1318                 result = canPerformWrap(javafx.scene.layout.FlowPane.class);
1319                 break;
1320             }
1321             case WRAP_IN_GRID_PANE: {
1322                 result = canPerformWrap(javafx.scene.layout.GridPane.class);
1323                 break;
1324             }
1325             case WRAP_IN_GROUP: {
1326                 result = canPerformWrap(javafx.scene.Group.class);
1327                 break;
1328             }
1329             case WRAP_IN_HBOX: {
1330                 result = canPerformWrap(javafx.scene.layout.HBox.class);
1331                 break;
1332             }
1333             case WRAP_IN_PANE: {
1334                 result = canPerformWrap(javafx.scene.layout.Pane.class);
1335                 break;
1336             }
1337             case WRAP_IN_SCROLL_PANE: {
1338                 result = canPerformWrap(javafx.scene.control.ScrollPane.class);
1339                 break;
1340             }
1341             case WRAP_IN_SPLIT_PANE: {
1342                 result = canPerformWrap(javafx.scene.control.SplitPane.class);
1343                 break;
1344             }
1345             case WRAP_IN_STACK_PANE: {
1346                 result = canPerformWrap(javafx.scene.layout.StackPane.class);
1347                 break;
1348             }
1349             case WRAP_IN_TAB_PANE: {
1350                 result = canPerformWrap(javafx.scene.control.TabPane.class);
1351                 break;
1352             }
1353             case WRAP_IN_TEXT_FLOW: {
1354                 result = canPerformWrap(javafx.scene.text.TextFlow.class);
1355                 break;
1356             }
1357             case WRAP_IN_TILE_PANE: {
1358                 result = canPerformWrap(javafx.scene.layout.TilePane.class);
1359                 break;
1360             }
1361             case WRAP_IN_TITLED_PANE: {
1362                 result = canPerformWrap(javafx.scene.control.TitledPane.class);
1363                 break;
1364             }
1365             case WRAP_IN_TOOL_BAR: {
1366                 result = canPerformWrap(javafx.scene.control.ToolBar.class);
1367                 break;
1368             }
1369             case WRAP_IN_VBOX: {
1370                 result = canPerformWrap(javafx.scene.layout.VBox.class);
1371                 break;
1372             }
1373             default:
1374                 result = false;
1375                 break;
1376         }
1377 
1378         return result;
1379     }
1380 
1381     /**
1382      * Performs the specified control action.
1383      *
1384      * @param controlAction the control action to be performed.
1385      */
1386     public void performControlAction(ControlAction controlAction) {
1387         switch(controlAction) {
1388             case COPY: {
1389                 performCopy();
1390                 break;
1391             }
1392             case EDIT_INCLUDED_FILE: {
1393                 performEditIncludedFile();
1394                 break;
1395             }
1396             case REVEAL_INCLUDED_FILE: {
1397                 performRevealIncludedFile();
1398                 break;
1399             }
1400             case SELECT_ALL: {
1401                 performSelectAll();
1402                 break;
1403             }
1404             case SELECT_NONE: {
1405                 performSelectNone();
1406                 break;
1407             }
1408             case SELECT_PARENT: {
1409                 performSelectParent();
1410                 break;
1411             }
1412             case SELECT_NEXT: {
1413                 performSelectNext();
1414                 break;
1415             }
1416             case SELECT_PREVIOUS: {
1417                 performSelectPrevious();
1418                 break;
1419             }
1420             case TOGGLE_SAMPLE_DATA: {
1421                 setSampleDataEnabled( ! isSampleDataEnabled());
1422                 break;
1423             }
1424             default:
1425                 throw new UnsupportedOperationException("Not yet implemented"); //NOI18N
1426         }
1427     }
1428 
1429     /**
1430      * Returns true if the specified control action is permitted.
1431      *
1432      * @param controlAction the control action to be tested.
1433      * @return true if the specified control action is permitted.
1434      */
1435     public boolean canPerformControlAction(ControlAction controlAction) {
1436         final boolean result;
1437 
1438         // If there is no document loaded, we cannot perform control actions
1439         if (getFxomDocument() == null || getFxomDocument().getFxomRoot() == null) {
1440             return false;
1441         }
1442         switch(controlAction) {
1443             case COPY: {
1444                 result = canPerformCopy();
1445                 break;
1446             }
1447             case EDIT_INCLUDED_FILE:
1448             case REVEAL_INCLUDED_FILE: {
1449                 result = canPerformIncludedFileAction();
1450                 break;
1451             }
1452             case SELECT_ALL: {
1453                 result = canPerformSelectAll();
1454                 break;
1455             }
1456             case SELECT_NONE: {
1457                 result = canPerformSelectNone();
1458                 break;
1459             }
1460             case SELECT_PARENT: {
1461                 result = canPerformSelectParent();
1462                 break;
1463             }
1464             case SELECT_NEXT: {
1465                 result = canPerformSelectNext();
1466                 break;
1467             }
1468             case SELECT_PREVIOUS: {
1469                 result = canPerformSelectPrevious();
1470                 break;
1471             }
1472             case TOGGLE_SAMPLE_DATA: {
1473                 result = true;
1474                 break;
1475             }
1476             default:
1477                 result = false;
1478                 break;
1479         }
1480 
1481         return result;
1482     }
1483 
1484     /**
1485      * Performs the 'import' FXML edit action.
1486      * This action creates an object matching the root node of the selected
1487      * FXML file and insert it in the document (either as root if the document
1488      * is empty or under the selection common ancestor node otherwise).
1489      *
1490      * @param fxmlFile the FXML file to be imported
1491      */
1492     public void performImportFxml(File fxmlFile) {
1493         performImport(fxmlFile);
1494     }
1495 
1496     /**
1497      * Performs the 'import' media edit action.
1498      * This action creates an object matching the type of the selected
1499      * media file (either ImageView or MediaView) and insert it in the document
1500      * (either as root if the document is empty or under the selection common
1501      * ancestor node otherwise).
1502      *
1503      * @param mediaFile the media file to be imported
1504      */
1505     public void performImportMedia(File mediaFile) {
1506         performImport(mediaFile);
1507     }
1508 
1509     private void performImport(File file) {
1510         final ImportFileJob job = new ImportFileJob(file, this);
1511         if (job.isExecutable()) {
1512             jobManager.push(job);
1513         } else {
1514             final String target;
1515             if (job.getTargetObject() == null) {
1516                 target = null;
1517             } else {
1518                 final Object sceneGraphTarget
1519                         = job.getTargetObject().getSceneGraphObject();
1520                 if (sceneGraphTarget == null) {
1521                     target = null;
1522                 } else {
1523                     target = sceneGraphTarget.getClass().getSimpleName();
1524                 }
1525             }
1526             if (target != null) {
1527                 getMessageLog().logWarningMessage(
1528                         "import.from.file.failed.target",
1529                         file.getName(), target);
1530             } else {
1531                 getMessageLog().logWarningMessage(
1532                         "import.from.file.failed",
1533                         file.getName());
1534             }
1535         }
1536     }
1537 
1538     /**
1539      * Performs the 'include' FXML edit action.
1540      * As opposed to the 'import' edit action, the 'include' action does not
1541      * copy the FXML content but adds an fx:include element to the FXML document.
1542      *
1543      * @param fxmlFile the FXML file to be included
1544      */
1545     public void performIncludeFxml(File fxmlFile) {
1546         final IncludeFileJob job = new IncludeFileJob(fxmlFile, this);
1547         if (job.isExecutable()) {
1548             jobManager.push(job);
1549         } else {
1550             final String target;
1551             if (job.getTargetObject() == null) {
1552                 target = null;
1553             } else {
1554                 final Object sceneGraphTarget
1555                         = job.getTargetObject().getSceneGraphObject();
1556                 if (sceneGraphTarget == null) {
1557                     target = null;
1558                 } else {
1559                     target = sceneGraphTarget.getClass().getSimpleName();
1560                 }
1561             }
1562             if (target != null) {
1563                 getMessageLog().logWarningMessage(
1564                         "include.file.failed.target",
1565                         fxmlFile.getName(), target);
1566             } else {
1567                 getMessageLog().logWarningMessage(
1568                         "include.file.failed",
1569                         fxmlFile.getName());
1570             }
1571         }
1572     }
1573 
1574     /**
1575      * Performs the 'insert' edit action. This action creates an object
1576      * matching the specified library item and insert it in the document
1577      * (according the selection state).
1578      *
1579      * @param libraryItem the library item describing the object to be inserted.
1580      */
1581     public void performInsert(LibraryItem libraryItem) {
1582         final Job job;
1583         final FXOMObject target;
1584 
1585         assert canPerformInsert(libraryItem); // (1)
1586 
1587         final FXOMDocument newItemDocument = libraryItem.instantiate();
1588         assert newItemDocument != null; // Because (1)
1589         final FXOMObject newObject = newItemDocument.getFxomRoot();
1590         assert newObject != null;
1591         newObject.moveToFxomDocument(getFxomDocument());
1592         final FXOMObject rootObject = getFxomDocument().getFxomRoot();
1593         if (rootObject == null) { // Empty document
1594             final String description
1595                     = I18N.getString("drop.job.insert.library.item", libraryItem.getName());
1596             job = new SetDocumentRootJob(newObject, true /* usePredefinedSize */, description, this);
1597 
1598         } else {
1599             if (selection.isEmpty() || selection.isSelected(rootObject)) {
1600                 // No selection or root is selected -> we insert below root
1601                 target = rootObject;
1602             } else {
1603                 // Let's use the common parent of the selected objects.
1604                 // It might be null if selection holds some non FXOMObject entries
1605                 target = selection.getAncestor();
1606             }
1607             job = new InsertAsSubComponentJob(newObject, target, -1, this);
1608         }
1609 
1610         jobManager.push(job);
1611     }
1612 
1613     /**
1614      * Returns true if the 'insert' action is permitted with the specified
1615      * library item.
1616      *
1617      * @param libraryItem the library item describing the object to be inserted.
1618      * @return true if the 'insert' action is permitted.
1619      */
1620     public boolean canPerformInsert(LibraryItem libraryItem) {
1621         final FXOMObject targetCandidate;
1622         final boolean result;
1623 
1624         if (getFxomDocument() == null) {
1625             result = false;
1626         } else {
1627             assert (libraryItem.getLibrary().getClassLoader() == null)
1628                     || (libraryItem.getLibrary().getClassLoader() == getFxomDocument().getClassLoader());
1629             final FXOMDocument newItemDocument = libraryItem.instantiate();
1630             if (newItemDocument == null) {
1631                 // For some reason, library is unable to instantiate this item
1632                 result = false;
1633             } else {
1634                 final FXOMObject newItemRoot = newItemDocument.getFxomRoot();
1635                 newItemRoot.moveToFxomDocument(getFxomDocument());
1636                 assert newItemDocument.getFxomRoot() == null;
1637                 final FXOMObject rootObject = getFxomDocument().getFxomRoot();
1638                 if (rootObject == null) { // Empty document
1639                     final SetDocumentRootJob job = new SetDocumentRootJob(
1640                             newItemRoot, true /* usePredefinedSize */, "unused", this); //NOI18N
1641                     result = job.isExecutable();
1642                 } else {
1643                     if (selection.isEmpty() || selection.isSelected(rootObject)) {
1644                         // No selection or root is selected -> we insert below root
1645                         targetCandidate = rootObject;
1646                     } else {
1647                         // Let's use the common parent of the selected objects.
1648                         // It might be null if selection holds some non FXOMObject entries
1649                         targetCandidate = selection.getAncestor();
1650                     }
1651                     final InsertAsSubComponentJob job = new InsertAsSubComponentJob(
1652                             newItemRoot, targetCandidate, -1, this);
1653                     result = job.isExecutable();
1654                 }
1655             }
1656         }
1657 
1658         return result;
1659     }
1660 
1661     /**
1662      * Performs the 'wrap' edit action. This action creates an object
1663      * matching the specified class and reparent all the selected objects
1664      * below this new object.
1665      *
1666      * @param wrappingClass the wrapping class
1667      */
1668     public void performWrap(Class<? extends Parent> wrappingClass) {
1669         assert canPerformWrap(wrappingClass);
1670         final AbstractWrapInJob job = AbstractWrapInJob.getWrapInJob(this, wrappingClass);
1671         jobManager.push(job);
1672     }
1673 
1674     /**
1675      * Returns true if the 'wrap' action is permitted with the specified class.
1676      *
1677      * @param wrappingClass the wrapping class.
1678      * @return true if the 'wrap' action is permitted.
1679      */
1680     public boolean canPerformWrap(Class<? extends Parent> wrappingClass) {
1681         if (getClassesSupportingWrapping().contains(wrappingClass) == false) {
1682             return false;
1683         }
1684         final AbstractWrapInJob job = AbstractWrapInJob.getWrapInJob(this, wrappingClass);
1685         return job.isExecutable();
1686     }
1687 
1688     private static List<Class<? extends Parent>> classesSupportingWrapping;
1689 
1690     /**
1691      * Return the list of classes that can be passed to
1692      * {@link EditorController#performWrap(java.lang.Class)}.
1693      *
1694      * @return the list of classes.
1695      */
1696     public synchronized static Collection<Class<? extends Parent>> getClassesSupportingWrapping() {
1697         if (classesSupportingWrapping == null) {
1698             classesSupportingWrapping = new ArrayList<>();
1699             classesSupportingWrapping.add(javafx.scene.layout.AnchorPane.class);
1700             classesSupportingWrapping.add(javafx.scene.layout.BorderPane.class);
1701             classesSupportingWrapping.add(javafx.scene.control.ButtonBar.class);
1702             classesSupportingWrapping.add(javafx.scene.control.DialogPane.class);
1703             classesSupportingWrapping.add(javafx.scene.layout.FlowPane.class);
1704             classesSupportingWrapping.add(javafx.scene.layout.GridPane.class);
1705             classesSupportingWrapping.add(javafx.scene.Group.class);
1706             classesSupportingWrapping.add(javafx.scene.layout.HBox.class);
1707             classesSupportingWrapping.add(javafx.scene.layout.Pane.class);
1708             classesSupportingWrapping.add(javafx.scene.control.ScrollPane.class);
1709             classesSupportingWrapping.add(javafx.scene.control.SplitPane.class);
1710             classesSupportingWrapping.add(javafx.scene.layout.StackPane.class);
1711             classesSupportingWrapping.add(javafx.scene.control.TabPane.class);
1712             classesSupportingWrapping.add(javafx.scene.text.TextFlow.class);
1713             classesSupportingWrapping.add(javafx.scene.layout.TilePane.class);
1714             classesSupportingWrapping.add(javafx.scene.control.TitledPane.class);
1715             classesSupportingWrapping.add(javafx.scene.control.ToolBar.class);
1716             classesSupportingWrapping.add(javafx.scene.layout.VBox.class);
1717             classesSupportingWrapping = Collections.unmodifiableList(classesSupportingWrapping);
1718         }
1719 
1720         return classesSupportingWrapping;
1721     }
1722 
1723     /**
1724      * Performs the copy control action.
1725      */
1726     private void performCopy() {
1727         assert canPerformCopy(); // (1)
1728         assert selection.getGroup() instanceof ObjectSelectionGroup; // Because of (1)
1729         final ObjectSelectionGroup osg = (ObjectSelectionGroup) selection.getGroup();
1730 
1731         final ClipboardEncoder encoder = new ClipboardEncoder(osg.getSortedItems());
1732         assert encoder.isEncodable();
1733         Clipboard.getSystemClipboard().setContent(encoder.makeEncoding());
1734     }
1735 
1736     /**
1737      * Returns true if the selection is not empty.
1738      *
1739      * @return if the selection is not empty.
1740      */
1741     private boolean canPerformCopy() {
1742         return selection.getGroup() instanceof ObjectSelectionGroup;
1743     }
1744 
1745     /**
1746      * Performs the select all control action.
1747      * Select all sub components of the selection common ancestor.
1748      */
1749     private void performSelectAll() {
1750         assert canPerformSelectAll(); // (1)
1751         final FXOMObject rootObject = getFxomDocument().getFxomRoot();
1752         if (selection.isEmpty()) { // (1)
1753             // If the current selection is empty, we select the root object
1754             selection.select(rootObject);
1755         } else if (selection.getGroup() instanceof ObjectSelectionGroup) {
1756             // Otherwise, select all sub components of the common ancestor ??
1757             final FXOMObject ancestor = selection.getAncestor();
1758             assert ancestor != null; // Because of (1)
1759             final DesignHierarchyMask mask = new DesignHierarchyMask(ancestor);
1760             final Set<FXOMObject> selectableObjects = new HashSet<>();
1761             // BorderPane special case : use accessories
1762             if (mask.getFxomObject().getSceneGraphObject() instanceof BorderPane) {
1763                 final FXOMObject top = mask.getAccessory(Accessory.TOP);
1764                 final FXOMObject left = mask.getAccessory(Accessory.LEFT);
1765                 final FXOMObject center = mask.getAccessory(Accessory.CENTER);
1766                 final FXOMObject right = mask.getAccessory(Accessory.RIGHT);
1767                 final FXOMObject bottom = mask.getAccessory(Accessory.BOTTOM);
1768                 for (FXOMObject accessoryObject : new FXOMObject[]{
1769                     top, left, center, right, bottom}) {
1770                     if (accessoryObject != null) {
1771                         selectableObjects.add(accessoryObject);
1772                     }
1773                 }
1774             } else {
1775                 assert mask.isAcceptingSubComponent(); // Because of (1)
1776                 selectableObjects.addAll(mask.getSubComponents());
1777             }
1778             selection.select(selectableObjects);
1779         } else if (selection.getGroup() instanceof GridSelectionGroup) {
1780             // Select ALL rows / columns
1781             final GridSelectionGroup gsg = (GridSelectionGroup) selection.getGroup();
1782             final FXOMObject gridPane = gsg.getParentObject();
1783             assert gridPane instanceof FXOMInstance;
1784             final DesignHierarchyMask gridPaneMask = new DesignHierarchyMask(gridPane);
1785             int size = 0;
1786             switch (gsg.getType()) {
1787                 case ROW:
1788                     size = gridPaneMask.getRowsSize();
1789                     break;
1790                 case COLUMN:
1791                     size = gridPaneMask.getColumnsSize();
1792                     break;
1793                 default:
1794                     assert false;
1795                     break;
1796             }
1797             // Select first index
1798             selection.select((FXOMInstance) gridPane, gsg.getType(), 0);
1799             for (int index = 1; index < size; index++) {
1800                 selection.toggleSelection((FXOMInstance) gridPane, gsg.getType(), index);
1801             }
1802         } else {
1803             assert selection.getGroup() == null :
1804                     "Add implementation for " + selection.getGroup(); //NOI18N
1805 
1806         }
1807     }
1808 
1809     /**
1810      * Returns true if the root object is not selected and if the sub components
1811      * of the selection common ancestor are not all already selected.
1812      *
1813      * @return if the root object is not selected and if the sub components of
1814      * the selection common ancestor are not all already selected.
1815      */
1816     private boolean canPerformSelectAll() {
1817         assert getFxomDocument() != null && getFxomDocument().getFxomRoot() != null;
1818         if (selection.isEmpty()) { // (1)
1819             return true;
1820         } else if (selection.getGroup() instanceof ObjectSelectionGroup) {
1821             final FXOMObject rootObject = getFxomDocument().getFxomRoot();
1822             // Cannot select all if root is selected
1823             if (selection.isSelected(rootObject)) { // (1)
1824                 return false;
1825             } else {
1826                 // Cannot select all if all sub components are already selected
1827                 final FXOMObject ancestor = selection.getAncestor();
1828                 assert ancestor != null; // Because of (1)
1829                 final DesignHierarchyMask mask = new DesignHierarchyMask(ancestor);
1830                 // BorderPane special case : use accessories
1831                 if (mask.getFxomObject().getSceneGraphObject() instanceof BorderPane) {
1832                     final FXOMObject top = mask.getAccessory(Accessory.TOP);
1833                     final FXOMObject left = mask.getAccessory(Accessory.LEFT);
1834                     final FXOMObject center = mask.getAccessory(Accessory.CENTER);
1835                     final FXOMObject right = mask.getAccessory(Accessory.RIGHT);
1836                     final FXOMObject bottom = mask.getAccessory(Accessory.BOTTOM);
1837                     for (FXOMObject bpAccessoryObject : new FXOMObject[] {
1838                         top, left, center, right, bottom}) {
1839                         if (bpAccessoryObject != null
1840                                 && selection.isSelected(bpAccessoryObject) == false) {
1841                             return true;
1842                         }
1843                     }
1844                 } else if (mask.isAcceptingSubComponent()) {
1845                     for (FXOMObject subComponentObject : mask.getSubComponents()) {
1846                         if (selection.isSelected(subComponentObject) == false) {
1847                             return true;
1848                         }
1849                     }
1850                 }
1851             }
1852         } else if (selection.getGroup() instanceof GridSelectionGroup) {
1853             final GridSelectionGroup gsg = (GridSelectionGroup) selection.getGroup();
1854             // GridSelectionGroup => at least 1 row/column is selected
1855             assert gsg.getIndexes().isEmpty() == false;
1856             return true;
1857         } else {
1858             assert selection.getGroup() == null :
1859                     "Add implementation for " + selection.getGroup(); //NOI18N
1860         }
1861         return false;
1862     }
1863 
1864     /**
1865      * Performs the select parent control action.
1866      * If the selection is multiple, we select the common ancestor.
1867      */
1868     private void performSelectParent() {
1869         assert canPerformSelectParent(); // (1)
1870         final FXOMObject ancestor = selection.getAncestor();
1871         assert ancestor != null; // Because of (1)
1872         selection.select(ancestor);
1873     }
1874 
1875     /**
1876      * Returns true if the selection is not empty and the root object is not
1877      * selected.
1878      *
1879      * @return if the selection is not empty and the root object is not
1880      * selected.
1881      */
1882     private boolean canPerformSelectParent() {
1883         assert getFxomDocument() != null && getFxomDocument().getFxomRoot() != null;
1884         final FXOMObject rootObject = getFxomDocument().getFxomRoot();
1885         return !selection.isEmpty() && !selection.isSelected(rootObject);
1886     }
1887 
1888     /**
1889      * Performs the select next control action.
1890      */
1891     private void performSelectNext() {
1892         assert canPerformSelectNext(); // (1)
1893 
1894         final AbstractSelectionGroup asg = selection.getGroup();
1895         if (asg instanceof ObjectSelectionGroup) {
1896             final ObjectSelectionGroup osg = (ObjectSelectionGroup) asg;
1897             final Set<FXOMObject> items = osg.getItems();
1898             assert items.size() == 1; // Because of (1)
1899             final FXOMObject selectedObject = items.iterator().next();
1900             final FXOMObject nextSibling = selectedObject.getNextSlibing();
1901             assert nextSibling != null; // Because of (1)
1902             selection.select(nextSibling);
1903         } else {
1904             assert asg instanceof GridSelectionGroup; // Because of (1)
1905             final GridSelectionGroup gsg = (GridSelectionGroup) asg;
1906             final FXOMObject gridPane = gsg.getParentObject();
1907             final DesignHierarchyMask mask = new DesignHierarchyMask(gridPane);
1908             assert gridPane instanceof FXOMInstance;
1909             final Set<Integer> indexes = gsg.getIndexes();
1910             assert indexes.size() == 1; // Because of (1)
1911             int selectedIndex = indexes.iterator().next();
1912             int nextIndex = selectedIndex + 1;
1913             int size = 0;
1914             switch (gsg.getType()) {
1915                 case ROW:
1916                     size = mask.getRowsSize();
1917                     break;
1918                 case COLUMN:
1919                     size = mask.getColumnsSize();
1920                     break;
1921                 default:
1922                     assert false;
1923                     break;
1924             }
1925             assert nextIndex < size; // Because of (1)
1926             selection.select((FXOMInstance) gridPane, gsg.getType(), nextIndex);
1927         }
1928     }
1929 
1930     /**
1931      * Returns true if the selection is single and the container of the selected
1932      * object container contains a child next to the selected one.
1933      *
1934      * @return if the selection is single and the container of the selected
1935      * object container contains a child next to the selected one.
1936      */
1937     private boolean canPerformSelectNext() {
1938         assert getFxomDocument() != null && getFxomDocument().getFxomRoot() != null;
1939         if (selection.isEmpty()) {
1940             return false;
1941         }
1942         final AbstractSelectionGroup asg = selection.getGroup();
1943         if (asg instanceof ObjectSelectionGroup) {
1944             final ObjectSelectionGroup osg = (ObjectSelectionGroup) asg;
1945             final Set<FXOMObject> items = osg.getItems();
1946             if (items.size() != 1) {
1947                 return false;
1948             }
1949             final FXOMObject selectedObject = items.iterator().next();
1950             return selectedObject.getNextSlibing() != null;
1951         } else if (asg instanceof GridSelectionGroup) {
1952             final GridSelectionGroup gsg = (GridSelectionGroup) asg;
1953             final Set<Integer> indexes = gsg.getIndexes();
1954             if (indexes.size() != 1) {
1955                 return false;
1956             }
1957             final FXOMObject gridPane = gsg.getParentObject();
1958             final DesignHierarchyMask mask = new DesignHierarchyMask(gridPane);
1959             int size = 0;
1960             switch (gsg.getType()) {
1961                 case ROW:
1962                     size = mask.getRowsSize();
1963                     break;
1964                 case COLUMN:
1965                     size = mask.getColumnsSize();
1966                     break;
1967                 default:
1968                     assert false;
1969                     break;
1970             }
1971             final int index = indexes.iterator().next();
1972             return index < size - 1;
1973         } else {
1974             assert selection.getGroup() == null :
1975                     "Add implementation for " + selection.getGroup(); //NOI18N
1976         }
1977         return false;
1978     }
1979 
1980     /**
1981      * Performs the select previous control action.
1982      */
1983     private void performSelectPrevious() {
1984         assert canPerformSelectPrevious(); // (1)
1985 
1986         final AbstractSelectionGroup asg = selection.getGroup();
1987         if (asg instanceof ObjectSelectionGroup) {
1988             final ObjectSelectionGroup osg = (ObjectSelectionGroup) asg;
1989             final Set<FXOMObject> items = osg.getItems();
1990             assert items.size() == 1; // Because of (1)
1991             final FXOMObject selectedObject = items.iterator().next();
1992             final FXOMObject previousSibling = selectedObject.getPreviousSlibing();
1993             assert previousSibling != null; // Because of (1)
1994             selection.select(previousSibling);
1995         } else {
1996             assert asg instanceof GridSelectionGroup; // Because of (1)
1997             final GridSelectionGroup gsg = (GridSelectionGroup) asg;
1998             final FXOMObject gridPane = gsg.getParentObject();
1999             assert gridPane instanceof FXOMInstance;
2000             final Set<Integer> indexes = gsg.getIndexes();
2001             assert indexes.size() == 1; // Because of (1)
2002             int selectedIndex = indexes.iterator().next();
2003             int previousIndex = selectedIndex - 1;
2004             assert previousIndex >= 0; // Because of (1)
2005             selection.select((FXOMInstance) gridPane, gsg.getType(), previousIndex);
2006         }
2007     }
2008 
2009     /**
2010      * Returns true if the selection is single and the container of the selected
2011      * object container contains a child previous to the selected one.
2012      *
2013      * @return if the selection is single and the container of the selected
2014      * object container contains a child previous to the selected one.
2015      */
2016     private boolean canPerformSelectPrevious() {
2017         assert getFxomDocument() != null && getFxomDocument().getFxomRoot() != null;
2018         if (selection.isEmpty()) {
2019             return false;
2020         }
2021         final AbstractSelectionGroup asg = selection.getGroup();
2022         if (asg instanceof ObjectSelectionGroup) {
2023             final ObjectSelectionGroup osg = (ObjectSelectionGroup) asg;
2024             final Set<FXOMObject> items = osg.getItems();
2025             if (items.size() != 1) {
2026                 return false;
2027             }
2028             final FXOMObject selectedObject = items.iterator().next();
2029             return selectedObject.getPreviousSlibing() != null;
2030         } else if (asg instanceof GridSelectionGroup) {
2031             final GridSelectionGroup gsg = (GridSelectionGroup) asg;
2032             final Set<Integer> indexes = gsg.getIndexes();
2033             if (indexes.size() != 1) {
2034                 return false;
2035             }
2036             final int index = indexes.iterator().next();
2037             return index > 0;
2038         } else {
2039             assert selection.getGroup() == null :
2040                     "Add implementation for " + selection.getGroup(); //NOI18N
2041         }
2042         return false;
2043     }
2044 
2045     /**
2046      * Performs the select none control action.
2047      */
2048     private void performSelectNone() {
2049         assert canPerformSelectNone();
2050         selection.clear();
2051     }
2052 
2053     /**
2054      * Returns true if the selection is not empty.
2055      *
2056      * @return if the selection is not empty.
2057      */
2058     private boolean canPerformSelectNone() {
2059         return getSelection().isEmpty() == false;
2060     }
2061 
2062     /**
2063      * If selection contains single FXOM object and this an fx:include instance, then
2064      * returns the included file. Else returns null.
2065      *
2066      * If the selection is single and is an included FXOM object :
2067      * 1) if included file source does not start with /,
2068      *    it's a path relative to the document location.
2069      *   - if FXOM document location is null (document not saved yet), return null
2070      *   - else return selection included file
2071      *
2072      * 2) if included file source starts with /,
2073      *    it's a path relative to the document class loader.
2074      *   - if FXOM document class loader is null, return null
2075      *   - else return selection included file
2076      *
2077      * @return the included file associated to the selected object or null.
2078      */
2079     public File getIncludedFile() {
2080         final AbstractSelectionGroup asg = getSelection().getGroup();
2081         if (asg instanceof ObjectSelectionGroup == false) {
2082             return null;
2083         }
2084         final ObjectSelectionGroup osg = (ObjectSelectionGroup) asg;
2085         if (osg.getItems().size() != 1) {
2086             return null;
2087         }
2088         final FXOMObject fxomObject = osg.getItems().iterator().next();
2089         if (fxomObject instanceof FXOMIntrinsic == false) {
2090             return null;
2091         }
2092         final FXOMIntrinsic fxomIntrinsic = (FXOMIntrinsic) fxomObject;
2093         if (fxomIntrinsic.getType() != FXOMIntrinsic.Type.FX_INCLUDE) {
2094             return null;
2095         }
2096         final String source = fxomIntrinsic.getSource();
2097         if (source == null) {
2098             return null; // Can this happen ?
2099         }
2100         if (source.startsWith("/")) { //NOI18N
2101             // Source relative to FXOM document class loader
2102             final ClassLoader classLoader = getFxomDocument().getClassLoader();
2103             if (classLoader != null) {
2104                 final PrefixedValue pv = new PrefixedValue(
2105                         PrefixedValue.Type.CLASSLOADER_RELATIVE_PATH, source);
2106                 final URL url = pv.resolveClassLoaderRelativePath(classLoader);
2107                 final File file;
2108                 try {
2109                     file = new File(url.toURI());
2110                 } catch (URISyntaxException ex) {
2111                     throw new IllegalArgumentException(ex);
2112                 }
2113                 return file;
2114             }
2115         } else {
2116             // Source relative to FXOM document location
2117             final URL location = getFxmlLocation();
2118             if (location != null) {
2119                 final PrefixedValue pv = new PrefixedValue(
2120                         PrefixedValue.Type.DOCUMENT_RELATIVE_PATH, source);
2121                 final URL url = pv.resolveDocumentRelativePath(location);
2122                 final File file;
2123                 try {
2124                     file = new File(url.toURI());
2125                 } catch (URISyntaxException ex) {
2126                     throw new IllegalArgumentException(ex);
2127                 }
2128                 return file;
2129             }
2130         }
2131         return null;
2132     }
2133 
2134     /**
2135      * Returns true if the selection is an included file that can be edited/revealed.
2136      *
2137      * @return true if the selection is an included file that can be edited/revealed.
2138      */
2139     private boolean canPerformIncludedFileAction() {
2140         return getIncludedFile() != null;
2141     }
2142 
2143     private void performEditIncludedFile() {
2144         assert canPerformIncludedFileAction(); // (1)
2145         final File includedFile = getIncludedFile();
2146         assert includedFile != null; // Because of (1)
2147         try {
2148             EditorPlatform.open(includedFile.getAbsolutePath());
2149         } catch (IOException ioe) {
2150             final ErrorDialog errorDialog = new ErrorDialog(null);
2151             errorDialog.setTitle(I18N.getString("error.file.open.title"));
2152             errorDialog.setMessage(I18N.getString("error.file.open.message",
2153                     includedFile.getAbsolutePath()));
2154             errorDialog.setDebugInfoWithThrowable(ioe);
2155             errorDialog.showAndWait();
2156         }
2157     }
2158 
2159     private void performRevealIncludedFile() {
2160         assert canPerformIncludedFileAction(); // (1)
2161         final File includedFile = getIncludedFile();
2162         assert includedFile != null; // Because of (1)
2163         try {
2164             EditorPlatform.revealInFileBrowser(includedFile);
2165         } catch (IOException ioe) {
2166             final ErrorDialog errorDialog = new ErrorDialog(null);
2167             errorDialog.setTitle(I18N.getString("error.file.reveal.title"));
2168             errorDialog.setMessage(I18N.getString("error.file.reveal.message",
2169                     includedFile.getAbsolutePath()));
2170             errorDialog.setDetails(I18N.getString("error.write.details"));
2171             errorDialog.setDebugInfoWithThrowable(ioe);
2172             errorDialog.showAndWait();
2173         }
2174     }
2175 
2176     /**
2177      * Returns true if the 'set effect' action is permitted with the current
2178      * selection.
2179      * In other words, returns true if the selection contains only Node objects.
2180      *
2181      * @return true if the 'set effect' action is permitted.
2182      */
2183     public boolean canPerformSetEffect() {
2184         return isSelectionNode();
2185     }
2186 
2187     /**
2188      * Performs the 'set effect' edit action. This method creates an instance of
2189      * the specified effect class and sets it in the effect property of the
2190      * selected objects.
2191      *
2192      * @param effectClass class of the effect to be added (never null)
2193      */
2194     public void performSetEffect(Class<? extends Effect> effectClass) {
2195         assert canPerformSetEffect(); // (1)
2196 
2197         final Effect effect = Utils.newInstance(effectClass);
2198         final PropertyName pn = new PropertyName("effect"); //NOI18N
2199 
2200         final PropertyMetadata pm
2201                 = Metadata.getMetadata().queryProperty(Node.class, pn);
2202         assert pm instanceof ValuePropertyMetadata;
2203         final ValuePropertyMetadata vpm = (ValuePropertyMetadata) pm;
2204         final ModifySelectionJob job = new ModifySelectionJob(vpm, effect, this);
2205         getJobManager().push(job);
2206     }
2207 
2208     /**
2209      * Returns true if the 'add context menu' action is permitted with the current
2210      * selection.
2211      * In other words, returns true if the selection contains only Control objects.
2212      *
2213      * @return true if the 'add context menu' action is permitted.
2214      */
2215     public boolean canPerformAddContextMenu() {
2216         return isSelectionControl();
2217     }
2218 
2219     /**
2220      * Performs the 'add context menu' edit action. This method creates an instance of
2221      * ContextMenu and sets it in the contextMenu property of the
2222      * selected objects.
2223      */
2224     public void performAddContextMenu() {
2225         assert canPerformAddContextMenu();
2226         final Job addContextMenuJob = new AddContextMenuToSelectionJob(this);
2227         getJobManager().push(addContextMenuJob);
2228     }
2229 
2230     /**
2231      * Returns true if the 'add tooltip' action is permitted with the current
2232      * selection.
2233      * In other words, returns true if the selection contains only Control objects.
2234      *
2235      * @return true if the 'add tooltip' action is permitted.
2236      */
2237     public boolean canPerformAddTooltip() {
2238         return isSelectionControl();
2239     }
2240 
2241     /**
2242      * Performs the 'add tooltip' edit action. This method creates an instance of
2243      * Tooltip and sets it in the tooltip property of the
2244      * selected objects.
2245      */
2246     public void performAddTooltip() {
2247         assert canPerformAddTooltip(); // (1)
2248         final Job addTooltipJob = new AddTooltipToSelectionJob(this);
2249         getJobManager().push(addTooltipJob);
2250    }
2251 
2252     /**
2253      * Returns the URL of the CSS style associated to EditorController class.
2254      * This stylesheet contains rules shareable by all other components of
2255      * SB kit.
2256      *
2257      * @return URL of EditorController class style sheet (never null).
2258      */
2259     private static URL stylesheet = null;
2260     public synchronized static URL getStylesheet() {
2261         if (stylesheet == null) {
2262             stylesheet = EditorController.class.getResource("EditorController.css"); //NOI18N
2263             assert stylesheet != null;
2264         }
2265         return stylesheet;
2266     }
2267 
2268 
2269     /**
2270      * Returns the last directory selected from the file chooser.
2271      *
2272      * @return the last selected directory (never null).
2273      */
2274     public static File getNextInitialDirectory() {
2275         return nextInitialDirectory;
2276     }
2277 
2278     /**
2279      * @treatAsPrivate
2280      *
2281      * Updates the initial directory used by the file chooser.
2282      *
2283      * @param chosenFile the selected file from which the initial directory is set.
2284      */
2285     public static void updateNextInitialDirectory(File chosenFile) {
2286         assert chosenFile != null;
2287 
2288         final Path chosenFolder = chosenFile.toPath().getParent();
2289         if (chosenFolder != null) {
2290             nextInitialDirectory = chosenFolder.toFile();
2291         }
2292     }
2293 
2294     /**
2295      * @treatAsPrivate
2296      *
2297      * @return true if the current FXOM document represents a 3D layout, false
2298      *         otherwise.
2299      */
2300     public boolean is3D() {
2301         boolean res = false;
2302         FXOMDocument doc = getFxomDocument();
2303 
2304         if (doc != null) {
2305             Object sgroot = doc.getSceneGraphRoot();
2306 
2307             if (sgroot instanceof Node) {
2308                 final Bounds rootBounds = ((Node)sgroot).getLayoutBounds();
2309                 res = (rootBounds.getDepth() > 0);
2310             }
2311         }
2312 
2313         return res;
2314     }
2315 
2316 
2317     /**
2318      * @treatAsPrivate
2319      *
2320      * @return true if the current FXOM document is an instance of a Node, false
2321      * otherwise.
2322      */
2323     public boolean isNode() {
2324         boolean res = false;
2325         FXOMDocument doc = getFxomDocument();
2326 
2327         if (doc != null) {
2328             Object sgroot = doc.getSceneGraphRoot();
2329 
2330             if (sgroot instanceof Node) {
2331                 res = true;
2332             }
2333         }
2334 
2335         return res;
2336     }
2337 
2338     /**
2339      * @treatAsPrivate
2340      *
2341      * @return true if the current selection objects are all instances of a Node,
2342      * false otherwise.
2343      */
2344     public boolean isSelectionNode() {
2345         final AbstractSelectionGroup asg = selection.getGroup();
2346         if (asg instanceof ObjectSelectionGroup) {
2347             final ObjectSelectionGroup osg = (ObjectSelectionGroup) asg;
2348             for (FXOMObject fxomObject : osg.getItems()) {
2349                 final boolean isNode = fxomObject.getSceneGraphObject() instanceof Node;
2350                 if (isNode == false) {
2351                     return false;
2352                 }
2353             }
2354         } else {
2355             return false;
2356         }
2357         return true;
2358     }
2359 
2360     /*
2361      * Private
2362      */
2363 
2364     private boolean isSelectionControl() {
2365         final AbstractSelectionGroup asg = selection.getGroup();
2366         if (asg instanceof ObjectSelectionGroup) {
2367             final ObjectSelectionGroup osg = (ObjectSelectionGroup) asg;
2368             for (FXOMObject fxomObject : osg.getItems()) {
2369                 final boolean isControl = fxomObject.getSceneGraphObject() instanceof Control;
2370                 if (isControl == false) {
2371                     return false;
2372                 }
2373             }
2374         } else {
2375             return false;
2376         }
2377         return true;
2378     }
2379 
2380     private void updateFxomDocument(String fxmlText, URL fxmlLocation, ResourceBundle resources) throws IOException {
2381         final FXOMDocument newFxomDocument;
2382 
2383         if (fxmlText != null) {
2384             newFxomDocument = new FXOMDocument(fxmlText, fxmlLocation, getLibrary().getClassLoader(), resources);
2385         } else {
2386             newFxomDocument = null;
2387         }
2388         jobManager.clear();
2389         selection.clear();
2390         messageLog.clear();
2391         errorReport.setFxomDocument(newFxomDocument);
2392         fxomDocumentProperty.setValue(newFxomDocument);
2393 
2394         watchingController.fxomDocumentDidChange();
2395 
2396     }
2397 
2398     private final ChangeListener<ClassLoader> libraryClassLoaderListener
2399             = (ov, t, t1) -> libraryClassLoaderDidChange();
2400 
2401     private void libraryClassLoaderDidChange() {
2402         if (getFxomDocument() != null) {
2403             errorReport.forget();
2404             getFxomDocument().setClassLoader(libraryProperty.get().getClassLoader());
2405         }
2406     }
2407 
2408     private void resourcesDidChange() {
2409         if (getFxomDocument() != null) {
2410             errorReport.forget();
2411             getFxomDocument().setResources(getResources());
2412         }
2413     }
2414 
2415     private void jobManagerRevisionDidChange() {
2416         errorReport.forget();
2417         watchingController.jobManagerRevisionDidChange();
2418 //        setPickModeEnabled(false);
2419     }
2420 }