boot-fx/src/main/java/org/netbeans/html/boot/fx/FXBrwsr.java
author Jaroslav Tulach <jtulach@netbeans.org>
Fri, 02 Oct 2015 08:57:14 +0200
changeset 1006 a0f79e32d526
parent 953 cd7d2aca34be
child 1008 c535c36881af
permissions -rw-r--r--
Ability to run the JavaFX presenter in headless mode is useful for testing
     1 /**
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved.
    31  *
    32  * If you wish your version of this file to be governed by only the CDDL
    33  * or only the GPL Version 2, indicate your decision by adding
    34  * "[Contributor] elects to include this software in this distribution
    35  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    36  * single choice of license, a recipient has the option to distribute
    37  * your version of this file under either the CDDL, the GPL Version 2 or
    38  * to extend the choice of license to its licensees as provided above.
    39  * However, if you add GPL Version 2 code and therefore, elected the GPL
    40  * Version 2 license, then the option applies only if the new code is
    41  * made subject to such option by the copyright holder.
    42  */
    43 package org.netbeans.html.boot.fx;
    44 
    45 import java.net.URL;
    46 import java.util.ResourceBundle;
    47 import java.util.concurrent.CountDownLatch;
    48 import java.util.concurrent.Executors;
    49 import java.util.logging.Level;
    50 import java.util.logging.Logger;
    51 import java.util.prefs.Preferences;
    52 import javafx.application.Application;
    53 import javafx.application.Platform;
    54 import javafx.beans.value.ChangeListener;
    55 import javafx.beans.value.ObservableValue;
    56 import javafx.concurrent.Worker;
    57 import javafx.event.ActionEvent;
    58 import javafx.event.EventHandler;
    59 import javafx.geometry.Insets;
    60 import javafx.geometry.Pos;
    61 import javafx.geometry.Rectangle2D;
    62 import javafx.scene.Scene;
    63 import javafx.scene.control.Button;
    64 import javafx.scene.control.TextField;
    65 import javafx.scene.layout.BorderPane;
    66 import javafx.scene.layout.HBox;
    67 import javafx.scene.layout.VBox;
    68 import javafx.scene.text.Text;
    69 import javafx.scene.web.PromptData;
    70 import javafx.scene.web.WebEvent;
    71 import javafx.scene.web.WebView;
    72 import javafx.stage.Modality;
    73 import javafx.stage.Screen;
    74 import javafx.stage.Stage;
    75 import javafx.stage.Window;
    76 import javafx.stage.WindowEvent;
    77 import javafx.util.Callback;
    78 
    79 /** This is an implementation class, to implement browser builder API. Just
    80  * include this JAR on classpath and the browser builder API will find
    81  * this implementation automatically.
    82  */
    83 public class FXBrwsr extends Application {
    84     private static final Logger LOG = Logger.getLogger(FXBrwsr.class.getName());
    85     private static FXBrwsr INSTANCE;
    86     private static final CountDownLatch FINISHED = new CountDownLatch(1);
    87     private BorderPane root;
    88 
    89     public static synchronized WebView findWebView(final URL url, final FXPresenter onLoad) {
    90         if (INSTANCE == null) {
    91             final String callee = findCalleeClassName();
    92             Executors.newFixedThreadPool(1).submit(new Runnable() {
    93                 @Override
    94                 public void run() {
    95                     try {
    96                         FXBrwsr.launch(FXBrwsr.class, callee);
    97                     } catch (Throwable ex) {
    98                         ex.printStackTrace();
    99                     } finally {
   100                         FINISHED.countDown();
   101                     }
   102                 }
   103             });
   104         }
   105         while (INSTANCE == null) {
   106             try {
   107                 FXBrwsr.class.wait();
   108             } catch (InterruptedException ex) {
   109                 // wait more
   110             }
   111         }
   112         if (!Platform.isFxApplicationThread()) {
   113             final WebView[] arr = {null};
   114             final CountDownLatch waitForResult = new CountDownLatch(1);
   115             Platform.runLater(new Runnable() {
   116                 @Override
   117                 public void run() {
   118                     arr[0] = INSTANCE.newView(url, onLoad);
   119                     waitForResult.countDown();
   120                 }
   121             });
   122             for (;;) {
   123                 try {
   124                     waitForResult.await();
   125                     break;
   126                 } catch (InterruptedException ex) {
   127                     LOG.log(Level.INFO, null, ex);
   128                 }
   129             }
   130             return arr[0];
   131         } else {
   132             return INSTANCE.newView(url, onLoad);
   133         }
   134     }
   135 
   136     static synchronized Stage findStage() throws InterruptedException {
   137         while (INSTANCE == null) {
   138             FXBrwsr.class.wait();
   139         }
   140         return INSTANCE.stage;
   141     }
   142 
   143     private Stage stage;
   144 
   145     @Override
   146     public void start(Stage primaryStage) throws Exception {
   147         BorderPane r = new BorderPane();
   148         Object[] arr = findInitialSize(this.getParameters().getRaw().get(0));
   149         Scene scene = new Scene(r, (Double)arr[2], (Double)arr[3]);
   150         primaryStage.setScene(scene);
   151         this.root = r;
   152         this.stage = primaryStage;
   153         synchronized (FXBrwsr.class) {
   154             INSTANCE = this;
   155             FXBrwsr.class.notifyAll();
   156         }
   157         primaryStage.setX((Double)arr[0]);
   158         primaryStage.setY((Double)arr[1]);
   159         if (arr[4] != null) {
   160             scene.getWindow().setOnCloseRequest((EventHandler<WindowEvent>) arr[4]);
   161         }
   162         if (Boolean.getBoolean("fxpresenter.headless")) {
   163             return;
   164         }
   165         primaryStage.show();
   166     }
   167 
   168     static String findCalleeClassName() {
   169         StackTraceElement[] frames = new Exception().getStackTrace();
   170         for (StackTraceElement e : frames) {
   171             String cn = e.getClassName();
   172             if (cn.startsWith("org.netbeans.html.")) { // NOI18N
   173                 continue;
   174             }
   175             if (cn.startsWith("net.java.html.")) { // NOI18N
   176                 continue;
   177             }
   178             if (cn.startsWith("java.")) { // NOI18N
   179                 continue;
   180             }
   181             if (cn.startsWith("javafx.")) { // NOI18N
   182                 continue;
   183             }
   184             if (cn.startsWith("com.sun.")) { // NOI18N
   185                 continue;
   186             }
   187             return cn;
   188         }
   189         return "org.netbeans.html"; // NOI18N
   190     }
   191 
   192     private static Object[] findInitialSize(String callee) {
   193         final Preferences prefs = Preferences.userRoot().node(callee.replace('.', '/'));
   194         Rectangle2D screen = Screen.getPrimary().getBounds();
   195         double x = prefs.getDouble("x", screen.getWidth() * 0.05); // NOI18N
   196         double y = prefs.getDouble("y", screen.getHeight() * 0.05); // NOI18N
   197         double width = prefs.getDouble("width", screen.getWidth() * 0.9); // NOI18N
   198         double height = prefs.getDouble("height", screen.getHeight() * 0.9); // NOI18N
   199 
   200         Object[] arr = {
   201             x, y, width, height, null
   202         };
   203 
   204         if (!callee.equals("org.netbeans.html")) { // NOI18N
   205             arr[4] = new EventHandler<WindowEvent>() {
   206                 @Override
   207                 public void handle(WindowEvent event) {
   208                     Window window = (Window) event.getSource();
   209                     prefs.putDouble("x", window.getX()); // NOI18N
   210                     prefs.putDouble("y", window.getY()); // NOI18N
   211                     prefs.putDouble("width", window.getWidth()); // NOI18N
   212                     prefs.putDouble("height", window.getHeight()); // NOI18N
   213                 }
   214             };
   215         }
   216 
   217         return arr;
   218     }
   219 
   220     private WebView newView(final URL url, final FXPresenter onLoad) {
   221         final WebView view = new WebView();
   222         view.setContextMenuEnabled(false);
   223         view.getEngine().setOnAlert(new EventHandler<WebEvent<String>>() {
   224             @Override
   225             public void handle(WebEvent<String> t) {
   226                 final Stage dialogStage = new Stage();
   227                 dialogStage.initModality(Modality.WINDOW_MODAL);
   228                 dialogStage.initOwner(stage);
   229                 ResourceBundle r = ResourceBundle.getBundle("org/netbeans/html/boot/fx/Bundle"); // NOI18N
   230                 dialogStage.setTitle(r.getString("AlertTitle")); // NOI18N
   231                 final Button button = new Button(r.getString("AlertCloseButton")); // NOI18N
   232                 final Text text = new Text(t.getData());
   233                 VBox box = new VBox();
   234                 box.setAlignment(Pos.CENTER);
   235                 box.setSpacing(10);
   236                 box.setPadding(new Insets(10));
   237                 box.getChildren().addAll(text, button);
   238                 dialogStage.setScene(new Scene(box));
   239                 button.setCancelButton(true);
   240                 button.setOnAction(new CloseDialogHandler(dialogStage, null));
   241                 dialogStage.centerOnScreen();
   242                 dialogStage.showAndWait();
   243             }
   244         });
   245         view.getEngine().setConfirmHandler(new Callback<String, Boolean>() {
   246             @Override
   247             public Boolean call(String question) {
   248                 final Stage dialogStage = new Stage();
   249                 dialogStage.initModality(Modality.WINDOW_MODAL);
   250                 dialogStage.initOwner(stage);
   251                 ResourceBundle r = ResourceBundle.getBundle("org/netbeans/html/boot/fx/Bundle"); // NOI18N
   252                 dialogStage.setTitle(r.getString("ConfirmTitle")); // NOI18N
   253                 final Button ok = new Button(r.getString("ConfirmOKButton")); // NOI18N
   254                 final Button cancel = new Button(r.getString("ConfirmCancelButton")); // NOI18N
   255                 final Text text = new Text(question);
   256                 final Insets ins = new Insets(10);
   257                 final VBox box = new VBox();
   258                 box.setAlignment(Pos.CENTER);
   259                 box.setSpacing(10);
   260                 box.setPadding(ins);
   261                 final HBox buttons = new HBox(10);
   262                 buttons.getChildren().addAll(ok, cancel);
   263                 buttons.setAlignment(Pos.CENTER);
   264                 buttons.setPadding(ins);
   265                 box.getChildren().addAll(text, buttons);
   266                 dialogStage.setScene(new Scene(box));
   267                 ok.setCancelButton(false);
   268 
   269                 final boolean[] res = new boolean[1];
   270                 ok.setOnAction(new CloseDialogHandler(dialogStage, res));
   271                 cancel.setCancelButton(true);
   272                 cancel.setOnAction(new CloseDialogHandler(dialogStage, null));
   273                 dialogStage.centerOnScreen();
   274                 dialogStage.showAndWait();
   275                 return res[0];
   276             }
   277         });
   278         view.getEngine().setPromptHandler(new Callback<PromptData, String>() {
   279             @Override
   280             public String call(PromptData prompt) {
   281                 final Stage dialogStage = new Stage();
   282                 dialogStage.initModality(Modality.WINDOW_MODAL);
   283                 dialogStage.initOwner(stage);
   284                 ResourceBundle r = ResourceBundle.getBundle("org/netbeans/html/boot/fx/Bundle"); // NOI18N
   285                 dialogStage.setTitle(r.getString("PromptTitle")); // NOI18N
   286                 final Button ok = new Button(r.getString("PromptOKButton")); // NOI18N
   287                 final Button cancel = new Button(r.getString("PromptCancelButton")); // NOI18N
   288                 final Text text = new Text(prompt.getMessage());
   289                 final TextField line = new TextField();
   290                 if (prompt.getDefaultValue() != null) {
   291                     line.setText(prompt.getDefaultValue());
   292                 }
   293                 final Insets ins = new Insets(10);
   294                 final VBox box = new VBox();
   295                 box.setAlignment(Pos.CENTER);
   296                 box.setSpacing(10);
   297                 box.setPadding(ins);
   298                 final HBox buttons = new HBox(10);
   299                 buttons.getChildren().addAll(ok, cancel);
   300                 buttons.setAlignment(Pos.CENTER);
   301                 buttons.setPadding(ins);
   302                 box.getChildren().addAll(text, line, buttons);
   303                 dialogStage.setScene(new Scene(box));
   304                 ok.setCancelButton(false);
   305 
   306                 final boolean[] res = new boolean[1];
   307                 ok.setOnAction(new CloseDialogHandler(dialogStage, res));
   308                 cancel.setCancelButton(true);
   309                 cancel.setOnAction(new CloseDialogHandler(dialogStage, null));
   310                 dialogStage.centerOnScreen();
   311                 dialogStage.showAndWait();
   312                 return res[0] ? line.getText() : null;
   313             }
   314         });
   315         root.setCenter(view);
   316         final Worker<Void> w = view.getEngine().getLoadWorker();
   317         w.stateProperty().addListener(new ChangeListener<Worker.State>() {
   318             private String previous;
   319 
   320             @Override
   321             public void changed(ObservableValue<? extends Worker.State> ov, Worker.State t, Worker.State newState) {
   322                 if (newState.equals(Worker.State.SUCCEEDED)) {
   323                     if (checkValid()) {
   324                         FXConsole.register(view.getEngine());
   325                         onLoad.onPageLoad();
   326                     }
   327                 }
   328                 if (newState.equals(Worker.State.FAILED)) {
   329                     throw new IllegalStateException("Failed to load " + url);
   330                 }
   331             }
   332             private boolean checkValid() {
   333                 final String crnt = view.getEngine().getLocation();
   334                 if (previous != null && !previous.equals(crnt)) {
   335                     w.stateProperty().removeListener(this);
   336                     return false;
   337                 }
   338                 previous = crnt;
   339                 return true;
   340             }
   341 
   342         });
   343         class Title implements ChangeListener<String> {
   344 
   345             private String title;
   346 
   347             public Title() {
   348                 super();
   349             }
   350 
   351             @Override
   352             public void changed(ObservableValue<? extends String> ov, String t, String t1) {
   353                 title = view.getEngine().getTitle();
   354                 if (title != null) {
   355                     stage.setTitle(title);
   356                 }
   357             }
   358         }
   359         final Title x = new Title();
   360         view.getEngine().titleProperty().addListener(x);
   361         x.changed(null, null, null);
   362         return view;
   363     }
   364 
   365     static void waitFinished() {
   366         for (;;) {
   367             try {
   368                 FINISHED.await();
   369                 break;
   370             } catch (InterruptedException ex) {
   371                 LOG.log(Level.INFO, null, ex);
   372             }
   373         }
   374     }
   375 
   376     private static final class CloseDialogHandler implements EventHandler<ActionEvent> {
   377         private final Stage dialogStage;
   378         private final boolean[] res;
   379 
   380         public CloseDialogHandler(Stage dialogStage, boolean[] res) {
   381             this.dialogStage = dialogStage;
   382             this.res = res;
   383         }
   384 
   385         @Override
   386         public void handle(ActionEvent t) {
   387             dialogStage.close();
   388             if (res != null) {
   389                 res[0] = true;
   390             }
   391         }
   392     }
   393 
   394 }