Adding SwingJS interface and utility classes
authorBobHanson <hansonr@stolaf.edu>
Thu, 28 May 2020 21:34:15 +0000 (16:34 -0500)
committerBobHanson <hansonr@stolaf.edu>
Thu, 28 May 2020 21:34:15 +0000 (16:34 -0500)
24 files changed:
src/jalview/util/Platform.java
src/javajs/async/Assets.java [new file with mode: 0644]
src/javajs/async/Async.java [new file with mode: 0644]
src/javajs/async/AsyncColorChooser.java [new file with mode: 0644]
src/javajs/async/AsyncDialog.java [new file with mode: 0644]
src/javajs/async/AsyncFileChooser.java [new file with mode: 0644]
src/javajs/async/AsyncSwingWorker.java [new file with mode: 0644]
src/javajs/async/SwingJSUtils.java [new file with mode: 0644]
src/swingjs/api/Interface.java [new file with mode: 0644]
src/swingjs/api/JSFileHandler.java [new file with mode: 0644]
src/swingjs/api/JSUtilI.java [new file with mode: 0644]
src/swingjs/api/js/DOMNode.java [new file with mode: 0644]
src/swingjs/api/js/HTML5Applet.java [new file with mode: 0644]
src/swingjs/api/js/HTML5AudioContext.java [new file with mode: 0644]
src/swingjs/api/js/HTML5Canvas.java [new file with mode: 0644]
src/swingjs/api/js/HTML5CanvasContext2D.java [new file with mode: 0644]
src/swingjs/api/js/HTML5DataTransfer.java [new file with mode: 0644]
src/swingjs/api/js/HTML5Video.java [new file with mode: 0644]
src/swingjs/api/js/J2SInterface.java [new file with mode: 0644]
src/swingjs/api/js/JQuery.java [new file with mode: 0644]
src/swingjs/api/js/JQueryObject.java [new file with mode: 0644]
src/swingjs/api/js/JSFunction.java [new file with mode: 0644]
src/swingjs/api/js/JSInterface.java [new file with mode: 0644]
src/swingjs/api/js/README.txt [new file with mode: 0644]

index 121ac1b..4d2a09e 100644 (file)
@@ -22,7 +22,6 @@ package jalview.util;
 
 import jalview.javascript.json.JSON;
 
-import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Toolkit;
 import java.awt.event.MouseEvent;
@@ -49,6 +48,8 @@ import org.json.simple.parser.ParseException;
 
 import com.stevesoft.pat.Regex;
 
+import swingjs.api.JSUtilI;
+
 /**
  * System platform information used by Applet and Application
  * 
@@ -63,6 +64,20 @@ public class Platform
   private static Boolean isNoJSMac = null, isNoJSWin = null, isMac = null,
           isWin = null;
 
+  private static swingjs.api.JSUtilI jsutil;
+
+  static {
+          if (isJS) {
+      try
+      {
+        jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
+      } catch (InstantiationException | IllegalAccessException
+              | ClassNotFoundException e)
+      {
+        e.printStackTrace();
+      }
+          }
+  }
   // private static Boolean isHeadless = null;
 
   /**
@@ -336,12 +351,7 @@ public class Platform
     {
       return;
     }
-    /**
-     * @j2sNative
-     * 
-     *            swingjs.JSUtil.cacheFileData$S$O(path, data);
-     * 
-     */
+    jsutil.cachePathData(path, data);
   }
 
   public static void cacheFileData(File file)
@@ -356,19 +366,13 @@ public class Platform
 
   public static byte[] getFileBytes(File f)
   {
-    // TODO temporary doubling of 秘bytes and _bytes;
-    // just remove _bytes when new transpiler has been installed
-    return /** @j2sNative f && (f.秘bytes || f._bytes) || */
-    null;
+    // JavaScript only
+    return jsutil.getBytes(f);
   }
 
   public static byte[] getFileAsBytes(String fileStr)
   {
-    byte[] bytes = null;
-    // BH 2018 hack for no support for access-origin
-    /**
-     * @j2sNative bytes = swingjs.JSUtil.getFileAsBytes$O(fileStr)
-     */
+    byte[] bytes = (byte[]) jsutil.getFile(fileStr, false);
     cacheFileData(fileStr, bytes);
     return bytes;
   }
@@ -376,14 +380,7 @@ public class Platform
   @SuppressWarnings("unused")
   public static String getFileAsString(String url)
   {
-    String ret = null;
-    /**
-     * @j2sNative
-     * 
-     *            ret = swingjs.JSUtil.getFileAsString$S(url);
-     * 
-     * 
-     */
+    String ret = (String) jsutil.getFile(url, true);
     cacheFileData(url, ret);
     return ret;
   }
@@ -396,16 +393,13 @@ public class Platform
     }
     @SuppressWarnings("unused")
     byte[] bytes = getFileAsBytes(urlstring);
-    // TODO temporary doubling of 秘bytes and _bytes;
-    // just remove _bytes when new transpiler has been installed
-    /**
-     * @j2sNative f.秘bytes = f._bytes = bytes;
-     */
+    jsutil.setFileBytes(f, bytes);
     return true;
   }
 
   public static void addJ2SBinaryType(String ext)
   {
+
     /**
      * @j2sNative
      * 
diff --git a/src/javajs/async/Assets.java b/src/javajs/async/Assets.java
new file mode 100644 (file)
index 0000000..b57528b
--- /dev/null
@@ -0,0 +1,594 @@
+package javajs.async;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import swingjs.api.JSUtilI;
+
+/**
+ * The Assets class allows assets such as images and property files to be
+ * combined into zip files rather than delivered individually. The Assets
+ * instance is a singleton served by a set of static methods. In particular, the
+ * three add(...) methods are used to create asset references, which include an
+ * arbitrary name, a path to a zip file asset, and one or more class paths that
+ * are covered by this zip file asset.
+ * 
+ * For example:
+ * 
+ * <code>
+       static {
+               try {
+                       Assets.add(new Assets.Asset("osp", "osp-assets.zip", "org/opensourcephysics/resources"));
+                       Assets.add(new Assets.Asset("tracker", "tracker-assets.zip",
+                                       "org/opensourcephysics/cabrillo/tracker/resources"));
+                       Assets.add(new Assets.Asset("physlets", "physlet-assets.zip", new String[] { "opticsimages", "images" }));
+                       // add the Info.assets last so that it can override these defaults
+                       if (OSPRuntime.isJS) {
+                               Assets.add(OSPRuntime.jsutil.getAppletInfo("assets"));
+                       }
+               } catch (Exception e) {
+                       OSPLog.warning("Error reading assets path. ");
+               }
+
+       }
+ * </code>
+ * 
+ * It is not clear that Java is well-served by this zip-file loading, but
+ * certainly JavaScript is. What could be 100 downloads is just one, and SwingJS
+ * (but not Java) can cache individual ZipEntry instances in order to unzip them
+ * independently only when needed. This is potentially a huge savings.
+ * 
+ * Several static methods can be used to retrieve assets. Principal among those
+ * are:
+ * 
+ * <code>
+ * getAssetBytes(String fullPath)
+ * getAssetString(String fullPath)
+ * getAssetStream(String fullPath)
+ * </code>
+ * 
+ * If an asset is not found in a zip file, then it will be loaded from its fullPath. 
+ * 
+ * 
+ * 
+ * @author hansonr
+ *
+ */
+
+public class Assets {
+
+       public static boolean isJS = /** @j2sNative true || */
+                       false;
+
+       public static JSUtilI jsutil;
+
+       static {
+               try {
+                       if (isJS) {
+                               jsutil = ((JSUtilI) Class.forName("swingjs.JSUtil").newInstance());
+                       }
+
+               } catch (Exception e) {
+                       System.err.println("Assets could not create swinjs.JSUtil instance");
+               }
+       }
+
+       private Map<String, Map<String, ZipEntry>> htZipContents = new HashMap<>();
+
+       private static boolean doCacheZipContents = true;
+
+       private static Assets instance = new Assets();
+
+       private Assets() {
+       }
+
+       private Map<String, Asset> assetsByPath = new HashMap<>();
+
+       private String[] sortedList = new String[0];
+
+       /**
+        * If this object has been cached by SwingJS, add its bytes to the URL, URI, or
+        * File
+        * 
+        * @param URLorURIorFile
+        * @return
+        */
+       public static byte[] addJSCachedBytes(Object URLorURIorFile) {
+               return (isJS ? jsutil.addJSCachedBytes(URLorURIorFile) : null);
+       }
+
+       public static class Asset {
+               String name;
+               URI uri;
+               String classPath;
+               String zipPath;
+               String[] classPaths;
+
+               public Asset(String name, String zipPath, String[] classPaths) {
+                       this.name = name;
+                       this.zipPath = zipPath;
+                       this.classPaths = classPaths;
+               }
+
+               public Asset(String name, String zipPath, String classPath) {
+                       this.name = name;
+                       this.zipPath = zipPath;
+                       uri = getAbsoluteURI(zipPath); // no spaces expected here.
+                       this.classPath = classPath.endsWith("/") ? classPath : classPath + "/";
+               }
+
+               public URL getURL(String fullPath) throws MalformedURLException {
+                       return (fullPath.indexOf(classPath) < 0 ? null
+                                       : new URL("jar", null, uri + "!/" + fullPath));//.replaceAll(" ", "%20")));
+               }
+
+               @Override
+               public String toString() {
+                       return "{" + "\"name\":" + "\"" + name + "\"," + "\"zipPath\":" + "\"" + zipPath + "\"," + "\"classPath\":"
+                                       + "\"" + classPath + "\"" + "}";
+               }
+
+       }
+
+       public static Assets getInstance() {
+               return instance;
+       }
+
+       /**
+        * The difference here is that URL will not insert the %20 for space that URI will.
+        * 
+        * @param path
+        * @return
+        */
+       @SuppressWarnings("deprecation")
+       public static URL getAbsoluteURL(String path) {
+               URL url = null;
+               try {
+                       url = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURL() : new URL(path));
+                       if (path.indexOf("!/")>=0)
+                               url = new URL("jar", null, url.toString());
+               } catch (MalformedURLException e) {
+                       e.printStackTrace();
+               }
+               return url;
+       }
+
+       public static URI getAbsoluteURI(String path) {
+               URI uri = null;
+               try {
+                       uri = (path.indexOf(":/") < 0 ? new File(new File(path).getAbsolutePath()).toURI() : new URI(path));
+               } catch (URISyntaxException e) {
+                       e.printStackTrace();
+               }
+               return uri;
+       }
+
+       /**
+        * Allows passing a Java Asset or array of Assets or a JavaScript Object or
+        * Object array that contains name, zipPath, and classPath keys; in JavaScript,
+        * the keys can have multiple .
+        * 
+        * @param o
+        */
+       public static void add(Object o) {
+               if (o == null)
+                       return;
+               try {
+                       if (o instanceof Object[]) {
+                               Object[] a = (Object[]) o;
+                               for (int i = 0; i < a.length; i++)
+                                       add(a[i]);
+                               return;
+                       }
+                       // In JavaScript this may not actually be an Asset, only a proxy for that.
+                       // Just testing for keys. Only one of classPath and classPaths is allowed.
+                       Asset a = (Asset) o;
+                       if (a.name == null || a.zipPath == null || a.classPath == null && a.classPaths == null
+                                       || a.classPath != null && a.classPaths != null) {
+                               throw new NullPointerException("Assets could not parse " + o);
+                       }
+                       if (a.classPaths == null) {
+                               // not possible in Java, but JavaScript may be passing an array of class paths
+                               add(a.name, a.zipPath, a.classPath);
+                       } else {
+                               add(a.name, a.zipPath, a.classPaths);
+                       }
+               } catch (Throwable t) {
+                       throw new IllegalArgumentException(t.getMessage());
+               }
+       }
+
+       public static void add(String name, String zipFile, String path) {
+               add(name, zipFile, new String[] { path });
+       }
+
+       private static HashSet<String> loadedAssets = new HashSet<>();
+       
+       public static boolean hasLoaded(String name) {
+               return loadedAssets.contains(name);
+       }
+       
+       public static void reset() {
+               getInstance().htZipContents.clear();
+               getInstance().assetsByPath.clear();
+               getInstance().sortedList = new String[0];
+       }
+
+       public static void add(String name, String zipFile, String[] paths) {
+               getInstance()._add(name, zipFile, paths);
+       }
+
+       private void _add(String name, String zipFile, String[] paths) {
+               if (hasLoaded(name)) {
+                       System.err.println("Assets warning: Asset " + name + " already exists");
+               }
+               loadedAssets.add(name);
+               for (int i = paths.length; --i >= 0;) {
+                       assetsByPath.put(paths[i], new Asset(name, zipFile, paths[i]));
+               }
+               resort();
+       }
+       
+
+       /**
+        * Gets the asset, preferably from a zip file asset, but not necessarily.
+        * 
+        * @param assetPath
+        * @return
+        */
+       public static byte[] getAssetBytes(String assetPath) {
+               return getAssetBytes(assetPath, false);
+       }
+
+       /**
+        * Gets the asset, preferably from a zip file asset, but not necessarily.
+        * 
+        * @param assetPath
+        * @return
+        */
+       public static String getAssetString(String assetPath) {
+               return getAssetString(assetPath, false);
+       }
+
+       /**
+        * Gets the asset, preferably from a zip file asset, but not necessarily.
+        * 
+        * @param assetPath
+        * @return
+        */
+       public static InputStream getAssetStream(String assetPath) {
+               return getAssetStream(assetPath, false);
+       }
+
+       /**
+        * Gets the asset from a zip file.
+        * 
+        * @param assetPath
+        * @return
+        */
+       public static byte[] getAssetBytesFromZip(String assetPath) {
+               return getAssetBytes(assetPath, true);
+       }
+
+       /**
+        * Gets the asset from a zip file.
+        * 
+        * @param assetPath
+        * @return
+        */
+       public static String getAssetStringFromZip(String assetPath) {
+               return getAssetString(assetPath, true);
+       }
+
+       /**
+        * Gets the asset from a zip file.
+        * 
+        * @param assetPath
+        * @return
+        */
+       public static InputStream getAssetStreamFromZip(String assetPath) {
+               return getAssetStream(assetPath, true);
+       }
+
+
+       /**
+        * Get the contents of a path from a zip file asset as byte[], optionally loading
+        * the resource directly using a class loader.
+        * 
+        * @param path
+        * @param zipOnly
+        * @return
+        */
+       private static byte[] getAssetBytes(String path, boolean zipOnly) {
+               byte[] bytes = null;
+               try {
+                       URL url = getInstance()._getURLFromPath(path, true);
+                       if (url == null && !zipOnly) {
+                               url = getAbsoluteURL(path);
+                               //url = Assets.class.getResource(path);
+                       }
+                       if (url == null)
+                               return null;
+                       if (isJS) {
+                               bytes = jsutil.getURLBytes(url);
+                               if (bytes == null) {
+                                       url.openStream();
+                                       bytes = jsutil.getURLBytes(url);
+                               }
+                       } else {
+                               bytes = getLimitedStreamBytes(url.openStream(), -1, null);
+                       }
+               } catch (Throwable t) {
+                       t.printStackTrace();
+               }
+               return bytes;
+       }
+
+       /**
+        * Get the contents of a path from a zip file asset as a String, optionally
+        * loading the resource directly using a class loader.
+        * 
+        * @param path
+        * @param zipOnly
+        * @return
+        */
+       private static String getAssetString(String path, boolean zipOnly) {
+               byte[] bytes = getAssetBytes(path, zipOnly);
+               return (bytes == null ? null : new String(bytes));
+       }
+
+       /**
+        * Get the contents of a path from a zip file asset as an InputStream, optionally
+        * loading the resource directly using a class loader.
+        * 
+        * @param path
+        * @param zipOnly
+        * @return
+        */
+       private static InputStream getAssetStream(String path, boolean zipOnly) {
+               try {
+                       URL url = getInstance()._getURLFromPath(path, true);
+                       if (url == null && !zipOnly) {
+                               url = Assets.class.getResource(path);
+                       }
+                       if (url != null)
+                               return url.openStream();
+               } catch (Throwable t) {
+               }
+               return null;
+       }
+       /**
+        * Determine the path to an asset. If not found in a zip file asset, return the
+        * absolute path to this resource.
+        * 
+        * @param fullPath
+        * @return
+        */
+       public static URL getURLFromPath(String fullPath) {
+               return getInstance()._getURLFromPath(fullPath, false);
+       }
+
+       /**
+        * Determine the path to an asset. If not found in a zip file asset, optionally
+        * return null or the absolute path to this resource.
+        * 
+        * @param fullPath
+        * @param zipOnly
+        * @return the URL to this asset, or null if not found.
+        */
+       public static URL getURLFromPath(String fullPath, boolean zipOnly) {
+               return getInstance()._getURLFromPath(fullPath, zipOnly);
+       }
+
+       private URL _getURLFromPath(String fullPath, boolean zipOnly) {
+               URL url = null;
+               try {
+                       if (fullPath.startsWith("/"))
+                               fullPath = fullPath.substring(1);
+                       for (int i = sortedList.length; --i >= 0;) {
+                               if (fullPath.startsWith(sortedList[i])) {
+                                       url = assetsByPath.get(sortedList[i]).getURL(fullPath);
+                                       ZipEntry ze = findZipEntry(url);
+                                       if (ze == null)
+                                               break;
+                                       if (isJS) {
+                                               jsutil.setURLBytes(url, jsutil.getZipBytes(ze));
+                                       }
+                                       return url;
+                               }
+                       }
+                       if (!zipOnly)
+                               return getAbsoluteURL(fullPath);
+               } catch (MalformedURLException e) {
+               }
+               return null;
+       }
+
+       public static ZipEntry findZipEntry(URL url) {
+               String[] parts = getJarURLParts(url.toString());
+               if (parts == null || parts[0] == null || parts[1].length() == 0)
+                       return null;
+               return findZipEntry(parts[0], parts[1]);
+       }
+
+       public static ZipEntry findZipEntry(String zipFile, String fileName) {
+               return getZipContents(zipFile).get(fileName);
+       }
+
+       /**
+        * Gets the contents of a zip file.
+        * 
+        * @param zipPath the path to the zip file
+        * @return a set of file names in alphabetical order
+        */
+       public static Map<String, ZipEntry> getZipContents(String zipPath) {
+               return getInstance()._getZipContents(zipPath);
+       }
+
+       private Map<String, ZipEntry> _getZipContents(String zipPath) {
+               URL url = getURLWithCachedBytes(zipPath); // BH carry over bytes if we have them already
+               Map<String, ZipEntry> fileNames = htZipContents.get(url.toString());
+               if (fileNames != null)
+                       return fileNames;
+               try {
+                       // Scan URL zip stream for files.
+                       return readZipContents(url.openStream(), url);
+               } catch (Exception ex) {
+                       ex.printStackTrace();
+                       return null;
+               }
+       }
+
+       /**
+        * Deconstruct a jar URL into two parts, before and after "!/".
+        * 
+        * @param source
+        * @return
+        */
+       public static String[] getJarURLParts(String source) {
+               int n = source.indexOf("!/");
+               if (n < 0)
+                       return null;
+               String jarfile = source.substring(0, n).replace("jar:", "");
+               while (jarfile.startsWith("//"))
+                       jarfile = jarfile.substring(1);
+               return new String[] { jarfile, (n == source.length() - 2 ? null : source.substring(n + 2)) };
+       }
+
+       /**
+        * Get the contents of any URL as a byte array. This method does not do any asset check. It just gets the url data as a byte array.
+        * 
+        * @param url
+        * @return byte[]
+        * 
+        * @author hansonr
+        */
+       public static byte[] getURLContents(URL url) {
+               if (url == null)
+                       return null;
+               try {
+                       if (isJS) {
+                               // Java 9! return new String(url.openStream().readAllBytes());
+                               return jsutil.readAllBytes(url.openStream());
+                       }
+                       return getLimitedStreamBytes(url.openStream(), -1, null);
+               } catch (IOException e) {
+                       e.printStackTrace();
+               }
+               return null;
+       }
+
+       /**
+        * 
+        * Convert a file path to a URL, retrieving any cached file data, as from DnD.
+        * Do not do any actual data transfer. This is a swingjs.JSUtil service.
+        * 
+        * @param path
+        * @return
+        */
+       private static URL getURLWithCachedBytes(String path) {
+               URL url = getAbsoluteURL(path);
+               if (url != null)
+                       addJSCachedBytes(url);
+               return url;
+       }
+
+       private Map<String, ZipEntry> readZipContents(InputStream is, URL url) throws IOException {
+               HashMap<String, ZipEntry> fileNames = new HashMap<String, ZipEntry>();
+               if (doCacheZipContents)
+                       htZipContents.put(url.toString(), fileNames);
+               ZipInputStream input = new ZipInputStream(is);
+               ZipEntry zipEntry = null;
+               int n = 0;
+               while ((zipEntry = input.getNextEntry()) != null) {
+                       if (zipEntry.isDirectory() || zipEntry.getSize() == 0)
+                               continue;
+                       n++;
+                       String fileName = zipEntry.getName();
+                       fileNames.put(fileName, zipEntry); // Java has no use for the ZipEntry, but JavaScript can read it.
+               }
+               input.close();
+               System.out.println("Assets: " + n + " zip entries found in " + url); //$NON-NLS-1$
+               return fileNames;
+       }
+
+       private void resort() {
+               sortedList = new String[assetsByPath.size()];
+               int i = 0;
+               for (String path : assetsByPath.keySet()) {
+                       sortedList[i++] = path;
+               }
+               Arrays.sort(sortedList);
+       }
+
+
+       /**
+        * Only needed for Java
+        * 
+        * @param is
+        * @param n
+        * @param out
+        * @return
+        * @throws IOException
+        */
+       private static byte[] getLimitedStreamBytes(InputStream is, long n, OutputStream out) throws IOException {
+
+               // Note: You cannot use InputStream.available() to reliably read
+               // zip data from the web.
+
+               boolean toOut = (out != null);
+               int buflen = (n > 0 && n < 1024 ? (int) n : 1024);
+               byte[] buf = new byte[buflen];
+               byte[] bytes = (out == null ? new byte[n < 0 ? 4096 : (int) n] : null);
+               int len = 0;
+               int totalLen = 0;
+               if (n < 0)
+                       n = Integer.MAX_VALUE;
+               while (totalLen < n && (len = is.read(buf, 0, buflen)) > 0) {
+                       totalLen += len;
+                       if (toOut) {
+                               out.write(buf, 0, len);
+                       } else {
+                               if (totalLen > bytes.length)
+                                       bytes = Arrays.copyOf(bytes, totalLen * 2);
+                               System.arraycopy(buf, 0, bytes, totalLen - len, len);
+                               if (n != Integer.MAX_VALUE && totalLen + buflen > bytes.length)
+                                       buflen = bytes.length - totalLen;
+                       }
+               }
+               if (toOut)
+                       return null;
+               if (totalLen == bytes.length)
+                       return bytes;
+               buf = new byte[totalLen];
+               System.arraycopy(bytes, 0, buf, 0, totalLen);
+               return buf;
+       }
+
+       /**
+        * Return all assets in the form that is appropriate for the Info.assets value in SwingJS.
+        * 
+        */
+       @Override
+       public String toString() {
+               String s = "[";
+               for (int i = 0; i < sortedList.length; i++) {
+                       Asset a = assetsByPath.get(sortedList[i]);
+                       s += (i == 0 ? "" : ",") + a;
+               }
+               return s + "]";
+       }
+
+}
diff --git a/src/javajs/async/Async.java b/src/javajs/async/Async.java
new file mode 100644 (file)
index 0000000..f6f31e5
--- /dev/null
@@ -0,0 +1,189 @@
+package javajs.async;
+
+/**
+ * A package to manage asynchronous aspects of SwingJS
+ * 
+ * The javajs.async package simplifies the production of methods that can be
+ * used equally well in Java and in JavaScript for handling "pseudo-modal"
+ * blocking in JavaScript, meaning the user is locked out of other interactions,
+ * as in Java, but the code is not actually blocking the thread.
+ * 
+ * Included in this package are
+ * 
+ * Async
+ * 
+ * Provides few simple generic static methods.
+ * 
+ * Async.isJS() -- true if we are running this in JavaScript
+ * 
+ * Async.javaSleep() -- bypassing Thread.sleep() for JavaScript; allowing it for
+ * Java
+ * 
+ * 
+ * AsyncDialog
+ * 
+ * Provides several very useful methods that act as a replacement for direct
+ * JOptionPane or JDialog calls, including both a synchronous callback
+ * (manifested in Java) and an asynchronous callback (JavaScript), both
+ * resulting in the same effect, except for the fact that the JavaScript code
+ * has returned immediately from the call with an "ignore me" reference, while
+ * Java is waiting at that call to return a value from JOptionPane (which is
+ * saved by AsyncDialog and not delivered as a return value).
+ * 
+ * AsyncDialog does not extend JOptionPane, but it mirrors that classes public
+ * methods. There are a LOT of public methods in JOPtionPane. I suppose we
+ * should implement them all. In practice, AsyncDialog calls standard
+ * JOptionPane static classes for the dialogs.
+ * 
+ * Initially, the following methods are implemented:
+ * 
+ * public void showConfirmDialog(Component frame, Object message, String title,
+ * ActionListener a)
+ * 
+ * public void showConfirmDialog(Component frame, Object message, String title,
+ * int optionType, ActionListener a)
+ * 
+ * public void showConfirmDialog(Component frame, Object message, String title,
+ * int optionType, int messageType, ActionListener a)
+ * 
+ * public void showInputDialog(Component frame, Object message, ActionListener
+ * a)
+ * 
+ * public void showInputDialog(Component frame, Object message, String title,
+ * int messageType, Icon icon, Object[] selectionValues, Object
+ * initialSelectionValue, ActionListener a)
+ * 
+ * public void showMessageDialog(Component frame, Object message, ActionListener
+ * a)
+ * 
+ * public void showOptionDialog(Component frame, Object message, String title,
+ * int optionType, int messageType, Icon icon, Object[] options, Object
+ * initialValue, ActionListener a)
+ * 
+ * 
+ * All nonstatic methods, requiring new AsyncDialog(), also require an
+ * ActionListener. This listener will get a call to actionPerformed(ActionEvent)
+ * where:
+ * 
+ * event.getSource() is a reference to the originating AsyncDialog (super
+ * JOptionPane) for all information that a standard JOptionPane can provide,
+ * along with the two methods int getOption() and Object getChoice().
+ * 
+ * event.getID() is a reference to the standard JOptionPane int return code.
+ * 
+ * event.getActionCommand() also holds a value, but it may or may not be of
+ * value.
+ * 
+ * 
+ * A few especially useful methods are static, allowing just one or two expected
+ * callbacks of interest:
+ * 
+ * AsyncDialog.showOKAsync(Component parent, Object message, String title,
+ * Runnable ok)
+ * 
+ * AsyncDialog.showYesAsync (Component parent, Object message, String title,
+ * Runnable yes)
+ * 
+ * AsyncDialog.showYesNoAsync (Component parent, Object message, String title,
+ * Runnable yes, Runnable no)
+ * 
+ * These methods provide a fast way to adjust JOptionPane calls to be
+ * asynchronous.
+ * 
+ * 
+ * 
+ * AsyncFileChooser extends javax.swing.JFileChooser
+ * 
+ * Accepted constructors include:
+ * 
+ * public AsyncFileChooser()
+ * 
+ * public AsyncFileChooser(File file)
+ * 
+ * public AsyncFileChooser(File file, FileSystemView view)
+ * 
+ * (Note, however, that FileSystemView has no equivalent in JavaScript.)
+ * 
+ * It's three public methods include:
+ * 
+ * public void showDialog(Component frame, String btnLabel, Runnable ok,
+ * Runnable cancel)
+ * 
+ * public void showOpenDialog(Component frame, Runnable ok, Runnable cancel)
+ * 
+ * public void showSaveDialog(Component frame, Runnable ok, Runnable cancel)
+ * 
+ * 
+ * ActionListener is not needed here, as the instance of new AsyncFileChooser()
+ * already has direct access to all the JFileChooser public methods such as
+ * getSelectedFile() and getSelectedFiles().
+ * 
+ * As a subclass of JFileChooser, it accepts all three public showXXXX methods
+ * of JFileChooser, namely:
+ * 
+ * public void showDialog(Component frame, String btnLabel)
+ * 
+ * public void showOpenDialog(Component frame)
+ * 
+ * public void showSaveDialog(Component frame)
+ * 
+ * 
+ * None of these are recommended. AsyncFileChooser will indicate errors if the
+ * first of these two are called. (showSaveDialog is fine, as it is modal even
+ * in JavaScript. However it is not recommended that showSaveDialog(Component
+ * frame) be used, as in the future browsers may implement some sort of file
+ * saver in HTML5.
+ * 
+ * 
+ * 
+ * AsyncColorChooser
+ * 
+ * 
+ * AsyncColorChooser accesses JColorChooser asynchronously, using a private
+ * SwingJS setting that tells JColorChooser to report back to it with property
+ * changes. It is constructed using new AsyncColorChooser() and implements just
+ * two methods:
+ * 
+ * public void showDialog(Component component, String title, Color initialColor,
+ * ActionListener listener)
+ * 
+ * public Color getSelectedColor()
+ * 
+ * 
+ * The listener will get an actionPerformed(ActionEvent) callback with
+ * event.getID() equal to the color value or 0 if canceled. The
+ * getSelectedColor() method may also be called from this callback to retrieve
+ * the associated java.awt.Color object, using
+ * 
+ * ((AsyncColorChooser)e.getSource()).getSelectedColor()
+ * 
+ * As in Java, a null value for the selected color indicates that the
+ * JColorChooser was closed.
+ * 
+ * Bob Hanson 2019.11.07
+ * 
+ * 
+ * @author Bob Hanson hansonr_at_stolaf.edu
+ *
+ */
+public class Async {
+
+       public static boolean isJS() {
+               return  (/** @j2sNative 1 ? true : */false);
+       }
+
+       /**
+        * No sleep in JavaScript
+        * @param ms
+        */
+       public static void javaSleep(int ms) {
+               if (!isJS()) {
+                       try {
+                               Thread.sleep(ms);
+                       } catch (InterruptedException e) {
+                       }
+               }
+       
+       }
+
+}
diff --git a/src/javajs/async/AsyncColorChooser.java b/src/javajs/async/AsyncColorChooser.java
new file mode 100644 (file)
index 0000000..2474833
--- /dev/null
@@ -0,0 +1,67 @@
+package javajs.async;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import javax.swing.JColorChooser;
+import javax.swing.plaf.UIResource;
+
+/**
+ * A simple Asynchronous file chooser for JavaScript; synchronous with Java.
+ * 
+ * Allows two modes -- using an ActionListener (setAction(ActionListener) or constructor(ActionListener))
+ * 
+ * @author Bob Hanson
+ */
+
+public class AsyncColorChooser implements PropertyChangeListener {
+
+       private ActionListener listener;
+       private Color selectedColor;
+
+       public void showDialog(Component component, String title, Color initialColor, ActionListener listener) {
+               setListener(listener);
+               process(JColorChooser.showDialog(component, title, initialColor));
+               unsetListener();
+       }
+
+       public Color getSelectedColor() {
+               return selectedColor;
+       }
+
+
+       @Override
+       public void propertyChange(PropertyChangeEvent evt) {
+               // JavaScript only
+               Color c = (Color) evt.getNewValue();
+               
+               switch (evt.getPropertyName()) {
+               case "SelectedColor":
+                       process(c);
+                       break;
+               }
+       }
+
+       private void setListener(ActionListener a) {
+               listener = a;
+               /** @j2sNative Clazz.load("javax.swing.JColorChooser");javax.swing.JColorChooser.listener = this */
+       }
+
+       private void unsetListener() {
+               /** @j2sNative javax.swing.JColorChooser.listener = null */
+       }
+
+       
+       
+       private void process(Color c) {
+               if (c instanceof UIResource)
+                       return;
+               selectedColor = c;
+               listener.actionPerformed(new ActionEvent(this, c == null ? 0 : c.getRGB(), c == null ? null : c.toString()));
+       }
+       
+}
diff --git a/src/javajs/async/AsyncDialog.java b/src/javajs/async/AsyncDialog.java
new file mode 100644 (file)
index 0000000..5752f48
--- /dev/null
@@ -0,0 +1,282 @@
+package javajs.async;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+
+import javax.swing.Icon;
+import javax.swing.JOptionPane;
+import javax.swing.plaf.UIResource;
+
+/**
+ * A class to manage asynchronous input, option, and confirmation dialogs.
+ * 
+ * @author Bob Hanson hansonr_at_stolaf.edu
+ *
+ */
+public class AsyncDialog implements PropertyChangeListener {
+
+// see discussion in net.sf.j2s.core/doc/Differences.txt
+//
+// Confirmation dialog example. Note moving the parent component into the constructor.
+// Original:
+//
+//             private void promptQuit() {
+//                     int sel = JOptionPane.showConfirmDialog(null, PROMPT_EXIT, NAME, JOptionPane.YES_NO_OPTION);
+//                     switch (sel) {
+//                     case JOptionPane.YES_OPTION:
+//                             resultsTab.clean();
+//                             seqs.dispose();
+//                             if (fromMain) {
+//                                     System.exit(0);
+//                             }
+//                             break;
+//                     }
+//             }
+//
+// revised: 
+//
+//             private void promptQuitAsync() {
+//                     new AsyncDialog().showConfirmDialog(null, PROMPT_EXIT, NAME, JOptionPane.YES_NO_OPTION, new ActionListener() {
+//
+//     @Override
+//     public void actionPerformed(ActionEvent e) {
+//         int sel = ((AsyncDialog)e.getSource()).getOption();
+//             switch (sel) {
+//             case JOptionPane.YES_OPTION:
+//                     resultsTab.clean();
+//                     seqs.dispose();
+//                     if (fromMain) {
+//                             System.exit(0);
+//                     }
+//                     break;
+//             }
+//     }}));
+//             }
+
+       
+       public AsyncDialog() {
+       }
+       
+       private ActionListener actionListener;
+       private Object choice;
+       private Object[] options;
+       private Object value;
+       private boolean wantsInput;
+
+       // These options can be supplemented as desired.
+
+       
+       /**
+        * Synchronous call; OK in JavaScript as long as we are using a JavaScript prompt() call
+        * 
+        * @param frame
+        * @param msg
+        * @return
+        */
+       @Deprecated
+       public static String showInputDialog(Component frame, String msg) {
+               return JOptionPane.showInputDialog(frame, msg);
+       }
+
+       public void showInputDialog(Component frame, Object message, ActionListener a) {
+               setListener(a);
+               wantsInput = true;
+               process(JOptionPane.showInputDialog(frame, message));
+               unsetListener();
+       }
+
+       public void showInputDialog(Component frame, Object message, String title, int messageType, Icon icon,
+                       Object[] selectionValues, Object initialSelectionValue, ActionListener a) {
+               setListener(a);
+               wantsInput = true;
+               process(JOptionPane.showInputDialog(frame, message, title, messageType, icon, selectionValues,
+                               initialSelectionValue));
+               unsetListener();
+       }
+
+       public void showMessageDialog(Component frame, Object message, ActionListener a) {
+               setListener(a);
+               JOptionPane.showMessageDialog(frame, message);
+               unsetListener();
+               if (/** @j2sNative false || */true)
+                       process("" + message);
+       }
+
+       public void showOptionDialog(Component frame, Object message, String title, int optionType, int messageType,
+                       Icon icon, Object[] options, Object initialValue, ActionListener a) {
+               actionListener = a;
+               this.options = options;
+               setListener(a);
+               process(JOptionPane.showOptionDialog(frame, message, title, optionType, messageType, icon, options,
+                               initialValue));
+               unsetListener();
+       }
+
+       public void showConfirmDialog(Component frame, Object message, String title, ActionListener a) {
+               showConfirmDialog(frame, message, title, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, a);
+       }
+
+       public void showConfirmDialog(Component frame, Object message, String title, int optionType, ActionListener a) {
+               showConfirmDialog(frame, message, title, optionType, JOptionPane.QUESTION_MESSAGE, a);
+       }
+
+       public void showConfirmDialog(Component frame, Object message, String title, int optionType, int messageType,
+                       ActionListener a) {
+               setListener(a);
+               process(JOptionPane.showConfirmDialog(frame, message, title, optionType, messageType));
+               unsetListener();
+       }
+
+       /**
+        * retrieve selection from the ActionEvent, for which "this" is getSource()
+        * 
+        * @return
+        */
+       public Object getChoice() {
+               return choice;
+       }
+
+       public int getOption() {
+               if (!(choice instanceof Integer)) {
+                       throw new java.lang.IllegalArgumentException("AsyncDialog.getOption called for non-Integer choice");
+               }
+               return ((Integer) choice).intValue();
+       }
+
+       /**
+        * A dialog option that allows for YES, NO, and CLOSE options via
+        * ActionListener. ActionEvent.getID() contains the reply.
+        * 
+        * @param parent   The parent component for the dialog
+        * @param message  The text of the message to display
+        * @param title    Optional title defaults to "Question"
+        * @param listener Handle options based on an ActionEvent
+        */
+       public static void showYesNoAsync(Component parent, Object message, String title, ActionListener listener) {
+               new AsyncDialog().showConfirmDialog(parent, message, (title == null ? "Question" : title),
+                               JOptionPane.YES_NO_OPTION, listener);
+       }
+
+       /**
+        * A dialog option that involves just a YES follower.
+        * @param parent
+        * @param message
+        * @param title TODO
+        * @param yes
+        */
+       public static void showYesAsync(Component parent, Object message, String title, Runnable yes) {
+               AsyncDialog.showYesNoAsync(parent, message, title, new ActionListener() {
+
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (e.getID() == JOptionPane.YES_OPTION) {
+                                       yes.run();
+                               }
+                       }
+                       
+               });
+       }
+
+       /**
+        * A dialog option that involves just an OK follower.
+        * @param parent
+        * @param message
+        * @param title
+        * @param ok
+        */
+       public static void showOKAsync(Component parent, Object message, String title, Runnable ok) {
+               new AsyncDialog().showConfirmDialog(parent, message, title, JOptionPane.OK_CANCEL_OPTION, new ActionListener() {
+
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (e.getID() == JOptionPane.OK_OPTION) {
+                                       ok.run();
+                               }
+                       }
+                       
+               });
+       }
+
+
+       private void setListener(ActionListener a) {
+               actionListener = a;
+               @SuppressWarnings("unused")
+               Class c = JOptionPane.class; // loads the class
+               /** @j2sNative c.$clazz$.listener = this */
+       }
+
+       private void unsetListener() {
+               /** @j2sNative javax.swing.JOptionPane.listener = null */
+       }
+
+       /**
+        * Switch from property change to action.
+        * 
+        */
+       @Override
+       public void propertyChange(PropertyChangeEvent evt) {
+               value = evt.getNewValue();
+               switch (evt.getPropertyName()) {
+               case "inputValue":                      
+                       process(value);
+                       break;
+               case "value":
+                       if (value != null && options == null && !(value instanceof Integer)) {
+                               process(getOptionIndex(((JOptionPane) evt.getSource()).getOptions(), value));
+                               return;
+                       }
+                       if (options != null) {
+                               int i = getOptionIndex(options, value);
+                               value = Integer.valueOf(i >= 0 ? i : JOptionPane.CLOSED_OPTION);
+                       } 
+                       process(value);
+                       break;
+               }
+       }
+
+       private int getOptionIndex(Object[] options, Object val) {
+               if (options != null)
+                       for (int i = 0; i < options.length; i++) {
+                               if (options[i] == val)
+                                       return i;
+                       }
+               return -1;
+       }
+
+       public Object getValue() {
+               if (wantsInput || options == null)
+                       return value;
+               int val = ((Integer) value).intValue();
+               return (val < 0 ? null : options[val]);
+       }
+       
+       private boolean processed;
+
+       /**
+        * Return for confirm dialog.
+        * 
+        * @param ret may be JavaScript NaN, testable as ret != ret or ret != - -ret
+        */
+       private void process(int ret) {
+               if (ret != -(-ret) || processed)
+                       return;
+               processed = true;
+               choice = ret;
+               actionListener.actionPerformed(new ActionEvent(this, ret, "SelectedOption"));
+       }
+
+       private void process(Object ret) {
+               if (ret instanceof UIResource || processed)
+                       return;
+               processed = true;
+               choice = ret;
+               actionListener.actionPerformed(new ActionEvent(this, 
+                               ret == null ? JOptionPane.CANCEL_OPTION : JOptionPane.OK_OPTION, 
+                                               (ret == null ? null : ret.toString())));
+       }
+
+
+}
diff --git a/src/javajs/async/AsyncFileChooser.java b/src/javajs/async/AsyncFileChooser.java
new file mode 100644 (file)
index 0000000..849fe83
--- /dev/null
@@ -0,0 +1,217 @@
+package javajs.async;
+
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.File;
+import java.util.function.Function;
+
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+import javax.swing.filechooser.FileSystemView;
+
+/**
+ * A simple Asynchronous file chooser for JavaScript and Java.
+ * 
+ * Requires an OK runnable; JavaScript can return notification of cancel for
+ * file reading only, not saving.
+ * 
+ * @author Bob Hanson
+ */
+
+public class AsyncFileChooser extends JFileChooser implements PropertyChangeListener {
+
+       private int optionSelected;
+       private Runnable ok, cancel; // sorry, no CANCEL in JavaScript for file open
+       private boolean isAsyncSave = true;
+       private static boolean notified;
+
+       public AsyncFileChooser() {
+               super();
+       }
+
+       public AsyncFileChooser(File file) {
+               super(file);
+       }
+
+       public AsyncFileChooser(File file, FileSystemView view) {
+               super(file, view);
+       }
+
+       @Deprecated
+       @Override
+       public int showDialog(Component frame, String btnText) {
+               // This one can come from JFileChooser - default is OPEN
+               return super.showDialog(frame, btnText);
+       }
+
+       private int err() {
+               try {
+                       throw new java.lang.IllegalAccessException("Warning! AsyncFileChooser interface bypassed!");
+               } catch (IllegalAccessException e) {
+                       e.printStackTrace();
+               }
+               return JFileChooser.ERROR_OPTION;
+       }
+
+       @Deprecated
+       @Override
+       public int showOpenDialog(Component frame) {
+               return err();
+       }
+
+       @Override
+       public int showSaveDialog(Component frame) {
+               isAsyncSave  = false;
+               return super.showSaveDialog(frame);
+       }
+
+       /**
+        * 
+        * @param frame
+        * @param btnLabel "open" or "save"
+        * @param ok
+        * @param cancel must be null; JavaScript cannot capture a cancel from a file dialog
+        */
+       public void showDialog(Component frame, String btnLabel, Runnable ok, Runnable cancel) {
+               this.ok = ok;
+               if (getDialogType() != JFileChooser.SAVE_DIALOG && cancel != null)
+                       notifyCancel();
+               process(super.showDialog(frame, btnLabel));
+       }
+
+       /**
+        * 
+        * @param frame
+        * @param ok
+        * @param cancel must be null; JavaScript cannot capture a cancel from a file dialog
+        */
+       public void showOpenDialog(Component frame, Runnable ok, Runnable cancel) {
+               this.ok = ok;
+               if (cancel != null)
+                       notifyCancel();
+               process(super.showOpenDialog(frame));
+       }
+
+       /**
+        * 
+        * This just completes the set. It is not necessary for JavaScript, because JavaScript
+        * will just throw up a simple modal OK/Cancel message anyway.
+        * 
+        * @param frame
+        * @param ok
+        * @param cancel must be null
+        */
+       public void showSaveDialog(Component frame, Runnable ok, Runnable cancel) {
+               this.ok = ok;
+               this.cancel = cancel;
+               process(super.showSaveDialog(frame));
+       }
+
+       
+       /**
+        * Locate a file for input or output. Note that JavaScript will not return on cancel for OPEN_DIALOG.
+        * 
+        * @param title       The title for the dialog
+        * @param mode        OPEN_DIALOG or SAVE_DIALOG
+        * @param processFile function to use when complete
+        */
+         public static void getFileAsync(Component parent, String title, int mode, Function<File, Void> processFile) {
+                 // BH no references to this method. So changing its signature for asynchonous use
+                 // And it didn't do as advertised - ran System.exit(0) if canceled
+           // create and display a file dialog
+               AsyncFileChooser fc = new AsyncFileChooser();
+               fc.setDialogTitle(title);
+               Runnable after = new Runnable() {
+
+                       @Override
+                       public void run() {
+                               processFile.apply(fc.getSelectedFile());
+                       }
+       
+               };
+               if (mode == JFileChooser.OPEN_DIALOG) {
+                       fc.showOpenDialog(parent, after, after);  
+               } else {
+                       fc.showSaveDialog(parent, after, after);  
+               }
+                               
+         }
+           
+               /**
+                * Run yes.run() if a file doesn't exist or if the user allows it, else run no.run()
+                * @param parent
+                * @param filename
+                * @param title
+                * @param yes (approved)
+                * @param no (optional)
+                */
+               public static void checkReplaceFileAsync(Component parent, File outfile, String title, Runnable yes, Runnable no) {
+                       if (outfile.exists()) {
+                               AsyncDialog.showYesNoAsync(parent,
+                                               outfile + " exists. Replace it?", null, new ActionListener() {
+               
+                                                       @Override
+                                                       public void actionPerformed(ActionEvent e) {
+                                                               switch (e.getID()) {
+                                                               case JOptionPane.YES_OPTION:
+                                                                       yes.run();
+                                                                       break;
+                                                               default:
+                                                                       if (no != null)
+                                                                               no.run();
+                                                                       break;
+                                                               }
+                                                       }
+               
+                                               });
+               
+                       } else {
+                               yes.run();
+                       }
+               
+               }
+
+       private void notifyCancel() {
+               if (!notified) {
+                       System.err.println("developer note: JavaScript cannot fire a FileChooser CANCEL action");
+               }
+               notified = true;
+       }
+
+       @Override
+       public void propertyChange(PropertyChangeEvent evt) {
+               switch (evt.getPropertyName()) {
+               case "SelectedFile":
+               case "SelectedFiles":
+                       process(optionSelected = (evt.getNewValue() == null ? CANCEL_OPTION : APPROVE_OPTION));
+                       break;
+               }
+       }
+
+       private void process(int ret) {
+               if (ret != -(-ret))
+                       return; // initial JavaScript return is NaN
+               optionSelected = ret;
+               File f = getSelectedFile();
+               if (f == null) {
+                       if (cancel != null)
+                               cancel.run();
+               } else {
+                       if (ok != null)
+                               ok.run();
+               }
+       }
+
+       public int getSelectedOption() {
+               return optionSelected;
+       }
+
+       public static byte[] getFileBytes(File f) {
+               return /** @j2sNative f.秘bytes || */null;
+       }
+
+}
diff --git a/src/javajs/async/AsyncSwingWorker.java b/src/javajs/async/AsyncSwingWorker.java
new file mode 100644 (file)
index 0000000..06a6d6e
--- /dev/null
@@ -0,0 +1,358 @@
+package javajs.async;
+
+import java.awt.Component;
+
+import javax.swing.ProgressMonitor;
+import javax.swing.SwingUtilities;
+import javax.swing.SwingWorker;
+
+import javajs.async.SwingJSUtils.StateHelper;
+import javajs.async.SwingJSUtils.StateMachine;
+
+/**
+ * Executes synchronous or asynchronous tasks using a SwingWorker in Java or JavaScript,
+ * equivalently.
+ * 
+ * Unlike a standard SwingWorker, AsyncSwingWorker may itself be asynchronous.
+ * For example, it might load a file asynchronously, or carry out a background
+ * process in JavaScript much like one might be done in Java, but with only a
+ * single thread.
+ * 
+ * Whereas a standard SwingWorker would execute done() long before the
+ * asynchronous task completed, this class will wait until progress has been
+ * asynchronously set greater or equal to its max value or the task is canceled
+ * before executing that method.
+ * 
+ * Three methods must be supplied by the subclass:
+ * 
+ * void initAsync()
+ * 
+ * int doInBackgroundAsync(int progress)
+ * 
+ * void doneAsync()
+ * 
+ * Both initAsync() and doneAsync() are technically optional - they may be
+ * empty. doInBackgroundAsync(), however, is the key method where, like
+ * SwingWorker's doInBackground, the main work is done. The supplied progress
+ * parameter reminds the subclass of where it is at, and the return value allows
+ * the subclass to update the progress field in both the SwingWorker and the
+ * ProgressMonitor.
+ * 
+ * If it is desired to run the AsyncSwingWorker synchonously, call the 
+ * executeSynchronously() method rather than execute(). Never call SwingWorker.run(). 
+ * 
+ * 
+ * @author hansonr
+ *
+ */
+public abstract class AsyncSwingWorker extends SwingWorker<Void, Void> implements StateMachine {
+       
+
+       public static final String DONE_ASYNC = "DONE_ASYNC";
+       public static final String CANCELED_ASYNC = "CANCELED_ASYNC";
+
+       protected int progressAsync;
+       
+       /**
+        * Override to provide initial tasks.
+        */
+       abstract public void initAsync();
+       
+       /**
+        * Given the last progress, do some portion of the task that the SwingWorker would do in the background, and return the new progress.
+        * returning max or above will complete the task.
+        * 
+        * @param progress
+        * @return new progress
+        */
+       abstract public int doInBackgroundAsync(int progress);
+       
+       /**
+        * Do something when the task is finished or canceled.
+        * 
+        */
+       abstract public void doneAsync();
+
+
+       protected ProgressMonitor progressMonitor;
+       protected int delayMillis;
+       protected String note;
+       protected int min;
+       protected int max;
+       protected int progressPercent;
+
+       protected boolean isAsync;
+       private Exception exception;
+       
+       /**
+        * Construct an asynchronous SwingWorker task that optionally will display a
+        * ProgressMonitor. Progress also can be monitored by adding a PropertyChangeListener
+        * to the AsyncSwingWorker and looking for the "progress" event, just the same as for a 
+        * standard SwingWorker.
+        * 
+        * @param owner optional owner for the ProgressMonitor, typically a JFrame or JDialog.
+        * 
+        * @param title A non-null title indicates we want to use a ProgressMonitor with that title line.
+        * 
+        * @param delayMillis A positive number indicating the delay we want before executions, during which progress will be reported. 
+        * 
+        * @param min  The first progress value. No range limit.
+        * 
+        * @param max  The last progress value. No range limit; may be greater than min.
+        * 
+        */
+       public AsyncSwingWorker(Component owner, String title, int delayMillis, int min, int max) {
+               if (title != null && delayMillis > 0) {
+                       progressMonitor = new ProgressMonitor(owner, title, "", Math.min(min,  max), Math.max(min, max));
+                       progressMonitor.setProgress(Math.min(min,  max)); // displays monitor
+               }
+               this.delayMillis = Math.max(0, delayMillis);
+               this.isAsync = (delayMillis > 0);
+               
+               this.min = min;
+               this.max = max;
+       }
+
+       public void executeAsync() {
+               super.execute();
+       }
+       
+       public void executeSynchronously() {
+               isAsync = false;
+               delayMillis = 0;
+               try {
+                       doInBackground();
+               } catch (Exception e) {
+                       exception = e;
+                       e.printStackTrace();
+                       cancelAsync();
+               }
+       }
+
+       public Exception getException() {
+               return exception;
+       }
+
+       public int getMinimum() {
+               return min;
+       }
+
+       public void setMinimum(int min) {
+               this.min = min;
+               if (progressMonitor != null)
+                       progressMonitor.setMinimum(min);
+       }
+
+       public int getMaximum() {
+               return max;
+       }
+
+       public void setMaximum(int max) {
+               if (progressMonitor != null)
+                       progressMonitor.setMaximum(max);
+               this.max = max;
+       }
+
+
+       public int getProgressPercent() {
+               return progressPercent;
+       }
+
+       public void setNote(String note) {
+               this.note = note;
+               if (progressMonitor != null)
+                       progressMonitor.setNote(note);
+       }
+
+
+       
+       /**
+        * Cancel the asynchronous process.
+        * 
+        */
+       public void cancelAsync() {
+               helper.interrupt();
+       }
+
+       /**
+        * Check to see if the asynchronous process has been canceled. 
+        *
+        * @return true if StateHelper is not alive anymore
+        * 
+        */
+       public boolean isCanceledAsync() {
+               return !helper.isAlive();
+       }
+       
+       /**
+        * Check to see if the asynchronous process is completely done.
+        * 
+        * @return true only if the StateMachine is at STATE_DONE
+        * 
+        */
+       public boolean isDoneAsync() {
+               return helper.getState() == STATE_DONE;
+       }
+
+       /**
+        * Override to set a more informed note for the ProcessMonitor.
+        * 
+        * @param progress
+        * @return
+        */
+       public String getNote(int progress) {
+               return String.format("Completed %d%%.\n", progress);
+       }
+       
+       /**
+        * Retrieve the last note delivered by the ProcessMonitor.
+        * 
+        * @return
+        */
+       public String getNote() {
+               return note;
+       }
+
+       public int getProgressAsync() {
+               return progressAsync;
+       }
+
+       /**
+        * Set the [min,max] progress safely.
+        * 
+        * SwingWorker only allows progress between 0 and 100. 
+        * This method safely translates [min,max] to [0,100].
+        * 
+        * @param n
+        */
+       public void setProgressAsync(int n) {
+               n = (max > min ? Math.max(min, Math.min(n, max))
+                               : Math.max(max, Math.min(n, min)));
+               progressAsync = n;
+               n = (int) ((n - min) * 100 / (max - min));
+               n = (n < 0 ? 0 : n > 100 ? 100 : n);
+               progressPercent = n;
+       }
+       
+       
+       ///// the StateMachine /////
+       
+       
+       private final static int STATE_INIT = 0;
+       private final static int STATE_LOOP = 1;
+       private final static int STATE_WAIT = 2;
+       private final static int STATE_DONE = 99;
+
+       private StateHelper helper;
+       
+       /**
+        * The StateMachine's main loop.
+        * 
+        * Note that a return from this method will exit doInBackground, trigger the
+        * isDone() state on the underying worker, and scheduling its done() for
+        * execution on the AWTEventQueue.
+        *
+        * Since this happens essentially immediately, it is unlikely that
+        * SwingWorker.isCancelled() will ever be true. Thus, the SwingWorker task
+        * itself won't be cancelable in Java or in JavaScript, since its
+        * doInBackground() method is officially complete, and isDone() is true well
+        * before we are "really" done. FutureTask will not set isCancelled() true once
+        * the task has run.
+        * 
+        * We are using an asynchronous task specifically because we want to have the
+        * opportunity for the ProgressMonitor to report in JavaScript. We will have to
+        * cancel our task and report progress explicitly using our own methods.
+        * 
+        */
+       @Override
+       public boolean stateLoop() {
+               while (helper.isAlive()) {
+                       switch (helper.getState()) {
+                       case STATE_INIT:
+                               setProgressAsync(min);
+                               initAsync();
+                               helper.setState(STATE_WAIT);
+                               continue;
+                       case STATE_LOOP:
+                               if (checkCanceled()) {
+                                       helper.setState(STATE_DONE);
+                                       firePropertyChange("state", null, CANCELED_ASYNC);
+                                       continue;
+                               } else {
+                                       progressAsync = doInBackgroundAsync(progressAsync);
+                                       setProgressAsync(progressAsync);
+                                       setNote(getNote(progressAsync));
+                                       setProgress(progressPercent);
+                                       if (progressMonitor != null)
+                                               progressMonitor.setProgress(max > min ? progressAsync : max + min - progressAsync);
+                                       helper.setState(progressAsync == max ? STATE_DONE : STATE_WAIT);
+                                       continue;
+                               }
+                       case STATE_WAIT:
+                               helper.setState(STATE_LOOP);
+                               helper.sleep(delayMillis);
+                               return true;
+                       default:
+                       case STATE_DONE:
+                               if (progressMonitor != null)
+                                       progressMonitor.close();
+                               // Put the doneAsync() method on the AWTEventQueue
+                               // just as for SwingWorker.done().
+                               if (isAsync)
+                               {
+                                       SwingUtilities.invokeLater(doneRunnable);
+                               } else {
+                                       doneRunnable.run();
+                               }
+
+                               return false;
+                       }
+               }
+               return false;
+       }
+       
+       private Runnable doneRunnable = new Runnable() {
+               @Override
+               public void run() {
+                       doneAsync();
+                       firePropertyChange("state", null, DONE_ASYNC);
+               }
+
+       };
+
+
+    private boolean checkCanceled() {
+       if (isMonitorCanceled() || isCancelled()) {
+               helper.interrupt();
+               return true;
+       }
+               return false;
+       }
+
+       //// final SwingWorker methods not to be used by subclasses ////
+
+       private boolean isMonitorCanceled() {
+               return (progressMonitor != null && progressMonitor.isCanceled());
+       }
+
+       /**
+        * see SwingWorker, made final here.
+        * 
+        */
+       @Override
+       final protected Void doInBackground() throws Exception {
+               helper = new StateHelper(this);
+               setProgressAsync(min);
+               helper.next(STATE_INIT);
+               return null;
+       }
+
+       /**
+        * see SwingWorker, made final here. Nothing to do.
+        * 
+        */
+       @Override
+       final public void done() {
+       }
+
+}
diff --git a/src/javajs/async/SwingJSUtils.java b/src/javajs/async/SwingJSUtils.java
new file mode 100644 (file)
index 0000000..82a0265
--- /dev/null
@@ -0,0 +1,651 @@
+package javajs.async;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Image;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.stream.Collectors;
+
+import javax.imageio.ImageIO;
+import javax.swing.Timer;
+
+/**
+ * A set of generally useful SwingJS-related methods. Includes:
+ * 
+ * alternatives to using getCodeBase() for loading resources, due to issues in
+ * Eclipse setting that incorrectly (but no problem in JavaScript)
+ * 
+ * 
+ * 
+ * @author hansonr
+ *
+ */
+public class SwingJSUtils {
+       /**
+        * Set the dimension for the applet prior to j2sApplet's call to 
+        * run the applet. Must be used to create a static field:
+        * 
+        * <code>
+        *   private static Dimension dim = 
+        * </code>
+        * 
+        * 
+        * Then, if it is desired also to have Java also set this, add
+        * 
+        *  if (dim != null) setSize(dim);  
+        *  
+        *  to the applet's init() method.
+        * 
+        * @param w
+        * @param h
+        * @return the Dimension
+        * 
+        * @author hansonr
+        */
+       public static Dimension setDim(int w, int h) {
+               String baseURI = (/** @j2sNative document.body.baseURI || */
+               null);
+               boolean isTest = (baseURI == null || baseURI.indexOf("_applet.html") >= 0);
+               if (!isTest)
+                       return null;
+               /**
+                * @j2sNative
+                * 
+                *                      J2S.thisApplet.__Info.width = w; J2S.thisApplet.__Info.height = h;
+                */
+               return new Dimension(w, h);
+       }
+
+       /**
+        * Reliably load a resource of a specific type from the code directory
+        * 
+        * adaptable - here we are returning an image or a string
+        * 
+        * @param cl       the classname of the object to return (Image.class,
+        *                 String.class) null for InputStream
+        * @param filename
+        * @return
+        * 
+        * @author hansonr
+        */
+       public static Object getResource(Class<?> baseClass, String filename, Class<?> cl) {
+               System.out.println("mpUtils.SwingJSUtils.getResource " + baseClass.getCanonicalName() + " " + filename);
+               InputStream is = baseClass.getResourceAsStream(filename);
+               if (cl == Image.class) {
+                       try {
+                               return ImageIO.read(is);
+                       } catch (IOException e) {
+                               e.printStackTrace();
+                       }
+               } else if (cl == String.class) {
+                       return new BufferedReader(new InputStreamReader(is)).lines().collect(Collectors.joining("\n"));
+               }
+               return is;
+       }
+
+       /**
+        * Pre-fetch images during the static entry of the class. This should provide
+        * plenty of clock ticks, since the file transfer is synchronous, and all we are
+        * waiting for is the DOM image object to initialize.
+        * 
+        * @param cl
+        * @param images
+        * @param root
+        * @param nImages
+        * @param ext
+        */
+       public static void loadImagesStatic(Class<?> cl, Image[] images, String root, String ext, int nImages) {
+               for (int i = nImages; --i >= 0;) {
+
+                       // Bild laden und beim MediaTracker registrieren
+                       // MediaTracker ladekontrolle = new MediaTracker(this);
+
+                       // BH SwingJS -- adding generally useful method for loading data
+                       // avoiding the use of getCodeBase(), which for some reason does not work in
+                       // Eclipse.
+
+                       images[i] = (Image) getResource(cl, root + i + "." + ext, Image.class);
+//                     /**
+//                      * @j2sNative
+//                      * $("body").append(images[i]._imgNode);
+//                      * 
+//                      */
+//                       ladekontrolle.addImage(scharf[i],i);
+                       // Warten , bis Bild ganz geladen ist
+
+//                       try {ladekontrolle.waitForID(i);}
+//                       catch (InterruptedException e)
+//                          {}
+               }
+       }
+
+       /**
+        * Fill an array with images based on a String[] listing
+        * @param cl reference class
+        * @param root  optional root path, ending in "/"
+        * @param names source file names
+        * @param images  array to fill
+        */
+       public static void loadImagesStatic(Class<?> cl, String root, String[] names, Image[] images) {
+               for (int i = names.length; --i >= 0;) {
+                       images[i] = (Image) getResource(cl, root + names[i], Image.class);
+               }
+       }
+
+       /**
+        * Eclipse-friendly image getting
+        * 
+        * @param c
+        * @param fileName
+        * @return
+        */
+       public static Image getImage(Component c, String fileName) {
+               return getImage(c.getClass(), fileName);
+       }
+
+       /**
+        * Eclipse-friendly image getting
+        * 
+        * @param c
+        * @param fileName
+        * @return
+        */
+       public static Image getImage(Class<?> c, String fileName) {
+               return (Image) getResource(c, fileName, Image.class);
+       }
+
+       /**
+        * Clear the component graphic. BH added this for JavaScript because changing
+        * the browser zoom can change the size of the canvas for unknown reasons.
+        * 
+        * @param c
+        */
+       public static void clearComponent(Component c) {
+               Graphics gc = c.getGraphics();
+               gc.clearRect(0, 0, c.getWidth(), c.getHeight());
+               gc.dispose();
+       }
+
+       
+       /**
+        * A simple interface to the machine loop, generally of the form 
+        * <code>
+        *   public boolean stateLoop() {
+        *   while (stateHepler.isAlive()) {
+        *     switch (stateHelper.getState()) {
+        *     case STATE_XXX:
+        *        ...
+        *        return stateHelper.delayState(100,STATE_YYY);
+        *     case STATE_YYY:
+        *        ...
+        *        stateHelper.setState(STATE_ZZZ);
+        *        continue;
+        *     case STATE_ZZZ:
+        *        ...
+        *        return stateHelper.delayAction(100, MY_ID, "myCommand", myListener, STATE_XXX);        *   
+        *     case STATE_DONE:
+        *        ...
+        *        stateHelper.interrupt();
+        *        return false;
+        *     }
+        *     return true;
+        *   }
+        *   return false;
+        *   }
+        * </code>
+        * @author hansonr
+        *
+        */
+       public interface StateMachine {
+
+               public boolean stateLoop();
+
+       }
+       /**
+        * StateHelper is a class that facilitates moving from an asychronous multithreaded model to a state-oriented model of programming
+        * for SwingJS animations and other asynchronous business.
+        *  
+        * @author hansonr
+        *
+        */
+       public static class StateHelper {
+               
+               public static final int UNCHANGED = Integer.MIN_VALUE;
+
+               private StateMachine machine;
+               private int state;
+               private int level;
+               
+               private boolean interrupted;
+               
+
+               public StateHelper(StateMachine machine) {
+                       this.machine = machine;
+               }
+
+               public void interrupt() {
+                       interrupted = true;
+               }
+               
+               public boolean isInterrupted() {
+                       return interrupted;
+               }
+               
+               public boolean isAlive() {
+                       return !interrupted;
+               }
+               
+               public void restart() {
+                       interrupted = false;
+               }
+               
+               public void setState(int state) {
+                       this.state = this.stateNext = state;
+               }
+
+               public int getState() {
+                       return state;
+               }
+
+               public void setLevel(int level) {
+                       this.level = this.levelNext = level;
+               }
+
+               public int getLevel() {
+                       return level;
+               }
+               
+               public void setNextState(int next) {
+                       stateNext = next; 
+               }
+               
+               public int getNextState() {
+                       return stateNext;
+               }
+
+               public int getNextLevel() {
+                       return levelNext;
+               }
+
+               public void setNextLevel(int next) {
+                       levelNext = next; 
+               }
+
+               /** 
+                * 
+                * NOTE: this method must remain private; it is accessed via p$1
+                * 
+                * @return
+                */
+               private boolean nextState() {
+                       return next(stateNext, levelNext);
+               }
+               /**
+                * Set the state and run machine.stateLoop().
+                * 
+                * @param state something meaningful to the machine
+                * 
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               public boolean next(int state) {
+                       return next(state, 0);
+               }
+               
+               /**
+                * Set the state and level, and then run machine.stateLoop(). Driven directly or via delayedState or delayedAction
+                * 
+                * @param state something meaningful to the machine
+                * @param level something meaningful to the machine
+                * 
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               public boolean next(int state, int level) {
+                       return nextStatePriv(this, state, level);
+               }
+
+               private static boolean nextStatePriv(Object oThis, int state, int level) {
+                       StateHelper me = (StateHelper) oThis;
+                       if (me.interrupted)
+                               return false;
+                       if (level != UNCHANGED)
+                               me.level = level;
+                       if (state != UNCHANGED)
+                               me.state = state;
+                       return me.machine.stateLoop();
+               }
+
+               /**
+                * After the given number of milliseseconds, set the new state and run the machines stateLoop with unchanged level
+                * 
+                * @param ms the number of milliseconds to delay; 0 to execute synchronously             * 
+                * @param stateNext  the next state to run
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               public boolean delayedState(int ms, int stateNext) {
+                       return delayedState(ms, stateNext, level);
+               }
+
+               private Timer stateTimer;
+
+               private int stateNext;
+               private int levelNext;
+               
+               /**
+                * After the given number of milliseseconds, set the new state and level, and
+                * run the machines stateLoop
+                * 
+                * @param ms        the number of milliseconds to delay; 0 to execute
+                *                  synchronously *
+                * @param stateNext the next state
+                * @param levelNext the next level
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               
+               public boolean delayedState(int ms, int stateNext, int levelNext) {
+                       if (interrupted)
+                               return false;
+                       if (ms == 0)
+                               return next(stateNext, levelNext);
+                       if (stateNext != UNCHANGED)
+                               this.stateNext = stateNext;
+                       if (levelNext != UNCHANGED)
+                               this.levelNext = levelNext;
+                       
+                       /**
+                        * @j2sNative
+                        * var me = this;
+                        * setTimeout(function(){
+                        *  p$1.nextState.apply(me, []);
+                        * },ms);
+                        */
+                       {
+                               // Java only
+
+                               if (stateTimer == null) {
+                                       stateTimer = new Timer(ms, new ActionListener() {
+                                               @Override
+                                               public void actionPerformed(ActionEvent e) {
+                                                       nextState();
+                                               }
+
+                                       });
+                                       stateTimer.setRepeats(false);
+                                       stateTimer.start();
+                               } else {
+                                       stateTimer.restart();
+                               }
+                       }
+                       return true;
+               }
+
+               /**
+                * Fire an actionPerformed event after a given number of milliseconds
+                * 
+                * @param ms       delay milliseconds. if 0, then this action will be called
+                *                 synchronously
+                * @param id       id for this event, possibly ACTION_PERFORMED (1001), but not
+                *                 necessarily
+                * @param command  key for ActionEvent.getCommand()
+                * @param listener ActionListener to be called.
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               public boolean delayedAction(int ms, int id, String command, ActionListener listener) {
+                       return delayedAction(ms, id, command, listener, UNCHANGED, UNCHANGED);
+               }
+
+               /**
+                * Fire an actionPerformed event after a given number of milliseconds
+                * 
+                * @param ms       delay milliseconds. if 0, then this action will be called
+                *                 synchronously
+                * @param id       id for this event, possibly ACTION_PERFORMED (1001), but not
+                *                 necessarily
+                * @param command  key for ActionEvent.getCommand()
+                * @param listener ActionListener to be called.
+                * 
+                * @param state    the next state to go to after this listener is called; UNCHANGED to let the listener take care of this.
+                * 
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               public boolean delayedAction(int ms, int id, String command, ActionListener listener, int state) {
+                       return delayedAction(ms, id, command, listener, state, UNCHANGED);
+               }               
+               
+               /**
+                * Fire an actionPerformed event after a given number of milliseconds. Setting BOTH stateNext and levelNext to UNCHANGED (Integer.MIN_VALUE)
+                * allows the listener to handle continuing the loop.
+                * 
+                * @param ms       delay milliseconds. if 0, then this action will be called
+                *                 synchronously
+                * @param id       id for this event, possibly ACTION_PERFORMED (1001), but not
+                *                 necessarily
+                * @param command  key for ActionEvent.getCommand()
+                * @param listener ActionListener to be called.
+                * @param stateNext  state to run after the event is processed by the listener, or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle this.
+                * @param levelNext  level to run after the event is processed by the listener, or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle this.
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               public boolean delayedAction(int ms, int id, String command, ActionListener listener, int stateNext, int levelNext) {
+                       if (interrupted)
+                               return false; 
+                       ActionEvent event = new ActionEvent(this, id, command);
+                       if (ms == 0) {
+                               listener.actionPerformed(event);
+                               return (stateNext == UNCHANGED && levelNext == UNCHANGED || nextStatePriv(this, stateNext == UNCHANGED ? state : stateNext, levelNext == UNCHANGED ? level : levelNext));
+                       }
+                       
+                       StateHelper me = this;
+                       
+                       Timer timer = new Timer(ms, id == ActionEvent.ACTION_PERFORMED ? listener : new ActionListener() {
+                               @Override
+                               public void actionPerformed(ActionEvent e) {
+                                       if (!interrupted)
+                                               listener.actionPerformed(event);
+                                       if (!interrupted && (stateNext != UNCHANGED || levelNext != UNCHANGED))
+                                               nextStatePriv(me, stateNext == UNCHANGED ? state : stateNext, levelNext == UNCHANGED ? level : levelNext);
+                               }
+                               
+                       });
+                       timer.setRepeats(false);
+                       timer.start();
+                       return true;
+               }
+
+               public static void delayedRun(int ms, Runnable runnable) {
+                       new StateHelper(null).delayedRun(ms, runnable, UNCHANGED, UNCHANGED);
+               }
+
+               
+               /**
+                * Fire an actionPerformed event after a given number of milliseconds. Setting
+                * BOTH stateNext and levelNext to UNCHANGED (Integer.MIN_VALUE) allows the
+                * listener to handle continuing the loop.
+                * 
+                * @param ms        delay milliseconds. if 0, then this action will be called
+                *                  synchronously
+                * @param id        id for this event, possibly ACTION_PERFORMED (1001), but not
+                *                  necessarily
+                * @param command   key for ActionEvent.getCommand()
+                * @param listener  ActionListener to be called.
+                * @param stateNext state to run after the event is processed by the listener,
+                *                  or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle
+                *                  this.
+                * @param levelNext level to run after the event is processed by the listener,
+                *                  or UNCHANGED (Integer.MIN_VALUE) to allow listener to handle
+                *                  this.
+                * @return not interrupted
+                * 
+                * @author Bob Hanson hansonr@stolaf.edu
+                */
+               public boolean delayedRun(int ms, Runnable runnable, int stateNext, int levelNext) {
+                       if (interrupted)
+                               return false;
+                       if (ms == 0) {
+                               return (stateNext == UNCHANGED && levelNext == UNCHANGED || nextStateIfUnchanged(this, runnable,
+                                               stateNext == UNCHANGED ? state : stateNext, levelNext == UNCHANGED ? level : levelNext));
+                       }
+                       StateHelper me = this;
+                       /**
+                        * @j2sNative
+                        * 
+                        * setTimeout(function() {
+                        * 
+                        *    me.nextStateIfUnchanged$O$O$I$I.apply(me, [me, runnable, stateNext, levelNext]);
+                        * 
+                        * },ms);
+                        */
+                       {
+                               Timer timer = new Timer(ms, new ActionListener() {
+                                       @Override
+                                       public void actionPerformed(ActionEvent e) {
+                                               nextStateIfUnchanged(me, runnable, stateNext, levelNext);
+                                       }
+
+                               });
+                               timer.setRepeats(false);
+                               timer.start();
+                       }
+                       return true;
+               }
+
+               protected boolean nextStateIfUnchanged(Object oThis, Object runnable, int stateNext, int levelNext) {
+                       StateHelper me = (StateHelper)(oThis);
+                       if (!me.interrupted)
+                               ((Runnable) runnable).run();
+                       if (!me.interrupted && (stateNext != UNCHANGED || levelNext != UNCHANGED))
+                               nextStatePriv(oThis, stateNext == UNCHANGED ? me.state : stateNext,
+                                               levelNext == UNCHANGED ? me.level : levelNext);
+                       return true;
+               }
+
+               /**
+                * sleep and then execute the next state
+                * @param ms
+                */
+               public void sleep(int ms) {
+                       int next = stateNext;
+                       delayedState(ms, next);
+               }
+       }
+       
+       /**
+        * open a "url-like" input stream
+        * @param base
+        * @param fileName
+        * @return
+        */
+       public static BufferedInputStream openStream(Class<?> base, String fileName) {
+               String s = (String) getResource(base, fileName, String.class);
+        return new BufferedInputStream(new ByteArrayInputStream(s.getBytes()));
+       }
+
+
+       public static class Performance {
+
+               public final static int TIME_RESET = 0;
+
+               public final static int TIME_MARK = 1;
+
+               public static final int TIME_SET = 2;
+
+               public static final int TIME_GET = 3;
+
+               public static long time, mark, set, duration;
+
+               /**
+                * typical usage:
+                * 
+                * Performance.timeCheck(null, Platform.TIME_MARK);
+                * 
+                * ...
+                * 
+                * Performance.timeCheck("some message", Platform.TIME_MARK);
+                * 
+                * reset...[set/mark]n...get  (total time) (time spent between set and mark)
+                * 
+                * set...get   (total time) (time spent between set and get)
+                * 
+                * long t0 = now(0); ........ ; dt = now(t0); (time since t0)e
+                * 
+                * @param msg
+                * @param mode
+                */
+               public static void timeCheck(String msg, int mode) {
+                       msg = timeCheckStr(msg, mode);
+                       if (msg != null)
+                               System.err.println(msg);
+               }
+
+               public static long now(long t) {
+                       return System.currentTimeMillis() - t;
+               }
+               
+               public static String timeCheckStr(String msg, int mode) {
+                       long t = System.currentTimeMillis();
+                       switch (mode) {
+                       case TIME_RESET:
+                               time = mark = t;
+                               duration = set = 0;
+                               if (msg != null) {
+                                       return ("Platform: timer reset\t\t\t" + msg);
+                               }
+                               break;
+                       case TIME_SET:
+                               if (time == 0)
+                                       time = t;
+                               set = t;
+                               break;
+                       case TIME_MARK:
+                               if (set > 0) {
+                                       // total time between set/mark points
+                                       duration += (t - set);
+                               } else {
+                                       if (time == 0) {
+                                               time = mark = t;
+                                       }
+                                       if (msg != null) {
+                                               long m0 = mark;
+                                               mark = t;
+                                               return ("Platform: timer mark\t" + ((t - time) / 1000f) + "\t" + ((t - m0) / 1000f) + "\t"
+                                                               + msg);
+                                       }
+                                       mark = t;
+                               }
+                               break;
+                       case TIME_GET:
+                               if (msg != null) {
+                                       if (mark < set)
+                                               duration = t - set;
+                                       return ("Platform: timer get\t" + ((t - time) / 1000f) + "\t" + ((duration) / 1000f) + "\t" + msg);
+                               }
+                               set = 0;
+                               break;
+                       }
+                       return null;
+               }
+
+       }
+
+}
\ No newline at end of file
diff --git a/src/swingjs/api/Interface.java b/src/swingjs/api/Interface.java
new file mode 100644 (file)
index 0000000..6616780
--- /dev/null
@@ -0,0 +1,78 @@
+/* $RCSfile$
+ * $Author$
+ * $Date$
+ * $Revision$
+ *
+ * Some portions of this file have been modified by Robert Hanson hansonr.at.stolaf.edu 2012-2017
+ * for use in SwingJS via transpilation into JavaScript using Java2Script.
+ *
+ * Copyright (C) 2006  The Jmol Development Team
+ *
+ * Contact: jmol-developers@lists.sf.net
+ *
+ *  This library is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public
+ *  License as published by the Free Software Foundation; either
+ *  version 2.1 of the License, or (at your option) any later version.
+ *
+ *  This library is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this library; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ *  02110-1301, USA.
+ */
+
+package swingjs.api;
+
+public class Interface {
+       
+       private static String instances=""; 
+
+       public static Object getInstanceWithParams(String name, Class<?>[] classes, Object... params) {
+               try {
+                       Class<?> cl = Class.forName(name);
+                       return  cl.getConstructor(classes).newInstance(params);
+               } catch (Exception e) {
+                       return null;
+               }
+       }
+  public static Object getInstance(String name, boolean isQuiet) {
+       Object x = null;
+       /**
+        * @j2sNative
+        * 
+        * Clazz._isQuietLoad = isQuiet;
+        */
+       {}
+    try {
+       if (!isQuiet && instances.indexOf(name + ";") <= 0) {
+               System.out.println("swingjs.api.Interface creating instance of " + name);
+               instances += name + ";";
+       }
+       Class<?> y = Class.forName(name); 
+      if (y != null)
+       x = y.newInstance();
+    } catch (Throwable e) {
+      System.out.println("Swingjs.api.Interface Error creating instance for " + name + ": \n" + e);
+      /**
+       * @j2sNative
+       * 
+       * if (e.stack)System.out.println(e.stack);
+       */
+      {}
+    } finally {
+       /**
+        * @j2sNative
+        * 
+        * Clazz._isQuietLoad = false;
+        */
+       {}      
+    }
+    return x;          
+  }
+
+}
diff --git a/src/swingjs/api/JSFileHandler.java b/src/swingjs/api/JSFileHandler.java
new file mode 100644 (file)
index 0000000..fde82e8
--- /dev/null
@@ -0,0 +1,7 @@
+package swingjs.api;
+
+public interface JSFileHandler {
+
+       void handleFileLoaded(Object data, String fileName);
+
+}
diff --git a/src/swingjs/api/JSUtilI.java b/src/swingjs/api/JSUtilI.java
new file mode 100644 (file)
index 0000000..1b6ff9b
--- /dev/null
@@ -0,0 +1,332 @@
+package swingjs.api;
+
+import java.awt.Component;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Properties;
+import java.util.function.Function;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import javax.swing.JComponent;
+
+import swingjs.api.js.HTML5Applet;
+
+public interface JSUtilI {
+
+       /**
+        * The HTML5 canvas delivers [r g b a r g b a ...] which is not a Java option.
+        * The closest Java option is TYPE_4BYTE_ABGR, but that is not quite what we
+        * need. SwingJS decodes TYPE_4BYTE_HTML5 as TYPE_4BYTE_RGBA"
+        * 
+        * ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB);
+        * 
+        * int[] nBits = { 8, 8, 8, 8 };
+        * 
+        * int[] bOffs = { 0, 1, 2, 3 };
+        * 
+        * colorModel = new ComponentColorModel(cs, nBits, true, false,
+        * Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
+        * 
+        * raster = Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, width, height,
+        * width * 4, 4, bOffs, null);
+        * 
+        * Note, however, that this buffer type should only be used for direct buffer access
+        * using
+        * 
+        * 
+        * 
+        */
+       public static final int TYPE_4BYTE_HTML5 = -6;
+       
+       /**
+        * The HTML5 VIDEO element wrapped in a BufferedImage. 
+        * 
+        * To be extended to allow video capture?
+        */
+       public static final int TYPE_HTML5_VIDEO = Integer.MIN_VALUE;
+
+       /**
+        * Indicate to SwingJS that the given file type is binary.
+        * 
+        * @param ext
+        */
+       void addBinaryFileType(String ext);
+
+       /**
+        * Indicate to SwingJS that we can load files using AJAX from the given domain,
+        * such as "www.stolaf.edu", because we know that CORS access has been provided.
+        * 
+        * @param domain
+        */
+       void addDirectDatabaseCall(String domain);
+
+       /**
+        * Cache or uncache data under the given path name.
+        * 
+        * @param path
+        * @param data null to remove from the cache
+        */
+       void cachePathData(String path, Object data);
+
+       /**
+        * Get the HTML5 object corresponding to the specified Component, or the current thread if null.
+        * 
+        * @param c  the associated component, or null for the current thread
+        * @return HTML5 applet object
+        */
+       HTML5Applet getAppletForComponent(Component c);
+
+       /**
+        * Get an attribute applet.foo for the applet found using getApplet(null).
+        * 
+        * @param key
+        * @return
+        */
+       Object getAppletAttribute(String key);
+
+
+       /**
+        * Get an attribute of applet's Info map for the applet found using
+        * getApplet(null). That is, applet.__Info[InfoKey].
+        * 
+        * @param infoKey
+        */
+       Object getAppletInfo(String infoKey);
+
+       /**
+        * Get the code base (swingjs/j2s, probably) for the applet found using
+        * getApplet(null).
+        * 
+        * @return
+        */
+       URL getCodeBase();
+
+       /**
+        * Get the document base (wherever the page is) for the applet found using
+        * getApplet(null).
+        * 
+        * @return
+        */
+
+       URL getDocumentBase();
+
+       /**
+        * Get an attribute from the div on the page that is associated with this frame,
+        * i.e. with id frame.getName() + "-div".
+        * 
+        * @param frame
+        * @param type  "node" or "dim"
+        * @return
+        */
+       Object getEmbeddedAttribute(Component frame, String type);
+
+       /**
+        * Get a file synchronously.
+        * 
+        * @param path
+        * @param asString true for String; false for byte[]
+        * @return byte[] or String
+        */
+       Object getFile(String path, boolean asString);
+
+       /**
+        * Get the 秘bytes field associated with a file, but only if the File object itself has
+        * them attached, not downloading them.
+        * 
+        * @param f
+        * @return
+        */
+       byte[] getBytes(File f);
+
+       /**
+        * Retrieve a HashMap consisting of whatever the application wants, but
+        * guaranteed to be unique to this app context, that is, for the applet found using
+        * getApplet(null).
+        * 
+        * @param contextKey
+        * @return
+        */
+       HashMap<?, ?> getJSContext(Object contextKey);
+
+       /**
+        * Load a resource -- probably a core file -- if and only if a particular class
+        * has not been instantialized. We use a String here because if we used a .class
+        * object, that reference itself would simply load the class, and we want the
+        * core package to include that as well.
+        * 
+        * @param resourcePath
+        * @param className
+        */
+       void loadResourceIfClassUnknown(String resource, String className);
+
+       /**
+        * Read all applet.__Info properties  for the applet found using
+        * getApplet(null) that start with the given prefix, such as "jalview_".
+        * A null prefix retrieves all properties. Note that non-string properties will be
+        * stringified.
+        * 
+        * @param prefix an application prefix, or null for all properties
+        * @param p      properties to be appended to
+        */
+       void readInfoProperties(String prefix, Properties p);
+
+       /**
+        * Set an attribute for the applet found using
+        * getApplet(null). That is, applet[key] = val.
+        * 
+        * @param key
+        * @param val
+        */
+       void setAppletAttribute(String key, Object val);
+
+       /**
+        * Set an attribute of applet's Info map for the applet found using
+        * getApplet(null). That is, applet.__Info[key] = val.
+        * 
+        * @param infoKey
+        * @param val
+        */
+       void setAppletInfo(String infoKey, Object val);
+
+       /**
+        * Set the given File object's 秘bytes field from an InputStream or a byte[] array.
+        * If the file is a JSTempFile, then also cache those bytes.
+        * 
+        * @param f
+        * @param isOrBytes BufferedInputStream, ByteArrayInputStream, FileInputStream, or byte[]
+        * @return
+        */
+       boolean setFileBytes(File f, Object isOrBytes);
+
+       /**
+        * Set the given URL object's _streamData field from an InputStream or a byte[] array.
+        * 
+        * @param f
+        * @param isOrBytes BufferedInputStream, ByteArrayInputStream, FileInputStream, or byte[]
+        * @return
+        */
+       boolean setURLBytes(URL url, Object isOrBytes);
+
+       /**
+        * Same as setFileBytes.
+        * 
+        * @param is
+        * @param outFile
+        * @return
+        */
+       boolean streamToFile(InputStream is, File outFile);
+
+         /**
+          * Switch the flag in SwingJS to use or not use the JavaScript Map object in
+          * Hashtable, HashMap, and HashSet. Default is enabled.
+          *       * 
+          */
+       void setJavaScriptMapObjectEnabled(boolean enabled);
+
+
+       /**
+        * Open a URL in a browser tab.
+        * 
+        * @param url
+        * @param target null or specific tab, such as "_blank"
+        */
+       void displayURL(String url, String target);
+
+       /**
+        * Retrieve cached bytes for a path (with unnormalized name)
+        * from J2S._javaFileCache.
+        * 
+        * @param path
+        * 
+        * @return byte[] or null
+        */
+       byte[] getCachedBytes(String path);
+       
+       /**
+        * Attach cached bytes to a file-like object, including URL,
+        * or anything having a 秘bytes field (File, URI, Path)
+        * from J2S._javaFileCache. That is, allow two such objects
+        * to share the same underlying byte[ ] array.
+        * 
+        * 
+        * @param URLorURIorFile
+        * @return byte[] or null
+        */
+       byte[] addJSCachedBytes(Object URLorURIorFile);
+
+       /**
+        * Seek an open ZipInputStream to the supplied ZipEntry, if possible.
+        * 
+        * @param zis the ZipInputStream
+        * @param ze  the ZipEntry
+        * @return the length of this entry, or -1 if, for whatever reason, this was not possible
+        */
+       long seekZipEntry(ZipInputStream zis, ZipEntry ze);
+
+       /**
+        * Retrieve the byte array associated with a ZipEntry.
+        * 
+        * @param ze
+        * @return
+        */
+       byte[] getZipBytes(ZipEntry ze);
+
+       /**
+        * Java 9 method to read all (remaining) bytes from an InputStream. In SwingJS,
+        * this may just create a new reference to an underlying Int8Array without
+        * copying it.
+        * 
+        * @param zis
+        * @return
+        * @throws IOException 
+        */
+       byte[] readAllBytes(InputStream zis) throws IOException;
+
+       /**
+        * Java 9 method to transfer all (remaining) bytes from an InputStream to an OutputStream.
+        * 
+        * @param is
+        * @param out
+        * @return
+        * @throws IOException
+        */
+       long transferTo(InputStream is, OutputStream out) throws IOException;
+
+       /**
+        * Retrieve any bytes already attached to this URL.
+        * 
+        * @param url
+        * @return
+        */
+       byte[] getURLBytes(URL url);
+
+       /**
+        * Set a message in the lower-left-hand corner SwingJS status block.
+        * 
+        * @param msg
+        * @param doFadeOut
+        */
+       void showStatus(String msg, boolean doFadeOut);
+
+       /**
+        * Asynchronously retrieve the byte[] for a URL.
+        * 
+        * @param url
+        * @param whenDone
+        */
+       void getURLBytesAsync(URL url, Function<byte[], Void> whenDone);
+
+       /**
+        * Experimental method to completely disable a Swing Component's user interface.
+        * 
+        * @param jc
+        * @param enabled
+        */
+       void setUIEnabled(JComponent jc, boolean enabled);
+
+}
diff --git a/src/swingjs/api/js/DOMNode.java b/src/swingjs/api/js/DOMNode.java
new file mode 100644 (file)
index 0000000..4de6ee0
--- /dev/null
@@ -0,0 +1,309 @@
+package swingjs.api.js;
+
+import java.awt.Dimension;
+import java.awt.Rectangle;
+
+/**
+ * A mix of direct DOM calls on DOM nodes and convenience methods to do that.
+ * 
+ * NOTE: DO NOT OVERLOAD THESE METHODS, as this package will not be qualified.
+ * 
+ * @author hansonr
+ *
+ */
+public interface DOMNode {
+
+       public static JQuery jQuery = /** @j2sNative jQuery.$ || (jQuery.$ = jQuery) || */null;
+
+       // "abstract" in the sense that these are the exact calls to JavaScript
+       
+
+       public void addEventListener(String event, Object listener);
+       public void removeEventListener(String event);
+       public void removeEventListener(String event, Object listener);
+
+
+
+       public String[] getAttributeNames();
+
+       public String getAttribute(String name);
+
+       public void setAttribute(String attr, String val);
+
+       public void appendChild(DOMNode node);
+       
+       public void prepend(DOMNode node);
+       
+       public void insertBefore(DOMNode node, DOMNode refNode);
+       
+       public DOMNode removeChild(DOMNode node);
+
+       public void focus();
+       public boolean hasFocus();
+       public void blur();
+
+       public DOMNode removeAttribute(String attr);
+       
+       public void setSelectionRange(int start, int end, String direction);
+
+       public Rectangle getBoundingClientRect();
+       
+       // static convenience methods
+
+       public static DOMNode createElement(String key, String id) {
+               DOMNode node = null;
+               /**
+                * @j2sNative
+                *                                      node = document.createElement(key);
+                *                                      id && (node.id = id);
+                */
+               return node;
+       }
+
+       public static DOMNode getElement(String id) {
+               return (/**  @j2sNative  document.getElementById(id) ||*/ null);
+       }
+
+       public static DOMNode createTextNode(String text) {
+               return (/** @j2sNative document.createTextNode(text) || */ null); 
+       }
+
+       public static DOMNode getParent(DOMNode node) {
+               return (/**  @j2sNative  node.parentNode ||*/ null);
+       }
+       
+       public static DOMNode getPreviousSibling(DOMNode node) {
+               return (/**  @j2sNative  node.previousSibling ||*/ null);
+       }
+       
+       public static DOMNode firstChild(DOMNode node) {
+               return  (/**  @j2sNative node.firstChild ||*/ null);
+       }
+
+       public static DOMNode lastChild(DOMNode node) {
+               return  (/**  @j2sNative node.lastChild ||*/ null);
+       }
+
+       public static DOMNode setZ(DOMNode node, int z) {
+               return setStyles(node, "z-index", "" + z);
+       }
+
+       public static Object getAttr(Object node, String attr) {
+               /**
+                * @j2sNative
+                * 
+                * if (!node)
+                *   return null;
+                * var a = node[attr];
+                * return (typeof a == "undefined" ? null : a); 
+                */
+               {
+               return null;
+               }
+       }
+
+       public static int getAttrInt(DOMNode node, String attr) {
+               return  (/**  @j2sNative node && node[attr] ||*/ 0);
+       }
+
+       public static String getStyle(DOMNode node, String style) {
+               return  (/**  @j2sNative node && node.style[style] ||*/ null);
+       }
+
+       public static void getCSSRectangle(DOMNode node, Rectangle r) {
+               /**
+                * @j2sNative
+                * 
+                *       r.x = parseInt(node.style.left.split("p")[0]);
+                *       r.y = parseInt(node.style.top.split("p")[0]);
+                *       r.width = parseInt(node.style.width.split("p")[0]);
+                *       r.height = parseInt(node.style.height.split("p")[0]);
+                * 
+                */
+       }
+
+       public static DOMNode setAttr(DOMNode node, String attr, Object val) {
+               /**
+                * @j2sNative
+                * 
+                *                      attr && (node[attr] = (val == "秘TRUE" ? true : val == "秘FALSE" ? false : val));
+                * 
+                */
+               return node;
+       }
+
+
+       public static void setAttrInt(DOMNode node, String attr, int val) {
+               /**
+                * @j2sNative
+                * 
+                *                      node[attr] = val;
+                * 
+                */
+       }
+
+
+       /**
+        * allows for null key to be skipped (used in audio)
+        * 
+        * @param node
+        * @param attr
+        * @return
+        */
+       public static DOMNode setAttrs(DOMNode node, Object... attr) {
+               /**
+                * @j2sNative
+                * 
+                *            for (var i = 0; i < attr.length;) { 
+                *              C$.setAttr(node, attr[i++],attr[i++]);
+                *            }
+                */
+               return node;
+       }
+
+       public static DOMNode setStyles(DOMNode node, String... attr) {
+               /**
+                * @j2sNative
+                * 
+                *            if (node) for (var i = 0; i < attr.length;) {
+                *             node.style[attr[i++]] = attr[i++];
+                *             }
+                * 
+                */
+               return node;
+       }
+
+       public static DOMNode setSize(DOMNode node, int width, int height) {
+               return setStyles(node, "width", width + "px", "height", height + "px");
+       }
+
+       public static DOMNode setPositionAbsolute(DOMNode node) {
+               return DOMNode.setStyles(node, "position", "absolute");
+       }
+
+       public static void setVisible(DOMNode node, boolean visible) {
+               setStyles(node, "display", visible ? "block" : "none");
+       }
+
+       public static DOMNode setTopLeftAbsolute(DOMNode node, int top, int left) {
+               DOMNode.setStyles(node, "top", top + "px");
+               DOMNode.setStyles(node, "left", left + "px");
+               return DOMNode.setStyles(node, "position", "absolute");
+       }
+
+       public static void addHorizontalGap(DOMNode domNode, int gap) {
+               DOMNode label = DOMNode.setStyles(DOMNode.createElement("label", null), 
+                               "letter-spacing", gap + "px", "font-size", "0pt");
+               label.appendChild(DOMNode.createTextNode("."));
+               domNode.appendChild(label);
+       }
+
+       public static void appendChildSafely(DOMNode parent, DOMNode node) {
+               /**
+                * @j2sNative
+                * if (!parent || node.parentElement == parent)
+                *   return;
+                */
+               parent.appendChild(node);
+       }
+       
+       // static jQuery calls
+       
+       /**
+        * jQuery height()
+        * 
+        * @param node
+        * @return height
+        */
+       public static int getHeight(DOMNode node) {
+               return jQuery.$(node).height();
+       }
+
+       /**
+        * jQuery width()
+        * 
+        * @param node
+        * @return width
+        */
+       public static int getWidth(DOMNode node) {
+               return jQuery.$(node).width();
+       }
+
+       /**
+        * jQuery remove()
+        * 
+        * Remove this node and return its parent. Automatically removing all events
+        * attached to it.
+        * 
+        * @param node
+        * @return parent or null
+        */
+       public static void dispose(DOMNode node) {
+               if (node != null)               
+                       jQuery.$(node).remove();
+       }
+
+       /**
+        * Just remove the node, keeping its events and data 
+        * @param node
+        */
+       public static void remove(DOMNode node) {
+               
+               // NOTE: IE does not have node.remove()
+               
+               DOMNode p = getParent(node);
+               if (p != null)
+                       p.removeChild(node);
+       }
+
+       /**
+        * just detaches all the nodes; doesn't remove their listeners
+        * @param node
+        */
+       public static void detachAll(DOMNode node) {
+               /**
+                * @j2sNative
+                *  if(node)
+                *    while(node.lastChild)
+                *      node.removeChild(node.lastChild);
+                */
+       }
+       
+       /**
+        * jQuery detach() + append()
+        * 
+        * @param node
+        * @param container
+        * @return parent if container is null, or container if it is not null
+        */
+       public static DOMNode transferTo(DOMNode node, DOMNode container) {
+               if (node == null)
+                       return null;
+               DOMNode p = getParent(node);
+               try {
+                       if (p != null)
+                               jQuery.$(node).detach();
+               } catch (Throwable e) {
+                       // ignore
+               }
+                if (container == null)
+                       return p; 
+                jQuery.$(container).append(node);
+               return container;
+       }
+
+       public static Object getEmbedded(String name, String type) {
+               DOMNode node = DOMNode.getElement(name + "-div");
+               if (node == null)
+                       return null;
+               switch (type) {
+               case "node":
+                       return node;
+               case "dim":
+                       return new Dimension(DOMNode.getWidth(node), DOMNode.getHeight(node));
+               default:
+                       return DOMNode.getAttr(node, type);
+               }
+       }
+
+}
diff --git a/src/swingjs/api/js/HTML5Applet.java b/src/swingjs/api/js/HTML5Applet.java
new file mode 100644 (file)
index 0000000..b844476
--- /dev/null
@@ -0,0 +1,35 @@
+package swingjs.api.js;
+
+public interface HTML5Applet {
+
+       /**
+        * The canvas that is being used by the HTML5 applet 
+        * 
+        * @return
+        */
+       Object _getHtml5Canvas();
+
+       int _getHeight();
+
+       int _getWidth();
+
+       /**
+        * The div associated with the HTML5 applet 
+        * 
+        * @return
+        */
+       Object _getContentLayer();
+
+       /**
+        * Simple resizing for an inline applet
+        * 
+        * @param widthHeight
+        */
+       void _resizeApplet(int[] widthHeight);
+
+       void _show(boolean b);
+
+       String _getID();
+
+
+}
diff --git a/src/swingjs/api/js/HTML5AudioContext.java b/src/swingjs/api/js/HTML5AudioContext.java
new file mode 100644 (file)
index 0000000..fecc66b
--- /dev/null
@@ -0,0 +1,58 @@
+package swingjs.api.js;
+
+public interface HTML5AudioContext {
+
+       // https://developer.mozilla.org/en-US/docs/Web/API/AudioContext
+       // https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API
+       
+       void close();
+
+       float[] createBuffer(int nChannels, int frameCount, int sampleRate);
+
+       void createBufferSource();
+
+       void createMediaElementSource();
+
+       void createMediaStreamSource();
+
+       void createMediaStreamDestination();
+
+       void createScriptProcessor();
+
+       //void createStereoPanner();
+
+       void createAnalyser();
+
+       void createBiquadFilter();
+
+       void createChannelMerger();
+
+       void createChannelSplitter();
+
+       void createConvolver();
+
+       void createDelay();
+
+       void createDynamicsCompressor();
+
+       void createGain();
+
+       void createIIRFilter();
+
+       void createOscillator();
+
+       void createPanner();
+
+       void createPeriodicWave();
+
+       void createWaveShaper();
+
+       void createAudioWorker();
+
+       void decodeAudioData();
+
+       void resume();
+
+       void suspend();
+
+}
diff --git a/src/swingjs/api/js/HTML5Canvas.java b/src/swingjs/api/js/HTML5Canvas.java
new file mode 100644 (file)
index 0000000..f6d0604
--- /dev/null
@@ -0,0 +1,59 @@
+package swingjs.api.js;
+
+import java.awt.image.BufferedImage;
+
+public interface HTML5Canvas extends DOMNode {
+
+       HTML5CanvasContext2D getContext(String str2d);
+
+       /*
+        * Retrieves the byte[] data buffer from an HTML5 CANVAS element, optionally
+        * first setting its contents to a source IMG, CANVAS, or VIDEO element.
+        * 
+        */
+       static byte[] getDataBufferBytes(HTML5Canvas canvas, DOMNode sourceNode, int w, int h) {
+               if (sourceNode != null) {
+                       DOMNode.setAttrInt(canvas, "width", w);
+                       DOMNode.setAttrInt(canvas, "height", h);
+               }
+               HTML5CanvasContext2D ctx = canvas.getContext("2d");
+               if (sourceNode != null) {
+                       ctx.drawImage(sourceNode, 0, 0, w, h);
+               }
+               // Coerse int[] to byte[]
+               return (byte[]) (Object) ctx.getImageData(0, 0, w, h).data;
+       }
+
+       /**
+        * Install a source image (img, video, or canvas) into a matching BufferedImage 
+        * 
+        * @param sourceNode
+        * @param image
+        */
+       static void setImageNode(DOMNode sourceNode, BufferedImage image) {
+               /**
+                * @j2sNative
+                * 
+                *                      image._setImageNode$O$Z(sourceNode, false);
+                * 
+                */             {
+                       // image._setImageNode(sourceNode, false);
+                }
+       }
+       
+       
+
+       static HTML5Canvas createCanvas(int width, int height, String id) {
+               HTML5Canvas canvas = (HTML5Canvas) DOMNode.createElement("canvas", (id == null ? "img" + Math.random() : id + ""));
+               DOMNode.setStyles(canvas, "width", width + "px", "height", height + "px");
+               /**
+                * @j2sNative
+                * 
+                * canvas.width = width;
+                * canvas.height = height;
+                * 
+                */
+               return canvas;
+       }
+
+}
diff --git a/src/swingjs/api/js/HTML5CanvasContext2D.java b/src/swingjs/api/js/HTML5CanvasContext2D.java
new file mode 100644 (file)
index 0000000..226f270
--- /dev/null
@@ -0,0 +1,164 @@
+package swingjs.api.js;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D.Float;
+
+public abstract class HTML5CanvasContext2D {
+
+       public class ImageData {
+               public int[] data; 
+       }
+
+       public ImageData imageData;
+       
+       public Object[][] _aSaved;
+       
+       public double lineWidth;
+
+       public String font, fillStyle, strokeStyle;
+
+       public float globalAlpha;
+
+       public abstract void drawImage(DOMNode img, double sx,
+                       double sy, double swidth, double sheight, double dx, double dy, double width, double height);
+
+       public abstract ImageData getImageData(int x, int y, int width, int height);
+
+       public abstract void beginPath();
+
+       public abstract void moveTo(double x0, double y0);
+
+       public abstract void lineTo(double x1, double y1);
+
+       public abstract void stroke();
+
+       public abstract void save();
+
+       public abstract void scale(double f, double g);
+
+       public abstract void arc(double centerX, double centerY, double radius, double startAngle, double  endAngle, boolean counterclockwise);
+
+       public abstract void closePath();
+
+       public abstract void restore();
+
+       public abstract void translate(double x, double y);
+       
+       public abstract void rotate(double radians);
+
+       public abstract void fill();
+
+
+       public abstract void fill(String winding);
+
+       public abstract void rect(double x, double y, double width, double height);
+
+       public abstract void fillText(String s, double x, double y);
+
+       public abstract void fillRect(double x, double y, double width, double height);
+
+       public abstract void clearRect(double i, double j, double windowWidth, double windowHeight);
+
+       public abstract void setLineDash(int[] dash);
+
+       public abstract void clip();
+
+       public abstract void quadraticCurveTo(double d, double e, double f, double g);
+
+       public abstract void bezierCurveTo(double d, double e, double f, double g, double h, double i);
+
+       public abstract void drawImage(DOMNode img, double x, double y, double width, double height);
+
+       public abstract void putImageData(Object imageData, double x, double y);
+
+       public abstract void transform(double d, double shx, double e, double shy, double f, double g);
+
+
+       /**
+        * pull one save structure onto the stack array ctx._aSaved
+        * 
+        * @param ctx
+        * @return the length of the stack array after the push
+        */
+       public static int push(HTML5CanvasContext2D ctx, Object[] map) {
+               /**
+                * @j2sNative
+                * 
+                * (ctx._aSaved || (ctx._aSaved = [])).push(map); 
+                * return ctx._aSaved.length;
+                */
+               {
+                       return 0;
+               }
+       }
+
+       /**
+        * pull one save structure off the stack array ctx._aSaved
+        * 
+        * @param ctx
+        * @return
+        */
+       public static Object[] pop(HTML5CanvasContext2D ctx) {
+               /**
+                * @j2sNative
+                * 
+                * return (ctx._aSaved && ctx._aSaved.length > 0 ? ctx._aSaved.pop() : null); 
+                */
+               {
+                       return null;
+               }
+       }
+
+       public static int getSavedLevel(HTML5CanvasContext2D ctx) {
+               /**
+                * @j2sNative
+                * 
+                * return (ctx._aSaved ? ctx._aSaved.length : 0); 
+                */
+               {
+                       return 0;
+               }
+       }
+       
+       public static Object[][] getSavedStack(HTML5CanvasContext2D ctx) {
+          /**
+           * @j2sNative
+           * 
+           * return (ctx._aSaved || []);
+           */
+               {
+                       return null;
+               }
+               
+       }
+
+       public static double[] setMatrix(HTML5CanvasContext2D ctx, AffineTransform transform) {
+               double[] m = /**  @j2sNative ctx._m || */ null;
+               if (transform == null) {
+                       /** @j2sNative ctx._m = null; */
+                       return null;                    
+               }
+               if (m == null) {
+                       /**
+                        * @j2sNative
+                        * ctx._m = m = new Array(6);
+                        */
+                       transform.getMatrix(m);
+               }
+               return m;
+       }
+
+       public static void createLinearGradient(HTML5CanvasContext2D ctx, Float p1, Float p2, String css1, String css2) {
+               /**
+                * @j2sNative
+                * 
+                *   var grd = ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y);
+                *   grd.addColorStop(0,css1);
+                *   grd.addColorStop(1,css2);
+                *   ctx.fillStyle = grd;
+                */
+               }
+
+       abstract public void drawImage(DOMNode domNode, int x, int y);
+
+}
diff --git a/src/swingjs/api/js/HTML5DataTransfer.java b/src/swingjs/api/js/HTML5DataTransfer.java
new file mode 100644 (file)
index 0000000..b6d91ba
--- /dev/null
@@ -0,0 +1,7 @@
+package swingjs.api.js;
+
+public interface HTML5DataTransfer {
+
+       Object getData(String type);
+
+}
diff --git a/src/swingjs/api/js/HTML5Video.java b/src/swingjs/api/js/HTML5Video.java
new file mode 100644 (file)
index 0000000..a47732a
--- /dev/null
@@ -0,0 +1,473 @@
+package swingjs.api.js;
+
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.Frame;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.net.URL;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.function.Function;
+
+import javax.swing.BoxLayout;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import swingjs.api.JSUtilI;
+
+/**
+ * A full-service interface for HTML5 video element interaction. Allows setting
+ * and getting HTML5 video element properties. ActionListeners can be set to
+ * listen for JavaScript events associated with a video element.
+ * 
+ * Video is added using a JavaScript-only two-parameter constructor for
+ * ImageIcon with "jsvideo" as the description, allowing for video construction
+ * from byte[], File, or URL.
+ * 
+ * After adding the ImageIcon to a JLabel, calling
+ * jlabel.getClientProperty("jsvideo") returns an HTML5 object of type
+ * HTML5Video (the &lt;video&gt; tag), which has the full suite of HTML5 video
+ * element properties, methods, and events.
+ * 
+ * Access to event listeners is via the method addActionListener, below, which
+ * return an ActionEvent that has as its source both the video element source as
+ * well as the original JavaScript event as an Object[] { jsvideo, event }. The
+ * id of this ActionEvent is 12345, and its command is the name of the event,
+ * for example, "canplay" or "canplaythrough".
+ * 
+ * See https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement for
+ * details.
+ * 
+ * @author hansonr
+ *
+ */
+public interface HTML5Video extends DOMNode {
+
+       public interface Promise {
+
+       }
+
+       final static String[] eventTypes = new String[] { "audioprocess", // The input buffer of a ScriptProcessorNode is
+                                                                                                                                               // ready to be processed.
+                       "canplay", // The browser can play the media, but estimates that not enough data has been
+                                               // loaded to play the media up to its end without having to stop for further
+                                               // buffering of content.
+                       "canplaythrough", // The browser estimates it can play the media up to its end without stopping
+                                                               // for content buffering.
+                       "complete", // The rendering of an OfflineAudioContext is terminated.
+                       "durationchange", // The duration attribute has been updated.
+                       "emptied", // The media has become empty; for example, this event is sent if the media has
+                                               // already been loaded (or partially loaded), and the load() method is called to
+                                               // reload it.
+                       "ended", // Playback has stopped because the end of the media was reached.
+                       "loadeddata", // The first frame of the media has finished loading.
+                       "loadedmetadata", // The metadata has been loaded.
+                       "pause", // Playback has been paused.
+                       "play", // Playback has begun.
+                       "playing", // Playback is ready to start after having been paused or delayed due to lack of
+                                               // data.
+                       "progress", // Fired periodically as the browser loads a resource.
+                       "ratechange", // The playback rate has changed.
+                       "seeked", // A seek operation completed.
+                       "seeking", // A seek operation began.
+                       "stalled", // The user agent is trying to fetch media data, but data is unexpectedly not
+                                               // forthcoming.
+                       "suspend", // Media data loading has been suspended.
+                       "timeupdate", // The time indicated by the currentTimeattribute has been updated.
+                       "volumechange", // The volume has changed.
+                       "waiting", // Playback has stopped because of a temporary lack of data
+       };
+
+       // direct methods
+
+       public void addTextTrack() throws Throwable;
+
+       public Object captureStream() throws Throwable;
+
+       public String canPlayType(String mediaType) throws Throwable;
+
+       public void fastSeek(double time) throws Throwable;
+
+       public void load() throws Throwable;
+
+       public void mozCaptureStream() throws Throwable;
+
+       public void mozCaptureStreamUntilEnded() throws Throwable;
+
+       public void mozGetMetadata() throws Throwable;
+
+       public void pause() throws Throwable;
+
+       public Promise play() throws Throwable;
+
+       public Promise seekToNextFrame() throws Throwable;
+
+       public Promise setMediaKeys(Object mediaKeys) throws Throwable;
+
+       public Promise setSinkId(String id) throws Throwable;
+
+       // convenience methods
+
+       public static double getDuration(HTML5Video v) {
+               return /** @j2sNative v.duration || */
+               0;
+       }
+
+       public static double setCurrentTime(HTML5Video v, double time) {
+               return /** @j2sNative v.currentTime = time|| */
+               0;
+       }
+
+       public static double getCurrentTime(HTML5Video v) {
+               return /** @j2sNative v.currentTime|| */
+               0;
+       }
+
+       public static Dimension getSize(HTML5Video v) {
+               return new Dimension(/** @j2sNative v.videoWidth || */
+                               0, /** @j2sNative v.videoHeight|| */
+                               0);
+       }
+
+       /**
+        * 
+        * Create a BufferedIfmage from the current frame. The image will be of type
+        * swingjs.api.JSUtilI.TYPE_4BYTE_HTML5, matching the data buffer of HTML5
+        * images.
+        * 
+        * @param v
+        * @param imageType  if Integer.MIN_VALUE, swingjs.api.JSUtilI.TYPE_4BYTE_HTML5
+        * @return
+        */
+       public static BufferedImage getImage(HTML5Video v, int imageType) {
+               Dimension d = HTML5Video.getSize(v);
+               BufferedImage image = (BufferedImage) HTML5Video.getProperty(v, "_image");
+               if (image == null || image.getWidth() != d.width || image.getHeight() != d.height) {
+                       image = new BufferedImage(d.width, d.height, imageType == Integer.MIN_VALUE ? JSUtilI.TYPE_4BYTE_HTML5 : imageType);
+                       HTML5Video.setProperty(v, "_image", image);
+               }
+               HTML5Canvas.setImageNode(v, image);
+               return image;
+       }
+
+       // property setting and getting
+
+       /**
+        * Set a property of the the HTML5 video element using jsvideo[key] = value.
+        * Numbers and Booleans will be unboxed.
+        * 
+        * @param jsvideo the HTML5 video element
+        * @param key
+        * @param value
+        */
+       public static void setProperty(HTML5Video jsvideo, String key, Object value) {
+               if (value instanceof Number) {
+                       /** @j2sNative jsvideo[key] = +value; */
+               } else if (value instanceof Boolean) {
+                       /** @j2sNative jsvideo[key] = !!+value */
+               } else {
+                       /** @j2sNative jsvideo[key] = value; */
+               }
+       }
+
+       /**
+        * Get a property using jsvideo[key], boxing number as Double and boolean as
+        * Boolean.
+        * 
+        * @param jsvideo the HTML5 video element
+        * 
+        * @param key
+        * @return value or value boxed as Double or Boolean
+        */
+       @SuppressWarnings("unused")
+       public static Object getProperty(HTML5Video jsvideo, String key) {
+               Object val = (/** @j2sNative 1? jsvideo[key] : */
+               null);
+               if (val == null)
+                       return null;
+               switch (/** @j2sNative typeof val || */
+               "") {
+               case "number":
+                       return Double.valueOf(/** @j2sNative val || */
+                                       0);
+               case "boolean":
+                       return Boolean.valueOf(/** @j2sNative val || */
+                                       false);
+               default:
+                       return val;
+               }
+       }
+
+       // event action
+
+       /**
+        * Add an ActionListener for the designated events. When an event is fired,
+        * 
+        * @param jsvideo  the HTML5 video element
+        * @param listener
+        * @param events   array of events to listen to or null to listen on all video
+        *                 element event types
+        * @return an array of event/listener pairs that can be used for removal.
+        */
+       public static Object[] addActionListener(HTML5Video jsvideo, ActionListener listener, String... events) {
+               if (events == null || events.length == 0)
+                       events = eventTypes;
+               @SuppressWarnings("unused")
+               Function<Object, Void> f = new Function<Object, Void>() {
+
+                       @Override
+                       public Void apply(Object jsevent) {
+                               String name = (/** @j2sNative jsevent.type || */
+                               "?");
+                               System.out.println("HTML5Video " + name);
+                               ActionEvent e = new ActionEvent(new Object[] { jsvideo, jsevent }, 12345, name,
+                                               System.currentTimeMillis(), 0);
+                               listener.actionPerformed(e);
+                               return null;
+                       }
+               };
+               ArrayList<Object> listeners = new ArrayList<>();
+               for (int i = 0; i < events.length; i++) {
+                       Object func = /**
+                                                        * @j2sNative function(event){f.apply$O.apply(f, [event])} ||
+                                                        */
+                                       null;
+                       listeners.add(events[i]);
+                       listeners.add(func);
+                       if (jsvideo != null)
+                               jsvideo.addEventListener(events[i], func);
+
+               }
+               return listeners.toArray(new Object[listeners.size()]);
+       }
+
+       /**
+        * Remove action listener
+        * 
+        * @param jsvideo   the HTML5 video element
+        * @param listeners an array of event/listener pairs created by
+        *                  addActionListener
+        */
+       public static void removeActionListener(HTML5Video jsvideo, Object[] listeners) {
+               if (listeners == null) {
+                       for (int i = 0; i < eventTypes.length; i++) {
+                               jsvideo.removeEventListener(eventTypes[i]);
+                       }
+               }
+               
+               for (int i = 0; i < listeners.length; i += 2) {
+                       String event = (String) listeners[i];
+                       Object listener = listeners[i + 1];
+                       jsvideo.removeEventListener(event, listener);
+               }
+       }
+
+       /**
+        * Create an ImageIcon which, when placed in a JLabel, displays the video.
+        * 
+        * @param source
+        * @return
+        */
+       public static ImageIcon createIcon(Object source) {
+               try {
+                       if (source instanceof URL) {
+                               return new ImageIcon((URL) source, "jsvideo");
+                       } else if (source instanceof byte[]) {
+                               return new ImageIcon((byte[]) source, "jsvideo");
+                       } else if (source instanceof File) {
+                               return new ImageIcon(Files.readAllBytes(((File) source).toPath()));
+                       } else {
+                               return new ImageIcon(Files.readAllBytes(new File(source.toString()).toPath()));
+                       }
+               } catch (Throwable t) {
+                       return null;
+               }
+       }
+
+       /**
+        * Create a label that, when shown, displays the video.
+        * 
+        * @param source
+        * @return
+        */
+       public static JLabel createLabel(Object source) {
+               ImageIcon icon = (source instanceof ImageIcon ? (ImageIcon) source : createIcon(source));
+               return (icon == null ? null : new JLabel(icon));
+       }
+
+       /**
+        * Create a dialog that includes rudimentary controls. Optional maxWidth allows image downscaling by factors of two.
+        * 
+        * @param parent
+        * @param source 
+        * @param maxWidth
+        * @return
+        */
+       public static JDialog createDialog(Frame parent, Object source, int maxWidth, Function<HTML5Video, Void> whenReady) {
+               JDialog dialog = new JDialog(parent);
+               Container p = dialog.getContentPane();
+               p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
+               JLabel label = (source instanceof JLabel ? (JLabel) source : createLabel(source));
+               label.setAlignmentX(0.5f);
+               // not in Java! dialog.putClientProperty("jsvideo", label);
+               p.add(label);
+               label.setVisible(false);
+               p.add(getControls(label));
+               dialog.setModal(false);
+               dialog.pack();
+               dialog.setVisible(true);
+               dialog.setVisible(false);
+               HTML5Video jsvideo = (HTML5Video) label.getClientProperty("jsvideo");
+               HTML5Video.addActionListener(jsvideo, new ActionListener() {
+
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               if (label.getClientProperty("jsvideo.size") != null)
+                                       return;
+                               Dimension dim = HTML5Video.getSize(jsvideo);
+                               while (dim.width > maxWidth) {
+                                       dim.width /= 2;
+                                       dim.height /= 2;
+                               }
+                               label.putClientProperty("jsvideo.size", dim);
+                               label.setPreferredSize(dim);
+                               label.setVisible(true);
+//                             label.invalidate();
+                               dialog.pack();
+//                             dialog.setVisible(false);
+                               if (whenReady != null)
+                                       whenReady.apply(jsvideo);
+                       }
+                       
+               }, "canplaythrough");
+               HTML5Video.setCurrentTime(jsvideo,  0);
+               return dialog;
+       }
+
+       static JPanel getControls(JLabel label) {
+
+               JPanel controls = new JPanel();
+               controls.setAlignmentX(0.5f);
+               JButton btn = new JButton("play");
+               btn.addActionListener(new ActionListener() {
+
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               try {
+                                       ((HTML5Video) label.getClientProperty("jsvideo")).play();
+                               } catch (Throwable e1) {
+                                       e1.printStackTrace();
+                               }
+                       }
+
+               });
+               controls.add(btn);
+
+               btn = new JButton("pause");
+               btn.addActionListener(new ActionListener() {
+
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               try {
+                                       ((HTML5Video) label.getClientProperty("jsvideo")).pause();
+                               } catch (Throwable e1) {
+                                       e1.printStackTrace();
+                               }
+                       }
+
+               });
+               controls.add(btn);
+               
+               btn = new JButton("reset");
+               btn.addActionListener(new ActionListener() {
+
+                       @Override
+                       public void actionPerformed(ActionEvent e) {
+                               HTML5Video.setCurrentTime((HTML5Video) label.getClientProperty("jsvideo"), 0);
+                       }
+
+               });
+               controls.add(btn);
+
+               return controls;
+       }
+
+       /**
+        * Advance to the next frame, using seekToNextFrame() if available, or using the time difference supplied.
+        * 
+        * @param jsvideo
+        * @param dt  seconds to advance if seekToNextFrame() is not available
+        * @return true if can use seekToNextFrame()
+        * 
+        */
+       public static boolean nextFrame(HTML5Video jsvideo, double dt) {
+               Boolean canSeek = (Boolean) getProperty(jsvideo,"_canseek");
+               if (canSeek == null) {
+                       setProperty(jsvideo, "_canseek", canSeek = Boolean.valueOf(getProperty(jsvideo, "seekToNextFrame") != null));
+               }
+               try {                   
+                       if (canSeek) {
+                               jsvideo.seekToNextFrame();
+                       } else {
+                               HTML5Video.setCurrentTime(jsvideo, HTML5Video.getCurrentTime(jsvideo) + dt);                                            
+                       }
+               } catch (Throwable e1) {
+               }
+               return canSeek.booleanValue();
+       }
+
+       public static int getFrameCount(HTML5Video jsvideo) {
+               return (int) (getDuration(jsvideo) / 0.033334);
+       }
+
+// HTMLMediaElement properties
+
+//     audioTracks
+//     autoplay
+//     buffered Read only
+//     controller
+//     controls
+//     controlsList Read only
+//     crossOrigin
+//     currentSrc Read only
+//     currentTime
+//     defaultMuted
+//     defaultPlaybackRate
+//     disableRemotePlayback
+//     duration Read only
+//     ended Read only
+//     error Read only
+//     loop
+//     mediaGroup
+//     mediaKeys Read only
+//     mozAudioCaptured Read only
+//     mozFragmentEnd
+//     mozFrameBufferLength
+//     mozSampleRate Read only
+//     muted
+//     networkState Read only
+//     paused Read only
+//     playbackRate
+//     played Read only
+//     preload
+//     preservesPitch
+//     readyState Read only
+//     seekable Read only
+//     seeking Read only
+//     sinkId Read only
+//     src
+//     srcObject
+//     textTracks Read only
+//     videoTracks Read only
+//     volume
+//     initialTime Read only
+//     mozChannels Read only
+
+}
diff --git a/src/swingjs/api/js/J2SInterface.java b/src/swingjs/api/js/J2SInterface.java
new file mode 100644 (file)
index 0000000..8051c96
--- /dev/null
@@ -0,0 +1,83 @@
+package swingjs.api.js;
+
+import java.awt.Component;
+import java.awt.Point;
+import java.util.Hashtable;
+
+
+/**
+ * An interface to J2S.xxx() functions.
+ * 
+ * @author hansonr
+ *
+ */
+
+public interface J2SInterface {
+
+       void addBinaryFileType(String ext);
+
+       void addDirectDatabaseCall(String domain);
+       
+       boolean debugClip();
+       
+       
+       
+       HTML5Applet findApplet(String htmlName);
+
+       Object getCachedJavaFile(String key);
+
+       /**
+        * 
+        * @param isAll true for check of navigator; otherwise just J2S._lang from j2sLang=xx_XX in URI
+        * @return
+        */
+       String getDefaultLanguage(boolean isAll);
+
+       Object getFileData(String fileName, Object fWhenDone, boolean doProcess, boolean isBinary);
+
+       void getFileFromDialog(Object fWhenDone, String type);
+
+       Object getJavaResource(String resourceName, boolean isJavaPath);
+       
+       String getJavaVersion();
+
+       int getKeyModifiers(Object jQueryEvent);
+       
+       Point getMousePosition(Point p);
+       
+       String getResourcePath(String resourceName, boolean isJavaPath);
+
+       Hashtable<String, Object> getSetJavaFileCache(Object object);
+       
+       Object getSwing(); // JSSwingMenu 
+       
+       int getZ(HTML5Applet applet, String frameType);
+
+       boolean isBinaryUrl(String filename);
+
+       boolean isResourceLoaded(String file, boolean done);
+
+       void readyCallback(String appId, String fullId, boolean isReady, 
+                       Object javaApplet, Object javaAppletPanel);
+
+       void saveFile(String fileName, Object data, String mimeType, String encoding);
+       
+       void setDragDropTarget(Component target, DOMNode node, boolean adding);
+
+       void setDraggable(DOMNode tagNode, Object targetNodeOrFDown);
+       
+       void setKeyListener(DOMNode node);
+
+       void setMouse(DOMNode frameNode, boolean isSwingJS);
+
+       int setWindowZIndex(DOMNode domNode, int pos);
+
+       void unsetMouse(DOMNode frameNode);
+
+       String fixCachePath(String uri);
+
+       void showStatus(String msg, boolean doFadeOut);
+
+
+}
+
diff --git a/src/swingjs/api/js/JQuery.java b/src/swingjs/api/js/JQuery.java
new file mode 100644 (file)
index 0000000..4b21993
--- /dev/null
@@ -0,0 +1,15 @@
+package swingjs.api.js;
+
+public interface JQuery {
+
+  JQueryObject $(Object selector);
+
+  DOMNode parseXML(String xmlData);
+  
+  boolean contains(Object outer, Object inner);
+
+  Object parseJSON(String json);
+
+  Object data(Object node, String attr);
+
+}
diff --git a/src/swingjs/api/js/JQueryObject.java b/src/swingjs/api/js/JQueryObject.java
new file mode 100644 (file)
index 0000000..3e53f6e
--- /dev/null
@@ -0,0 +1,85 @@
+package swingjs.api.js;
+
+public interface JQueryObject {
+
+       public interface JQEvent {
+
+       }
+
+       public abstract void appendTo(Object obj);
+       public abstract JQueryObject append(Object span);
+
+       public abstract void bind(String actions, Object f);
+       public abstract void unbind(String actions);
+
+       public abstract void on(String eventName, Object f);
+
+       public abstract JQueryObject focus();
+       public abstract JQueryObject select();
+
+       public abstract int width();
+       public abstract int height();
+       public abstract Object offset();
+
+
+       public abstract void html(String html);
+
+       public abstract DOMNode get(int i);
+
+       public abstract String attr(String key);
+       public abstract JQueryObject attr(String key, String value);
+       public abstract JQueryObject css(String key, String value);
+
+       public abstract JQueryObject addClass(String name);     
+       public abstract JQueryObject removeClass(String name);
+       
+       public abstract JQueryObject show();
+       public abstract JQueryObject hide();
+
+       public abstract void resize(Object fHandleResize);
+
+
+       /**
+        * closest ancestor
+        * 
+        * @param selector
+        * @return
+        */
+       public abstract JQueryObject closest(String selector);
+
+       /**
+        * find all descendants
+        * 
+        * @param selector
+        * @return
+        */
+       public abstract JQueryObject find(String selector);
+
+       public abstract JQueryObject parent();
+       public abstract void before(Object obj);
+       public abstract void after(Object div);
+
+       
+       /**
+        * remove from tree, but do not clear events
+        */
+       public abstract void detach(); // like remove(), but does not change event settings
+       
+       /**
+        * remove from tree and clear all events -- for disposal only
+        */
+       public abstract void remove();
+
+       /**
+        * fully remove all children, clearing all events
+        */
+       public abstract void empty();
+
+       public abstract DOMNode getElement();
+       
+       public static DOMNode getDOMNode(JQueryObject jnode) {
+               return (jnode == null ? null : ((DOMNode[]) (Object) jnode)[0]);
+       }
+       
+
+}
diff --git a/src/swingjs/api/js/JSFunction.java b/src/swingjs/api/js/JSFunction.java
new file mode 100644 (file)
index 0000000..41d1fb3
--- /dev/null
@@ -0,0 +1,12 @@
+package swingjs.api.js;
+
+/**
+ * A flag that this object is really a JavaScript function that, for example,
+ * might be called from setTimeout(). 
+ * 
+ * @author Bob Hanson
+ *
+ */
+public interface JSFunction {
+
+}
diff --git a/src/swingjs/api/js/JSInterface.java b/src/swingjs/api/js/JSInterface.java
new file mode 100644 (file)
index 0000000..f79ae8d
--- /dev/null
@@ -0,0 +1,43 @@
+package swingjs.api.js;
+
+/**
+ * called by SwingJS JavaScript methods
+ * 
+ */
+public interface JSInterface {
+
+       int cacheFileByName(String fileName, boolean isAdd); // $S$Z
+
+       void cachePut(String key, Object data); // $S$O
+
+       void destroy();
+
+       String getFullName();
+
+       void openFileAsyncSpecial(String fileName, int flags); // $S$I
+
+       boolean processMouseEvent(int id, int x, int y, int modifiers, long time, Object jqevent, int scroll); // $I$I$I$I$J$O$I
+
+       void processTwoPointGesture(float[][][] touches); // AFFF
+
+       void setDisplay(HTML5Canvas canvas);
+
+       void setScreenDimension(int width, int height);
+
+       boolean setStatusDragDropped(int mode, int x, int y, String fileName);
+
+       void startHoverWatcher(boolean enable);
+
+       static void setCursor(String c) {
+               /**
+                * @j2sNative
+                * 
+                * try {
+                * 
+                *   document.body.style.cursor = c;
+                * 
+                * } catch (e) {}
+                */
+       }
+
+}
diff --git a/src/swingjs/api/js/README.txt b/src/swingjs/api/js/README.txt
new file mode 100644 (file)
index 0000000..ed8abb4
--- /dev/null
@@ -0,0 +1,6 @@
+package swingjs.api.js
+
+This package contains interfaces to HTML5 objects and JavaScript functions that 
+are potentially useful to developers without use of  @j2sNative. 
+
+Caution should be used. Their persistence is not guaranteed.
\ No newline at end of file