/*
* Copyright (c) 2012, Codename One 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. Codename One 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 Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/
package com.codename1.impl.javase;
import com.codename1.io.Log;
import com.codename1.ui.*;
import com.codename1.ui.events.ActionEvent;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.event.EventHandler;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebErrorEvent;
import javafx.scene.web.WebEvent;
import javafx.scene.web.WebView;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.SwingUtilities;
import javax.swing.event.MenuDragMouseEvent;
/**
*
* @author Chen
*/
public class SEBrowserComponent extends PeerComponent {
private static boolean firstTime = true;
private WebView web;
private javafx.embed.swing.JFXPanel panel;
private JPanel frm;
private JavaSEPort instance;
private String currentURL;
private boolean init = false;
private JPanel cnt;
private boolean lightweightMode;
private boolean lightweightModeSet;
private final JScrollBar hSelector, vSelector;
private AdjustmentListener adjustmentListener;
private BrowserComponent browserComp;
/**
* A bridge to inject java methods into the webview.
*/
public class Bridge {
/**
* A method injected into the webview to provide direct access to the getBrowserNavigationCallback
* that is registered. This is kind of a workaround since there doesn't seem to be any way
* to prevent the webview from loading a URL that is set via window.location.href and we need
* the browser navigation callback to be executed for the Javascript bridge to work. So we inject this
* here, and the JavascriptContext checks for this hook when trying to send messages.
* @param url
* @return
*/
public boolean shouldNavigate(String url) {
if (browserComp.getBrowserNavigationCallback() != null) {
return browserComp.getBrowserNavigationCallback().shouldNavigate(url);
}
return true;
}
}
public SEBrowserComponent(JavaSEPort instance, JPanel f, javafx.embed.swing.JFXPanel fx, final WebView web, final BrowserComponent p, final JScrollBar hSelector, JScrollBar vSelector) {
super(null);
this.web = web;
this.instance = instance;
this.frm = f;
this.panel = fx;
final JavaSEPort inst = instance;
browserComp = p;
WebEngine we = web.getEngine();
try {
Method mtd = we.getClass().getMethod("setUserDataDirectory",java.io.File.class);
mtd.invoke(we, new File(JavaSEPort.getAppHomeDir()));
} catch(Throwable t) {
System.out.println("It looks like you are running on a version of Java older than Java 8. We recommend upgrading");
t.printStackTrace();
}
we.setOnError(new EventHandler<WebErrorEvent>() {
@Override
public void handle(WebErrorEvent event) {
Log.p("WebError: " + event.toString());
}
});
SwingUtilities.invokeLater(new Runnable() {
public void run() {
cnt = new JPanel() {
public void paint(java.awt.Graphics g) {
// We want the native component to be hidden unless
// it is being drawn by Codename One via drawNativePeer()
// This allows the component to be present (respond to events)
// but gives us the flexibility to draw the component
// at the correct depth with the rest of the Codename One
// components.
if (SEBrowserComponent.this.instance.drawingNativePeer) {
super.paint(g);
} else {
}
}
};
cnt.setOpaque(false); // <--- Important if container is opaque it will cause
// all kinds of flicker due to painting conflicts with CN1 pipeline.
cnt.setLayout(new BorderLayout());
cnt.add(BorderLayout.CENTER, panel);
//cnt.setVisible(false);
}
});
web.getEngine().getLoadWorker().messageProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> ov, String t, String t1) {
if (t1.startsWith("Loading http:") || t1.startsWith("Loading file:") || t1.startsWith("Loading https:")) {
String url = t1.substring("Loading ".length());
if (!url.equals(currentURL)) {
p.fireWebEvent("onStart", new ActionEvent(url));
}
currentURL = url;
} else if ("Loading complete".equals(t1)) {
}
}
});
web.getEngine().setOnAlert(new EventHandler<WebEvent<String>>() {
@Override
public void handle(WebEvent<String> t) {
String msg = t.getData();
if (msg.startsWith("!cn1_message:")) {
System.out.println("Receiving message "+msg);
p.fireWebEvent("onMessage", new ActionEvent(msg.substring("!cn1_message:".length())));
}
}
});
web.getEngine().getLoadWorker().exceptionProperty().addListener(new ChangeListener<Throwable>() {
@Override
public void changed(ObservableValue<? extends Throwable> ov, Throwable t, Throwable t1) {
System.out.println("Received exception: "+t1.getMessage());
if (ov.getValue() != null) {
ov.getValue().printStackTrace();
}
if (t != ov.getValue() && t != null) {
t.printStackTrace();
}
if (t1 != ov.getValue() && t1 != t && t1 != null) {
t.printStackTrace();
}
}
});
web.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
@Override
public void changed(ObservableValue ov, State oldState, State newState) {
String url = web.getEngine().getLocation();
if (newState == State.SCHEDULED) {
p.fireWebEvent("onStart", new ActionEvent(url));
} else if (newState == State.RUNNING) {
p.fireWebEvent("onLoadResource", new ActionEvent(url));
} else if (newState == State.SUCCEEDED) {
if (!p.isNativeScrollingEnabled()) {
web.getEngine().executeScript("document.body.style.overflow='hidden'");
}
// Since I end of injecting firebug nearly every time I have to do some javascript code
// let's just add a client property to the BrowserComponent to enable firebug
if (Boolean.TRUE.equals(p.getClientProperty("BrowserComponent.firebug"))) {
web.getEngine().executeScript("if (!document.getElementById('FirebugLite')){E = document['createElement' + 'NS'] && document.documentElement.namespaceURI;E = E ? document['createElement' + 'NS'](E, 'script') : document['createElement']('script');E['setAttribute']('id', 'FirebugLite');E['setAttribute']('src', 'https://getfirebug.com/' + 'firebug-lite.js' + '#startOpened');E['setAttribute']('FirebugLite', '4');(document['getElementsByTagName']('head')[0] || document['getElementsByTagName']('body')[0]).appendChild(E);E = new Image;E['setAttribute']('src', 'https://getfirebug.com/' + '#startOpened');}");
}
netscape.javascript.JSObject window = (netscape.javascript.JSObject)web.getEngine().executeScript("window");
window.setMember("cn1application", new Bridge());
//web.getEngine().executeScript("window.addEventListener('unload', function(e){console.log('unloading...');return 'foobar';});");
p.fireWebEvent("onLoad", new ActionEvent(url));
}
currentURL = url;
repaint();
}
});
web.getEngine().getLoadWorker().exceptionProperty().addListener(new ChangeListener<Throwable>() {
@Override
public void changed(ObservableValue<? extends Throwable> ov, Throwable t, Throwable t1) {
t1.printStackTrace();
if(t1 == null) {
if(t == null) {
p.fireWebEvent("onError", new ActionEvent("Unknown error", -1));
} else {
p.fireWebEvent("onError", new ActionEvent(t.getMessage(), -1));
}
} else {
p.fireWebEvent("onError", new ActionEvent(t1.getMessage(), -1));
}
}
});
// Monitor the location property so that we can send the shouldLoadURL event.
// This allows us to cancel the loading of a URL if we want to handle it ourself.
web.getEngine().locationProperty().addListener(new ChangeListener<String>(){
@Override
public void changed(ObservableValue<? extends String> prop, String before, String after) {
if ( !p.getBrowserNavigationCallback().shouldNavigate(web.getEngine().getLocation()) ){
web.getEngine().getLoadWorker().cancel();
}
}
});
adjustmentListener = new AdjustmentListener() {
@Override
public void adjustmentValueChanged(AdjustmentEvent e) {
Display.getInstance().callSerially(new Runnable() {
public void run() {
onPositionSizeChange();
}
});
}
};
this.hSelector = hSelector;
this.vSelector = vSelector;
}
/**
* Executes a javascript string and returns the result as a String if
* appropriate.
* @param js
* @return
*/
public String executeAndReturnString(final String js){
final String[] result = new String[1];
final boolean[] complete = new boolean[]{false};
Platform.runLater(new Runnable() {
public void run() {
result[0] = ""+web.getEngine().executeScript(js);
synchronized(complete){
complete[0] = true;
complete.notify();
}
}
});
// We need to wait for the result of the javascript operation
// but we don't want to block the entire EDT, so we use invokeAndBlock
Display.getInstance().invokeAndBlock(new Runnable(){
public void run() {
while ( !complete[0] ){
synchronized(complete){
try {
complete.wait(20);
} catch (InterruptedException ex) {
}
}
}
}
});
return result[0];
}
public void execute(final String js) {
Platform.runLater(new Runnable() {
public void run() {
web.getEngine().executeScript(js);
}
});
}
private static final Object DEINIT_LOCK = new Object();
@Override
protected void deinitialize() {
//lightweightMode = true;
final boolean[] complete = new boolean[1];
synchronized(imageLock) {
peerImage = new BufferedImage(cnt.getWidth(), cnt.getHeight(), BufferedImage.TYPE_INT_ARGB);
System.out.println("PI width: "+peerImage.getWidth()+ "PI height "+peerImage.getHeight());
System.out.println("Creating peer image");
Graphics2D imageG = (Graphics2D)peerImage.createGraphics();
try {
instance.drawingNativePeer = true;
cnt.paint(imageG);
} catch (Exception ex){
} finally {
instance.drawingNativePeer = false;
imageG.dispose();
}
}
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (init) {
hSelector.removeAdjustmentListener(adjustmentListener);
vSelector.removeAdjustmentListener(adjustmentListener);
lastX=0;
lastY=0;
lastW=0;
lastH=0;
frm.remove(cnt);
init = false;
complete[0] = true;
frm.repaint();
//SEBrowserComponent.this.repaint();
}
synchronized(DEINIT_LOCK) {
DEINIT_LOCK.notify();
}
}
});
Display.getInstance().invokeAndBlock(new Runnable() {
public void run() {
while(!complete[0]) {
synchronized(DEINIT_LOCK) {
try {
DEINIT_LOCK.wait(20);
} catch(InterruptedException er) {}
}
}
}
});
super.deinitialize();
}
@Override
protected void initComponent() {
super.initComponent(); //To change body of generated methods, choose Tools | Templates.
init();
}
private static final Object INIT_LOCK = new Object();
private final Object imageLock = new Object();
private BufferedImage peerImage;
private void init() {
final boolean[] completed = new boolean[1];
synchronized(imageLock) {
peerImage = null;
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (!init) {
init = true;
frm.add(cnt, 0);
completed[0] = true;
synchronized (INIT_LOCK) {
INIT_LOCK.notify();
}
onPositionSizeChange();
hSelector.addAdjustmentListener(adjustmentListener);
vSelector.addAdjustmentListener(adjustmentListener);
frm.repaint();
SEBrowserComponent.this.repaint();
}
}
});
Display.getInstance().invokeAndBlock(new Runnable() {
public void run() {
while (!completed[0]) {
synchronized (INIT_LOCK) {
try {
INIT_LOCK.wait(20);
} catch (InterruptedException er) {
}
}
}
}
});
}
protected void setLightweightMode(final boolean l) {
}
protected boolean shouldRenderPeerImage() {
return false;
}
@Override
protected com.codename1.ui.geom.Dimension calcPreferredSize() {
return new com.codename1.ui.geom.Dimension((int) web.getWidth(), (int) web.getHeight());
}
int lastX, lastY, lastW, lastH;
double lastZoom;
@Override
protected void onPositionSizeChange() {
if(cnt == null) {
return;
}
Form f = getComponentForm();
if(cnt.getParent() == null &&
f != null &&
Display.getInstance().getCurrent() == f){
//();
return;
}
final int x = getAbsoluteX();
final int y = getAbsoluteY();
final int w = getWidth();
final int h = getHeight();
if (lastZoom == instance.zoomLevel && x==lastX && y==lastY && w==lastW && h==lastH) {
return;
}
lastX = x;
lastY=y;
lastW=w;
lastH=h;
lastZoom = instance.zoomLevel;
Runnable r = new Runnable() {
@Override
public void run() {
Platform.runLater(new Runnable() {
public void run() {
try {
Method setZoom = web.getClass().getMethod("setZoom", new Class[]{double.class});
setZoom.invoke(web, instance.zoomLevel);
} catch (NoSuchMethodException ex) {
Logger.getLogger(SEBrowserComponent.class.getName()).log(Level.SEVERE, null, ex);
} catch (SecurityException ex) {
Logger.getLogger(SEBrowserComponent.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalAccessException ex) {
Logger.getLogger(SEBrowserComponent.class.getName()).log(Level.SEVERE, null, ex);
} catch (IllegalArgumentException ex) {
Logger.getLogger(SEBrowserComponent.class.getName()).log(Level.SEVERE, null, ex);
} catch (InvocationTargetException ex) {
Logger.getLogger(SEBrowserComponent.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
cnt.setBounds((int) ((x + getScreenCoordinateX() + instance.canvas.x) * instance.zoomLevel),
(int) ((y + getScreenCoordinateY() + instance.canvas.y) * instance.zoomLevel),
(int) (w * instance.zoomLevel),
(int) (h * instance.zoomLevel));
cnt.validate();
}
};
if(SwingUtilities.isEventDispatchThread()) {
r.run();
return;
}
SwingUtilities.invokeLater(r);
}
int getScreenCoordinateX() {
Rectangle r = instance.getScreenCoordinates();
if(r == null) {
return 0;
}
return r.x;
}
int getScreenCoordinateY() {
Rectangle r = instance.getScreenCoordinates();
if(r == null) {
return 0;
}
return r.y;
}
void setProperty(String key, Object value) {
}
String getTitle() {
return web.getEngine().getTitle();
}
String getURL() {
return web.getEngine().getLocation();
}
void setURL(String url) {
web.getEngine().load(url);
}
void stop() {
}
void reload() {
web.getEngine().reload();
}
boolean hasBack() {
return web.getEngine().getHistory().getCurrentIndex() > 0;
}
boolean hasForward() {
return web.getEngine().getHistory().getCurrentIndex() < web.getEngine().getHistory().getMaxSize() - 1;
}
void back() {
web.getEngine().getHistory().go(-1);
}
void forward() {
web.getEngine().getHistory().go(1);
}
void clearHistory() {
}
void setPage(String html, String baseUrl) {
web.getEngine().loadContent(html);
repaint();
}
void exposeInJavaScript(Object o, String name) {
}
@Override
public void paint(Graphics g) {
if (!init) {
synchronized(imageLock) {
if (peerImage != null) {
Object nativeGraphics = Accessor.getNativeGraphics(g);
Graphics2D g2 = (Graphics2D)instance.getGraphics(nativeGraphics).create();
try {
g2.translate(getAbsoluteX(), getAbsoluteY());
if (instance.zoomLevel != 1) {
g2.scale(1/instance.zoomLevel, 1/instance.zoomLevel);
} else if (instance.takingScreenshot && instance.screenshotActualZoomLevel != 1) {
g2.scale(1/instance.screenshotActualZoomLevel, 1/instance.screenshotActualZoomLevel);
}
g2.drawImage(peerImage, 0,0, null);
} finally {
g2.dispose();
}
return;
}
}
}
instance.drawNativePeer(Accessor.getNativeGraphics(g), this, cnt);
onPositionSizeChange();
}
}