1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.oracle.javafx.scenebuilder.kit.editor.panel.library;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
  35 import com.oracle.javafx.scenebuilder.kit.editor.drag.source.AbstractDragSource;
  36 import com.oracle.javafx.scenebuilder.kit.editor.drag.source.DocumentDragSource;
  37 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N;
  38 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.AbstractFxmlPanelController;
  39 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AbstractModalDialog.ButtonID;
  40 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AlertDialog;
  41 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.ErrorDialog;
  42 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMArchive;
  43 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument;
  44 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  45 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject;
  46 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMProperty;
  47 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMPropertyT;
  48 import com.oracle.javafx.scenebuilder.kit.library.BuiltinLibrary;
  49 import com.oracle.javafx.scenebuilder.kit.library.Library;
  50 import com.oracle.javafx.scenebuilder.kit.library.LibraryItem;
  51 import com.oracle.javafx.scenebuilder.kit.library.LibraryItemNameComparator;
  52 import com.oracle.javafx.scenebuilder.kit.library.user.UserLibrary;
  53 import com.oracle.javafx.scenebuilder.kit.metadata.util.PrefixedValue;
  54 import com.oracle.javafx.scenebuilder.kit.metadata.util.PropertyName;
  55 
  56 import java.io.File;
  57 import java.io.IOException;
  58 import java.io.PrintWriter;
  59 import java.net.MalformedURLException;
  60 import java.net.URISyntaxException;
  61 import java.net.URL;
  62 import java.nio.file.FileSystemNotFoundException;
  63 import java.nio.file.Files;
  64 import java.nio.file.Path;
  65 import java.nio.file.Paths;
  66 import java.nio.file.StandardCopyOption;
  67 import java.nio.file.attribute.FileAttribute;
  68 import java.util.ArrayList;
  69 import java.util.Collections;
  70 import java.util.LinkedHashMap;
  71 import java.util.List;
  72 import java.util.Locale;
  73 import java.util.Timer;
  74 import java.util.TimerTask;
  75 import java.util.TreeSet;
  76 
  77 import javafx.application.Platform;
  78 import javafx.beans.value.ChangeListener;
  79 import javafx.collections.ListChangeListener;
  80 import javafx.event.EventHandler;
  81 import javafx.fxml.FXML;
  82 import javafx.scene.control.Accordion;
  83 import javafx.scene.control.Label;
  84 import javafx.scene.control.ListCell;
  85 import javafx.scene.control.ListView;
  86 import javafx.scene.control.TitledPane;
  87 import javafx.scene.input.Dragboard;
  88 import javafx.scene.input.KeyCode;
  89 import javafx.scene.input.KeyEvent;
  90 import javafx.scene.input.TransferMode;
  91 import javafx.scene.layout.StackPane;
  92 import javafx.stage.FileChooser;
  93 import javafx.stage.Stage;
  94 import javafx.stage.Window;
  95 import javafx.util.Callback;
  96 
  97 /**
  98  * This class creates and controls the <b>Library Panel</b> of Scene Builder
  99  * Kit.
 100  *
 101  */
 102 public class LibraryPanelController extends AbstractFxmlPanelController {
 103 
 104     private String searchPattern;
 105     ArrayList<LibraryItem> searchData = new ArrayList<>();
 106     private final String TEMP_FILE_EXTENSION = ".tmp"; //NOI18N
 107     // The name of the library section to keep opened. This is used when e.g.
 108     // a user jar file is imported to the library directory.
 109     // If the user was doing a search then we let the library layout unchanged.
 110     // If the user wasn't doing a search and custom content is updated then
 111     // we want to get Custom section opened.
 112     String sectionNameToKeepOpened = null;
 113     boolean initiateImportDialog = false;
 114     final List<File> jarAndFxmlFiles = new ArrayList<>();
 115     private String userLibraryPathString = null;
 116 
 117     @FXML
 118     private Accordion libAccordion;
 119     @FXML
 120     Label noSearchResults;
 121     @FXML ListView<LibraryListItem> libSearchList;
 122 
 123     @FXML ListView<LibraryListItem> libList = null;
 124 
 125     @FXML StackPane libPane;
 126 
 127     /*
 128      * Public
 129      */
 130 
 131     /**
 132      * Creates a library panel controller for the specified editor controller.
 133      *
 134      * @param c the editor controller (never null).
 135      */
 136     public LibraryPanelController(EditorController c) {
 137         super(LibraryPanelController.class.getResource("LibraryPanel.fxml"), I18N.getBundle(), c); //NOI18N
 138         startListeningToLibrary();
 139     }
 140 
 141     /**
 142      * Returns null or the search pattern applied to this library panel.
 143      *
 144      * @return null or the search pattern applied to this library panel.
 145      */
 146     public String getSearchPattern() {
 147         return searchPattern;
 148     }
 149 
 150     /**
 151      * Sets the search pattern to be applied to this library panel. When null
 152      * value is passed, the library panel displays all its items.
 153      *
 154      * @param searchPattern null or the search pattern to apply to this library
 155      * panel.
 156      */
 157     public void setSearchPattern(String searchPattern) {
 158         this.searchPattern = searchPattern.toUpperCase(Locale.ENGLISH);
 159         searchPatternDidChange();
 160     }
 161 
 162     /**
 163      * @treatAsPrivate Perform the import jar action.
 164      */
 165     public void performImportJarFxml() {
 166         // Open file chooser and get user selection
 167         final List<File> importedFiles = performSelectJarOrFxmlFile();
 168         processImportJarFxml(importedFiles);
 169     }
 170 
 171     /**
 172      * @treatAsPrivate Perform the import of the selection
 173      * @param objects the FXOM objects to import to customize the Library content.
 174      */
 175     public void performImportSelection(List<FXOMObject> objects) {
 176         processInternalImport(objects);
 177     }
 178 
 179     /*
 180      * AbstractPanelController
 181      */
 182 
 183     /**
 184      * @treatAsPrivate FXOM document did change.
 185      * @param oldDocument the previous fxom document or null
 186      */
 187     @Override
 188     protected void fxomDocumentDidChange(FXOMDocument oldDocument) {
 189     }
 190 
 191     /**
 192      * @treatAsPrivate User scene graph did change.
 193      */
 194     @Override
 195     protected void sceneGraphRevisionDidChange() {
 196     }
 197 
 198     /**
 199      * @treatAsPrivate User scene graph did change.
 200      */
 201     @Override
 202     protected void cssRevisionDidChange() {
 203     }
 204 
 205     /**
 206      * @treatAsPrivate Job manager revision did change.
 207      */
 208     @Override
 209     protected void jobManagerRevisionDidChange() {
 210         // FXOMDocument has been modified by a job.
 211         // Library panel should probably not care for now.
 212     }
 213 
 214     /**
 215      * @treatAsPrivate Selection did change.
 216      */
 217     @Override
 218     protected void editorSelectionDidChange() {
 219     }
 220 
 221     /*
 222      * AbstractFxmlPanelController
 223      */
 224 
 225     /**
 226      * @treatAsPrivate Controller did load fxml.
 227      */
 228     @Override
 229     protected void controllerDidLoadFxml() {
 230         assert libAccordion != null;
 231         assert libPane != null;
 232         assert libList != null;
 233         assert noSearchResults != null;
 234         assert libSearchList != null;
 235 
 236         startListeningToDrop();
 237         setDisplayMode(DISPLAY_MODE.SECTIONS);
 238         populateLibraryPanel();
 239         setUserLibraryPathString();
 240     }
 241 
 242     private void displayModeDidChange(DISPLAY_MODE displayMode) {
 243         if (libAccordion != null) {
 244             switch (displayMode) {
 245                 case SECTIONS:
 246                     libAccordion.setVisible(true);
 247                     libAccordion.setManaged(true);
 248                     noSearchResults.setVisible(false);
 249                     noSearchResults.setManaged(false);
 250                     libSearchList.setVisible(false);
 251                     libSearchList.setManaged(false);
 252                     getLibList().setVisible(false);
 253                     getLibList().setManaged(false);
 254                     break;
 255                 case SEARCH:
 256                     libAccordion.setVisible(false);
 257                     libAccordion.setManaged(false);
 258                     if (libSearchList.getItems().isEmpty()) {
 259                         noSearchResults.setVisible(true);
 260                         noSearchResults.setManaged(true);
 261                         libSearchList.setVisible(false);
 262                         libSearchList.setManaged(false);
 263                     } else {
 264                         noSearchResults.setVisible(false);
 265                         noSearchResults.setManaged(false);
 266                         libSearchList.setVisible(true);
 267                         libSearchList.setManaged(true);
 268                     }
 269                     getLibList().setVisible(false);
 270                     getLibList().setManaged(false);
 271                     break;
 272                 case LIST:
 273                     libAccordion.setVisible(false);
 274                     libAccordion.setManaged(false);
 275                     noSearchResults.setVisible(false);
 276                     noSearchResults.setManaged(false);
 277                     libSearchList.setVisible(false);
 278                     libSearchList.setManaged(false);
 279                     getLibList().setVisible(true);
 280                     getLibList().setManaged(true);
 281                     break;
 282                 default:
 283                     break;
 284             }
 285         }
 286     }
 287 
 288     /*
 289      * Private
 290      */
 291 
 292     /**
 293      * The display mode of the Library panel is used to choose how items
 294      * are rendered within the panel.
 295      */
 296     public enum DISPLAY_MODE {
 297 
 298         SECTIONS {
 299 
 300                     @Override
 301                     public String toString() {
 302                         return I18N.getString("library.panel.menu.view.sections");
 303                     }
 304                 },
 305         SEARCH,
 306         LIST {
 307 
 308                     @Override
 309                     public String toString() {
 310                         return I18N.getString("library.panel.menu.view.list");
 311                     }
 312                 }
 313     };
 314 
 315     private DISPLAY_MODE currentDisplayMode;
 316     private DISPLAY_MODE previousDisplayMode = DISPLAY_MODE.SECTIONS;
 317 
 318     public void setPreviousDisplayMode(DISPLAY_MODE displayMode) {
 319         this.previousDisplayMode = displayMode;
 320     }
 321 
 322     public void setDisplayMode(DISPLAY_MODE displayMode) {
 323         this.currentDisplayMode = displayMode;
 324         displayModeDidChange(displayMode);
 325     }
 326 
 327     public DISPLAY_MODE getDisplayMode() {
 328         return currentDisplayMode;
 329     }
 330 
 331     final ListChangeListener<LibraryItem> libraryItemListener = change -> libraryDidChange(null);
 332 
 333     private final ChangeListener<Library> libraryListener = (ov, t, t1) -> {
 334         // When a jar is imported this listener is called two times.
 335         // First the UserLibrary is turned into BuiltinLibrary, then it is
 336         // turned back into a UserLibrary with the up to date library dir
 337         // content.
 338 //            System.out.println("libraryListener called - t " + t + " - t1 " + t1);
 339         if (t instanceof UserLibrary) {
 340             t.getItems().removeListener(libraryItemListener);
 341             t.getItems().clear();
 342         }
 343         if (t1 instanceof UserLibrary) {
 344             t1.getItems().addListener(libraryItemListener);
 345             if (sectionNameToKeepOpened != null) {
 346                 sectionNameToKeepOpened = null;
 347             }
 348         }
 349         // libraryDidChange might not be called by several listeners.
 350         // Silencing the one below means I dunno how to get the selected index.
 351 //            libraryDidChange(t);
 352     };
 353 
 354     private void startListeningToLibrary() {
 355         getEditorController().libraryProperty().addListener(libraryListener);
 356     }
 357 
 358     // For now there is no scenario where this method might be of some use.
 359 //    private void stopListeningToLibrary() {
 360 //        getEditorController().libraryProperty().removeListener(libraryListener);
 361 //    }
 362 
 363     void libraryDidChange(Library oldLib) {
 364         if (libAccordion != null) {
 365             // Clear the content of the panel.
 366             libAccordion.getPanes().clear();
 367 
 368             // Reconstruct the panel content based on the new Library.
 369             populateLibraryPanel();
 370         }
 371     }
 372 
 373     private String getExpandedSectionName() {
 374         String sectionName = null;
 375 
 376         if (libAccordion != null && libAccordion.getExpandedPane() != null) {
 377             sectionName = libAccordion.getExpandedPane().getText();
 378         }
 379 
 380 //        System.out.println("getExpandedSectionName " + sectionName);
 381         return sectionName;
 382     }
 383 
 384 //    private String getSelectedItemName() {
 385 //        String selectedItemName = null;
 386 //
 387 //        if (libAccordion != null && libAccordion.getExpandedPane() != null) {
 388 //            final ListView<?> list = (ListView<?>)libAccordion.getExpandedPane().getContent();
 389 //            Object selectedItem = list.getSelectionModel().getSelectedItem();
 390 //            if (selectedItem instanceof LibraryListItem) {
 391 //                selectedItemName = ((LibraryListItem)selectedItem).getLibItem().getName();
 392 //            }
 393 //        }
 394 //
 395 ////        System.out.println("getSelectedItemName " + selectedItemName);
 396 //        return selectedItemName;
 397 //    }
 398 
 399     // We need to discover the sections and the content of each one before being
 400     // able to populate the panel.
 401     // Each section is a TitledPane that contains a ListView of LibraryItem.
 402     // The order section are listed is managed by the comparator that comes with
 403     // the Library. In each section the LibraryItem are sorted with alphabetical
 404     // ordering.
 405     // First TitledPane is expanded.
 406     private void populateLibraryPanel() {
 407         // libData is backend structure for all that we put in the Accordion.
 408         LinkedHashMap<String, ArrayList<LibraryItem>> libData = new LinkedHashMap<>();
 409         TreeSet<String> sectionNames = new TreeSet<>(getEditorController().getLibrary().getSectionComparator());
 410         List<TitledPane> panes = libAccordion.getPanes();
 411 
 412         searchData.clear();
 413         getLibList().getItems().clear();
 414 
 415         if (getEditorController().getLibrary().getItems().size() > 0) {
 416             // Construct a sorted set of all lib section names.
 417             for (LibraryItem item : getEditorController().getLibrary().getItems()) {
 418                 sectionNames.add(item.getSection());
 419             }
 420 
 421             // Create a sorted set of lib elements for each section.
 422             for (String sectionName : sectionNames) {
 423                 libData.put(sectionName, new ArrayList<>());
 424             }
 425 
 426             // Add each LibraryItem to the appropriate set.
 427             for (LibraryItem item : getEditorController().getLibrary().getItems()) {
 428                 libData.get(item.getSection()).add(item);
 429             }
 430 
 431             // Parse our lib data structure and populate the Accordion accordingly.
 432             for (String sectionName : sectionNames) {
 433                 ListView<LibraryListItem> itemsList = new ListView<>();
 434                 itemsList.setId(sectionName + "List"); // for QE //NOI18N
 435                 itemsList.setCellFactory(cb);
 436                 itemsList.addEventHandler(KeyEvent.KEY_RELEASED, keyEventHandler);
 437                 Collections.sort(libData.get(sectionName), new LibraryItemNameComparator());
 438                 for (LibraryItem item : libData.get(sectionName)) {
 439                     itemsList.getItems().add(new LibraryListItem(item));
 440                 }
 441                 TitledPane sectionPane = new TitledPane(sectionName, itemsList);
 442                 sectionPane.setId(sectionName); // for QE
 443                 sectionPane.setAnimated(true);
 444                 panes.add(sectionPane);
 445 
 446                 searchData.addAll(libData.get(sectionName));
 447 
 448                 getLibList().getItems().add(new LibraryListItem(sectionName));
 449                 for (LibraryItem item : libData.get(sectionName)) {
 450                     getLibList().getItems().add(new LibraryListItem(item));
 451                 }
 452             }
 453 
 454             if (libAccordion.getPanes().size() >= 1) {
 455                 expandPaneWithName(sectionNameToKeepOpened);
 456             }
 457 
 458             if (libSearchList.getCellFactory() == null) {
 459                 libSearchList.setCellFactory(cb);
 460             }
 461 
 462             if (getLibList().getCellFactory() == null) {
 463                 getLibList().setCellFactory(cb);
 464             }
 465 
 466             libSearchList.addEventHandler(KeyEvent.KEY_RELEASED, keyEventHandler);
 467             getLibList().addEventHandler(KeyEvent.KEY_RELEASED, keyEventHandler);
 468         }
 469     }
 470 
 471     private void expandPaneWithName(String paneName) {
 472         String sectionName = paneName;
 473 
 474         if (sectionName == null) {
 475             sectionName = BuiltinLibrary.TAG_CONTAINERS;
 476         }
 477 
 478         for (TitledPane tp : libAccordion.getPanes()) {
 479             if (tp.getText().equals(sectionName)) {
 480 //                System.out.println("expandPaneWithName - Expand section " + sectionName);
 481                 libAccordion.setExpandedPane(tp);
 482             }
 483         }
 484     }
 485 
 486     private void searchPatternDidChange() {
 487         if (searchPattern == null || searchPattern.isEmpty()) {
 488             currentDisplayMode = previousDisplayMode;
 489         } else {
 490             if (currentDisplayMode != DISPLAY_MODE.SEARCH) {
 491                 previousDisplayMode = currentDisplayMode;
 492                 currentDisplayMode = DISPLAY_MODE.SEARCH;
 493             }
 494         }
 495 
 496         // The filtering is done by ignoring case, and by retaining any item that
 497         // contains the given pattern. An opened question is to filter as soon as
 498         // the pattern is two or more characters long: for now we react from the
 499         // first character.
 500         //
 501         // It can occur the whole Library is changed under the foots of SceneBuilder
 502         // while filtering is on going. searchData is never null so if its
 503         // content is changing then the filtering result will be inacurrate:
 504         // that is acceptable.
 505         //
 506         if (currentDisplayMode.equals(DISPLAY_MODE.SEARCH)) {
 507             libSearchList.getItems().clear();
 508             final ArrayList<LibraryItem> rawFilteredItem = new ArrayList<>();
 509             for (LibraryItem item : searchData) {
 510                 if (item.getName().toUpperCase(Locale.ROOT).contains(searchPattern)) {
 511                     rawFilteredItem.add(item);
 512                 }
 513             }
 514             Collections.sort(rawFilteredItem, new LibraryItemNameComparator());
 515             for (LibraryItem item : rawFilteredItem) {
 516                 libSearchList.getItems().add(new LibraryListItem(item));
 517             }
 518             rawFilteredItem.clear();
 519         }
 520 
 521         setDisplayMode(currentDisplayMode);
 522     }
 523 
 524     // Key events listened onto the ListView
 525     // For some reason the listener when set on the cell (see LibraryListCell)
 526     // is never called, probably because it is the ListView which has the focus.
 527     private final EventHandler<KeyEvent> keyEventHandler = e -> handleKeyEvent(e);
 528 
 529     private final Callback<ListView<LibraryListItem>, ListCell<LibraryListItem>> cb
 530             = param -> new LibraryListCell(getEditorController());
 531 
 532     private void handleKeyEvent(KeyEvent e) {
 533         // On ENTER we try to insert the item which is selected within the Library.
 534         if (e.getCode() == KeyCode.ENTER) {
 535             // This way of doing things requires the use of an @SuppressWarnings("unchecked")
 536 //            final LibraryItem item = ((ListView<LibraryItem>)e.getSource()).getSelectionModel().getSelectedItem();
 537             // hence this other way below ...
 538             Object source = e.getSource();
 539             assert source instanceof ListView;
 540             final ListView<?> list = (ListView<?>)source;
 541             Object rawItem = list.getSelectionModel().getSelectedItem();
 542             assert rawItem instanceof LibraryListItem;
 543             final LibraryListItem listitem = (LibraryListItem)rawItem;
 544             final LibraryItem item = listitem.getLibItem();
 545 
 546             if (getEditorController().canPerformInsert(item)) {
 547                 getEditorController().performInsert(item);
 548             }
 549 
 550             e.consume();
 551         }
 552     }
 553 
 554     private void startListeningToDrop() {
 555         libPane.setOnDragDropped(t -> {
 556 //                System.out.println("libPane onDragDropped");
 557             AbstractDragSource dragSource = getEditorController().getDragController().getDragSource();
 558             if (dragSource instanceof DocumentDragSource) {
 559                 processInternalImport(((DocumentDragSource)dragSource).getDraggedObjects());
 560             } else {
 561                 initiateImportDialog = false;
 562                 jarAndFxmlFiles.clear();
 563                 t.setDropCompleted(true);
 564                 // Drop gesture is only valid when the Library is an instance of UserLibrary
 565                 if (getEditorController().getLibrary() instanceof UserLibrary) {
 566                     Dragboard db = t.getDragboard();
 567                     if (db.hasFiles()) {
 568                         final List<File> files = db.getFiles();
 569                         for (File file : files) {
 570                             // Keep only jar and fxml files
 571                             if (file.isFile() && (file.getName().endsWith(".jar") || file.getName().endsWith(".fxml"))) { //NOI18N
 572 //                                System.out.println("libPane onDragDropped - Retaining file " + file.getName());
 573                                 jarAndFxmlFiles.add(file);
 574                             }
 575                         }
 576 
 577                         // The import dialog might be kept opened by the user quite
 578                         // a long time.
 579                         // On Mac (not on Win), after around 20 seconds of opening
 580                         // time of the import dialog window the user sees a move from
 581                         // the lib panel to the Finder of the file icon, as if the drag
 582                         // is rejected.
 583                         // In order to silence (mask ?) this issue there's:
 584                         // - the delegation to setOnDragExited of the call of processImportJarFxml
 585                         // so that current handler returns fast.
 586                         // - the runLater in setOnDragExited, wrapped with a Timer set with a 1 second delay
 587                         // Is there a way to be notified when the import dialog
 588                         // can be run without interfering with the drag and drop sequence ?
 589                         initiateImportDialog = true;
 590                     }
 591                 }
 592             }
 593         });
 594 
 595         libPane.setOnDragExited(t -> {
 596 //                System.out.println("libPane onDragExited");
 597             if (initiateImportDialog) {
 598                 initiateImportDialog = false;
 599                 final Timer timer = new Timer(true);
 600                 final TimerTask timerTask = new TimerTask() {
 601 
 602                     @Override
 603                     public void run() {
 604                         Platform.runLater(() -> processImportJarFxml(jarAndFxmlFiles));
 605                         // I don't need to use the timer later on so by
 606                         // cancelling it right here I'm sure free resources
 607                         // that otherwise would prevent the JVM from exiting.
 608                         timer.cancel();
 609                     }
 610                 };
 611                 timer.schedule(timerTask, 600); // milliseconds
 612             }
 613         });
 614 
 615 
 616         libPane.setOnDragOver(t -> {
 617 //                System.out.println("libPane onDragOver");
 618             AbstractDragSource dragSource = getEditorController().getDragController().getDragSource();
 619             Dragboard db = t.getDragboard();
 620             // db has file when dragging a file from native file manager (Mac Finder, Windows Explorer, ...).
 621             // dragSource is not null if the user drags something from Hierarchy or Content panel.
 622             if (db.hasFiles() || dragSource != null) {
 623                 t.acceptTransferModes(TransferMode.COPY);
 624             }
 625         });
 626 
 627         // This one is called only if lib is the source of the drop.
 628         libPane.setOnDragDone(t -> {
 629             assert getEditorController().getDragController().getDragSource() != null;
 630             getEditorController().getDragController().end();
 631             t.getDragboard().clear();
 632             t.consume();
 633         });
 634 
 635     }
 636 
 637     // An internal import is an import to the Library initiated from within
 638     // SceneBuilder (from Content or Hierarchy).
 639     // We stop the watching thread to avoid potential parsing of a file that
 640     // would not yet be properly finalized on disk.
 641     private void processInternalImport(List<FXOMObject> objects) {
 642         sectionNameToKeepOpened = getExpandedSectionName();
 643         setUserLibraryPathString();
 644         Path libPath = Paths.get(userLibraryPathString);
 645         boolean hasDependencies = false;
 646         // Selection can be multiple: as soon as one has dependencies
 647         // we won't import anything.
 648         // Copy of the dependencies remains something to do (DTL-5879).
 649         for (FXOMObject asset : objects) {
 650             if (hasDependencies(asset)) {
 651                 hasDependencies = true;
 652                 break;
 653             }
 654         }
 655 
 656         if (hasDependencies) {
 657             userLibraryUpdateRejected();
 658         } else {
 659             ((UserLibrary) getEditorController().getLibrary()).stopWatching();
 660 
 661             try {
 662                 // The selection can be multiple, in which case each asset is
 663                 // processed separately.
 664                 for (FXOMObject asset : objects) {
 665                     // Create an FXML layout as a String
 666                     ArrayList<FXOMObject> selection = new ArrayList<>();
 667                     selection.add(asset);
 668                     final FXOMArchive fxomArchive = new FXOMArchive(selection);
 669                     final FXOMArchive.Entry entry0 = fxomArchive.getEntries().get(0);
 670                     String fxmlText = entry0.getFxmlText();
 671 
 672                     // Write the FXML layout into a dedicated file stored in the user Library dir.
 673                     // We use the tag name of the top element and append a number:
 674                     // if Library dir already contains SplitPane_1.fxml and top element has
 675                     // tag name SplitPane then we will create SplitPane_2.fxml and so on.
 676                     // Note the tag name is most of the time identical to the Java class name, see DTL-6643.
 677                     String prefix = asset.getGlueElement().getTagName();
 678                     File fxmlFile = getUniqueFxmlFileName(prefix, userLibraryPathString);
 679                     writeFxmlFile(fxmlFile, fxmlText, libPath);
 680                 }
 681             } finally {
 682                 if (currentDisplayMode.equals(DISPLAY_MODE.SECTIONS)) {
 683                     sectionNameToKeepOpened = UserLibrary.TAG_USER_DEFINED;
 684                 }
 685 
 686                 ((UserLibrary) getEditorController().getLibrary()).startWatching();
 687             }
 688         }
 689     }
 690 
 691     private void writeFxmlFile(File targetFile, String text, Path libPath) {
 692         Path targetFilePath = Paths.get(targetFile.getPath());
 693         createUserLibraryDir(libPath);
 694 
 695         try {
 696             // Create the new file
 697             Files.createFile(targetFilePath);
 698 
 699             // Write content of file
 700             try (PrintWriter writer = new PrintWriter(targetFile, "UTF-8")) { //NOI18N
 701                 writer.write(text);
 702             }
 703         } catch (IOException ioe) {
 704             final ErrorDialog errorDialog = new ErrorDialog(null);
 705             errorDialog.setTitle(I18N.getString("error.file.create.title"));
 706             errorDialog.setMessage(I18N.getString("error.file.create.message", targetFilePath.normalize().toString()));
 707             errorDialog.setDetails(I18N.getString("error.write.details"));
 708             errorDialog.setDebugInfoWithThrowable(ioe);
 709             errorDialog.showAndWait();
 710         }
 711     }
 712 
 713     private File getUniqueFxmlFileName(String prefix, String libDir) {
 714         int suffix = 0;
 715         File file = null;
 716         while (file == null || file.exists()) {
 717             suffix++;
 718             file = new File(libDir + File.separator + prefix + "_" + suffix + ".fxml"); //NOI18N
 719         }
 720 
 721         return file;
 722     }
 723 
 724     private void processImportJarFxml(List<File> importedFiles) {
 725         if (importedFiles != null && !importedFiles.isEmpty()) {
 726             sectionNameToKeepOpened = getExpandedSectionName();
 727             Path libPath = Paths.get(((UserLibrary)getEditorController().getLibrary()).getPath());
 728             // Create UserLibrary dir if missing
 729             if (createUserLibraryDir(libPath)) {
 730                 final List<File> fxmlFiles = getSubsetOfFiles(".fxml", importedFiles); //NOI18N
 731 
 732                 if (!fxmlFiles.isEmpty() && enoughFreeSpaceOnDisk(fxmlFiles) && ! hasDependencies(fxmlFiles)) {
 733                     copyFilesToUserLibraryDir(fxmlFiles);
 734 
 735                     if (currentDisplayMode.equals(DISPLAY_MODE.SECTIONS)) {
 736                         sectionNameToKeepOpened = UserLibrary.TAG_USER_DEFINED;
 737                     }
 738                 }
 739 
 740                 final List<File> jarFiles = getSubsetOfFiles(".jar", importedFiles); //NOI18N
 741                 // For jar files we delegate to the import dialog.
 742                 if (!jarFiles.isEmpty() && enoughFreeSpaceOnDisk(jarFiles)) {
 743                     // From here we know we will initiate the import dialog.
 744                     // This is why we put application window on the front.
 745                     // From there the import dialog window, which is application modal,
 746                     // should come on top of it.
 747                     final Window window = getPanelRoot().getScene().getWindow();
 748                     if (window instanceof Stage) {
 749                         final Stage stage = (Stage) window;
 750                         stage.toFront();
 751                     }
 752 
 753                     final ImportWindowController iwc
 754                             = new ImportWindowController(this, jarFiles, window);
 755                     iwc.setToolStylesheet(getEditorController().getToolStylesheet());
 756                     // See comment in OnDragDropped handle set in method startListeningToDrop.
 757                     ButtonID userChoice = iwc.showAndWait();
 758 
 759                     if (userChoice.equals(ButtonID.OK) && currentDisplayMode.equals(DISPLAY_MODE.SECTIONS)) {
 760                         sectionNameToKeepOpened = UserLibrary.TAG_USER_DEFINED;
 761                     }
 762                 }
 763             }
 764         }
 765     }
 766 
 767     private List<File> getSubsetOfFiles(String pattern, List<File> files) {
 768         final List<File> res = new ArrayList<>();
 769 
 770         for (File file : files) {
 771             if (file.getName().endsWith(pattern)) {
 772                 res.add(file);
 773             }
 774         }
 775 
 776         return res;
 777     }
 778 
 779     private boolean createUserLibraryDir(Path libPath) {
 780         boolean dirCreated = false;
 781         try {
 782             // Files.createDirectories do nothing if provided Path already exists.
 783             Files.createDirectories(libPath, new FileAttribute<?>[]{});
 784             dirCreated = true;
 785         } catch (IOException ioe) {
 786             final ErrorDialog errorDialog = new ErrorDialog(null);
 787             errorDialog.setTitle(I18N.getString("error.dir.create.title"));
 788             errorDialog.setMessage(I18N.getString("error.dir.create.message", libPath.normalize().toString()));
 789             errorDialog.setDetails(I18N.getString("error.write.details"));
 790             errorDialog.setDebugInfoWithThrowable(ioe);
 791             errorDialog.showAndWait();
 792         }
 793 
 794         return dirCreated;
 795     }
 796 
 797     private boolean enoughFreeSpaceOnDisk(List<File> files) {
 798         long totalSize = Long.MAX_VALUE; // bytes
 799         try {
 800             for (File file : files) {
 801                 Path targetPath = Paths.get(file.getAbsolutePath());
 802                 totalSize += Files.size(targetPath);
 803             }
 804         } catch (IOException ioe) {
 805             final ErrorDialog errorDialog = new ErrorDialog(null);
 806             errorDialog.setTitle(I18N.getString("error.disk.space.title"));
 807             errorDialog.setMessage(I18N.getString("error.disk.space.message"));
 808             errorDialog.setDetails(I18N.getString("error.write.details"));
 809             errorDialog.setDebugInfoWithThrowable(ioe);
 810             errorDialog.showAndWait();
 811         }
 812 
 813         final File libFile = new File(((UserLibrary)getEditorController().getLibrary()).getPath());
 814         return totalSize < libFile.getFreeSpace();
 815     }
 816 
 817     // Each copy is done via an intermediate temporary file that is renamed if
 818     // the copy goes well (for atomicity). If a copy fails we try to erase the
 819     // temporary file to stick to an as clean as possible disk content.
 820     // TODO fix DTL-5879 [When copying FXML files in lib dir we have to copy files which are external references as well]
 821     void copyFilesToUserLibraryDir(List<File> files) {
 822         int errorCount = 0;
 823         IOException savedIOE = null;
 824         String savedFileName = ""; //NOI18N
 825         Path tempTargetPath = null;
 826         setUserLibraryPathString();
 827 
 828         // Here we deactivate the UserLib so that it unlocks the files contained
 829         // in the lib dir in the file system meaning (especially on Windows).
 830         ((UserLibrary) getEditorController().getLibrary()).stopWatching();
 831 
 832         try {
 833             for (File file : files) {
 834                 savedFileName = file.getName();
 835                 tempTargetPath = Paths.get(userLibraryPathString, file.getName() + TEMP_FILE_EXTENSION);
 836                 Path ultimateTargetPath = Paths.get(userLibraryPathString, file.getName());
 837                 Files.deleteIfExists(tempTargetPath);
 838                 Files.copy(file.toPath(), tempTargetPath, StandardCopyOption.REPLACE_EXISTING);
 839                 Files.move(tempTargetPath, ultimateTargetPath, StandardCopyOption.ATOMIC_MOVE);
 840             }
 841         } catch (IOException ioe) {
 842             errorCount++;
 843             savedIOE = ioe;
 844         } finally {
 845             if (tempTargetPath != null) {
 846                 try {
 847                     Files.deleteIfExists(tempTargetPath);
 848                 } catch (IOException ioe) {
 849                     errorCount++;
 850                     savedIOE = ioe;
 851                 }
 852             }
 853         }
 854 
 855         ((UserLibrary) getEditorController().getLibrary()).startWatching();
 856 
 857         if (errorCount > 0) {
 858             final ErrorDialog errorDialog = new ErrorDialog(null);
 859             errorDialog.setTitle(I18N.getString("error.copy.title"));
 860             if (errorCount == 1) {
 861                 errorDialog.setMessage(I18N.getString("error.copy.message.single", savedFileName, userLibraryPathString));
 862                 errorDialog.setDebugInfoWithThrowable(savedIOE);
 863             } else {
 864                 errorDialog.setMessage(I18N.getString("error.copy.message.multiple", errorCount, userLibraryPathString));
 865             }
 866             errorDialog.setDetails(I18N.getString("error.write.details"));
 867             errorDialog.showAndWait();
 868         }
 869     }
 870 
 871     /**
 872      * Open a file chooser that allows to select one or more FXML and JAR file.
 873      * @return the list of selected files
 874      */
 875     private List<File> performSelectJarOrFxmlFile() {
 876         FileChooser fileChooser = new FileChooser();
 877         fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(I18N.getString("lib.filechooser.filter.msg"),
 878                 "*.fxml", "*.jar")); //NOI18N
 879         fileChooser.setInitialDirectory(EditorController.getNextInitialDirectory());
 880         List<File> selectedFiles = fileChooser.showOpenMultipleDialog(null);
 881         if (!selectedFiles.isEmpty()) {
 882             // Keep track of the user choice for next time
 883             EditorController.updateNextInitialDirectory(selectedFiles.get(0));
 884         }
 885         return selectedFiles;
 886     }
 887 
 888     private void userLibraryUpdateRejected() {
 889         final AlertDialog dialog = new AlertDialog(null);
 890         dialog.setTitle(I18N.getString("alert.import.reject.dependencies.title"));
 891         dialog.setMessage(I18N.getString("alert.import.reject.dependencies.message"));
 892         dialog.setDetails(I18N.getString("alert.import.reject.dependencies.details"));
 893         dialog.setActionButtonDisable(true);
 894         dialog.setActionButtonVisible(false);
 895         dialog.setOKButtonDisable(true);
 896         dialog.setOKButtonVisible(false);
 897         dialog.setCancelButtonTitle(I18N.getString("label.close"));
 898         dialog.showAndWait();
 899     }
 900 
 901     private static final PropertyName valueName = new PropertyName("value"); //NOI18N
 902 
 903     private boolean hasDependencies(List<File> fxmlFiles) {
 904         boolean hasDependencies = false;
 905         boolean scanWentWell = true;
 906 
 907         for (File fxmlFile : fxmlFiles) {
 908             try {
 909                 if (hasDependencies(fxmlFile)) {
 910                     hasDependencies = true;
 911                     break;
 912                 }
 913             } catch (IOException ioe) {
 914                 scanWentWell = false;
 915                 hasDependencies = true; // not sure but better take no risk
 916                 final ErrorDialog errorDialog = new ErrorDialog(null);
 917                 errorDialog.setTitle(I18N.getString("error.import.reject.dependencies.scan.title"));
 918                 errorDialog.setMessage(I18N.getString("error.import.reject.dependencies.scan.message"));
 919                 errorDialog.setDetails(I18N.getString("error.import.reject.dependencies.scan.details"));
 920                 errorDialog.setDebugInfoWithThrowable(ioe);
 921                 errorDialog.showAndWait();
 922             }
 923         }
 924 
 925         if (hasDependencies && scanWentWell) {
 926             userLibraryUpdateRejected();
 927         }
 928 
 929         return hasDependencies;
 930     }
 931 
 932     private boolean hasDependencies(File fxmlFile) throws IOException {
 933         boolean res = false;
 934         URL location;
 935 
 936         location = fxmlFile.toURI().toURL();
 937         FXOMDocument fxomDocument =
 938                 new FXOMDocument(FXOMDocument.readContentFromURL(location), location,
 939                         getEditorController().getFxomDocument().getClassLoader(),
 940                         getEditorController().getFxomDocument().getResources());
 941         res = hasDependencies(fxomDocument.getFxomRoot());
 942 
 943         return res;
 944     }
 945 
 946     private boolean hasDependencies(FXOMObject rootFxomObject) {
 947         final List<Path> targetPaths = getDependenciesPaths(rootFxomObject);
 948         return targetPaths.size() > 0;
 949     }
 950 
 951     private List<Path> getDependenciesPaths(FXOMObject rootFxomObject) {
 952 
 953         final List<Path> targetPaths = new ArrayList<>();
 954 
 955         for (FXOMPropertyT p : rootFxomObject.collectPropertiesT()) {
 956             final Path path = extractPath(p);
 957             if (path != null) {
 958                 targetPaths.add(path);
 959             }
 960         }
 961 
 962         for (FXOMObject fxomObject : rootFxomObject.collectObjectWithSceneGraphObjectClass(URL.class)) {
 963             if (fxomObject instanceof FXOMInstance) {
 964                 final FXOMInstance urlInstance = (FXOMInstance) fxomObject;
 965                 final FXOMProperty valueProperty = urlInstance.getProperties().get(valueName);
 966                 if (valueProperty instanceof FXOMPropertyT) {
 967                     FXOMPropertyT valuePropertyT = (FXOMPropertyT) valueProperty;
 968                     final Path path = extractPath(valuePropertyT);
 969                     if (path != null) {
 970                         targetPaths.add(path);
 971                     }
 972                 } else {
 973                     assert false : "valueProperty.getName() = " + valueProperty.getName();
 974                 }
 975             }
 976         }
 977 
 978         return targetPaths;
 979     }
 980 
 981 
 982     private Path extractPath(FXOMPropertyT p) {
 983         Path result;
 984 
 985         final PrefixedValue pv = new PrefixedValue(p.getValue());
 986         if (pv.isPlainString()) {
 987             try {
 988                 final URL url = new URL(pv.getSuffix());
 989                 result = Paths.get(url.toURI());
 990             } catch(MalformedURLException|URISyntaxException x) {
 991                 result = null;
 992             }
 993         } else if (pv.isDocumentRelativePath()) {
 994             final URL documentLocation = p.getFxomDocument().getLocation();
 995             if (documentLocation == null) {
 996                 result = null;
 997             } else {
 998                 final URL url = pv.resolveDocumentRelativePath(documentLocation);
 999                 if (url == null) {
1000                     result = null;
1001                 } else {
1002                     try {
1003                         result = Paths.get(url.toURI());
1004                     } catch(FileSystemNotFoundException|URISyntaxException x) {
1005                         result = null;
1006                     }
1007                 }
1008             }
1009         } else if (pv.isClassLoaderRelativePath()) {
1010             final ClassLoader classLoader = p.getFxomDocument().getClassLoader();
1011             if (classLoader == null) {
1012                 result = null;
1013             } else {
1014                 final URL url = pv.resolveClassLoaderRelativePath(classLoader);
1015                 if (url == null) {
1016                     result = null;
1017                 } else {
1018                     try {
1019                         result = Paths.get(url.toURI());
1020                     } catch(URISyntaxException x) {
1021                         result = null;
1022                     }
1023                 }
1024 
1025             }
1026         } else {
1027             result = null;
1028         }
1029 
1030         return result;
1031     }
1032 
1033     private void setUserLibraryPathString() {
1034         if (getEditorController().getLibrary() instanceof UserLibrary
1035                 && userLibraryPathString == null) {
1036             userLibraryPathString = ((UserLibrary) getEditorController().getLibrary()).getPath();
1037             assert userLibraryPathString != null;
1038         }
1039     }
1040 
1041     private ListView<LibraryListItem> getLibList() {
1042         if (libList == null) {
1043             libList = new ListView<>();
1044         }
1045 
1046         return libList;
1047     }
1048 }