Make Browser JavaScript part more robust when the widget is disposed
[gpl/argeo-suite.git] / swt / org.argeo.app.swt / src / org / argeo / app / swt / js / SwtBrowserJsPart.java
index 8ae34d5ecc10a228b98f35b16b0625aca2d3da12..6782f5dd22a03c6d371d914340cd226805b371f8 100644 (file)
@@ -1,11 +1,17 @@
 package org.argeo.app.swt.js;
 
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.function.Function;
 
 import org.argeo.api.cms.CmsLog;
+import org.argeo.api.cms.ux.CmsView;
+import org.argeo.app.ux.js.JsClient;
+import org.argeo.cms.swt.CmsSwtUtils;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.browser.Browser;
 import org.eclipse.swt.browser.BrowserFunction;
@@ -15,26 +21,35 @@ import org.eclipse.swt.layout.GridData;
 import org.eclipse.swt.layout.GridLayout;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
 
 /**
  * A part using a {@link Browser} and remote JavaScript components on the client
  * side.
  */
-public class SwtBrowserJsPart {
+public class SwtBrowserJsPart implements JsClient {
        private final static CmsLog log = CmsLog.getLog(SwtBrowserJsPart.class);
 
        private final static String GLOBAL_THIS_ = "globalThis.";
 
        private final Browser browser;
-       private final CompletableFuture<Boolean> pageLoaded = new CompletableFuture<>();
+       private final CompletableFuture<Boolean> readyStage = new CompletableFuture<>();
+
+       /**
+        * Tasks that were requested before the context was ready. Typically
+        * configuration methods on the part while the user interfaces is being build.
+        */
+       private List<PreReadyToDo> preReadyToDos = new ArrayList<>();
 
        public SwtBrowserJsPart(Composite parent, int style, String url) {
+               CmsView cmsView = CmsSwtUtils.getCmsView(parent);
                this.browser = new Browser(parent, 0);
                if (parent.getLayout() instanceof GridLayout)
                        browser.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
                // TODO other layouts
 
-               browser.setUrl(url);
+               URI u = cmsView.toBackendUri(url);
+               browser.setUrl(u.toString());
                browser.addProgressListener(new ProgressListener() {
                        static final long serialVersionUID = 1L;
 
@@ -43,10 +58,15 @@ public class SwtBrowserJsPart {
                                try {
                                        init();
                                        loadExtensions();
-                                       pageLoaded.complete(true);
+                                       // execute todos in order
+                                       for (PreReadyToDo toDo : preReadyToDos) {
+                                               toDo.run();
+                                       }
+                                       preReadyToDos.clear();
+                                       readyStage.complete(true);
                                } catch (Exception e) {
                                        log.error("Cannot initialise " + url + " in browser", e);
-                                       pageLoaded.complete(false);
+                                       readyStage.complete(false);
                                }
                        }
 
@@ -60,55 +80,63 @@ public class SwtBrowserJsPart {
         * LIFECYCLE
         */
 
-       /** Called when the page has been loaded. */
+       /**
+        * Called when the page has been loaded, typically in order to initialise
+        * JavaScript objects. One MUST use {@link #doExecute(String, Object...)} in
+        * order to do so, since the context is not yet considered ready and calls to
+        * {@link #evaluate(String, Object...)} will block.
+        */
        protected void init() {
        }
 
-       /** To be overridden with calls to {@link #loadExtension(String)}. */
+       /**
+        * To be overridden with calls to {@link #loadExtension(String)}.
+        */
        protected void loadExtensions() {
 
        }
 
        protected void loadExtension(String url) {
-//                     String js = """
-//                                     var script = document.createElement("script");
-//                                     script.src = '%s';
-//                                     document.head.appendChild(script);
-//                                     """;
-//                     browser.evaluate(String.format(Locale.ROOT, js, url));
-               browser.evaluate(String.format(Locale.ROOT, "import('%s')", url));
+               URI u = CmsSwtUtils.getCmsView(getControl()).toBackendUri(url);
+               browser.evaluate(String.format(Locale.ROOT, "import('%s')", u.toString()));
        }
 
-       protected CompletionStage<Boolean> getReadyStage() {
-               return pageLoaded.minimalCompletionStage();
+       public CompletionStage<Boolean> getReadyStage() {
+               return readyStage.minimalCompletionStage();
        }
 
        /*
         * JAVASCRIPT ACCESS
         */
 
-       /**
-        * Execute this JavaScript on the client side after making sure that the page
-        * has been loaded and the map object has been created.
-        * 
-        * @param js   the JavaScript code, possibly formatted according to
-        *             {@link String#format}, with {@link Locale#ROOT} as locale (for
-        *             stability of decimal separator, as expected by JavaScript.
-        * @param args the optional arguments of
-        *             {@link String#format(String, Object...)}
-        */
-       protected CompletionStage<Object> evaluate(String js, Object... args) {
-               CompletableFuture<Object> res = pageLoaded.thenApply((ready) -> {
-                       if (!ready)
-                               throw new IllegalStateException("Component is not initialised.");
-                       Object result = browser.evaluate(String.format(Locale.ROOT, js, args));
-                       return result;
-               });
-               return res.minimalCompletionStage();
+       @Override
+       public Object evaluate(String js, Object... args) {
+               assert browser.getDisplay().equals(Display.findDisplay(Thread.currentThread())) : "Not the proper UI thread.";
+               if (!readyStage.isDone())
+                       throw new IllegalStateException("Methods returning a result can only be called after UI initialisation.");
+               if (browser.isDisposed())
+                       return null;
+               Object result = browser.evaluate(String.format(Locale.ROOT, js, args));
+               return result;
+       }
+
+       @Override
+       public void execute(String js, Object... args) {
+               String jsToExecute = String.format(Locale.ROOT, js, args);
+               if (readyStage.isDone()) {
+                       if (browser.isDisposed())
+                               return;
+                       boolean success = browser.execute(jsToExecute);
+                       if (!success)
+                               throw new RuntimeException("JavaScript execution failed.");
+               } else {
+                       PreReadyToDo toDo = new PreReadyToDo(jsToExecute);
+                       preReadyToDos.add(toDo);
+               }
        }
 
-       /** @return the globally usable function name. */
-       protected String createJsFunction(String name, Function<Object[], Object> toDo) {
+       @Override
+       public String createJsFunction(String name, Function<Object[], Object> toDo) {
                // browser functions must be directly on window (RAP specific)
                new BrowserFunction(browser, name) {
 
@@ -122,23 +150,43 @@ public class SwtBrowserJsPart {
                return "window." + name;
        }
 
-       /** Directly executes */
+       /**
+        * Directly executes, even if {@link #getReadyStage()} is not completed. Except
+        * in initialisation, {@link #evaluate(String, Object...)} should be used
+        * instead.
+        */
        protected void doExecute(String js, Object... args) {
+               if (browser.isDisposed())
+                       return;
                browser.execute(String.format(Locale.ROOT, js, args));
        }
 
-       protected CompletionStage<Object> callMethod(String jsObject, String methodCall, Object... args) {
-               return evaluate(jsObject + '.' + methodCall, args);
+       @Override
+       public String getJsVarName(String name) {
+               return GLOBAL_THIS_ + name;
        }
 
-       protected String getJsVarName(String name) {
-               return GLOBAL_THIS_ + name;
+       class PreReadyToDo implements Runnable {
+               private String js;
+
+               public PreReadyToDo(String js) {
+                       this.js = js;
+               }
+
+               @Override
+               public void run() {
+                       if (browser.isDisposed())
+                               return;
+                       boolean success = browser.execute(js);
+                       if (!success && log.isTraceEnabled())
+                               log.error("Pre-ready JavaScript failed: " + js);
+               }
        }
 
        /*
         * ACCESSORS
         */
-       
+
        public Control getControl() {
                return browser;
        }