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.DocumentWindowController.ActionStatus;
  35 import com.oracle.javafx.scenebuilder.app.about.AboutWindowController;
  36 import com.oracle.javafx.scenebuilder.app.i18n.I18N;
  37 import com.oracle.javafx.scenebuilder.app.menubar.MenuBarController;
  38 import com.oracle.javafx.scenebuilder.app.preferences.PreferencesController;
  39 import com.oracle.javafx.scenebuilder.app.preferences.PreferencesRecordGlobal;
  40 import com.oracle.javafx.scenebuilder.app.preferences.PreferencesWindowController;
  41 import com.oracle.javafx.scenebuilder.app.template.FxmlTemplates;
  42 import com.oracle.javafx.scenebuilder.app.template.TemplateDialogController;
  43 import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
  44 import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform;
  45 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.AlertDialog;
  46 import com.oracle.javafx.scenebuilder.kit.editor.panel.util.dialog.ErrorDialog;
  47 import com.oracle.javafx.scenebuilder.kit.library.BuiltinLibrary;
  48 import com.oracle.javafx.scenebuilder.kit.library.user.UserLibrary;
  49 import com.oracle.javafx.scenebuilder.kit.metadata.Metadata;
  50 import com.oracle.javafx.scenebuilder.kit.util.Deprecation;
  51 import com.oracle.javafx.scenebuilder.kit.util.control.effectpicker.EffectPicker;
  52 
  53 import java.io.File;
  54 import java.io.IOException;
  55 import java.net.URI;
  56 import java.net.URISyntaxException;
  57 import java.net.URL;
  58 import java.nio.file.Path;
  59 import java.nio.file.Paths;
  60 import java.util.ArrayList;
  61 import java.util.Collections;
  62 import java.util.HashMap;
  63 import java.util.List;
  64 import java.util.Map;
  65 import java.util.concurrent.CountDownLatch;
  66 import java.util.logging.Level;
  67 import java.util.logging.Logger;
  68 
  69 import javafx.application.Application;
  70 import javafx.application.Platform;
  71 import javafx.beans.value.ChangeListener;
  72 import javafx.stage.FileChooser;
  73 import javafx.stage.Stage;
  74 
  75 /**
  76  *
  77  */
  78 public class SceneBuilderApp extends Application implements AppPlatform.AppNotificationHandler {
  79 
  80     public enum ApplicationControlAction {
  81 
  82         ABOUT,
  83         NEW_FILE,
  84         NEW_ALERT_DIALOG,
  85         NEW_ALERT_DIALOG_CSS,
  86         NEW_ALERT_DIALOG_I18N,
  87         NEW_BASIC_APPLICATION,
  88         NEW_BASIC_APPLICATION_CSS,
  89         NEW_BASIC_APPLICATION_I18N,
  90         NEW_COMPLEX_APPLICATION,
  91         NEW_COMPLEX_APPLICATION_CSS,
  92         NEW_COMPLEX_APPLICATION_I18N,
  93         OPEN_FILE,
  94         CLOSE_FRONT_WINDOW,
  95         USE_DEFAULT_THEME,
  96         USE_DARK_THEME,
  97         SHOW_PREFERENCES,
  98         EXIT
  99     }
 100 
 101     public enum ToolTheme {
 102 
 103         DEFAULT {
 104                     @Override
 105                     public String toString() {
 106                         return I18N.getString("prefs.tool.theme.default");
 107                     }
 108                 },
 109         DARK {
 110                     @Override
 111                     public String toString() {
 112                         return I18N.getString("prefs.tool.theme.dark");
 113                     }
 114                 }
 115     }
 116 
 117     private static SceneBuilderApp singleton;
 118     private static String darkToolStylesheet;
 119     private static final CountDownLatch launchLatch = new CountDownLatch(1);
 120 
 121     private final List<DocumentWindowController> windowList = new ArrayList<>();
 122     private final PreferencesWindowController preferencesWindowController
 123             = new PreferencesWindowController();
 124     private final AboutWindowController aboutWindowController
 125             = new AboutWindowController();
 126     private UserLibrary userLibrary;
 127     private ToolTheme toolTheme = ToolTheme.DEFAULT;
 128 
 129 
 130     /*
 131      * Public
 132      */
 133     public static SceneBuilderApp getSingleton() {
 134         return singleton;
 135     }
 136 
 137     public SceneBuilderApp() {
 138         assert singleton == null;
 139         singleton = this;
 140 
 141         /*
 142          * We spawn our two threads for handling background startup.
 143          */
 144         final Runnable p0 = () -> backgroundStartPhase0();
 145         final Runnable p1 = () -> {
 146             try {
 147                 launchLatch.await();
 148                 backgroundStartPhase2();
 149             } catch(InterruptedException x) {
 150                 // JavaFX thread has been interrupted. Simply exits.
 151             }
 152         };
 153         final Thread phase0 = new Thread(p0, "Phase 0"); //NOI18N
 154         final Thread phase1 = new Thread(p1, "Phase 1"); //NOI18N
 155         phase0.setDaemon(true);
 156         phase1.setDaemon(true);
 157 
 158         // Note : if you suspect a race condition bug, comment the two next
 159         // lines to make startup fully sequential.
 160         phase0.start();
 161         phase1.start();
 162     }
 163 
 164     public void performControlAction(ApplicationControlAction a, DocumentWindowController source) {
 165         switch (a) {
 166             case ABOUT:
 167                 aboutWindowController.openWindow();
 168                 break;
 169 
 170             case NEW_FILE:
 171                 final DocumentWindowController newWindow = makeNewWindow();
 172                 newWindow.loadWithDefaultContent();
 173                 newWindow.openWindow();
 174                 break;
 175 
 176             case NEW_ALERT_DIALOG:
 177             case NEW_BASIC_APPLICATION:
 178             case NEW_COMPLEX_APPLICATION:
 179                 performNewTemplate(a);
 180                 break;
 181 
 182             case NEW_ALERT_DIALOG_CSS:
 183             case NEW_ALERT_DIALOG_I18N:
 184             case NEW_BASIC_APPLICATION_CSS:
 185             case NEW_BASIC_APPLICATION_I18N:
 186             case NEW_COMPLEX_APPLICATION_CSS:
 187             case NEW_COMPLEX_APPLICATION_I18N:
 188                 performNewTemplateWithResources(a);
 189                 break;
 190 
 191             case OPEN_FILE:
 192                 performOpenFile(source);
 193                 break;
 194 
 195             case CLOSE_FRONT_WINDOW:
 196                 performCloseFrontWindow();
 197                 break;
 198 
 199             case USE_DEFAULT_THEME:
 200                 performUseToolTheme(ToolTheme.DEFAULT);
 201                 break;
 202 
 203             case USE_DARK_THEME:
 204                 performUseToolTheme(ToolTheme.DARK);
 205                 break;
 206 
 207             case SHOW_PREFERENCES:
 208                 preferencesWindowController.openWindow();
 209                 break;
 210 
 211             case EXIT:
 212                 performExit();
 213                 break;
 214         }
 215     }
 216 
 217 
 218     public boolean canPerformControlAction(ApplicationControlAction a, DocumentWindowController source) {
 219         final boolean result;
 220         switch (a) {
 221             case ABOUT:
 222             case NEW_FILE:
 223             case NEW_ALERT_DIALOG:
 224             case NEW_BASIC_APPLICATION:
 225             case NEW_COMPLEX_APPLICATION:
 226             case NEW_ALERT_DIALOG_CSS:
 227             case NEW_ALERT_DIALOG_I18N:
 228             case NEW_BASIC_APPLICATION_CSS:
 229             case NEW_BASIC_APPLICATION_I18N:
 230             case NEW_COMPLEX_APPLICATION_CSS:
 231             case NEW_COMPLEX_APPLICATION_I18N:
 232             case OPEN_FILE:
 233             case SHOW_PREFERENCES:
 234             case EXIT:
 235                 result = true;
 236                 break;
 237 
 238             case CLOSE_FRONT_WINDOW:
 239                 result = windowList.isEmpty() == false;
 240                 break;
 241 
 242             case USE_DEFAULT_THEME:
 243                 result = toolTheme != ToolTheme.DEFAULT;
 244                 break;
 245 
 246             case USE_DARK_THEME:
 247                 result = toolTheme != ToolTheme.DARK;
 248                 break;
 249 
 250             default:
 251                 result = false;
 252                 assert false;
 253                 break;
 254         }
 255         return result;
 256     }
 257 
 258     public void performOpenRecent(DocumentWindowController source, final File fxmlFile) {
 259         assert fxmlFile != null && fxmlFile.exists();
 260 
 261         final List<File> fxmlFiles = new ArrayList<>();
 262         fxmlFiles.add(fxmlFile);
 263         performOpenFiles(fxmlFiles, source);
 264     }
 265 
 266     public void documentWindowRequestClose(DocumentWindowController fromWindow) {
 267         closeWindow(fromWindow);
 268     }
 269 
 270     public UserLibrary getUserLibrary() {
 271         return userLibrary;
 272     }
 273 
 274     public List<DocumentWindowController> getDocumentWindowControllers() {
 275         return Collections.unmodifiableList(windowList);
 276     }
 277 
 278     public DocumentWindowController lookupDocumentWindowControllers(URL fxmlLocation) {
 279         assert fxmlLocation != null;
 280 
 281         DocumentWindowController result = null;
 282         try {
 283             final URI fxmlURI = fxmlLocation.toURI();
 284             for (DocumentWindowController dwc : windowList) {
 285                 final URL docLocation = dwc.getEditorController().getFxmlLocation();
 286                 if ((docLocation != null) && fxmlURI.equals(docLocation.toURI())) {
 287                     result = dwc;
 288                     break;
 289                 }
 290             }
 291         } catch (URISyntaxException x) {
 292             // Should not happen
 293             throw new RuntimeException("Bug in " + getClass().getSimpleName(), x); //NOI18N
 294         }
 295 
 296         return result;
 297     }
 298 
 299     public DocumentWindowController lookupUnusedDocumentWindowController() {
 300         DocumentWindowController result = null;
 301 
 302         for (DocumentWindowController dwc : windowList) {
 303             if (dwc.isUnused()) {
 304                 result = dwc;
 305                 break;
 306             }
 307         }
 308 
 309         return result;
 310     }
 311 
 312     public void toggleDebugMenu() {
 313         final boolean visible;
 314 
 315         if (windowList.isEmpty()) {
 316             visible = false;
 317         } else {
 318             final DocumentWindowController dwc = windowList.get(0);
 319             visible = dwc.getMenuBarController().isDebugMenuVisible();
 320         }
 321 
 322         for (DocumentWindowController dwc : windowList) {
 323             dwc.getMenuBarController().setDebugMenuVisible(!visible);
 324         }
 325 
 326         if (EditorPlatform.IS_MAC) {
 327             MenuBarController.getSystemMenuBarController().setDebugMenuVisible(!visible);
 328         }
 329     }
 330 
 331     public static synchronized String getDarkToolStylesheet() {
 332         if (darkToolStylesheet == null) {
 333             final URL url = SceneBuilderApp.class.getResource("css/ThemeDark.css"); //NOI18N
 334             assert url != null;
 335             darkToolStylesheet = url.toExternalForm();
 336         }
 337         return darkToolStylesheet;
 338     }
 339 
 340     /*
 341      * Application
 342      */
 343     @Override
 344     public void start(Stage stage) throws Exception {
 345         launchLatch.countDown();
 346         setApplicationUncaughtExceptionHandler();
 347 
 348         try {
 349             if (AppPlatform.requestStart(this, getParameters()) == false) {
 350                 // Start has been denied because another instance is running.
 351                 Platform.exit();
 352             }
 353             // else {
 354             //      No other Scene Builder instance is already running.
 355             //      AppPlatform.requestStart() has/will invoke(d) handleLaunch().
 356             //      start() has now finished its job and should imply return.
 357             // }
 358 
 359         } catch (IOException x) {
 360             final ErrorDialog errorDialog = new ErrorDialog(null);
 361             errorDialog.setTitle(I18N.getString("alert.title.start"));
 362             errorDialog.setMessage(I18N.getString("alert.start.failure.message"));
 363             errorDialog.setDetails(I18N.getString("alert.start.failure.details"));
 364             errorDialog.setDebugInfoWithThrowable(x);
 365             errorDialog.showAndWait();
 366             Platform.exit();
 367         }
 368 
 369         logTimestamp(ACTION.START);
 370     }
 371 
 372     /*
 373      * AppPlatform.AppNotificationHandler
 374      */
 375     @Override
 376     public void handleLaunch(List<String> files) {
 377         setApplicationUncaughtExceptionHandler();
 378 
 379         // Creates the user library
 380         userLibrary = new UserLibrary(AppPlatform.getUserLibraryFolder());
 381 
 382         userLibrary.explorationCountProperty().addListener((ChangeListener<Number>) (ov, t, t1) -> userLibraryExplorationCountDidChange());
 383 
 384         userLibrary.startWatching();
 385 
 386         if (files.isEmpty()) {
 387             // Creates an empty document
 388             final DocumentWindowController newWindow = makeNewWindow();
 389             newWindow.loadWithDefaultContent();
 390             newWindow.openWindow();
 391 
 392             // Show ScenicView Tool when the JVM is started with option -Dscenic.
 393             // NetBeans: set it on [VM Options] line in [Run] category of project's Properties.
 394             if (System.getProperty("scenic") != null) { //NOI18N
 395                 Platform.runLater(new ScenicViewStarter(newWindow.getScene()));
 396             }
 397         } else {
 398             // Open files passed as arguments by the platform
 399             handleOpenFilesAction(files);
 400         }
 401 
 402         // On Mac, AppPlatform disables implicit exit.
 403         // So we need to set a default system menu bar.
 404         if (Platform.isImplicitExit() == false) {
 405             Deprecation.setDefaultSystemMenuBar(MenuBarController.getSystemMenuBarController().getMenuBar());
 406         }
 407     }
 408 
 409     @Override
 410     public void handleOpenFilesAction(List<String> files) {
 411         assert files != null;
 412         assert files.isEmpty() == false;
 413 
 414         final List<File> fileObjs = new ArrayList<>();
 415         for (String file : files) {
 416             fileObjs.add(new File(file));
 417         }
 418 
 419         performOpenFiles(fileObjs, null);
 420     }
 421 
 422     @Override
 423     public void handleMessageBoxFailure(Exception x) {
 424         final ErrorDialog errorDialog = new ErrorDialog(null);
 425         errorDialog.setTitle(I18N.getString("alert.title.messagebox"));
 426         errorDialog.setMessage(I18N.getString("alert.messagebox.failure.message"));
 427         errorDialog.setDetails(I18N.getString("alert.messagebox.failure.details"));
 428         errorDialog.setDebugInfoWithThrowable(x);
 429         errorDialog.showAndWait();
 430     }
 431 
 432     @Override
 433     public void handleQuitAction() {
 434 
 435         /*
 436          * Note : this callback is called on Mac OS X only when the user
 437          * selects the 'Quit App' command in the Application menu.
 438          *
 439          * Before calling this callback, FX automatically sends a close event
 440          * to each open window ie DocumentWindowController.performCloseAction()
 441          * is invoked for each open window.
 442          *
 443          * When we arrive here, windowList is empty if the user has confirmed
 444          * the close operation for each window : thus exit operation can
 445          * be performed. If windowList is not empty,  this means the user has
 446          * cancelled at least one close operation : in that case, exit operation
 447          * should be not be executed.
 448          */
 449         if (windowList.isEmpty()) {
 450             logTimestamp(ACTION.STOP);
 451             Platform.exit();
 452         }
 453     }
 454 
 455     /**
 456      * Normally ignored in correctly deployed JavaFX application.
 457      * But on Mac OS, this method seems to be called by the javafx launcher.
 458      */
 459     public static void main(String[] args) {
 460         launch(args);
 461     }
 462 
 463     /*
 464      * Private
 465      */
 466     public DocumentWindowController makeNewWindow() {
 467         final DocumentWindowController result = new DocumentWindowController();
 468         windowList.add(result);
 469         return result;
 470     }
 471 
 472     private void closeWindow(DocumentWindowController w) {
 473         assert windowList.contains(w);
 474         windowList.remove(w);
 475         w.closeWindow();
 476     }
 477 
 478     private static String displayName(String pathString) {
 479         return Paths.get(pathString).getFileName().toString();
 480     }
 481 
 482     /*
 483      * Private (control actions)
 484      */
 485     private void performOpenFile(DocumentWindowController fromWindow) {
 486         final FileChooser fileChooser = new FileChooser();
 487 
 488         fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(I18N.getString("file.filter.label.fxml"),
 489                 "*.fxml")); //NOI18N
 490         fileChooser.setInitialDirectory(EditorController.getNextInitialDirectory());
 491         final List<File> fxmlFiles = fileChooser.showOpenMultipleDialog(null);
 492         if (fxmlFiles != null) {
 493             assert fxmlFiles.isEmpty() == false;
 494             EditorController.updateNextInitialDirectory(fxmlFiles.get(0));
 495             performOpenFiles(fxmlFiles, fromWindow);
 496         }
 497     }
 498 
 499     private void performNewTemplate(ApplicationControlAction action) {
 500         final DocumentWindowController newTemplateWindow = makeNewWindow();
 501         final URL url = FxmlTemplates.getContentURL(action);
 502         newTemplateWindow.loadFromURL(url);
 503         newTemplateWindow.openWindow();
 504     }
 505 
 506     private void performNewTemplateWithResources(ApplicationControlAction action) {
 507         final TemplateDialogController tdc = new TemplateDialogController(action);
 508         tdc.setToolStylesheet(getToolStylesheet());
 509         tdc.openWindow();
 510     }
 511 
 512     private void performCloseFrontWindow() {
 513         if (preferencesWindowController != null
 514                 && preferencesWindowController.getStage().isFocused()) {
 515             preferencesWindowController.closeWindow();
 516         } else {
 517             for (DocumentWindowController dwc : windowList) {
 518                 if (dwc.isFrontDocumentWindow()) {
 519                     dwc.performCloseFrontDocumentWindow();
 520                     break;
 521                 }
 522             }
 523         }
 524     }
 525 
 526     private void performOpenFiles(List<File> fxmlFiles,
 527             DocumentWindowController fromWindow) {
 528         assert fxmlFiles != null;
 529         assert fxmlFiles.isEmpty() == false;
 530 
 531         final Map<File, IOException> exceptions = new HashMap<>();
 532         for (File fxmlFile : fxmlFiles) {
 533             try {
 534                 final DocumentWindowController dwc
 535                         = lookupDocumentWindowControllers(fxmlFile.toURI().toURL());
 536                 if (dwc != null) {
 537                     // fxmlFile is already opened
 538                     dwc.getStage().toFront();
 539                 } else {
 540                     // Open fxmlFile
 541                     final DocumentWindowController hostWindow;
 542                     final DocumentWindowController unusedWindow
 543                             = lookupUnusedDocumentWindowController();
 544                     if (unusedWindow != null) {
 545                         hostWindow = unusedWindow;
 546                     } else {
 547                         hostWindow = makeNewWindow();
 548                     }
 549                     hostWindow.loadFromFile(fxmlFile);
 550                     hostWindow.openWindow();
 551                 }
 552             } catch (IOException xx) {
 553                 exceptions.put(fxmlFile, xx);
 554             }
 555         }
 556 
 557         switch (exceptions.size()) {
 558             case 0: { // Good
 559                 // Update recent items with opened files
 560                 final PreferencesController pc = PreferencesController.getSingleton();
 561                 final PreferencesRecordGlobal recordGlobal = pc.getRecordGlobal();
 562                 recordGlobal.addRecentItems(fxmlFiles);
 563                 break;
 564             }
 565             case 1: {
 566                 final File fxmlFile = exceptions.keySet().iterator().next();
 567                 final Exception x = exceptions.get(fxmlFile);
 568                 final ErrorDialog errorDialog = new ErrorDialog(null);
 569                 errorDialog.setMessage(I18N.getString("alert.open.failure1.message", displayName(fxmlFile.getPath())));
 570                 errorDialog.setDetails(I18N.getString("alert.open.failure1.details"));
 571                 errorDialog.setDebugInfoWithThrowable(x);
 572                 errorDialog.setTitle(I18N.getString("alert.title.open"));
 573                 errorDialog.showAndWait();
 574                 break;
 575             }
 576             default: {
 577                 final ErrorDialog errorDialog = new ErrorDialog(null);
 578                 if (exceptions.size() == fxmlFiles.size()) {
 579                     // Open operation has failed for all the files
 580                     errorDialog.setMessage(I18N.getString("alert.open.failureN.message"));
 581                     errorDialog.setDetails(I18N.getString("alert.open.failureN.details"));
 582                 } else {
 583                     // Open operation has failed for some files
 584                     errorDialog.setMessage(I18N.getString("alert.open.failureMofN.message",
 585                             exceptions.size(), fxmlFiles.size()));
 586                     errorDialog.setDetails(I18N.getString("alert.open.failureMofN.details"));
 587                 }
 588                 errorDialog.setTitle(I18N.getString("alert.title.open"));
 589                 errorDialog.showAndWait();
 590                 break;
 591             }
 592         }
 593     }
 594 
 595     private void performExit() {
 596 
 597         // Check if an editing session is on going
 598         for (DocumentWindowController dwc : windowList) {
 599             if (dwc.getEditorController().isTextEditingSessionOnGoing()) {
 600                 // Check if we can commit the editing session
 601                 if (dwc.getEditorController().canGetFxmlText() == false) {
 602                     // Commit failed
 603                     return;
 604                 }
 605             }
 606         }
 607 
 608         // Collects the documents with pending changes
 609         final List<DocumentWindowController> pendingDocs = new ArrayList<>();
 610         for (DocumentWindowController dwc : windowList) {
 611             if (dwc.isDocumentDirty()) {
 612                 pendingDocs.add(dwc);
 613             }
 614         }
 615 
 616         // Notifies the user if some documents are dirty
 617         final boolean exitConfirmed;
 618         switch (pendingDocs.size()) {
 619             case 0: {
 620                 exitConfirmed = true;
 621                 break;
 622             }
 623 
 624             case 1: {
 625                 final DocumentWindowController dwc0 = pendingDocs.get(0);
 626                 exitConfirmed = dwc0.performCloseAction() == ActionStatus.DONE;
 627                 break;
 628             }
 629 
 630             default: {
 631                 assert pendingDocs.size() >= 2;
 632 
 633                 final AlertDialog d = new AlertDialog(null);
 634                 d.setMessage(I18N.getString("alert.review.question.message", pendingDocs.size()));
 635                 d.setDetails(I18N.getString("alert.review.question.details"));
 636                 d.setOKButtonTitle(I18N.getString("label.review.changes"));
 637                 d.setActionButtonTitle(I18N.getString("label.discard.changes"));
 638                 d.setActionButtonVisible(true);
 639 
 640                 switch (d.showAndWait()) {
 641                     default:
 642                     case OK: { // Review
 643                         int i = 0;
 644                         ActionStatus status;
 645                         do {
 646                             status = pendingDocs.get(i++).performCloseAction();
 647                         } while ((status == ActionStatus.DONE) && (i < pendingDocs.size()));
 648                         exitConfirmed = (status == ActionStatus.DONE);
 649                         break;
 650                     }
 651                     case CANCEL: {
 652                         exitConfirmed = false;
 653                         break;
 654                     }
 655                     case ACTION: { // Do not review
 656                         exitConfirmed = true;
 657                         break;
 658                     }
 659                 }
 660                 break;
 661             }
 662         }
 663 
 664         // Exit if confirmed
 665         if (exitConfirmed) {
 666             for (DocumentWindowController dwc : new ArrayList<>(windowList)) {
 667                 // Write to java preferences before closing
 668                 dwc.updatePreferences();
 669                 documentWindowRequestClose(dwc);
 670             }
 671             logTimestamp(ACTION.STOP);
 672             // TODO (elp): something else here ?
 673             Platform.exit();
 674         }
 675     }
 676 
 677     private enum ACTION {START, STOP};
 678 
 679     private void logTimestamp(ACTION type) {
 680         switch (type) {
 681             case START:
 682                 Logger.getLogger(this.getClass().getName()).info(I18N.getString("log.start"));
 683                 break;
 684             case STOP:
 685                 Logger.getLogger(this.getClass().getName()).info(I18N.getString("log.stop"));
 686                 break;
 687             default:
 688                 assert false;
 689         }
 690     }
 691 
 692     private void setApplicationUncaughtExceptionHandler() {
 693         if (Thread.getDefaultUncaughtExceptionHandler() == null) {
 694             // Register a Default Uncaught Exception Handler for the application
 695             Thread.setDefaultUncaughtExceptionHandler(new SceneBuilderUncaughtExceptionHandler());
 696         }
 697     }
 698 
 699     private static class SceneBuilderUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
 700 
 701         @Override
 702         public void uncaughtException(Thread t, Throwable e) {
 703             // Print the details of the exception in SceneBuilder log file
 704             Logger.getLogger(this.getClass().getName()).log(Level.SEVERE, "An exception was thrown:", e); //NOI18N
 705         }
 706     }
 707 
 708 
 709     private void performUseToolTheme(ToolTheme toolTheme) {
 710         this.toolTheme = toolTheme;
 711 
 712         final String toolStylesheet = getToolStylesheet();
 713 
 714         for (DocumentWindowController dwc : windowList) {
 715             dwc.setToolStylesheet(toolStylesheet);
 716         }
 717         preferencesWindowController.setToolStylesheet(toolStylesheet);
 718         aboutWindowController.setToolStylesheet(toolStylesheet);
 719     }
 720 
 721 
 722     private String getToolStylesheet() {
 723         final String result;
 724 
 725         switch(this.toolTheme) {
 726 
 727             default:
 728             case DEFAULT:
 729                 result = EditorController.getBuiltinToolStylesheet();
 730                 break;
 731 
 732             case DARK:
 733                 result = getDarkToolStylesheet();
 734                 break;
 735         }
 736 
 737         return result;
 738     }
 739 
 740 
 741     /*
 742      * Background startup
 743      *
 744      * To speed SB startup, we create two threads which anticipate some
 745      * initialization tasks and offload the JFX thread:
 746      *  - 'Phase 0' thread executes tasks that do not require JFX initialization
 747      *  - 'Phase 1' thread executes tasks that requires JFX initialization
 748      *
 749      * Tasks executed here must be carefully chosen:
 750      * 1) they must be thread-safe
 751      * 2) they should be order-safe : whether they are executed in background
 752      *    or by the JFX thread should make no difference.
 753      *
 754      * Currently we simply anticipate creation of big singleton instances
 755      * (like Metadata, Preferences...)
 756      */
 757 
 758     private void backgroundStartPhase0() {
 759         assert Platform.isFxApplicationThread() == false; // Warning
 760 
 761         PreferencesController.getSingleton();
 762         Metadata.getMetadata();
 763     }
 764 
 765     private void backgroundStartPhase2() {
 766         assert Platform.isFxApplicationThread() == false; // Warning
 767         assert launchLatch.getCount() == 0; // i.e JavaFX is initialized
 768 
 769         BuiltinLibrary.getLibrary();
 770         if (EditorPlatform.IS_MAC) {
 771             MenuBarController.getSystemMenuBarController();
 772         }
 773         EffectPicker.getEffectClasses();
 774     }
 775 
 776     private void userLibraryExplorationCountDidChange() {
 777         // We can have 0, 1 or N FXML file, same for JAR one.
 778         final int numOfFxmlFiles = userLibrary.getFxmlFileReports().size();
 779         final int numOfJarFiles = userLibrary.getJarReports().size();
 780         final int jarCount = userLibrary.getJarReports().size();
 781         final int fxmlCount = userLibrary.getFxmlFileReports().size();
 782 
 783         switch (numOfFxmlFiles + numOfJarFiles) {
 784             case 0: // Case 0-0
 785                 final int previousNumOfJarFiles = userLibrary.getPreviousJarReports().size();
 786                 final int previousNumOfFxmlFiles = userLibrary.getPreviousFxmlFileReports().size();
 787                 if (previousNumOfFxmlFiles > 0 || previousNumOfJarFiles > 0) {
 788                     logInfoMessage("log.user.exploration.0");
 789                 }
 790                 break;
 791             case 1:
 792                 Path path;
 793                 if (numOfFxmlFiles == 1) { // Case 1-0
 794                     path = userLibrary.getFxmlFileReports().get(0);
 795                 } else { // Case 0-1
 796                     path = userLibrary.getJarReports().get(0).getJar();
 797                 }
 798                 logInfoMessage("log.user.exploration.1", path.getFileName());
 799                 break;
 800             default:
 801                 switch (numOfFxmlFiles) {
 802                     case 0: // Case 0-N
 803                         logInfoMessage("log.user.jar.exploration.n", jarCount);
 804                         break;
 805                     case 1:
 806                         final Path fxmlName = userLibrary.getFxmlFileReports().get(0).getFileName();
 807                         if (numOfFxmlFiles == numOfJarFiles) { // Case 1-1
 808                             final Path jarName = userLibrary.getJarReports().get(0).getJar().getFileName();
 809                             logInfoMessage("log.user.fxml.jar.exploration.1.1", fxmlName, jarName);
 810                         } else { // Case 1-N
 811                             logInfoMessage("log.user.fxml.jar.exploration.1.n", fxmlName, jarCount);
 812                         }
 813                         break;
 814                     default:
 815                         switch (numOfJarFiles) {
 816                             case 0: // Case N-0
 817                                 logInfoMessage("log.user.fxml.exploration.n", fxmlCount);
 818                                 break;
 819                             case 1: // Case N-1
 820                                 final Path jarName = userLibrary.getJarReports().get(0).getJar().getFileName();
 821                                 logInfoMessage("log.user.fxml.jar.exploration.n.1", fxmlCount, jarName);
 822                                 break;
 823                             default: // Case N-N
 824                                 logInfoMessage("log.user.fxml.jar.exploration.n.n", fxmlCount, jarCount);
 825                                 break;
 826                         }
 827                         break;
 828                 }
 829                 break;
 830         }
 831     }
 832 
 833     private void logInfoMessage(String key) {
 834         for (DocumentWindowController dwc : windowList) {
 835             dwc.getEditorController().getMessageLog().logInfoMessage(key, I18N.getBundle());
 836         }
 837     }
 838 
 839     private void logInfoMessage(String key, Object... args) {
 840         for (DocumentWindowController dwc : windowList) {
 841             dwc.getEditorController().getMessageLog().logInfoMessage(key, I18N.getBundle(), args);
 842         }
 843     }
 844 }