From: BobHanson Date: Mon, 1 Jun 2020 17:32:19 +0000 (-0500) Subject: swingjs/api, javajs/async X-Git-Url: http://source.jalview.org/gitweb/?a=commitdiff_plain;h=bde30b90c1af8f17e869d41afb7f2a53f9c5d38d;p=jalview.git swingjs/api, javajs/async --- diff --git a/src/javajs/async/Assets.java b/src/javajs/async/Assets.java new file mode 100644 index 0000000..b57528b --- /dev/null +++ b/src/javajs/async/Assets.java @@ -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: + * + * + 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. "); + } + + } + * + * + * 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: + * + * + * getAssetBytes(String fullPath) + * getAssetString(String fullPath) + * getAssetStream(String fullPath) + * + * + * 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> htZipContents = new HashMap<>(); + + private static boolean doCacheZipContents = true; + + private static Assets instance = new Assets(); + + private Assets() { + } + + private Map 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 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 getZipContents(String zipPath) { + return getInstance()._getZipContents(zipPath); + } + + private Map _getZipContents(String zipPath) { + URL url = getURLWithCachedBytes(zipPath); // BH carry over bytes if we have them already + Map 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 readZipContents(InputStream is, URL url) throws IOException { + HashMap fileNames = new HashMap(); + 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 index 0000000..f6f31e5 --- /dev/null +++ b/src/javajs/async/Async.java @@ -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 index 0000000..2474833 --- /dev/null +++ b/src/javajs/async/AsyncColorChooser.java @@ -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 index 0000000..5752f48 --- /dev/null +++ b/src/javajs/async/AsyncDialog.java @@ -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 index 0000000..849fe83 --- /dev/null +++ b/src/javajs/async/AsyncFileChooser.java @@ -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 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 index 0000000..df103cd --- /dev/null +++ b/src/javajs/async/AsyncSwingWorker.java @@ -0,0 +1,385 @@ +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 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 = (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; + + protected StateHelper getHelper() { + return helper; + } + + private boolean isPaused; + + protected void setPaused(boolean tf) { + isPaused = tf; + } + + protected boolean isPaused() { + return isPaused; + } + + /** + * 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() && !isPaused) { + 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); + } else { + int ret = doInBackgroundAsync(progressAsync); + if (!helper.isAlive() || isPaused) { + continue; + } + progressAsync = ret; + 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: + stopProgressMonitor(); + // Put the doneAsync() method on the AWTEventQueue + // just as for SwingWorker.done(). + if (isAsync) { + SwingUtilities.invokeLater(doneRunnable); + } else { + doneRunnable.run(); + } + + return false; + } + } + if (!helper.isAlive()) { + stopProgressMonitor(); + } + return false; + } + + private void stopProgressMonitor() { + if (progressMonitor != null) { + progressMonitor.close(); + progressMonitor = null; + } + } + + 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 index 0000000..82a0265 --- /dev/null +++ b/src/javajs/async/SwingJSUtils.java @@ -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: + * + * + * private static Dimension dim = + * + * + * + * 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 + * + * 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; + * } + * + * @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 index 0000000..6616780 --- /dev/null +++ b/src/swingjs/api/Interface.java @@ -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 index 0000000..fde82e8 --- /dev/null +++ b/src/swingjs/api/JSFileHandler.java @@ -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 index 5625f79..1b6ff9b 100644 --- a/src/swingjs/api/JSUtilI.java +++ b/src/swingjs/api/JSUtilI.java @@ -2,184 +2,331 @@ 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 { - /** - * 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 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); - - /** - * Same as setFileBytes, but also caches the data if it is a JSTempFile. - * - * @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); + /** + * 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 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 index 0000000..4de6ee0 --- /dev/null +++ b/src/swingjs/api/js/DOMNode.java @@ -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/HTML5AudioContext.java b/src/swingjs/api/js/HTML5AudioContext.java new file mode 100644 index 0000000..fecc66b --- /dev/null +++ b/src/swingjs/api/js/HTML5AudioContext.java @@ -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 index 0000000..f6d0604 --- /dev/null +++ b/src/swingjs/api/js/HTML5Canvas.java @@ -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 index 0000000..226f270 --- /dev/null +++ b/src/swingjs/api/js/HTML5CanvasContext2D.java @@ -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 index 0000000..b6d91ba --- /dev/null +++ b/src/swingjs/api/js/HTML5DataTransfer.java @@ -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 index 0000000..a47732a --- /dev/null +++ b/src/swingjs/api/js/HTML5Video.java @@ -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 <video> 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 f = new Function() { + + @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 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 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 index 0000000..8051c96 --- /dev/null +++ b/src/swingjs/api/js/J2SInterface.java @@ -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 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 index 0000000..4b21993 --- /dev/null +++ b/src/swingjs/api/js/JQuery.java @@ -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 index 0000000..3e53f6e --- /dev/null +++ b/src/swingjs/api/js/JQueryObject.java @@ -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 index 0000000..41d1fb3 --- /dev/null +++ b/src/swingjs/api/js/JSFunction.java @@ -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 index 0000000..f79ae8d --- /dev/null +++ b/src/swingjs/api/js/JSInterface.java @@ -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) {} + */ + } + +}