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 }