com.codename1.ui.BrowserComponent Maven / Gradle / Ivy
/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores
* CA 94065 USA or visit www.oracle.com if you need additional information or
* have any questions.
*/
package com.codename1.ui;
import com.codename1.io.JSONParser;
import com.codename1.io.Log;
import com.codename1.io.URL;
import com.codename1.io.Util;
import com.codename1.processing.Result;
import com.codename1.ui.events.ActionEvent;
import com.codename1.ui.events.ActionListener;
import com.codename1.ui.layouts.BorderLayout;
import com.codename1.ui.util.EventDispatcher;
import com.codename1.ui.events.BrowserNavigationCallback;
import com.codename1.ui.plaf.Style;
import com.codename1.ui.util.UITimer;
import com.codename1.util.AsyncResource;
import com.codename1.util.Base64;
import com.codename1.util.Callback;
import com.codename1.util.CallbackAdapter;
import com.codename1.util.StringUtil;
import com.codename1.util.SuccessCallback;
import com.codename1.util.regex.StringReader;
import java.io.IOException;
import java.net.URI;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.Vector;
/**
* The browser component is an interface to an embeddable native platform browser on platforms
* that support embedding the native browser in place, if you need wide compatibility and flexibility
* you should check out the HTMLComponent which provides a lightweight 100% cross platform
* web component.
* This component will only work on platforms that support embedding a native browser which
* exclude earlier versions of Blackberry devices and J2ME devices.
* Its recommended that you place this component in a fixed position (none scrollable) on the screen without other
* focusable components to prevent confusion between focus authority and allow the component to scroll
* itself rather than CodenameOne making that decision for it.
*
* On Android this component might show a native progress indicator dialog. You can disable that functionality
* using the {@Display.getInstance().setProperty("WebLoadingHidden", "true");} call.
*
*
* The following code shows the basic usage of the {@code BrowserComponent}:
*
*
*
*
* Debugging on Android
*
* You can use Chrome's remote debugging features to debug the contents of a BrowserComponent. On Android 4.4 (KitKat)
* and higher, you will need to define the "android.webContentsDebuggingEnabled" display property in order for this to work. You can define this inside your app's init() method:
* Display.getInstance().setProperty("android.webContentsDebuggingEnabled", "true");
* @author Shai Almog
*/
public class BrowserComponent extends Container {
private Hashtable listeners;
private PeerComponent internal;
private boolean pinchToZoom = true;
private boolean nativeScrolling = true;
private boolean ready = false;
private boolean fireCallbacksOnEdt=true;
PeerComponent getInternal() {
return internal;
}
/**
* String constant for web event listener {@link #addWebEventListener(java.lang.String, com.codename1.ui.events.ActionListener)}
*/
public static final String onStart = "onStart";
/**
* String constant for web event listener {@link #addWebEventListener(java.lang.String, com.codename1.ui.events.ActionListener)}
*/
public static final String onLoad = "onLoad";
/**
* String constant for web event listener {@link #addWebEventListener(java.lang.String, com.codename1.ui.events.ActionListener)}
*/
public static final String onError = "onError";
/**
* String constant for web event listener. Use this event types to register to receive messages
* in a cross-domain-safe way from the web page. To send a message from the webpage, the page should
* include a function like:
* {@code
* function postToCN1(msg) {
* if (window.cn1PostMessage) {
* // Case 1: Running inside native app in a WebView
* window.cn1PostMessage(msg);
* } else {
* // Case 2: Running inside a Javascript app in an iframe
* window.parent.postMessage(msg, '*');
* }
* }
* }
* Receiving a message:
* {@code
* myBrowserComponent.addWebEventListener(BrowserComponent.onMessage, e->{
* CN.callSerially(()->{
* Log.p("Message: "+e.getSource());
* Dialog.show("Here", (String)e.getSource(), "OK", null);
* });
* });
* }
*/
public static final String onMessage = "onMessage";
private BrowserNavigationCallback browserNavigationCallback = new BrowserNavigationCallback(){
public boolean shouldNavigate(String url) {
return true;
}
};
/**
* Sets whether javascript callbacks should be run on the EDT. Default is {@literal true}.
* @param edt True if callbacks should be run on EDT. False if they should be run on the platform's main thread.
* @since 5.0
*/
public void setFireCallbacksOnEdt(boolean edt) {
this.fireCallbacksOnEdt = edt;
}
/**
* Checks if javascript callbacks are run on the EDT.
* @return True if javascript callbacks are run on the EDT.
* @since 5.0
* @see #setFireCallbacksOnEdt(boolean)
*/
public boolean isFireCallbacksOnEdt() {
return fireCallbacksOnEdt;
}
/**
* Set the browser navigation callback which allows handling a case where
* a URL invocation can be delegated to Java code. This allows binding
* Java side functionality to JavaScript functionality in the same
* way PhoneGap/Cordova work
* @param callback the callback interface
* @deprecated Use {@link #addBrowserNavigationCallback(com.codename1.ui.events.BrowserNavigationCallback) Instead
*/
public void setBrowserNavigationCallback(BrowserNavigationCallback callback){
this.browserNavigationCallback = callback;
}
/**
* Async method for capturing a screenshot of the browser content. Currently only supported
* in the simulator. Also, only displays the visible rectangle of the BrowserComponent,
* not the entire page.
* @return AsyncResource resolving to an Image of the webview contents.
* @since 7.0
*/
public AsyncResource captureScreenshot() {
if (internal != null) {
AsyncResource i = Display.impl.captureBrowserScreenshot(internal);
if (i != null) {
return i;
}
}
AsyncResource out = new AsyncResource();
if (internal != null) {
out.complete(internal.toImage());
} else {
out.complete(toImage());
}
return out;
}
/**
* The browser navigation callback interface allows handling a case where
* a URL invocation can be delegated to Java code. This allows binding
* Java side functionality to JavaScript functionality in the same
* way PhoneGap/Cordova work
*
* @return the callback interface
* @deprecated Call {@link #fireBrowserNavigationCallbacks(java.lang.String) } to determine whether navigation should occur for a particulr URL.
*/
public BrowserNavigationCallback getBrowserNavigationCallback(){
return this.browserNavigationCallback;
}
/**
* List of registered browser navigation callbacks.
*/
private Vector browserNavigationCallbacks;
private Vector browserNavigationCallbacks() {
if (browserNavigationCallbacks == null) {
browserNavigationCallbacks = new Vector();
}
return browserNavigationCallbacks;
}
/**
* Adds a navigation callback.
* @param callback The callback to call before navigating to a URL.
*/
public void addBrowserNavigationCallback(BrowserNavigationCallback callback) {
browserNavigationCallbacks().add(callback);
}
/**
* Removes a navigation callback.
* @param callback The callback to call before navigating to a URL.
*/
public void removeBrowserNavigationCallback(BrowserNavigationCallback callback) {
if (browserNavigationCallbacks != null) {
browserNavigationCallbacks().remove(callback);
}
}
private static final String RETURN_URL_PREFIX = "/!cn1return/";
private Hashtable> returnValueCallbacks;
private Hashtable> returnValueCallbacks() {
if (returnValueCallbacks == null) {
returnValueCallbacks = new Hashtable>();
}
return returnValueCallbacks;
}
private int nextReturnValueCallbackId = 0;
private int addReturnValueCallback(SuccessCallback callback) {
int id = nextReturnValueCallbackId++;
while (returnValueCallbacks().containsKey(id)) {
id++;
}
returnValueCallbacks().put(id, callback);
nextReturnValueCallbackId = id+1;
if (nextReturnValueCallbackId > 10000) {
nextReturnValueCallbackId=0;
}
return id;
}
private SuccessCallback popReturnValueCallback(int id) {
if (returnValueCallbacks != null) {
return returnValueCallbacks.remove(id);
}
return null;
}
private JSONParser returnValueParser;
private JSONParser returnValueParser() {
if (returnValueParser == null) {
returnValueParser = new JSONParser();
}
return returnValueParser;
}
@Override
protected void initComponent() {
super.initComponent();
}
@Override
protected void deinitialize() {
uninstallMessageListener();
super.deinitialize();
}
/**
* Calls the {@literal postMessage()} method on the webpage's {@literal window} object.
* This is useful mainly for the Javascript port so that you don't have to worry about
* cross-domain issues, as postMessage() is supported cross-domain.
*
* To receive a message, the web page should register a "message" event listener, just as
* it would to receive messages from other windows in the browser. See MDN docs for postMessage()
* for more information.
*
* @param message The message to send.
* @param targetOrigin The target origin of the message. E.g. http://example.com:1234
* @since 7.0
*/
public void postMessage(final String message, final String targetOrigin) {
if (internal == null) {
onReady(new Runnable() {
public void run() {
postMessage(message, targetOrigin);
}
});
return;
}
if (!Display.impl.postMessage(internal, message, targetOrigin)) {
execute("window.postMessage(${0}, ${1})", new Object[]{message, targetOrigin});
}
}
private void installMessageListener() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
installMessageListener();
}
});
return;
}
if (!Display.impl.installMessageListener(internal)) {
messageCallback = new SuccessCallback() {
@Override
public void onSucess(JSRef value) {
fireWebEvent(onMessage, new ActionEvent(value.toString()));
}
};
addJSCallback("window.cn1PostMessage = function(msg){ callback.onSuccess(msg);};", messageCallback);
}
}
SuccessCallback messageCallback;
private void uninstallMessageListener() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
uninstallMessageListener();
}
});
return;
}
if (!Display.impl.installMessageListener(internal)) {
//if (messageCallback != null) {
// removeJSCallback(messageCallback);
// messageCallback = null;
//}
}
}
/**
* Decodes a URL
* @param s The string to decode.
* @param enc The encoding. E.g. UTF-8
* @return The decoded URL.
*/
private static String decodeURL(String s, String enc){
boolean needToChange = false;
int numChars = s.length();
StringBuilder sb = new StringBuilder(numChars > 500 ? numChars / 2 : numChars);
int i = 0;
char c;
byte[] bytes = null;
while (i < numChars) {
c = s.charAt(i);
switch (c) {
case '+':
sb.append(' ');
i++;
needToChange = true;
break;
case '%':
/*
* Starting with this instance of %, process all
* consecutive substrings of the form %xy. Each
* substring %xy will yield a byte. Convert all
* consecutive bytes obtained this way to whatever
* character(s) they represent in the provided
* encoding.
*/
try {
// (numChars-i)/3 is an upper bound for the number
// of remaining bytes
if (bytes == null)
bytes = new byte[(numChars-i)/3];
int pos = 0;
while ( ((i+2) < numChars) &&
(c=='%')) {
int v = Integer.parseInt(s.substring(i+1,i+3),16);
if (v < 0)
throw new IllegalArgumentException("URLDecoder: Illegal hex characters in escape (%) pattern - negative value");
bytes[pos++] = (byte) v;
i+= 3;
if (i < numChars)
c = s.charAt(i);
}
// A trailing, incomplete byte encoding such as
// "%x" will cause an exception to be thrown
if ((i < numChars) && (c=='%'))
throw new IllegalArgumentException(
"URLDecoder: Incomplete trailing escape (%) pattern");
try {
sb.append(new String(bytes, 0, pos, enc));
} catch (Throwable t) {
throw new RuntimeException(t.getMessage());
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"URLDecoder: Illegal hex characters in escape (%) pattern - "
+ e.getMessage());
}
needToChange = true;
break;
default:
sb.append(c);
i++;
break;
}
}
return (needToChange? sb.toString() : s);
}
/**
* Fires all of the registered browser navigation callbacks against the provided URL.
* @param url The URL to fire the navigation callbacks against.
* @return True if all of the callbacks say that they can browse. False otherwise.
*/
public boolean fireBrowserNavigationCallbacks(String url) {
boolean shouldNavigate = true;
if (browserNavigationCallback != null && !browserNavigationCallback.shouldNavigate(url)) {
shouldNavigate = false;
}
if (browserNavigationCallbacks != null) {
for (BrowserNavigationCallback cb : browserNavigationCallbacks) {
if (!cb.shouldNavigate(url)) {
shouldNavigate = false;
}
}
}
if ( !url.startsWith("javascript:") && url.indexOf(RETURN_URL_PREFIX) != -1 ){
//System.out.println("Received browser navigation callback "+url);
String result = decodeURL(url.substring(url.indexOf(RETURN_URL_PREFIX) + RETURN_URL_PREFIX.length()), "UTF-8");
//System.out.println("After decode "+result);
Result structResult = Result.fromContent(result, Result.JSON);
int callbackId = structResult.getAsInteger("callbackId");
final String value = structResult.getAsString("value");
final String type = structResult.getAsString("type");
final String errorMessage = structResult.getAsString("errorMessage");
final SuccessCallback callback = popReturnValueCallback(callbackId);
if (jsCallbacks != null && jsCallbacks.contains(callback)) {
// If this is a registered callback, then we treat it more like
// an event listener, and we retain it for future callbacks.
returnValueCallbacks.put(callbackId, callback);
}
if (callback != null) {
if (errorMessage != null) {
if (fireCallbacksOnEdt) {
Display.getInstance().callSerially(new Runnable() {
public void run() {
if (callback instanceof Callback) {
((Callback)callback).onError(this, new RuntimeException(errorMessage), 0, errorMessage);
}
}
});
} else {
if (callback instanceof Callback) {
((Callback)callback).onError(this, new RuntimeException(errorMessage), 0, errorMessage);
}
}
} else {
if (fireCallbacksOnEdt) {
Display.getInstance().callSerially(new Runnable() {
public void run() {
callback.onSucess(new JSRef(value, type));
}
});
} else {
callback.onSucess(new JSRef(value, type));
}
}
} else {
Log.e(new RuntimeException("Received return value from javascript, but no callback could be found for that ID"));
}
shouldNavigate = false;
}
return shouldNavigate;
}
private Container placeholder = new Container();
private LinkedList onReady = new LinkedList();
private void onReady(final Runnable r) {
if (!CN.isEdt()) {
CN.callSerially(new Runnable() {
public void run() {
onReady(r);
}
});
return;
}
onReady.add(r);
if (internal != null) {
while (!onReady.isEmpty()) {
onReady.remove(0).run();
}
}
}
private void onReady() {
if (internal != null) {
while (!onReady.isEmpty()) {
onReady.remove(0).run();
}
}
}
/**
* This constructor will work as expected when a browser component is supported, see isNativeBrowserSupported()
*/
public BrowserComponent() {
setUIID("BrowserComponent");
putClientProperty("BrowserComponent.useWKWebView", "true".equals(Display.getInstance().getProperty("BrowserComponent.useWKWebView", "true")));
setLayout(new BorderLayout());
addComponent(BorderLayout.CENTER, placeholder);
CN.callSerially(new Runnable() {
public void run() {
PeerComponent c = Display.impl.createBrowserComponent(BrowserComponent.this);
if (c == null) {
if (CN.isSimulator()) {
Log.p("Failed to create the browser component. Please ensure that you are either using a JDK that has JavaFX (e.g. ZuluFX), or that you have installed the Codename One CEF component. See https://www.codenameone.com/blog/big-changes-jcef.html for more information");
} else {
Log.p("Failed to create browser component. This platform may not support the native browser component");
}
return;
}
internal = c;
removeComponent(placeholder);
addComponent(BorderLayout.CENTER, internal);
onReady();
revalidateLater();
}
});
onReady(new Runnable() {
public void run() {
Style s = internal.getUnselectedStyle();
s.setPadding(0, 0, 0, 0);
s.setMargin(0, 0, 0, 0);
s.setBgTransparency(255);
s = getUnselectedStyle();
s.setPadding(0, 0, 0, 0);
}
});
addWebEventListener(onStart, new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
installMessageListener();
}
});
}
private final Object readyLock = new Object();
/**
* Uses invokeAndBlock to wait until the BrowserComponent is ready. The browser component
* is considered to be ready once the onLoad event has been fired for the first page.
*/
public void waitForReady() {
while (!ready) {
Display.getInstance().invokeAndBlock(new Runnable() {
public void run() {
synchronized(readyLock) {
Util.wait(readyLock, 1000);
}
}
});
}
}
/**
* Registers a callback to be run when the BrowserComponent is "ready". The browser component
* is considered to be ready once the onLoad event has been fired on the first page.
* If this method is called after the browser component is already "ready", then the callback
* will be executed immediately. Otherwise it will be called in the first onLoad event.
* @param onReady Callback to be executed when the browser component is ready.
* @return Self for chaining.
* @since 7.0
* @see #waitForReady()
*/
public BrowserComponent ready(final SuccessCallback onReady) {
if (ready) {
onReady.onSucess(this);
} else {
ActionListener l = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
removeWebEventListener(onStart, this);
onReady.onSucess(BrowserComponent.this);
}
};
addWebEventListener(onStart, l);
}
return this;
}
/**
* Returns a promise that will complete when the browser component is "ready". It is considered to be
* ready once it has received the start or load event from at least one page. Default timeout is 5000ms.
* @return AsyncResouce that will complete when the browser component is ready.
* @since 7.0
*/
public AsyncResource ready() {
return ready(5000);
}
/**
* Returns a promise that will complete when the browser component is "ready". It is considered to be
* ready once it has received the start or load event from at least one page.
* @return AsyncResouce that will complete when the browser component is ready.
* @param timeout Timeout in milliseconds to wait.
* @since 7.0
*/
public AsyncResource ready(int timeout) {
final AsyncResource out = new AsyncResource();
if (ready) {
out.complete(this);
} else {
class LoadWrapper {
Timer timer;
ActionListener l;
}
final LoadWrapper w = new LoadWrapper();
if (timeout > 0) {
w.timer = CN.setTimeout(timeout, new Runnable(){
public void run() {
w.timer = null;
if (w.l != null) {
removeWebEventListener(onStart, w.l);
removeWebEventListener(onLoad, w.l);
}
if (!out.isDone()) {
out.error(new RuntimeException("Timeout exceeded waiting for browser component to be ready"));
}
}
});
}
w.l = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
w.l = null;
if (w.timer != null) {
w.timer.cancel();
w.timer = null;
}
removeWebEventListener(onStart, this);
removeWebEventListener(onLoad, this);
if (!out.isDone()) {
out.complete(BrowserComponent.this);
}
}
};
addWebEventListener(onStart, w.l);
addWebEventListener(onLoad, w.l);
}
return out;
}
/**
* Returns true if the platform supports embedding a native browser component
*
* @return true if native browsing is supported
*/
public static boolean isNativeBrowserSupported() {
return Display.impl.isNativeBrowserComponentSupported();
}
/**
* This method allows customizing the properties of a web view in various ways including platform specific settings.
* When a property isn't supported by a specific platform it is just ignored.
*
* @param key see the documentation with the CodenameOne Implementation for further details
* @param value see the documentation with the CodenameOne Implementation for further details
*/
public void setProperty(final String key, final Object value) {
if (internal == null) {
onReady(new Runnable() {
public void run() {
setProperty(key, value);
}
});
return;
}
Display.impl.setBrowserProperty(internal, key, value);
}
/**
* The page title
* @return the title
*/
public String getTitle() {
if (internal == null) {
return null;
}
return Display.impl.getBrowserTitle(internal);
}
/**
* The page URL
* @return the URL
*/
public String getURL() {
if (internal == null) {
return tmpUrl;
}
return Display.impl.getBrowserURL(internal);
}
private String tmpUrl;
/**
* Sets the page URL, jar: URL's must be supported by the implementation
* @param url the URL
*/
public void setURL(final String url) {
if (internal == null) {
tmpUrl = url;
onReady(new Runnable() {
public void run() {
setURL(url);
}
});
return;
}
Display.impl.setBrowserURL(internal, url);
}
/**
* Sets the page URL.
* @param url The URL to the page to display.
*/
public void setURL(URL url) {
setURL(url.toString());
}
/**
* Sets the page URL.
* @param uri URI to the page to display.
*/
public void setURL(URI uri) {
setURL(uri.toString());
}
/**
* Sets the page URL, jar: URL's must be supported by the implementation. Notice this API isn't supported
* in all platforms see {@link #isURLWithCustomHeadersSupported() }
* @param url the URL
* @param headers headers to push into the request for the url
*/
public void setURL(final String url, final Map headers) {
if (internal == null) {
tmpUrl = url;
onReady(new Runnable() {
public void run() {
setURL(url, headers);
}
});
return;
}
Display.impl.setBrowserURL(internal, url, headers);
}
/**
* Returns true if the method {@link #setURL(java.lang.String, java.util.Map) } is supported
* @return false by default
*/
public boolean isURLWithCustomHeadersSupported() {
return Display.impl.isURLWithCustomHeadersSupported();
}
/**
* Sets the page URL while respecting the hierarchy of the html
* @param url the URL
*/
public void setURLHierarchy(final String url) throws IOException {
if (internal == null) {
onReady(new Runnable() {
public void run() {
try {
setURLHierarchy(url);
} catch (IOException ex) {
Log.e(ex);
}
}
});
return;
}
Display.impl.setBrowserPageInHierarchy(internal, url);
}
/**
* Reload the current page
*/
public void reload() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
reload();
}
});
return;
}
Display.impl.browserReload(internal);
}
/**
* Indicates whether back is currently available
* @return true if back should work
*/
public boolean hasBack() {
if (internal == null) {
return false;
}
return Display.impl.browserHasBack(internal);
}
/**
* Indicates whether forward is currently available
* @return true if forward should work
*/
public boolean hasForward() {
if (internal == null) {
return false;
}
return Display.impl.browserHasForward(internal);
}
/**
* Navigates back in the history
*/
public void back() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
back();
}
});
return;
}
Display.impl.browserBack(internal);
}
/**
* Navigates forward in the history
*/
public void forward() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
forward();
}
});
return;
}
Display.impl.browserForward(internal);
}
/**
* Clears navigation history
*/
public void clearHistory() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
clearHistory();
}
});
return;
}
Display.impl.browserClearHistory(internal);
}
/**
* Some platforms require that you enable pinch to zoom explicitly. This method has no
* effect if pinch to zoom isn't supported by the platform
*
* @param e true to enable pinch to zoom, false to disable it
*/
public void setPinchToZoomEnabled(final boolean e) {
pinchToZoom = e;
if (internal == null) {
onReady(new Runnable() {
public void run() {
setPinchToZoomEnabled(e);
}
});
return;
}
Display.impl.setPinchToZoomEnabled(internal, e);
}
/**
* This method is unreliable and is only here for consistency with setPinchToZoomEnabled,
* it will not return whether the platform supports pinch since this is very hard to detect
* properly.
* @return the last value for setPinchToZoomEnabled
*/
public boolean isPinchToZoomEnabled() {
return pinchToZoom;
}
/**
* This flag allows disabling the native browser scrolling on platforms that support it
* @param b true to enable native scrolling, notice that non-native scrolling might be problematic
*/
public void setNativeScrollingEnabled(final boolean b) {
nativeScrolling = b;
if (internal == null) {
onReady(new Runnable() {
public void run() {
setNativeScrollingEnabled(b);
}
});
return;
}
Display.impl.setNativeBrowserScrollingEnabled(internal, b);
}
/**
* This method is unreliable and is only here for consistency with setNativeScrollingEnabled.
*
* @return the last value for setNativeScrollingEnabled
*/
public boolean isNativeScrollingEnabled() {
return nativeScrolling;
}
/**
* Shows the given HTML in the native viewer
*
* @param html HTML web page
* @param baseUrl base URL to associate with the HTML
*/
public void setPage(final String html, final String baseUrl) {
if (internal == null) {
onReady(new Runnable() {
public void run() {
setPage(html, baseUrl);
}
});
return;
}
Display.impl.setBrowserPage(internal, html, baseUrl);
}
private EventDispatcher getEventDispatcher(String type, boolean autoCreate) {
if(listeners == null) {
if(!autoCreate) {
return null;
}
listeners = new Hashtable();
EventDispatcher ev = new EventDispatcher();
listeners.put(type, ev);
return ev;
}
EventDispatcher ev = (EventDispatcher)listeners.get(type);
if(ev == null) {
if(autoCreate) {
ev = new EventDispatcher();
listeners.put(type, ev);
}
}
return ev;
}
/**
* Adds a listener to the given event type name, event type names are platform specific but some
* must be fired for all platforms and will invoke the action listener when the appropriate event loads
*
* @param type platform specific but must support: onStart, onLoad, onError
* @param listener callback for the event
*/
public void addWebEventListener(String type, ActionListener listener) {
getEventDispatcher(type, true).addListener(listener);
}
/**
* Removes the listener, see addWebEventListener for details
*
* @param type see addWebEventListener for details
* @param listener see addWebEventListener for details
*/
public void removeWebEventListener(String type, ActionListener listener) {
EventDispatcher e = getEventDispatcher(type, false);
if(e != null) {
e.removeListener(listener);
if(!e.hasListeners()) {
listeners.remove(type);
}
}
}
/**
* Cancel the loading of the current page
*/
public void stop() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
stop();
}
});
return;
}
Display.impl.browserStop(internal);
}
/**
* Release native resources of this Browser Component
*/
public void destroy() {
if (internal == null) {
onReady(new Runnable() {
public void run() {
destroy();
}
});
return;
}
Display.impl.browserDestroy(internal);
}
/**
* Used internally by the implementation to fire an event from the native browser widget
*
* @param type the type of the event
* @param ev the event
*/
public void fireWebEvent(String type, ActionEvent ev) {
if (onLoad.equals(type)) {
synchronized(readyLock) {
ready = true;
readyLock.notifyAll();
}
}
EventDispatcher e = getEventDispatcher(type, false);
if(e != null) {
e.fireActionEvent(ev);
}
}
/**
* Executes the given JavaScript string within the current context
*
* @param javaScript the JavaScript string
*/
public void execute(final String javaScript) {
if (internal == null) {
onReady(new Runnable() {
public void run() {
execute(javaScript);
}
});
return;
}
Display.impl.browserExecute(internal, javaScript);
}
/**
* Executes given javascript string within current context.
* @param js The javascript to execute.
* @param params Parameters to inject into the javascript expression. The expression should contain placeholders of the form {@literal ${0} }, {@literal ${1} }, etc... to be replaced. See {@link #injectParameters(java.lang.String, java.lang.Object...) } for more information about injected parameters.
* by parameters.
*/
public void execute(String js, Object[] params) {
execute(injectParameters(js, params));
}
/**
* Executes the given JavaScript and returns a result string from the underlying platform
* where applicable.
* Note: Some platforms use {@link Display#invokeAndBlock(java.lang.Runnable) } inside this method which is very costly. Try to avoid this synchronous method, and
* prefer to use one of the asynchronous versions. E.g. {@link #execute(java.lang.String, com.codename1.util.SuccessCallback) }
* @param javaScript the JavaScript code to execute
* @return the string returned from the Javascript call
*/
public String executeAndReturnString(String javaScript){
if (internal == null) {
while (internal == null) {
CN.invokeAndBlock(new Runnable() {
public void run() {
Util.sleep(50);
}
});
}
}
if (Display.impl.supportsBrowserExecuteAndReturnString(internal)) {
return Display.impl.browserExecuteAndReturnString(internal, javaScript);
} else {
return executeAndWait("callback.onSuccess(eval(${0}))", new Object[]{javaScript}).toString();
}
}
/**
* Executes the given javascript and returns the result string from the underlying platform.
* Note: Some platforms use {@link Display#invokeAndBlock(java.lang.Runnable) } inside this method which is very costly. Try to avoid this synchronous method, and
* prefer to use one of the asynchronous versions. E.g. {@link #execute(java.lang.String, com.codename1.util.SuccessCallback) }
* @param javaScript The javascript to execute.
* @param params Parameters to inject into the javascript expression. The expression should contain placeholders of the form {@literal ${0} }, {@literal ${1} }, etc... to be replaced. See {@link #injectParameters(java.lang.String, java.lang.Object...) } for more information about injected parameters.
* @return The result as a string.
* @since 5.0
*/
public String executeAndReturnString(String javaScript, Object[] params) {
return executeAndReturnString(injectParameters(javaScript, params));
}
/**
* Creates a proxy for a Javascript object that makes it easier to call methods, retrieve,
* and manipulate properties on the object.
*/
public JSProxy createJSProxy(String javascriptExpression) {
return new JSProxy(javascriptExpression);
}
/**
* A thin wrapper around a Javascript variable that makes it easier to
* call methods on that variable.
*/
public class JSProxy {
/**
The javascript variable name. This can be any javascript expression that resolves
* to an object.
*/
private final String self;
/**
* Creats a new proxy.
* @param self The javascript expression that should resolve to the object that this
* will proxy. E.g. "window", or "document.getElementById('mybutton')". The expression
* is just stored as a string and is 'resolved' when calls are made on the proxy.
*/
private JSProxy(String self) {
this.self = self;
}
/**
* Calls a method on this javascript object.
* @param timeout The timeout in ms
* @param method The method name.
* @param args Arguments to pass to the method.
* @param callback Callback with the result of the method.
*/
public void call(int timeout, String method, Object[] args, SuccessCallback callback) {
StringBuilder js = new StringBuilder();
js.append("callback.onSuccess("+self+"."+method+"(");
int len = args.length;
for (int i=0; i 0) {
js.append(", ");
}
js.append("${"+i+"}");
}
js.append("))");
execute(js.toString(), args, callback);
}
/**
* Calls a method on this javascript object.
* @param method The method name.
* @param args Arguments to pass to the method.
* @param callback Callback with the result of the method.
*/
public void call(String method, Object[] args, SuccessCallback callback) {
call(0, method, args, callback);
}
/**
* Calls method on this javascript object and waits for the result using
* invokeAndBlock.
* @param timeout The timeout in ms
* @param method The method name.
* @param args Arguments for the method.
* @return JSRef with the result of the method call.
*/
public JSRef callAndWait(int timeout, String method, Object[] args) {
StringBuilder js = new StringBuilder();
js.append("callback.onSuccess("+self+"."+method+"(");
int len = args.length;
for (int i=0; i 0) {
js.append(", ");
}
js.append("${"+i+"}");
}
js.append("))");
return executeAndWait(timeout, js.toString(), args);
}
/**
* Calls method on this javascript object and waits for the result using
* invokeAndBlock.
* @param method The method name.
* @param args Arguments for the method.
* @return JSRef with the result of the method call.
*/
public JSRef callAndWait(String method, Object[] args) {
return callAndWait(0, method, args);
}
/**
* Gets a property of this javascript object.
* @param timeout Timeout in ms
* @param property The property name.
* @param callback Callback with the property value.
*/
public void get(int timeout, String property, SuccessCallback callback) {
StringBuilder js = new StringBuilder();
js.append("callback.onSuccess("+self+"."+property+")");
execute(timeout, js.toString(), callback);
}
/**
* Gets a property of this javascript object.
* @param property The property name.
* @param callback Callback with the property value.
*/
public void get(String property, SuccessCallback callback) {
get(0, property, callback);
}
/**
* Gets multiple properties as a batch.
* @param timeout Timeout in ms
* @param properties List of property names to retrieve.
* @param callback
*/
public void get(int timeout, Collection properties, final SuccessCallback