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.app;
  33 
  34 import com.oracle.javafx.scenebuilder.app.i18n.I18N;
  35 import com.oracle.javafx.scenebuilder.app.info.InfoPanelController;
  36 import com.oracle.javafx.scenebuilder.app.menubar.MenuBarController;
  37 import com.oracle.javafx.scenebuilder.app.message.MessageBarController;
  38 import com.oracle.javafx.scenebuilder.app.preferences.PreferencesController;
  39 import com.oracle.javafx.scenebuilder.app.preferences.PreferencesRecordDocument;
  40 import com.oracle.javafx.scenebuilder.app.preferences.PreferencesRecordGlobal;
  41 import com.oracle.javafx.scenebuilder.app.preview.PreviewWindowController;
  42 import com.oracle.javafx.scenebuilder.app.report.JarAnalysisReportController;
  43 import com.oracle.javafx.scenebuilder.app.selectionbar.SelectionBarController;
  44 import com.oracle.javafx.scenebuilder.app.skeleton.SkeletonWindowController;
  45 import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
  46 import com.oracle.javafx.scenebuilder.kit.editor.EditorController.ControlAction;
  47 import com.oracle.javafx.scenebuilder.kit.editor.EditorController.EditAction;
  48 import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform;
  49 import com.oracle.javafx.scenebuilder.kit.editor.job.Job;
  50 import com.oracle.javafx.scenebuilder.kit.editor.panel.content.ContentPanelController;
  51 import com.oracle.javafx.scenebuilder.kit.editor.panel.css.CssPanelController;
  52 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController;
  53 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.AbstractHierarchyPanelController.DisplayOption;
  54 import com.oracle.javafx.scenebuilder.kit.editor.panel.hierarchy.HierarchyPanelController;
  55 import com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.InspectorPanelController;
  56 import com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.InspectorPanelController.SectionId;
  57 import com.oracle.javafx.scenebuilder.kit.editor.panel.library.LibraryPanelController;
  58 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.AbstractFxmlWindowController;
  59 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AbstractModalDialog;
  60 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AbstractModalDialog.ButtonID;
  61 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AlertDialog;
  62 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.ErrorDialog;
  63 import com.oracle.javafx.scenebuilder.kit.editor.search.SearchController;
  64 import com.oracle.javafx.scenebuilder.kit.editor.selection.AbstractSelectionGroup;
  65 import com.oracle.javafx.scenebuilder.kit.editor.selection.GridSelectionGroup;
  66 import com.oracle.javafx.scenebuilder.kit.editor.selection.ObjectSelectionGroup;
  67 import com.oracle.javafx.scenebuilder.kit.editor.selection.Selection;
  68 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument;
  69 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMNodes;
  70 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  71 import com.oracle.javafx.scenebuilder.kit.library.Library;
  72 import com.oracle.javafx.scenebuilder.kit.library.user.UserLibrary;
  73 import com.sun.javafx.scene.control.behavior.KeyBinding;
  74 
  75 import java.io.File;
  76 import java.io.IOException;
  77 import java.io.UnsupportedEncodingException;
  78 import java.net.MalformedURLException;
  79 import java.net.URISyntaxException;
  80 import java.net.URL;
  81 import java.nio.file.Files;
  82 import java.nio.file.NoSuchFileException;
  83 import java.nio.file.Path;
  84 import java.nio.file.Paths;
  85 import java.nio.file.attribute.FileTime;
  86 import java.util.ArrayList;
  87 import java.util.Collections;
  88 import java.util.Comparator;
  89 import java.util.HashMap;
  90 import java.util.List;
  91 import java.util.Map;
  92 
  93 import javafx.beans.InvalidationListener;
  94 import javafx.beans.value.ChangeListener;
  95 import javafx.event.ActionEvent;
  96 import javafx.event.EventHandler;
  97 import javafx.fxml.FXML;
  98 import javafx.geometry.Insets;
  99 import javafx.scene.Node;
 100 import javafx.scene.control.Accordion;
 101 import javafx.scene.control.ComboBox;
 102 import javafx.scene.control.DialogPane;
 103 import javafx.scene.control.Menu;
 104 import javafx.scene.control.MenuButton;
 105 import javafx.scene.control.MenuItem;
 106 import javafx.scene.control.RadioMenuItem;
 107 import javafx.scene.control.SplitPane;
 108 import javafx.scene.control.TextInputControl;
 109 import javafx.scene.input.Clipboard;
 110 import javafx.scene.input.KeyCode;
 111 import javafx.scene.input.KeyCombination;
 112 import javafx.scene.input.KeyEvent;
 113 import javafx.scene.layout.StackPane;
 114 import javafx.scene.layout.VBox;
 115 import javafx.stage.FileChooser;
 116 import javafx.stage.FileChooser.ExtensionFilter;
 117 import javafx.stage.WindowEvent;
 118 
 119 /**
 120  *
 121  */
 122 public class DocumentWindowController extends AbstractFxmlWindowController {
 123     
 124     
 125     public enum DocumentControlAction {
 126         COPY,
 127         SELECT_ALL,
 128         SELECT_NONE,
 129         SAVE_FILE,
 130         SAVE_AS_FILE,
 131         REVERT_FILE,
 132         CLOSE_FILE,
 133         REVEAL_FILE,
 134         GOTO_CONTENT,
 135         GOTO_PROPERTIES,
 136         GOTO_LAYOUT,
 137         GOTO_CODE,
 138         TOGGLE_LIBRARY_PANEL,
 139         TOGGLE_DOCUMENT_PANEL,
 140         TOGGLE_CSS_PANEL,
 141         TOGGLE_LEFT_PANEL,
 142         TOGGLE_RIGHT_PANEL,
 143         TOGGLE_OUTLINES_VISIBILITY,
 144         TOGGLE_GUIDES_VISIBILITY,
 145         SHOW_PREVIEW_WINDOW,
 146         SHOW_PREVIEW_DIALOG,
 147         ADD_SCENE_STYLE_SHEET,
 148         SET_RESOURCE,
 149         REMOVE_RESOURCE,
 150         REVEAL_RESOURCE,
 151         HELP,
 152         SHOW_SAMPLE_CONTROLLER
 153     }
 154     
 155     public enum DocumentEditAction {
 156         DELETE,
 157         CUT,
 158         PASTE,
 159         IMPORT_FXML,
 160         IMPORT_MEDIA,
 161         INCLUDE_FXML
 162     }
 163     
 164     public enum ActionStatus {
 165         CANCELLED,
 166         DONE
 167     }
 168     
 169     private final EditorController editorController = new EditorController();
 170     private final MenuBarController menuBarController = new MenuBarController(this);
 171     private final ContentPanelController contentPanelController = new ContentPanelController(editorController);
 172     private final AbstractHierarchyPanelController hierarchyPanelController = new HierarchyPanelController(editorController);
 173     private final InfoPanelController infoPanelController = new InfoPanelController(editorController);
 174     private final InspectorPanelController inspectorPanelController = new InspectorPanelController(editorController);
 175     private final CssPanelDelegate cssPanelDelegate = new CssPanelDelegate(inspectorPanelController, this);
 176     private final CssPanelController cssPanelController = new CssPanelController(editorController, cssPanelDelegate);
 177     private final LibraryPanelController libraryPanelController = new LibraryPanelController(editorController);
 178     private final SelectionBarController selectionBarController = new SelectionBarController(editorController);
 179     private final MessageBarController messageBarController = new MessageBarController(editorController);
 180     private final SearchController librarySearchController = new SearchController(editorController);
 181     private final SearchController inspectorSearchController = new SearchController(editorController);
 182     private final SearchController cssPanelSearchController = new SearchController(editorController);;
 183     private final SceneStyleSheetMenuController sceneStyleSheetMenuController = new SceneStyleSheetMenuController(this);
 184     private final CssPanelMenuController cssPanelMenuController = new CssPanelMenuController(cssPanelController);
 185     private final ResourceController resourceController = new ResourceController((this));    
 186     private final DocumentWatchingController watchingController = new DocumentWatchingController(this);
 187     
 188     // The controller below are created lazily because they need an owner
 189     // and computing them here would be too costly (impact on start-up time):
 190     // - PreviewWindowController
 191     // - SkeletonWindowController
 192     // - JarAnalysisReportController
 193     private PreviewWindowController previewWindowController = null;
 194     private SkeletonWindowController skeletonWindowController = null;
 195     private JarAnalysisReportController jarAnalysisReportController = null;
 196 
 197     @FXML private StackPane libraryPanelHost;
 198     @FXML private StackPane librarySearchPanelHost;
 199     @FXML private StackPane hierarchyPanelHost;
 200     @FXML private StackPane infoPanelHost;
 201     @FXML private StackPane contentPanelHost;
 202     @FXML private StackPane inspectorPanelHost;
 203     @FXML private StackPane inspectorSearchPanelHost;
 204     @FXML private StackPane cssPanelHost;
 205     @FXML private StackPane cssPanelSearchPanelHost;
 206     @FXML private StackPane messageBarHost;
 207     @FXML private Accordion documentAccordion;
 208     @FXML private SplitPane mainSplitPane;
 209     @FXML private SplitPane leftRightSplitPane;
 210     @FXML private SplitPane libraryDocumentSplitPane;
 211     
 212     @FXML private MenuButton libraryMenuButton;
 213     @FXML private MenuItem libraryImportSelection;
 214     @FXML private RadioMenuItem libraryViewAsList;
 215     @FXML private RadioMenuItem libraryViewAsSections;
 216     @FXML private MenuItem libraryReveal;
 217     @FXML private Menu customLibraryMenu;
 218     
 219     @FXML private MenuItem cssPanelShowStyledOnlyMi;
 220     @FXML private MenuItem cssPanelSplitDefaultsMi;
 221     
 222     @FXML private RadioMenuItem showInfoMenuItem;
 223     @FXML private RadioMenuItem showFxIdMenuItem;
 224     @FXML private RadioMenuItem showNodeIdMenuItem;
 225 
 226     private SplitController bottomSplitController;
 227     private SplitController leftSplitController;
 228     private SplitController rightSplitController;
 229     private SplitController librarySplitController;
 230     private SplitController documentSplitController;
 231     
 232     private FileTime loadFileTime;
 233     private Job saveJob;
 234 
 235     private static List<String> imageExtensions;
 236     private static List<String> audioExtensions;
 237     private static List<String> videoExtensions;
 238     private static List<String> mediaExtensions;
 239 
 240     private final EventHandler<KeyEvent> mainKeyEventFilter = event -> {
 241         //------------------------------------------------------------------
 242         // TEXT INPUT CONTROL
 243         //------------------------------------------------------------------
 244         // Common editing actions handled natively and defined as application accelerators
 245         // 
 246         // The platform support is not mature/stable enough to rely on.
 247         // Indeed, the behavior may differ :
 248         // - when using system menu bar vs not using it
 249         // - when using accelerators vs using menu items
 250         // - depending on the focused control (TextField vs ComboBox)
 251         // 
 252         // On SB side, we decide for now to consume events that may be handled natively
 253         // so ALL actions are defined in our ApplicationMenu class.
 254         //
 255         // This may be revisit when platform implementation will be more reliable.
 256         //
 257         final Node focusOwner = getScene().getFocusOwner();
 258         final KeyCombination accelerator = getAccelerator(event);
 259         if (isTextInputControlEditing(focusOwner) == true 
 260                 && accelerator != null) {
 261             for (KeyBinding binding : SBTextInputControlBindings.getBindings()) {
 262                 // The event is handled natively
 263                 if (binding.getSpecificity(null, event) > 0) {
 264                     // 
 265                     // When using system menu bar, the event is handled natively 
 266                     // before the application receives it : we just consume the event 
 267                     // so the editing action is not performed a second time by the app.
 268                     if (menuBarController.getMenuBar().isUseSystemMenuBar()) {
 269                         event.consume();
 270                     }
 271                     break;
 272                 }
 273             }
 274         }
 275 
 276         //------------------------------------------------------------------
 277         // Hierarchy TreeView + select all
 278         //------------------------------------------------------------------
 279         // Select all is handled natively by TreeView (= hierarchy panel control).
 280         boolean modifierDown = (EditorPlatform.IS_MAC ? event.isMetaDown() : event.isControlDown());
 281         boolean isSelectAll = KeyCode.A.equals(event.getCode()) && modifierDown;
 282         if (getHierarchyPanelController().getPanelControl().isFocused() && isSelectAll) {
 283             // Consume the event so the control action is not performed natively.
 284             event.consume();
 285             // When using system menu bar, the control action is performed by the app.
 286             if (menuBarController.getMenuBar().isUseSystemMenuBar() == false) {
 287                 if (canPerformControlAction(DocumentControlAction.SELECT_ALL)) {
 288                     performControlAction(DocumentControlAction.SELECT_ALL);
 289                 }
 290             }
 291         }
 292 
 293         // MenuItems define a single accelerator.
 294         // BACK_SPACE key must be handled same way as DELETE key.
 295         boolean isBackspace = KeyCode.BACK_SPACE.equals(event.getCode());
 296         if (isTextInputControlEditing(focusOwner) == false && isBackspace) {
 297             if (canPerformEditAction(DocumentEditAction.DELETE)) {
 298                 performEditAction(DocumentEditAction.DELETE);
 299             }
 300             event.consume();
 301         }
 302     };
 303     
 304     /*
 305      * DocumentWindowController
 306      */
 307     
 308     public DocumentWindowController() {
 309         super(DocumentWindowController.class.getResource("DocumentWindow.fxml"), //NOI18N
 310                 I18N.getBundle(), false); // sizeToScene = false because sizing is defined in preferences
 311         editorController.setLibrary(SceneBuilderApp.getSingleton().getUserLibrary());
 312     }
 313     
 314     public EditorController getEditorController() {
 315         return editorController;
 316     }
 317     
 318     public MenuBarController getMenuBarController() {
 319         return menuBarController;
 320     }
 321     
 322     public ContentPanelController getContentPanelController() {
 323         return contentPanelController;
 324     }
 325     
 326     public InspectorPanelController getInspectorPanelController() {
 327         return inspectorPanelController;
 328     }
 329     
 330     public CssPanelController getCssPanelController() {
 331         return cssPanelController;
 332     }
 333     
 334     public AbstractHierarchyPanelController getHierarchyPanelController() {
 335         return hierarchyPanelController;
 336     }
 337     
 338     public InfoPanelController getInfoPanelController() {
 339         return infoPanelController;
 340     }
 341     
 342     public PreviewWindowController getPreviewWindowController() {
 343         return previewWindowController;
 344     }
 345     
 346     public SceneStyleSheetMenuController getSceneStyleSheetMenuController() {
 347         return sceneStyleSheetMenuController;
 348     }
 349     
 350     public ResourceController getResourceController() {
 351         return resourceController;
 352     }
 353     
 354     public DocumentWatchingController getWatchingController() {
 355         return watchingController;
 356     }
 357     
 358     public SplitController getBottomSplitController() {
 359         return bottomSplitController;
 360     }
 361     
 362     public SplitController getLeftSplitController() {
 363         return leftSplitController;
 364     }
 365     
 366     public SplitController getRightSplitController() {
 367         return rightSplitController;
 368     }
 369     
 370     public SplitController getLibrarySplitController() {
 371         return librarySplitController;
 372     }
 373     
 374     public SplitController getDocumentSplitController() {
 375         return documentSplitController;
 376     }
 377     
 378     public void loadFromFile(File fxmlFile) throws IOException {
 379         final URL fxmlURL = fxmlFile.toURI().toURL();
 380         final String fxmlText = FXOMDocument.readContentFromURL(fxmlURL);
 381         editorController.setFxmlTextAndLocation(fxmlText, fxmlURL);
 382         updateLoadFileTime();
 383         updateStageTitle(); // No-op if fxml has not been loaded yet
 384         updateFromDocumentPreferences();
 385         watchingController.update();
 386     }
 387     
 388     public void loadFromURL(URL fxmlURL) {
 389         assert fxmlURL != null;
 390         try {
 391             final String fxmlText = FXOMDocument.readContentFromURL(fxmlURL);
 392             editorController.setFxmlTextAndLocation(fxmlText, null);
 393             updateLoadFileTime();
 394             updateStageTitle(); // No-op if fxml has not been loaded yet
 395             updateFromDocumentPreferences();
 396             watchingController.update();
 397         } catch(IOException x) {
 398             throw new IllegalStateException(x);
 399         }
 400     }
 401 
 402     public void loadWithDefaultContent() {
 403         try {
 404             editorController.setFxmlTextAndLocation("", null); //NOI18N
 405             updateLoadFileTime();
 406             updateStageTitle(); // No-op if fxml has not been loaded yet
 407             watchingController.update();
 408         } catch (IOException x) {
 409             throw new IllegalStateException(x);
 410         }
 411     }
 412     
 413     public void reload() throws IOException {
 414         final FXOMDocument fxomDocument = editorController.getFxomDocument();
 415         assert (fxomDocument != null) && (fxomDocument.getLocation() != null);
 416         final URL fxmlURL = fxomDocument.getLocation();
 417         final String fxmlText = FXOMDocument.readContentFromURL(fxmlURL);
 418         editorController.setFxmlTextAndLocation(fxmlText, fxmlURL);
 419         updateLoadFileTime();
 420         // Here we do not invoke updateStageTitleAndPreferences() neither watchingController.update()
 421     }
 422     
 423     public String getFxmlText() {
 424         return editorController.getFxmlText();
 425     }
 426     
 427     public void refreshLibraryDisplayOption(LibraryPanelController.DISPLAY_MODE option) {
 428         switch (option) {
 429             case LIST:
 430                 libraryViewAsList.setSelected(true);
 431                 break;
 432             case SECTIONS:
 433                 libraryViewAsSections.setSelected(true);
 434                 break;
 435             default:
 436                 assert false;
 437                 break;
 438         }
 439         libraryPanelController.setDisplayMode(option);
 440     }
 441     
 442     public void refreshHierarchyDisplayOption(DisplayOption option) {
 443         switch(option) {
 444             case INFO:
 445                 showInfoMenuItem.setSelected(true);
 446                 break;
 447             case FXID:
 448                 showFxIdMenuItem.setSelected(true);
 449                 break;
 450             case NODEID:
 451                 showNodeIdMenuItem.setSelected(true);
 452                 break;
 453             default:
 454                 assert false;
 455                 break;
 456         }
 457         hierarchyPanelController.setDisplayOption(option);
 458     }
 459 
 460     public void refreshCssTableColumnsOrderingReversed(boolean cssTableColumnsOrderingReversed) {
 461         cssPanelController.setTableColumnsOrderingReversed(cssTableColumnsOrderingReversed);
 462     }
 463 
 464     public static final String makeTitle(FXOMDocument fxomDocument) {
 465         final String title;
 466         
 467         if (fxomDocument == null) {
 468             title = I18N.getString("label.no.document");
 469         } else if (fxomDocument.getLocation() == null) {
 470             title = I18N.getString("label.untitled");
 471         } else {
 472             String name = ""; //NOI18N
 473             try {
 474                 final File toto = new File(fxomDocument.getLocation().toURI());
 475                 name = toto.getName();
 476             } catch (URISyntaxException ex) {
 477                 throw new RuntimeException("Bug", ex); //NOI18N
 478             }
 479             title = name;
 480         }
 481         
 482         return title;
 483     }
 484     
 485     public boolean canPerformControlAction(DocumentControlAction controlAction) {
 486         final boolean result;
 487         
 488         switch(controlAction) {
 489             case COPY:
 490                 result = canPerformCopy();
 491                 break;
 492                 
 493             case SELECT_ALL:
 494                 result = canPerformSelectAll();
 495                 break;
 496                 
 497             case SELECT_NONE:
 498                 result = canPerformSelectNone();
 499                 break;
 500                 
 501             case SHOW_SAMPLE_CONTROLLER:
 502                 result = editorController.getFxomDocument() != null;
 503                 break;
 504                 
 505             case TOGGLE_LIBRARY_PANEL:
 506             case TOGGLE_DOCUMENT_PANEL:
 507             case TOGGLE_CSS_PANEL:
 508             case TOGGLE_LEFT_PANEL:
 509             case TOGGLE_RIGHT_PANEL:
 510             case TOGGLE_OUTLINES_VISIBILITY:
 511             case TOGGLE_GUIDES_VISIBILITY:
 512             case SHOW_PREVIEW_WINDOW:
 513                 result = true;
 514                 break;
 515                 
 516             case SHOW_PREVIEW_DIALOG:
 517                 final FXOMDocument fxomDocument = editorController.getFxomDocument();
 518                 if (fxomDocument != null) {
 519                     Object sceneGraphRoot = fxomDocument.getSceneGraphRoot();
 520                     return sceneGraphRoot instanceof DialogPane;
 521                 }
 522                 result = false;
 523                 break;
 524                 
 525             case SAVE_FILE:
 526                 result = isDocumentDirty()
 527                         || editorController.getFxomDocument().getLocation() == null; // Save new empty document
 528                 break;
 529                 
 530             case SAVE_AS_FILE:
 531             case CLOSE_FILE:
 532                 result = true;
 533                 break;
 534                 
 535             case REVERT_FILE:
 536                 result = isDocumentDirty() 
 537                         && editorController.getFxomDocument().getLocation() != null;
 538                 break;
 539                 
 540             case REVEAL_FILE:
 541                 result = (editorController.getFxomDocument() != null) 
 542                         && (editorController.getFxomDocument().getLocation() != null);
 543                 break;
 544                 
 545             case GOTO_CONTENT:
 546             case GOTO_PROPERTIES:
 547             case GOTO_LAYOUT:
 548             case GOTO_CODE:
 549                 result = true;
 550                 break;
 551                 
 552             case ADD_SCENE_STYLE_SHEET:
 553                 result = true;
 554                 break;
 555                 
 556             case SET_RESOURCE:
 557                 result = true;
 558                 break;
 559                 
 560             case REMOVE_RESOURCE:
 561             case REVEAL_RESOURCE:
 562                 result = resourceController.getResourceFile() != null;
 563                 break;
 564                 
 565             case HELP:
 566                 result = true;
 567                 break;
 568                 
 569             default:
 570                 result = false;
 571                 assert false;
 572                 break;
 573         }
 574        
 575         return result;
 576     }
 577     
 578     public void performControlAction(DocumentControlAction controlAction) {
 579         assert canPerformControlAction(controlAction);
 580         
 581         final PreferencesController pc = PreferencesController.getSingleton();
 582         final PreferencesRecordDocument recordDocument = pc.getRecordDocument(this);
 583         
 584         switch(controlAction) {
 585             case COPY:
 586                 performCopy();
 587                 break;
 588                 
 589             case SELECT_ALL:
 590                 performSelectAll();
 591                 break;
 592                 
 593             case SELECT_NONE:
 594                 performSelectNone();
 595                 break;
 596                 
 597             case SHOW_PREVIEW_WINDOW:
 598                 if (previewWindowController == null) {
 599                     previewWindowController = new PreviewWindowController(editorController, getStage());
 600                     previewWindowController.setToolStylesheet(getToolStylesheet());
 601                 }
 602                 previewWindowController.openWindow();
 603                 break;
 604                 
 605             case SHOW_PREVIEW_DIALOG:
 606                 if (previewWindowController == null) {
 607                     previewWindowController = new PreviewWindowController(editorController, getStage());
 608                     previewWindowController.setToolStylesheet(getToolStylesheet());
 609                 }
 610                 previewWindowController.openDialog();
 611                 break;
 612                 
 613             case SAVE_FILE:
 614                 performSaveOrSaveAsAction();
 615                 break;
 616                 
 617             case SAVE_AS_FILE:
 618                 performSaveAsAction();
 619                 break;
 620                 
 621             case REVERT_FILE:
 622                 performRevertAction();
 623                 break;
 624                 
 625             case CLOSE_FILE:
 626                 performCloseAction();
 627                 break;
 628                 
 629             case REVEAL_FILE:
 630                 performRevealAction();
 631                 break;
 632                 
 633             case GOTO_CONTENT:
 634                 contentPanelController.getGlassLayer().requestFocus();
 635                 break;
 636 
 637             case GOTO_PROPERTIES:
 638                 performGoToSection(SectionId.PROPERTIES);
 639                 break;
 640                 
 641             case GOTO_LAYOUT:
 642                 performGoToSection(SectionId.LAYOUT);
 643                 break;
 644                 
 645             case GOTO_CODE:
 646                 performGoToSection(SectionId.CODE);
 647                 break;
 648                 
 649             case TOGGLE_LEFT_PANEL:
 650                 if (leftSplitController.isTargetVisible()) {
 651                     assert librarySplitController.isTargetVisible()
 652                             || documentSplitController.isTargetVisible();
 653                     // Hide Left => hide both Library + Document
 654                     librarySplitController.hideTarget();
 655                     documentSplitController.hideTarget();
 656                     leftSplitController.hideTarget();
 657                 } else {
 658                     assert librarySplitController.isTargetVisible() == false
 659                             && documentSplitController.isTargetVisible() == false;
 660                     // Show Left => show both Library + Document
 661                     librarySplitController.showTarget();
 662                     documentSplitController.showTarget();
 663                     leftSplitController.showTarget();
 664 
 665                     // This workarounds layout issues when showing Left
 666                     libraryDocumentSplitPane.layout();
 667                     libraryDocumentSplitPane.setDividerPositions(0.5);
 668                 }
 669                 // Update preferences
 670                 recordDocument.setLibraryVisible(librarySplitController.isTargetVisible());
 671                 recordDocument.setDocumentVisible(documentSplitController.isTargetVisible());
 672                 recordDocument.setLeftVisible(leftSplitController.isTargetVisible());
 673                 break;
 674 
 675             case TOGGLE_RIGHT_PANEL:
 676                 rightSplitController.toggleTarget();
 677                 // Update preferences
 678                 recordDocument.setRightVisible(rightSplitController.isTargetVisible());
 679                 break;
 680                 
 681             case TOGGLE_CSS_PANEL:
 682                 // CSS panel is built lazely : initialize the CSS panel first
 683                 initializeCssPanel();
 684                 bottomSplitController.toggleTarget();
 685                 if (bottomSplitController.isTargetVisible()) {
 686                     // CSS panel is built lazely
 687                     // Need to update its table column ordering with preference value
 688                     final PreferencesRecordGlobal recordGlobal = pc.getRecordGlobal();
 689                     refreshCssTableColumnsOrderingReversed(recordGlobal.isCssTableColumnsOrderingReversed());
 690                     // Enable pick mode
 691                     editorController.setPickModeEnabled(true);
 692                 } else {
 693                     // Disable pick mode
 694                     editorController.setPickModeEnabled(false);
 695                 }
 696                 // Update preferences
 697                 recordDocument.setBottomVisible(bottomSplitController.isTargetVisible());
 698                 break;
 699                 
 700             case TOGGLE_LIBRARY_PANEL:
 701                 if (librarySplitController.isTargetVisible()) {
 702                     assert leftSplitController.isTargetVisible();
 703                     librarySplitController.hideTarget();
 704                     if (documentSplitController.isTargetVisible() == false) {
 705                         leftSplitController.hideTarget();
 706                     }
 707                 } else {
 708                     if (leftSplitController.isTargetVisible() == false) {
 709                         leftSplitController.showTarget();
 710                     }
 711                     librarySplitController.showTarget();
 712                 }
 713                 // Update preferences
 714                 recordDocument.setLibraryVisible(librarySplitController.isTargetVisible());
 715                 recordDocument.setLeftVisible(leftSplitController.isTargetVisible());
 716                 break;
 717                 
 718             case TOGGLE_DOCUMENT_PANEL:
 719                 if (documentSplitController.isTargetVisible()) {
 720                     assert leftSplitController.isTargetVisible();
 721                     documentSplitController.hideTarget();
 722                     if (librarySplitController.isTargetVisible() == false) {
 723                         leftSplitController.hideTarget();
 724                     }
 725                 } else {
 726                     if (leftSplitController.isTargetVisible() == false) {
 727                         leftSplitController.showTarget();
 728                     }
 729                     documentSplitController.showTarget();
 730                 }
 731                 // Update preferences
 732                 recordDocument.setDocumentVisible(documentSplitController.isTargetVisible());
 733                 recordDocument.setLeftVisible(leftSplitController.isTargetVisible());
 734                 break;
 735                 
 736             case TOGGLE_OUTLINES_VISIBILITY:
 737                 contentPanelController.setOutlinesVisible(
 738                         ! contentPanelController.isOutlinesVisible());
 739                 break;
 740                 
 741             case TOGGLE_GUIDES_VISIBILITY:
 742                 contentPanelController.setGuidesVisible(
 743                         ! contentPanelController.isGuidesVisible());
 744                 break;
 745                 
 746             case ADD_SCENE_STYLE_SHEET:
 747                 sceneStyleSheetMenuController.performAddSceneStyleSheet();
 748                 break;
 749                 
 750             case SET_RESOURCE:
 751                 resourceController.performSetResource();
 752                 // Update preferences
 753                 recordDocument.setI18NResourceFile(getResourceFile());
 754                 break;
 755                 
 756             case REMOVE_RESOURCE:
 757                 resourceController.performRemoveResource();
 758                 // Update preferences
 759                 recordDocument.setI18NResourceFile(getResourceFile());
 760                 break;
 761                 
 762             case REVEAL_RESOURCE:
 763                 resourceController.performRevealResource();
 764                 break;
 765                 
 766             case HELP:
 767                 performHelp();
 768                 break;
 769                 
 770             case SHOW_SAMPLE_CONTROLLER:
 771                 if (skeletonWindowController == null) {
 772                     skeletonWindowController = new SkeletonWindowController(editorController, getStage());
 773                     skeletonWindowController.setToolStylesheet(getToolStylesheet());
 774                 }
 775                 skeletonWindowController.openWindow();
 776                 break;
 777                 
 778             default:
 779                 assert false;
 780                 break;
 781         }
 782     }
 783     
 784     public boolean canPerformEditAction(DocumentEditAction editAction) {
 785         final boolean result;
 786         
 787         switch(editAction) {
 788             case DELETE:
 789                 result = canPerformDelete();
 790                 break;
 791                 
 792             case CUT:
 793                 result = canPerformCut();
 794                 break;
 795                 
 796             case IMPORT_FXML:
 797             case IMPORT_MEDIA:
 798                 result = true;
 799                 break;
 800 
 801             case INCLUDE_FXML:
 802                 // Cannot include as root or if the document is not saved yet
 803                 final FXOMDocument fxomDocument = editorController.getFxomDocument();
 804                 result = (fxomDocument != null) 
 805                         && (fxomDocument.getFxomRoot() != null)
 806                         && (fxomDocument.getLocation() != null);
 807                 break;
 808             
 809             case PASTE:
 810                 result = canPerformPaste();
 811                 break;
 812                 
 813             default:
 814                 result = false;
 815                 assert false;
 816                 break;
 817         }
 818        
 819         return result;
 820     }
 821     
 822     public void performEditAction(DocumentEditAction editAction) {
 823         assert canPerformEditAction(editAction);
 824         
 825         switch(editAction) {
 826             case DELETE:
 827                 performDelete();
 828                 break;
 829                 
 830             case CUT:
 831                 performCut();
 832                 break;
 833                 
 834             case IMPORT_FXML:
 835                 performImportFxml();
 836                 break;
 837                 
 838             case IMPORT_MEDIA:
 839                 performImportMedia();
 840                 break;
 841 
 842             case INCLUDE_FXML:
 843                 performIncludeFxml();
 844                 break;
 845 
 846             case PASTE:
 847                 performPaste();
 848                 break;
 849 
 850             default:
 851                 assert false;
 852                 break;
 853         }
 854     }
 855                 
 856     public boolean isLeftPanelVisible() {
 857         return leftSplitController.isTargetVisible();
 858     }
 859     
 860     
 861     public boolean isRightPanelVisible() {
 862         return rightSplitController.isTargetVisible();
 863     }
 864     
 865     
 866     public boolean isBottomPanelVisible() {
 867         return bottomSplitController.isTargetVisible();
 868     }
 869     
 870     
 871     public boolean isHierarchyPanelVisible() {
 872         return documentSplitController.isTargetVisible();
 873     }
 874     
 875     
 876     public boolean isLibraryPanelVisible() {
 877         return librarySplitController.isTargetVisible();
 878     }
 879     
 880     public File getResourceFile() {
 881         return resourceController.getResourceFile();
 882     }
 883     
 884     public void setResourceFile(File file) {
 885         resourceController.setResourceFile(file);
 886     }
 887     
 888     public boolean isDocumentDirty() {
 889         return getEditorController().getJobManager().getCurrentJob() != saveJob;
 890     }
 891     
 892     public boolean isUnused() {
 893         /*
 894          * A document window controller is considered as "unused" if: //NOI18N
 895          *  1) it has not fxml text
 896          *  2) it is not dirty
 897          *  3) it is unamed
 898          */
 899         
 900         final FXOMDocument fxomDocument = editorController.getFxomDocument();
 901         final boolean noFxmlText = (fxomDocument == null) || (fxomDocument.getFxomRoot() == null);
 902         final boolean clean = isDocumentDirty() == false;
 903         final boolean noName = (fxomDocument != null) && (fxomDocument.getLocation() == null);
 904         
 905         return noFxmlText && clean && noName;
 906     }
 907     
 908     public static class TitleComparator implements Comparator<DocumentWindowController> {
 909 
 910         @Override
 911         public int compare(DocumentWindowController d1, DocumentWindowController d2) {
 912             final int result;
 913             
 914             assert d1 != null;
 915             assert d2 != null;
 916             
 917             if (d1 == d2) {
 918                 result = 0;
 919             } else {
 920                 final String t1 = d1.getStage().getTitle();
 921                 final String t2 = d2.getStage().getTitle();
 922                 assert t1 != null;
 923                 assert t2 != null;
 924                 result = t1.compareTo(t2);
 925             }
 926             
 927             return result;
 928         }
 929         
 930     }
 931     
 932     public void initializeCssPanel() {
 933         assert cssPanelHost != null;
 934         assert cssPanelSearchPanelHost != null;
 935         if (cssPanelHost.getChildren().isEmpty()) {
 936             cssPanelHost.getChildren().add(cssPanelController.getPanelRoot());
 937         }
 938         if (cssPanelSearchPanelHost.getChildren().isEmpty()) {
 939             cssPanelSearchPanelHost.getChildren().add(cssPanelSearchController.getPanelRoot());
 940             addCssPanelSearchListener();
 941         }
 942     }
 943 
 944     public void updatePreferences() {
 945         final PreferencesController pc = PreferencesController.getSingleton();
 946         final URL fxmlLocation = getEditorController().getFxmlLocation();
 947         if (fxmlLocation == null) {
 948             // Document has not been saved => nothing to write
 949             // This is the case with initial empty document 
 950             return;
 951         }
 952         // Update record document
 953         final PreferencesRecordDocument recordDocument = pc.getRecordDocument(this);
 954         recordDocument.writeToJavaPreferences();
 955         // Update record global
 956         final PreferencesRecordGlobal recordGlobal = pc.getRecordGlobal();
 957         // recentItems may not contain the current document
 958         // if the Open Recent -> Clear menu has been invoked
 959         if (recordGlobal.containsRecentItem(fxmlLocation) == false) {
 960             recordGlobal.addRecentItem(fxmlLocation);
 961         }
 962     }
 963 
 964     /*
 965      * AbstractFxmlWindowController
 966      */
 967     
 968     @Override
 969     protected void controllerDidLoadFxml() {
 970         
 971         assert libraryPanelHost != null;
 972         assert librarySearchPanelHost != null;
 973         assert hierarchyPanelHost != null;
 974         assert infoPanelHost != null;
 975         assert contentPanelHost != null;
 976         assert inspectorPanelHost != null;
 977         assert inspectorSearchPanelHost != null;
 978         assert messageBarHost != null;
 979         assert mainSplitPane != null;
 980         assert mainSplitPane.getItems().size() == 2;
 981         assert leftRightSplitPane != null;
 982         assert leftRightSplitPane.getItems().size() == 3;
 983         assert libraryDocumentSplitPane != null;
 984         assert libraryDocumentSplitPane.getItems().size() == 2;
 985         assert documentAccordion != null;
 986         assert documentAccordion.getPanes().isEmpty() == false;
 987         assert libraryViewAsList != null;
 988         assert libraryViewAsSections != null;
 989         assert libraryReveal != null;
 990         assert libraryMenuButton != null;
 991         assert libraryImportSelection != null;
 992         assert customLibraryMenu != null;
 993         
 994         // Add a border to the Windows app, because of the specific window decoration on Windows.
 995         if (EditorPlatform.IS_WINDOWS) {
 996             getRoot().getStyleClass().add("windows-document-decoration");//NOI18N
 997         }
 998         
 999         mainSplitPane.addEventFilter(KeyEvent.KEY_PRESSED, mainKeyEventFilter);
1000         
1001         // Insert the menu bar
1002         assert getRoot() instanceof VBox;
1003         final VBox rootVBox = (VBox) getRoot();
1004         rootVBox.getChildren().add(0, menuBarController.getMenuBar());
1005         
1006         libraryPanelHost.getChildren().add(libraryPanelController.getPanelRoot());
1007         librarySearchPanelHost.getChildren().add(librarySearchController.getPanelRoot());
1008         hierarchyPanelHost.getChildren().add(hierarchyPanelController.getPanelRoot());
1009         infoPanelHost.getChildren().add(infoPanelController.getPanelRoot());
1010         contentPanelHost.getChildren().add(contentPanelController.getPanelRoot());
1011         inspectorPanelHost.getChildren().add(inspectorPanelController.getPanelRoot());
1012         inspectorSearchPanelHost.getChildren().add(inspectorSearchController.getPanelRoot());
1013         messageBarHost.getChildren().add(messageBarController.getPanelRoot());
1014         
1015         messageBarController.getSelectionBarHost().getChildren().add(
1016                 selectionBarController.getPanelRoot());
1017         
1018         inspectorSearchController.textProperty().addListener((ChangeListener<String>) (ov, oldStr, newStr) -> inspectorPanelController.setSearchPattern(newStr));
1019         
1020         librarySearchController.textProperty().addListener((ChangeListener<String>) (ov, oldStr, newStr) -> libraryPanelController.setSearchPattern(newStr));
1021         
1022         bottomSplitController = new SplitController(mainSplitPane, SplitController.Target.LAST);
1023         leftSplitController = new SplitController(leftRightSplitPane, SplitController.Target.FIRST);
1024         rightSplitController = new SplitController(leftRightSplitPane, SplitController.Target.LAST);
1025         librarySplitController = new SplitController(libraryDocumentSplitPane, SplitController.Target.FIRST);
1026         documentSplitController = new SplitController(libraryDocumentSplitPane, SplitController.Target.LAST);
1027         
1028         messageBarHost.heightProperty().addListener((InvalidationListener) o -> {
1029             final double h = messageBarHost.getHeight();
1030             contentPanelHost.setPadding(new Insets(h, 0.0, 0.0, 0.0));
1031         });
1032         
1033         documentAccordion.setExpandedPane(documentAccordion.getPanes().get(0));
1034         
1035         // Monitor the status of the document to set status icon accordingly in message bar
1036         getEditorController().getJobManager().revisionProperty().addListener((ChangeListener<Number>) (ov, t, t1) -> messageBarController.setDocumentDirty(isDocumentDirty()));
1037         
1038         // Setup title of the Library Reveal menu item according the underlying o/s.
1039         final String revealMenuKey;
1040         if (EditorPlatform.IS_MAC) {
1041             revealMenuKey = "menu.title.reveal.mac";
1042         } else if (EditorPlatform.IS_WINDOWS) {
1043             revealMenuKey = "menu.title.reveal.win";
1044         } else {
1045             assert EditorPlatform.IS_LINUX;
1046             revealMenuKey = "menu.title.reveal.linux";
1047         }
1048         libraryReveal.setText(I18N.getString(revealMenuKey));
1049         
1050         // We need to tune the content of the library menu according if there's
1051         // or not a selection likely to be dropped onto Library panel.
1052         libraryMenuButton.showingProperty().addListener((ChangeListener<Boolean>) (ov, t, t1) -> {
1053             if (t1) {
1054                 AbstractSelectionGroup asg = getEditorController().getSelection().getGroup();
1055                 libraryImportSelection.setDisable(true);
1056 
1057                 if (asg instanceof ObjectSelectionGroup) {
1058                     if (((ObjectSelectionGroup)asg).getItems().size() >= 1) {
1059                         libraryImportSelection.setDisable(false);
1060                     }
1061                 }
1062                 
1063                 // DTL-6439. The custom library menu shall be enabled only
1064                 // in the case there is a user library directory on disk.
1065                 Library lib = getEditorController().getLibrary();
1066                 if (lib instanceof UserLibrary) {
1067                     File userLibDir = new File(((UserLibrary)lib).getPath());
1068                     if (userLibDir.canRead()) {
1069                         customLibraryMenu.setDisable(false);
1070                     } else {
1071                         customLibraryMenu.setDisable(true);
1072                     }
1073                 }
1074             }
1075         });
1076     }
1077 
1078     @Override
1079     protected void controllerDidCreateStage() {
1080         updateStageTitle();
1081         updateFromDocumentPreferences();
1082     }
1083     
1084     @Override
1085     public void openWindow() {
1086         
1087         if (getStage().isShowing() == false) {
1088             // Starts watching document:
1089             //      - editorController watches files referenced from the FXML text
1090             //      - watchingController watches the document file, i18n resources, 
1091             //        preview stylesheets...
1092             assert editorController.isFileWatchingStarted() == false;
1093             editorController.startFileWatching();
1094             watchingController.start();
1095         }
1096         
1097         super.openWindow();
1098         
1099         // Give focus to the library search TextField
1100         assert librarySearchController != null;
1101         librarySearchController.requestFocus();
1102     }
1103     
1104     @Override
1105     public void closeWindow() {
1106         
1107         super.closeWindow();
1108         
1109         // Stops watching
1110         editorController.stopFileWatching();
1111         watchingController.stop();
1112     }
1113     
1114     @Override 
1115     public void onCloseRequest(WindowEvent event) {
1116         performCloseAction();
1117     }
1118 
1119     public boolean isFrontDocumentWindow() {
1120         return getStage().isFocused()
1121                 || (previewWindowController != null && previewWindowController.getStage().isFocused())
1122                 || (skeletonWindowController != null && skeletonWindowController.getStage().isFocused())
1123                 || (jarAnalysisReportController != null && jarAnalysisReportController.getStage().isFocused());
1124     }
1125 
1126     public void performCloseFrontDocumentWindow() {
1127         if (getStage().isFocused()) {
1128             performCloseAction();
1129         } else if (previewWindowController != null
1130                 && previewWindowController.getStage().isFocused()) {
1131             previewWindowController.closeWindow();
1132         } else if (skeletonWindowController != null
1133                 && skeletonWindowController.getStage().isFocused()) {
1134             skeletonWindowController.closeWindow();
1135         } else if (jarAnalysisReportController != null
1136                 && jarAnalysisReportController.getStage().isFocused()) {
1137             jarAnalysisReportController.closeWindow();
1138         }
1139     }
1140 
1141     
1142     @Override
1143     protected void toolStylesheetDidChange(String oldStylesheet) {
1144         super.toolStylesheetDidChange(oldStylesheet);
1145         editorController.setToolStylesheet(getToolStylesheet());
1146         // previewWindowController should not be affected by tool style sheet
1147         if (skeletonWindowController != null) {
1148             skeletonWindowController.setToolStylesheet(getToolStylesheet());
1149         }
1150         if (jarAnalysisReportController != null) {
1151             jarAnalysisReportController.setToolStylesheet(getToolStylesheet());
1152         }
1153     }
1154     
1155     
1156     //
1157     // Inspector menu
1158     //
1159     @FXML
1160     void onInspectorShowAllAction(ActionEvent event) {
1161         inspectorPanelController.setShowMode(InspectorPanelController.ShowMode.ALL);
1162         
1163     }
1164     
1165     @FXML
1166     void onInspectorShowEditedAction(ActionEvent event) {
1167         inspectorPanelController.setShowMode(InspectorPanelController.ShowMode.EDITED);
1168     }
1169     
1170     @FXML
1171     void onInspectorViewSectionsAction(ActionEvent event) {
1172         inspectorPanelController.setViewMode(InspectorPanelController.ViewMode.SECTION);
1173     }
1174     
1175     @FXML
1176     void onInspectorViewByPropertyNameAction(ActionEvent event) {
1177         inspectorPanelController.setViewMode(InspectorPanelController.ViewMode.PROPERTY_NAME);
1178     }
1179     
1180     @FXML
1181     void onInspectorViewByPropertyTypeAction(ActionEvent event) {
1182         inspectorPanelController.setViewMode(InspectorPanelController.ViewMode.PROPERTY_TYPE);
1183     }
1184     
1185     //
1186     // CSS menu
1187     //
1188     
1189     @FXML
1190     void onCssPanelViewRulesAction(ActionEvent event) {
1191         cssPanelMenuController.viewRules();
1192         cssPanelSplitDefaultsMi.setDisable(true);
1193         cssPanelShowStyledOnlyMi.setDisable(true);
1194     }
1195 
1196     @FXML
1197     void onCssPanelViewTableAction(ActionEvent event) {
1198         cssPanelMenuController.viewTable();
1199         cssPanelSplitDefaultsMi.setDisable(false);
1200         cssPanelShowStyledOnlyMi.setDisable(false);
1201     }
1202 
1203     @FXML
1204     void onCssPanelViewTextAction(ActionEvent event) {
1205         cssPanelMenuController.viewText();
1206         cssPanelSplitDefaultsMi.setDisable(true);
1207         cssPanelShowStyledOnlyMi.setDisable(true);
1208     }
1209 
1210     @FXML
1211     void onCssPanelCopyStyleablePathAction(ActionEvent event) {
1212         cssPanelMenuController.copyStyleablePath();
1213     }
1214 
1215     @FXML
1216     void onCssPanelSplitDefaultsAction(ActionEvent event) {
1217         cssPanelMenuController.splitDefaultsAction(cssPanelSplitDefaultsMi);
1218     }
1219 
1220     @FXML
1221     void onCssPanelShowStyledOnlyAction(ActionEvent event) {
1222         cssPanelMenuController.showStyledOnly(cssPanelShowStyledOnlyMi);
1223     }
1224     
1225     //
1226     // Hierarchy menu
1227     //
1228     @FXML
1229     void onHierarchyShowInfo(ActionEvent event) {
1230         hierarchyPanelController.setDisplayOption(AbstractHierarchyPanelController.DisplayOption.INFO);
1231         documentAccordion.setExpandedPane(documentAccordion.getPanes().get(0));
1232     }
1233     
1234     @FXML
1235     void onHierarchyShowFxId(ActionEvent event) {
1236         hierarchyPanelController.setDisplayOption(AbstractHierarchyPanelController.DisplayOption.FXID);
1237         documentAccordion.setExpandedPane(documentAccordion.getPanes().get(0));
1238     }
1239     
1240     @FXML
1241     void onHierarchyShowNodeId(ActionEvent event) {
1242         hierarchyPanelController.setDisplayOption(AbstractHierarchyPanelController.DisplayOption.NODEID);
1243         documentAccordion.setExpandedPane(documentAccordion.getPanes().get(0));
1244     }
1245     
1246     //
1247     // Library menu
1248     //
1249     @FXML
1250     void onLibraryImportJarFxml(ActionEvent event) {
1251         libraryPanelController.performImportJarFxml();
1252     }
1253     
1254     @FXML
1255     void onLibraryViewAsList(ActionEvent event) {
1256         if (libraryPanelController.getDisplayMode() != LibraryPanelController.DISPLAY_MODE.SEARCH) {
1257             libraryPanelController.setDisplayMode(LibraryPanelController.DISPLAY_MODE.LIST);
1258         } else {
1259             libraryPanelController.setPreviousDisplayMode(LibraryPanelController.DISPLAY_MODE.LIST);
1260         }
1261     }
1262     
1263     @FXML
1264     void onLibraryViewAsSections(ActionEvent event) {
1265         if (libraryPanelController.getDisplayMode() != LibraryPanelController.DISPLAY_MODE.SEARCH) {
1266             libraryPanelController.setDisplayMode(LibraryPanelController.DISPLAY_MODE.SECTIONS);
1267         } else {
1268             libraryPanelController.setPreviousDisplayMode(LibraryPanelController.DISPLAY_MODE.SECTIONS);
1269         }
1270     }
1271 
1272     // This method cannot be called if there is not a valid selection, a selection
1273     // eligible for being dropped onto Library panel.
1274     @FXML
1275     void onLibraryImportSelection(ActionEvent event) {
1276         AbstractSelectionGroup asg = getEditorController().getSelection().getGroup();
1277 
1278         if (asg instanceof ObjectSelectionGroup) {
1279             ObjectSelectionGroup osg = (ObjectSelectionGroup)asg;
1280             assert osg.getItems().isEmpty() == false;
1281             List<FXOMObject> selection = new ArrayList<>(osg.getItems());
1282             libraryPanelController.performImportSelection(selection);
1283         }
1284     }
1285     
1286     @FXML
1287     void onLibraryRevealCustomFolder(ActionEvent event) {
1288         String userLibraryPath = ((UserLibrary) getEditorController().getLibrary()).getPath();
1289         try {
1290             EditorPlatform.revealInFileBrowser(new File(userLibraryPath));
1291         } catch(IOException x) {
1292             final ErrorDialog errorDialog = new ErrorDialog(null);
1293             errorDialog.setMessage(I18N.getString("alert.reveal.failure.message", getStage().getTitle()));
1294             errorDialog.setDetails(I18N.getString("alert.reveal.failure.details"));
1295             errorDialog.setDebugInfoWithThrowable(x);
1296             errorDialog.showAndWait();
1297         }
1298     }
1299     
1300     @FXML
1301     void onLibraryShowJarAnalysisReport(ActionEvent event) {
1302         if (jarAnalysisReportController == null) {
1303             jarAnalysisReportController = new JarAnalysisReportController(getEditorController(), getStage());
1304             jarAnalysisReportController.setToolStylesheet(getToolStylesheet());
1305         }
1306         
1307         jarAnalysisReportController.openWindow();
1308     }
1309     
1310     /*
1311      * Private
1312      */
1313 
1314     private boolean canPerformSelectAll() {
1315         final boolean result;
1316         final Node focusOwner = this.getScene().getFocusOwner();
1317         if (isPopupEditing(focusOwner)) {
1318             return false;
1319         } else if (isTextInputControlEditing(focusOwner)) {
1320             final TextInputControl tic = getTextInputControl(focusOwner);
1321             final String text = tic.getText();
1322             final String selectedText = tic.getSelectedText();
1323             if (text == null || text.isEmpty()) {
1324                 result = false;
1325             } else {
1326                 // Check if the TextInputControl is not already ALL selected
1327                 result = selectedText == null
1328                         || selectedText.length() < tic.getText().length();
1329             }
1330         } else {
1331             result = getEditorController().canPerformControlAction(ControlAction.SELECT_ALL);
1332         }
1333         return result;
1334     }
1335 
1336     private void performSelectAll() {
1337         final Node focusOwner = this.getScene().getFocusOwner();
1338         if (isTextInputControlEditing(focusOwner)) {
1339             final TextInputControl tic = getTextInputControl(focusOwner);
1340             tic.selectAll();
1341         } else {
1342             this.getEditorController().performControlAction(ControlAction.SELECT_ALL);
1343         }
1344     }
1345 
1346     private boolean canPerformSelectNone() {
1347         boolean result;
1348         final Node focusOwner = this.getScene().getFocusOwner();
1349         if (isPopupEditing(focusOwner)) {
1350             return false;
1351         } else if (isTextInputControlEditing(focusOwner)) {
1352             final TextInputControl tic = getTextInputControl(focusOwner);
1353             result = tic.getSelectedText() != null && tic.getSelectedText().isEmpty() == false;
1354         } else {
1355             result = getEditorController().canPerformControlAction(ControlAction.SELECT_NONE);
1356         }
1357         return result;
1358     }
1359 
1360     private void performSelectNone() {
1361         final Node focusOwner = this.getScene().getFocusOwner();
1362         if (isTextInputControlEditing(focusOwner)) {
1363             final TextInputControl tic = getTextInputControl(focusOwner);
1364             tic.deselect();
1365         } else {
1366             this.getEditorController().performControlAction(ControlAction.SELECT_NONE);
1367         }
1368     }
1369     
1370     private boolean canPerformCopy() {
1371         boolean result;
1372         final Node focusOwner = this.getScene().getFocusOwner();
1373         if (isPopupEditing(focusOwner)) {
1374             return false;
1375         } else if (isTextInputControlEditing(focusOwner)) {
1376             final TextInputControl tic = getTextInputControl(focusOwner);
1377             result = tic.getSelectedText() != null && tic.getSelectedText().isEmpty() == false;
1378         } else if (isCssRulesEditing(focusOwner) || isCssTextEditing(focusOwner)) {
1379             result = true;
1380         } else {
1381             result = getEditorController().canPerformControlAction(ControlAction.COPY);
1382         }
1383         return result;
1384     }
1385 
1386     private void performCopy() {
1387         final Node focusOwner = this.getScene().getFocusOwner();
1388         if (isTextInputControlEditing(focusOwner)) {
1389             final TextInputControl tic = getTextInputControl(focusOwner);
1390             tic.copy();
1391         } else if (isCssRulesEditing(focusOwner)) {
1392             cssPanelController.copyRules();
1393         } else if (isCssTextEditing(focusOwner)) {
1394             // CSS text pane is a WebView
1395             // Let the WebView handle the copy action natively
1396         } else {
1397             this.getEditorController().performControlAction(ControlAction.COPY);
1398         }
1399     }
1400 
1401     private boolean canPerformCut() {
1402         boolean result;
1403         final Node focusOwner = this.getScene().getFocusOwner();
1404         if (isPopupEditing(focusOwner)) {
1405             return false;
1406         } else if (isTextInputControlEditing(focusOwner)) {
1407             final TextInputControl tic = getTextInputControl(focusOwner);
1408             result = tic.getSelectedText() != null && tic.getSelectedText().isEmpty() == false;
1409         } else {
1410             result = getEditorController().canPerformEditAction(EditAction.CUT);
1411         }
1412         return result;
1413     }
1414     
1415     private void performCut() {
1416         final Node focusOwner = this.getScene().getFocusOwner();
1417         if (isTextInputControlEditing(focusOwner)) {
1418             final TextInputControl tic = getTextInputControl(focusOwner);
1419             tic.cut();
1420         } else {
1421             this.getEditorController().performEditAction(EditAction.CUT);
1422         }
1423     }
1424 
1425     private boolean canPerformPaste() {
1426         boolean result;
1427         final Node focusOwner = this.getScene().getFocusOwner();
1428         // If there is FXML in the clipboard, we paste the FXML whatever the focus owner is
1429         if (getEditorController().canPerformEditAction(EditAction.PASTE)) {
1430             result = true;
1431         } else if (isTextInputControlEditing(focusOwner)) {
1432             result = Clipboard.getSystemClipboard().hasString();
1433         } else {
1434             result = false;
1435         }
1436         return result;
1437     }
1438     
1439     private void performPaste() {
1440         final Node focusOwner = this.getScene().getFocusOwner();
1441         // If there is FXML in the clipboard, we paste the FXML whatever the focus owner is
1442         if (getEditorController().canPerformEditAction(EditAction.PASTE)) {
1443             this.getEditorController().performEditAction(EditAction.PASTE);
1444             // Give focus to content panel
1445             contentPanelController.getGlassLayer().requestFocus();
1446         } else {
1447             assert isTextInputControlEditing(focusOwner);
1448             final TextInputControl tic = getTextInputControl(focusOwner);
1449             tic.paste();
1450         }
1451     }
1452 
1453     private boolean canPerformDelete() {
1454         boolean result;
1455         final Node focusOwner = this.getScene().getFocusOwner();
1456         if (isTextInputControlEditing(focusOwner)) {
1457             final TextInputControl tic = getTextInputControl(focusOwner);
1458             result = tic.getCaretPosition() < tic.getLength();
1459         } else {
1460             result = getEditorController().canPerformEditAction(EditAction.DELETE);
1461         }
1462         return result;
1463     }
1464 
1465     private void performDelete() {
1466 
1467         final Node focusOwner = this.getScene().getFocusOwner();
1468         if (isTextInputControlEditing(focusOwner)) {
1469             final TextInputControl tic = getTextInputControl(focusOwner);
1470             tic.deleteNextChar();
1471         } else {
1472 
1473             // Collects all the selected objects
1474             final List<FXOMObject> selectedObjects = new ArrayList<>();
1475             final Selection selection = editorController.getSelection();
1476             if (selection.getGroup() instanceof ObjectSelectionGroup) {
1477                 final ObjectSelectionGroup osg = (ObjectSelectionGroup) selection.getGroup();
1478                 selectedObjects.addAll(osg.getItems());
1479             } else if (selection.getGroup() instanceof GridSelectionGroup) {
1480                 final GridSelectionGroup gsg = (GridSelectionGroup) selection.getGroup();
1481                 selectedObjects.addAll(gsg.collectSelectedObjects());
1482             } else {
1483                 assert false;
1484             }
1485 
1486             // Collects fx:ids in selected objects and their descendants.
1487             // We filter out toggle groups because their fx:ids are managed automatically.
1488             final Map<String, FXOMObject> fxIdMap = new HashMap<>();
1489             for (FXOMObject selectedObject : selectedObjects) {
1490                 fxIdMap.putAll(selectedObject.collectFxIds());
1491             }
1492             FXOMNodes.removeToggleGroups(fxIdMap);
1493 
1494             // Checks if deleted objects have some fx:ids and ask for confirmation.
1495             final boolean deleteConfirmed;
1496             if (fxIdMap.isEmpty()) {
1497                 deleteConfirmed = true;
1498             } else {
1499                 final String message;
1500 
1501                 if (fxIdMap.size() == 1) {
1502                     if (selectedObjects.size() == 1) {
1503                         message = I18N.getString("alert.delete.fxid1of1.message");
1504                     } else {
1505                         message = I18N.getString("alert.delete.fxid1ofN.message");
1506                     }
1507                 } else {
1508                     if (selectedObjects.size() == fxIdMap.size()) {
1509                         message = I18N.getString("alert.delete.fxidNofN.message");
1510                     } else {
1511                         message = I18N.getString("alert.delete.fxidKofN.message");
1512                     }
1513                 }
1514 
1515                 final AlertDialog d = new AlertDialog(getStage());
1516                 d.setMessage(message);
1517                 d.setDetails(I18N.getString("alert.delete.fxid.details"));
1518                 d.setOKButtonTitle(I18N.getString("label.delete"));
1519 
1520                 deleteConfirmed = (d.showAndWait() == AbstractModalDialog.ButtonID.OK);
1521             }
1522 
1523             if (deleteConfirmed) {
1524                 editorController.performEditAction(EditAction.DELETE);
1525             }
1526         }
1527     }
1528 
1529     private void performImportFxml() {
1530 
1531         final FileChooser fileChooser = new FileChooser();
1532         final ExtensionFilter f
1533                 = new ExtensionFilter(I18N.getString("file.filter.label.fxml"),
1534                         "*.fxml"); //NOI18N
1535         fileChooser.getExtensionFilters().add(f);
1536         fileChooser.setInitialDirectory(EditorController.getNextInitialDirectory());
1537 
1538         File fxmlFile = fileChooser.showOpenDialog(getStage());
1539         if (fxmlFile != null) {
1540             // See DTL-5948: on Linux we anticipate an extension less path.
1541             final String path = fxmlFile.getPath();
1542             if (!path.endsWith(".fxml")) { //NOI18N
1543                 fxmlFile = new File(path + ".fxml"); //NOI18N
1544             }
1545 
1546             // Keep track of the user choice for next time
1547             EditorController.updateNextInitialDirectory(fxmlFile);
1548 
1549             this.getEditorController().performImportFxml(fxmlFile);
1550         }
1551     }
1552 
1553     private void performImportMedia() {
1554 
1555         final FileChooser fileChooser = new FileChooser();
1556         final ExtensionFilter imageFilter
1557                 = new ExtensionFilter(I18N.getString("file.filter.label.image"),
1558                         getImageExtensions());
1559         final ExtensionFilter audioFilter
1560                 = new ExtensionFilter(I18N.getString("file.filter.label.audio"),
1561                         getAudioExtensions());
1562         final ExtensionFilter videoFilter
1563                 = new ExtensionFilter(I18N.getString("file.filter.label.video"),
1564                         getVideoExtensions());
1565         final ExtensionFilter mediaFilter
1566                 = new ExtensionFilter(I18N.getString("file.filter.label.media"),
1567                         getMediaExtensions());
1568         
1569         fileChooser.getExtensionFilters().add(mediaFilter);
1570         fileChooser.getExtensionFilters().add(imageFilter);
1571         fileChooser.getExtensionFilters().add(audioFilter);
1572         fileChooser.getExtensionFilters().add(videoFilter);
1573 
1574         fileChooser.setInitialDirectory(EditorController.getNextInitialDirectory());
1575 
1576         File mediaFile = fileChooser.showOpenDialog(getStage());
1577         if (mediaFile != null) {
1578             
1579             // Keep track of the user choice for next time
1580             EditorController.updateNextInitialDirectory(mediaFile);
1581 
1582             this.getEditorController().performImportMedia(mediaFile);
1583         }
1584     }
1585     
1586     private static synchronized List<String> getImageExtensions() {
1587         if (imageExtensions == null) {
1588             imageExtensions = new ArrayList<>();
1589             imageExtensions.add("*.jpg"); //NOI18N
1590             imageExtensions.add("*.jpeg"); //NOI18N
1591             imageExtensions.add("*.png"); //NOI18N
1592             imageExtensions.add("*.gif"); //NOI18N
1593             imageExtensions = Collections.unmodifiableList(imageExtensions);
1594         }
1595         return imageExtensions;
1596     }
1597 
1598     private static synchronized List<String> getAudioExtensions() {
1599         if (audioExtensions == null) {
1600             audioExtensions = new ArrayList<>();
1601             audioExtensions.add("*.aif"); //NOI18N
1602             audioExtensions.add("*.aiff"); //NOI18N
1603             audioExtensions.add("*.mp3"); //NOI18N
1604             audioExtensions.add("*.m4a"); //NOI18N
1605             audioExtensions.add("*.wav"); //NOI18N
1606             audioExtensions.add("*.m3u"); //NOI18N
1607             audioExtensions.add("*.m3u8"); //NOI18N
1608             audioExtensions = Collections.unmodifiableList(audioExtensions);
1609         }
1610         return audioExtensions;
1611     }
1612 
1613     private static synchronized List<String> getVideoExtensions() {
1614         if (videoExtensions == null) {
1615             videoExtensions = new ArrayList<>();
1616             videoExtensions.add("*.flv"); //NOI18N
1617             videoExtensions.add("*.fxm"); //NOI18N
1618             videoExtensions.add("*.mp4"); //NOI18N
1619             videoExtensions.add("*.m4v"); //NOI18N
1620             videoExtensions = Collections.unmodifiableList(videoExtensions);
1621         }
1622         return videoExtensions;
1623     }
1624 
1625     private static synchronized List<String> getMediaExtensions() {
1626         if (mediaExtensions == null) {
1627             mediaExtensions = new ArrayList<>();
1628             mediaExtensions.addAll(getImageExtensions());
1629             mediaExtensions.addAll(getAudioExtensions());
1630             mediaExtensions.addAll(getVideoExtensions());
1631             mediaExtensions = Collections.unmodifiableList(mediaExtensions);
1632         }
1633         return mediaExtensions;
1634     }
1635     
1636     private void performIncludeFxml() {
1637 
1638         final FileChooser fileChooser = new FileChooser();
1639         final ExtensionFilter f
1640                 = new ExtensionFilter(I18N.getString("file.filter.label.fxml"),
1641                         "*.fxml"); //NOI18N
1642         fileChooser.getExtensionFilters().add(f);
1643         fileChooser.setInitialDirectory(EditorController.getNextInitialDirectory());
1644 
1645         File fxmlFile = fileChooser.showOpenDialog(getStage());
1646         if (fxmlFile != null) {
1647             // See DTL-5948: on Linux we anticipate an extension less path.
1648             final String path = fxmlFile.getPath();
1649             if (!path.endsWith(".fxml")) { //NOI18N
1650                 fxmlFile = new File(path + ".fxml"); //NOI18N
1651             }
1652 
1653             // Keep track of the user choice for next time
1654             EditorController.updateNextInitialDirectory(fxmlFile);
1655 
1656             this.getEditorController().performIncludeFxml(fxmlFile);
1657         }
1658     }
1659 
1660     /**
1661      * Returns true if the specified node is part of the main scene and is
1662      * either a TextInputControl or a ComboBox.
1663      * 
1664      * @param node the focused node of the main scene
1665      * @return 
1666      */
1667     private boolean isTextInputControlEditing(Node node) {
1668         return (node instanceof TextInputControl
1669                 || node instanceof ComboBox);
1670     }
1671 
1672     private TextInputControl getTextInputControl(Node node) {
1673         assert isTextInputControlEditing(node);
1674         final TextInputControl tic;
1675         if (node instanceof TextInputControl) {
1676             tic = (TextInputControl) node;
1677         } else {
1678             assert node instanceof ComboBox;
1679             final ComboBox<?> cb = (ComboBox<?>) node;
1680             tic = cb.getEditor();
1681         }
1682         return tic;
1683     }
1684     
1685     /**
1686      * Returns true if we are editing within a popup window :
1687      * either the specified node is showing a popup window
1688      * or the inline editing popup is showing.
1689      *
1690      * @param node the focused node of the main scene
1691      * @return
1692      */
1693     private boolean isPopupEditing(Node node) {
1694         return (node instanceof MenuButton && ((MenuButton) node).isShowing())
1695                 || editorController.getInlineEditController().isWindowOpened();
1696     }
1697     
1698     private boolean isCssRulesEditing(Node node) {
1699         final Node cssRules = cssPanelController.getRulesPane();
1700         if (cssRules != null) {
1701             return isDescendantOf(cssRules, node);
1702         }
1703         return false;
1704     }
1705 
1706     private boolean isCssTextEditing(Node node) {
1707         final Node cssText = cssPanelController.getTextPane();
1708         if (cssText != null) {
1709             return isDescendantOf(cssText, node);
1710         }
1711         return false;
1712     }
1713 
1714     private boolean isDescendantOf(Node container, Node node) {
1715         Node child = node;
1716         while (child != null) {
1717             if (child == container) {
1718                 return true;
1719             }
1720             child = child.getParent();
1721         }
1722         return false;
1723     }
1724 
1725     private KeyCombination getAccelerator(final KeyEvent event) {
1726         KeyCombination result = null;
1727         for (KeyCombination kc : menuBarController.getAccelerators()) {
1728             if (kc.match(event)) {
1729                 result = kc;
1730                 break;
1731             }
1732         }
1733         return result;
1734     }
1735 
1736     private void updateStageTitle() {
1737         if (libraryPanelHost != null) {
1738             getStage().setTitle(makeTitle(editorController.getFxomDocument()));
1739         } // else controllerDidLoadFxml() will invoke me again
1740     }
1741     
1742     private void updateFromDocumentPreferences() {
1743         if (libraryPanelHost != null) { // Layout is over
1744             // Refresh UI with preferences 
1745             final PreferencesController pc = PreferencesController.getSingleton();
1746             // Preferences global to the application
1747             final PreferencesRecordGlobal recordGlobal = pc.getRecordGlobal();
1748             recordGlobal.refresh(this);
1749             // Preferences specific to the document
1750             final PreferencesRecordDocument recordDocument = pc.getRecordDocument(this);
1751             recordDocument.readFromJavaPreferences();
1752             // Update UI accordingly
1753             recordDocument.refresh();
1754         }
1755     }
1756     
1757     private void resetDocumentPreferences() {
1758         final PreferencesController pc = PreferencesController.getSingleton();
1759         final PreferencesRecordDocument recordDocument = pc.getRecordDocument(this);
1760         recordDocument.resetDocumentPreferences();
1761     }
1762     
1763     ActionStatus performSaveOrSaveAsAction() {
1764         final ActionStatus result;
1765         
1766         if (editorController.getFxomDocument().getLocation() == null) {
1767             result = performSaveAsAction();
1768         } else {
1769             result = performSaveAction();
1770         }
1771         
1772         if (result.equals(ActionStatus.DONE)) {
1773             messageBarController.setDocumentDirty(false);
1774             saveJob = getEditorController().getJobManager().getCurrentJob();
1775         }
1776         
1777         return result;
1778     }
1779     
1780     private void addCssPanelSearchListener() {
1781         cssPanelSearchController.textProperty().addListener((ChangeListener<String>) (ov, oldStr, newStr) -> cssPanelController.setSearchPattern(newStr));
1782     }
1783     
1784     private void performGoToSection(SectionId sectionId) {
1785         // First make the right panel visible if not already the case
1786         if (isRightPanelVisible() == false) {
1787             performControlAction(DocumentControlAction.TOGGLE_RIGHT_PANEL);
1788         }
1789         inspectorPanelController.setExpandedSection(sectionId);
1790     }
1791     
1792     private ActionStatus performSaveAction() {
1793         final FXOMDocument fxomDocument = editorController.getFxomDocument();
1794         assert fxomDocument != null;
1795         assert fxomDocument.getLocation() != null;
1796         
1797         ActionStatus result;
1798         if (editorController.canGetFxmlText()) {
1799             final Path fxmlPath;
1800             try {
1801                 fxmlPath = Paths.get(fxomDocument.getLocation().toURI());
1802             } catch(URISyntaxException x) {
1803                 // Should not happen
1804                 throw new RuntimeException("Bug in " + getClass().getSimpleName(), x); //NOI18N
1805             }
1806             final String fileName = fxmlPath.getFileName().toString();
1807             
1808             try {
1809                 final boolean saveConfirmed;
1810                 if (checkLoadFileTime()) {
1811                     saveConfirmed = true;
1812                 } else {
1813                     final AlertDialog d = new AlertDialog(getStage());
1814                     d.setMessage(I18N.getString("alert.overwrite.message", fileName));
1815                     d.setDetails(I18N.getString("alert.overwrite.details"));
1816                     d.setOKButtonVisible(true);
1817                     d.setOKButtonTitle(I18N.getString("label.overwrite"));
1818                     d.setDefaultButtonID(ButtonID.CANCEL);
1819                     d.setShowDefaultButton(true);
1820                     saveConfirmed = (d.showAndWait() == ButtonID.OK);
1821                 }
1822             
1823                 if (saveConfirmed) {
1824                     try {
1825                         watchingController.removeDocumentTarget();
1826                         final byte[] fxmlBytes = editorController.getFxmlText().getBytes("UTF-8"); //NOI18N
1827                         Files.write(fxmlPath, fxmlBytes);
1828                         updateLoadFileTime();
1829                         watchingController.update();
1830 
1831                         editorController.getMessageLog().logInfoMessage(
1832                                 "log.info.save.confirmation", I18N.getBundle(), fileName);
1833                         result = ActionStatus.DONE;
1834                     } catch(UnsupportedEncodingException x) {
1835                         // Should not happen
1836                         throw new RuntimeException("Bug", x); //NOI18N
1837                     }
1838                 } else {
1839                     result = ActionStatus.CANCELLED;
1840                 }
1841             } catch(IOException x) {
1842                 final ErrorDialog d = new ErrorDialog(getStage());
1843                 d.setMessage(I18N.getString("alert.save.failure.message", fileName));
1844                 d.setDetails(I18N.getString("alert.save.failure.details"));
1845                 d.setDebugInfoWithThrowable(x);
1846                 d.showAndWait();
1847                 result = ActionStatus.CANCELLED;
1848             }
1849         } else {
1850             result = ActionStatus.CANCELLED;
1851         }
1852         
1853         return result;
1854     }
1855     
1856     
1857     private ActionStatus performSaveAsAction() {
1858         
1859         final ActionStatus result;
1860         if (editorController.canGetFxmlText()) {
1861             final FileChooser fileChooser = new FileChooser();
1862             final FileChooser.ExtensionFilter f 
1863                     = new FileChooser.ExtensionFilter(I18N.getString("file.filter.label.fxml"),
1864                             "*.fxml"); //NOI18N
1865             fileChooser.getExtensionFilters().add(f);
1866             fileChooser.setInitialDirectory(EditorController.getNextInitialDirectory());
1867 
1868             File fxmlFile = fileChooser.showSaveDialog(getStage());
1869             if (fxmlFile == null) {
1870                 result = ActionStatus.CANCELLED;
1871             } else {
1872                 boolean forgetSave = false;
1873                 // It is only on Linux where you can get the case the path doesn't
1874                 // end with the extension, thanks the behavior of the FX 8 FileChooser
1875                 // on this specific OS (see RT-31956).
1876                 // Below we ask the user if the extension shall be added or not.
1877                 // See DTL-5948.
1878                 final String path = fxmlFile.getPath();
1879                 if (! path.endsWith(".fxml")) { //NOI18N
1880                     try {
1881                         URL alternateURL = new URL(fxmlFile.toURI().toURL().toExternalForm() + ".fxml"); //NOI18N
1882                         File alternateFxmlFile = new File(alternateURL.toURI());
1883                         final AlertDialog d = new AlertDialog(getStage());
1884                         d.setMessage(I18N.getString("alert.save.noextension.message", fxmlFile.getName()));
1885                         String details = I18N.getString("alert.save.noextension.details");
1886 
1887                         if (alternateFxmlFile.exists()) {
1888                             details += "\n" //NOI18N
1889                                     + I18N.getString("alert.save.noextension.details.overwrite", alternateFxmlFile.getName());
1890                         }
1891 
1892                         d.setDetails(details);
1893                         d.setOKButtonVisible(true);
1894                         d.setOKButtonTitle(I18N.getString("alert.save.noextension.savewith"));
1895                         d.setDefaultButtonID(ButtonID.OK);
1896                         d.setShowDefaultButton(true);
1897                         d.setActionButtonDisable(false);
1898                         d.setActionButtonVisible(true);
1899                         d.setActionButtonTitle(I18N.getString("alert.save.noextension.savewithout"));
1900 
1901                         switch (d.showAndWait()) {
1902                             case ACTION:
1903                                 // Nothing to do, we save with the no extension name
1904                                 break;
1905                             case CANCEL:
1906                                 forgetSave = true;
1907                                 break;
1908                             case OK:
1909                                 fxmlFile = alternateFxmlFile;
1910                                 break;
1911                         }
1912                     } catch (MalformedURLException | URISyntaxException ex) {
1913                         forgetSave = true;
1914                     }
1915                 }
1916                 
1917                 // Transform File into URL
1918                 final URL newLocation;
1919                 try {
1920                     newLocation = fxmlFile.toURI().toURL();
1921                 } catch(MalformedURLException x) {
1922                     // Should not happen
1923                     throw new RuntimeException("Bug in " + getClass().getSimpleName(), x); //NOI18N
1924                 }
1925                 
1926                 // Checks if fxmlFile is the name of an already opened document
1927                 final DocumentWindowController dwc
1928                         = SceneBuilderApp.getSingleton().lookupDocumentWindowControllers(newLocation);
1929                 if (dwc != null && dwc != this) {
1930                     final Path fxmlPath = Paths.get(fxmlFile.toString());
1931                     final String fileName = fxmlPath.getFileName().toString();
1932                     final ErrorDialog d = new ErrorDialog(getStage());
1933                     d.setMessage(I18N.getString("alert.save.conflict.message", fileName));
1934                     d.setDetails(I18N.getString("alert.save.conflict.details"));
1935                     d.showAndWait();
1936                     result = ActionStatus.CANCELLED;
1937                 } else if (forgetSave) {
1938                     result = ActionStatus.CANCELLED;
1939                 } else {
1940                     // Recalculates references if needed
1941                     // TODO(elp)
1942 
1943                     // First change the location of the fxom document
1944                     editorController.setFxmlLocation(newLocation);
1945                     updateLoadFileTime();
1946                     updateStageTitle();
1947                     // We use same DocumentWindowController BUT we change its fxml :
1948                     // => reset document preferences
1949                     resetDocumentPreferences();
1950                     
1951                     watchingController.update();
1952 
1953                     // Now performs a regular save action
1954                     result = performSaveAction();
1955                     if (result.equals(ActionStatus.DONE)) {
1956                         messageBarController.setDocumentDirty(false);
1957                         saveJob = getEditorController().getJobManager().getCurrentJob();
1958                     }
1959                     
1960                     // Keep track of the user choice for next time
1961                     EditorController.updateNextInitialDirectory(fxmlFile);
1962 
1963                     // Update recent items with just saved file
1964                     final PreferencesController pc = PreferencesController.getSingleton();
1965                     final PreferencesRecordGlobal recordGlobal = pc.getRecordGlobal();
1966                     recordGlobal.addRecentItem(fxmlFile);
1967                 }
1968             }
1969         } else {
1970             result = ActionStatus.CANCELLED;
1971         }
1972         
1973         return result;
1974     }
1975     
1976     
1977     private void performRevertAction() {
1978         assert editorController.getFxomDocument() != null;
1979         assert editorController.getFxomDocument().getLocation() != null;
1980         
1981         final AlertDialog d = new AlertDialog(getStage());
1982         d.setMessage(I18N.getString("alert.revert.question.message", getStage().getTitle()));
1983         d.setDetails(I18N.getString("alert.revert.question.details"));
1984         d.setOKButtonTitle(I18N.getString("label.revert"));
1985 
1986         if (d.showAndWait() == AlertDialog.ButtonID.OK) {
1987             try {
1988                 reload();
1989             } catch(IOException x) {
1990                 final ErrorDialog errorDialog = new ErrorDialog(null);
1991                 errorDialog.setMessage(I18N.getString("alert.open.failure1.message", getStage().getTitle()));
1992                 errorDialog.setDetails(I18N.getString("alert.open.failure1.details"));
1993                 errorDialog.setDebugInfoWithThrowable(x);
1994                 errorDialog.setTitle(I18N.getString("alert.title.open"));
1995                 errorDialog.showAndWait();
1996                 SceneBuilderApp.getSingleton().documentWindowRequestClose(this);
1997             }
1998         }
1999     }
2000     
2001     
2002     ActionStatus performCloseAction() {
2003         
2004         // Makes sure that our window is front 
2005         getStage().toFront();
2006         
2007         // Check if an editing session is on going
2008         if (getEditorController().isTextEditingSessionOnGoing()) {
2009             // Check if we can commit the editing session
2010             if (getEditorController().canGetFxmlText() == false) {
2011                 // Commit failed
2012                 return ActionStatus.CANCELLED;
2013             }
2014         }
2015 
2016         // Checks if there are some pending changes
2017         final boolean closeConfirmed;
2018         if (isDocumentDirty()) {
2019             
2020             final AlertDialog d = new AlertDialog(getStage());
2021             d.setMessage(I18N.getString("alert.save.question.message", getStage().getTitle()));
2022             d.setDetails(I18N.getString("alert.save.question.details"));
2023             d.setOKButtonTitle(I18N.getString("label.save"));
2024             d.setActionButtonTitle(I18N.getString("label.do.not.save"));
2025             d.setActionButtonVisible(true);
2026             
2027             switch(d.showAndWait()) {
2028                 default:
2029                 case OK:
2030                     if (editorController.getFxomDocument().getLocation() == null) {
2031                         closeConfirmed = (performSaveAsAction() == ActionStatus.DONE);
2032                     } else {
2033                         closeConfirmed = (performSaveAction() == ActionStatus.DONE);
2034                     }
2035                     break;
2036                 case CANCEL:
2037                     closeConfirmed = false;
2038                     break;
2039                 case ACTION: // Do not save
2040                     closeConfirmed = true;
2041                     break;
2042             }
2043             
2044         } else {
2045             // No pending changes
2046             closeConfirmed = true;
2047         }
2048         
2049         // Closes if confirmed
2050         if (closeConfirmed) {
2051             SceneBuilderApp.getSingleton().documentWindowRequestClose(this);
2052             
2053             // Write java preferences at close time
2054             updatePreferences();
2055         }
2056                 
2057         return closeConfirmed ? ActionStatus.DONE : ActionStatus.CANCELLED;
2058     }
2059     
2060     
2061     private void performRevealAction() {
2062         assert editorController.getFxomDocument() != null;
2063         assert editorController.getFxomDocument().getLocation() != null;
2064         
2065         final URL location = editorController.getFxomDocument().getLocation();
2066         
2067         try {
2068             final File fxmlFile = new File(location.toURI());
2069             EditorPlatform.revealInFileBrowser(fxmlFile);
2070         } catch(IOException | URISyntaxException x) {
2071             final ErrorDialog errorDialog = new ErrorDialog(null);
2072             errorDialog.setMessage(I18N.getString("alert.reveal.failure.message", getStage().getTitle()));
2073             errorDialog.setDetails(I18N.getString("alert.reveal.failure.details"));
2074             errorDialog.setDebugInfoWithThrowable(x);
2075             errorDialog.showAndWait();
2076         }
2077     }
2078     
2079     
2080     private void updateLoadFileTime() {
2081         
2082         final URL fxmlURL = editorController.getFxmlLocation();
2083         if (fxmlURL == null) {
2084             loadFileTime = null;
2085         } else {
2086             try {
2087                 final Path fxmlPath = Paths.get(fxmlURL.toURI());
2088                 if (Files.exists(fxmlPath)) {
2089                     loadFileTime = Files.getLastModifiedTime(fxmlPath);
2090                 } else {
2091                     loadFileTime = null;
2092                 }
2093             } catch(URISyntaxException x) {
2094                 throw new RuntimeException("Bug", x); //NOI18N
2095             } catch(IOException x) {
2096                 loadFileTime = null;
2097             }
2098         }
2099     }
2100     
2101     
2102     private boolean checkLoadFileTime() throws IOException {
2103         assert editorController.getFxmlLocation() != null;
2104         
2105         /*
2106          *  loadFileTime == null
2107          *          => fxml file does not exist
2108          *          => TRUE
2109          *
2110          *  loadFileTime != null
2111          *          => fxml file does/did exist
2112          *
2113          *          currentFileTime == null
2114          *              => fxml file no longer exists
2115          *              => TRUE
2116          *
2117          *          currentFileTime != null
2118          *              => fxml file still exists
2119          *              => loadFileTime.compare(currentFileTime) == 0
2120          */
2121         
2122         boolean result;
2123         if (loadFileTime == null) {
2124             // editorController.getFxmlLocation() does not exist yet
2125             result = true;
2126         } else {
2127             try {
2128                 // editorController.getFxmlLocation() still exists
2129                 // Check if its file time matches loadFileTime
2130                 Path fxmlPath = Paths.get(editorController.getFxmlLocation().toURI());
2131                 FileTime currentFileTime = Files.getLastModifiedTime(fxmlPath);
2132                 result = loadFileTime.compareTo(currentFileTime) == 0;
2133             } catch(NoSuchFileException x) {
2134                 // editorController.getFxmlLocation() no longer exists
2135                 result = true;
2136             } catch(URISyntaxException x) {
2137                 throw new RuntimeException("Bug", x); //NOI18N
2138             }
2139         }
2140         
2141         return result;
2142     }
2143     
2144         
2145     private void performHelp() {
2146         try {
2147             EditorPlatform.open(EditorPlatform.DOCUMENTATION_URL);
2148         } catch (IOException ioe) {
2149             final ErrorDialog errorDialog = new ErrorDialog(null);
2150             errorDialog.setMessage(I18N.getString("alert.help.failure.message", EditorPlatform.DOCUMENTATION_URL));
2151             errorDialog.setDetails(I18N.getString("alert.messagebox.failure.details"));
2152             errorDialog.setDebugInfoWithThrowable(ioe);
2153             errorDialog.showAndWait();
2154         }
2155     }
2156 }
2157 
2158 /**
2159  * This class setup key bindings for the TextInputControl type classes and
2160  * provide a way to access the key binding list.
2161  */
2162 class SBTextInputControlBindings extends com.sun.javafx.scene.control.behavior.TextInputControlBindings {
2163 
2164     private SBTextInputControlBindings() {
2165         assert false;
2166     }
2167 
2168     public static List<KeyBinding> getBindings() {
2169         return BINDINGS;
2170     }
2171 }