/*******************************************************************************
* Copyright (c) 2004, 2012 IBM Corporation and others.
* Copyright (c) 2014 Pivotal Software, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - Initial API and implementation
* Jacek Pospychala - jacek.pospychala@pl.ibm.com - fix for bug 224887
* Kris De Volder - Renamed to 'STSBrowserViewer and
* modified to use as browser for STS dashboard.
* Miles Parker - Re-purposed Kris' STS wrapper into a JavaFx based implementation.
*******************************************************************************/
package org.springsource.ide.eclipse.commons.browser.javafx;
//Most of this code copied from org.eclipse.ui.internal.browser.BrowserViewer
// modified to reuse as a browser for STS dashboard.
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker.State;
import javafx.embed.swt.FXCanvas;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.PopupFeatures;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.util.Callback;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.internal.browser.BusyIndicator;
import org.eclipse.ui.internal.browser.IBrowserViewerContainer;
import org.eclipse.ui.internal.browser.ImageResource;
import org.eclipse.ui.internal.browser.Messages;
import org.eclipse.ui.internal.browser.ToolbarLayout;
import org.eclipse.ui.internal.browser.WebBrowserPreference;
import org.springsource.ide.eclipse.commons.browser.BrowserImages;
import org.springsource.ide.eclipse.commons.frameworks.core.util.Gtk3Check;
/**
* A Web browser widget. It provides a JavaFx WebView and adds an optional
* toolbar complete with a URL combo box, history, back & forward, and refresh
* buttons.
* <p>
* Use the style bits to choose which toolbars are available within the browser
* composite. You can access the embedded SWT Browser directly using the
* getBrowser() method.
* </p>
* <p>
* Additional capabilities are available when used as the internal Web browser,
* including status text and progress on the Eclipse window's status line, or
* moving the toolbar capabilities up into the main toolbar.
* </p>
* <dl>
* <dt><b>Styles:</b></dt>
* <dd>LOCATION_BAR, BUTTON_BAR</dd>
* <dt><b>Events:</b></dt>
* <dd>None</dd>
* </dl>
*
* @author Kris De Volder
* @author Miles Parker
*
* @since 1.0
*/
@SuppressWarnings("restriction")
public class JavaFxBrowserViewer extends Composite {
static {
if (Gtk3Check.isGTK3) {
throw new UnsupportedOperationException("JavaFX doesn't work with GTK3");
}
}
/**
* Style parameter (value 1) indicating that the URL and Go button will be
* on the local toolbar.
*/
public static final int LOCATION_BAR = 1 << 1;
/**
* Style parameter (value 2) indicating that the toolbar will be available
* on the web browser. This style parameter cannot be used without the
* LOCATION_BAR style.
*/
public static final int BUTTON_BAR = 1 << 2;
protected static final String PROPERTY_TITLE = "title"; //$NON-NLS-1$
private static final int MAX_HISTORY = 50;
public Clipboard clipboard;
public Combo combo;
protected boolean showToolbar;
protected boolean showURLbar;
protected ToolItem back;
protected ToolItem forward;
protected BusyIndicator busy;
protected boolean loading;
protected static java.util.List<String> history;
protected WebView browser;
protected boolean newWindow;
protected IBrowserViewerContainer container;
protected String title;
protected List<PropertyChangeListener> propertyListeners;
private String initialUrl;
/**
* Under development - do not use
*/
public static interface IBackNextListener {
public void updateBackNextBusy();
}
public IBackNextListener backNextListener;
private String homeUrl;
/**
* Creates a new Web browser given its parent and a style value describing
* its behavior and appearance.
* <p>
* The style value is either one of the style constants defined in the class
* header or class <code>SWT</code> which is applicable to instances of this
* class, or must be built by <em>bitwise OR</em>'ing together (that is,
* using the <code>int</code> "|" operator) two or more of those
* <code>SWT</code> style constants. The class description lists the style
* constants that are applicable to the class. Style bits are also inherited
* from superclasses.
* </p>
*
* @param parent a composite control which will be the parent of the new
* instance (cannot be null)
* @param style the style of control to construct
*/
public JavaFxBrowserViewer(Composite parent, int style) {
super(parent, SWT.NONE);
if ((style & LOCATION_BAR) != 0) {
showURLbar = true;
}
if ((style & BUTTON_BAR) != 0) {
showToolbar = true;
}
GridLayout layout = new GridLayout();
layout.marginHeight = 0;
layout.marginWidth = 0;
layout.horizontalSpacing = 0;
layout.verticalSpacing = 0;
layout.numColumns = 1;
setLayout(layout);
clipboard = new Clipboard(parent.getDisplay());
if (showToolbar || showURLbar) {
Composite toolbarComp = new Composite(this, SWT.NONE);
toolbarComp.setLayout(new ToolbarLayout());
toolbarComp.setLayoutData(new GridData(GridData.VERTICAL_ALIGN_BEGINNING | GridData.FILL_HORIZONTAL));
if (showToolbar) {
createToolbar(toolbarComp);
}
if (showURLbar) {
createLocationBar(toolbarComp);
}
if (showToolbar | showURLbar) {
busy = new BusyIndicator(toolbarComp, SWT.NONE);
busy.setLayoutData(new GridData(GridData.HORIZONTAL_ALIGN_END));
busy.addMouseListener(new MouseListener() {
public void mouseDoubleClick(MouseEvent e) {
// ignore
}
public void mouseDown(MouseEvent e) {
goHome();
}
public void mouseUp(MouseEvent e) {
// ignore
}
});
}
}
// Without this, the JavaFx app will dispose itself the first time the
// dashboard closes. See
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=422258#c3
Platform.setImplicitExit(false);
final FXCanvas fxCanvas = new FXCanvas(this, SWT.NONE);
fxCanvas.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).align(SWT.FILL, SWT.FILL).create());
fxCanvas.setLayout(GridLayoutFactory.fillDefaults().create());
browser = new WebView();
browser.setVisible(false);
BorderPane border = new BorderPane();
Scene scene = new Scene(border);
border.setCenter(browser);
fxCanvas.setScene(scene);
if (showURLbar) {
updateHistory();
}
if (showToolbar) {
updateBackNextBusy();
}
addBrowserListeners();
if (initialUrl != null) {
browser.getEngine().load(initialUrl);
}
}
/**
* Returns the underlying SWT browser widget.
*
* @return the underlying browser
*/
public WebView getBrowser() {
return browser;
}
public String getHomeUrl() {
return homeUrl;
}
public void setHomeUrl(String homeUrl) {
this.homeUrl = homeUrl;
}
/**
* Navigate to the home URL.
*/
public void goHome() {
if (homeUrl != null) {
browser.getEngine().load(homeUrl);
}
else {
browser.getEngine().load("");
}
}
/**
* Loads a URL.
*
* @param url the URL to be loaded
* @return true if the operation was successful and false otherwise.
* @exception IllegalArgumentException <ul>
* <li>ERROR_NULL_ARGUMENT - if the url is null</li>
* </ul>
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
* @see #getURL()
*/
public void setURL(String url) {
setURL(url, true);
}
protected void updateBackNextBusy() {
if (!back.isDisposed()) {
back.setEnabled(isBackEnabled());
}
if (!forward.isDisposed()) {
forward.setEnabled(isForwardEnabled());
}
if (!busy.isDisposed()) {
busy.setBusy(loading);
}
if (backNextListener != null) {
backNextListener.updateBackNextBusy();
}
}
/**
*
*/
private void addBrowserListeners() {
if (browser == null) {
return;
}
// Add listener for new window creation so that we can instead of
// opening a separate
// new window in which the session is lost, we can instead open a new
// window in a new
// shell within the browser area thereby maintaining the session.
browser.getEngine().setCreatePopupHandler(new Callback<PopupFeatures, WebEngine>() {
@Override
public WebEngine call(PopupFeatures config) {
Shell shell2 = new Shell(getShell(), SWT.SHELL_TRIM);
shell2.setLayout(new FillLayout());
shell2.setText(Messages.viewWebBrowserTitle);
shell2.setImage(getShell().getImage());
int style = 0;
if (showURLbar) {
style += LOCATION_BAR;
}
if (showToolbar) {
style += BUTTON_BAR;
}
JavaFxBrowserViewer browser2 = new JavaFxBrowserViewer(shell2, style);
browser2.setVisible(true);
browser2.newWindow = true;
return browser2.getBrowser().getEngine();
}
});
browser.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
@Override
public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
int progressWorked = (int) (browser.getEngine().getLoadWorker().getProgress() * 100.0);
boolean done = newState == State.SUCCEEDED || newState == State.FAILED;
if (container != null) {
IProgressMonitor monitor = container.getActionBars().getStatusLineManager().getProgressMonitor();
if (done) {
monitor.done();
}
else if (progressWorked == 0) {
monitor.beginTask("", 100); //$NON-NLS-1$
}
else {
monitor.worked(progressWorked);
}
}
if (showToolbar) {
if (!busy.isBusy() && !done) {
loading = true;
}
else if (busy.isBusy() && done) {
loading = false;
}
updateBackNextBusy();
updateHistory();
}
}
});
if (showURLbar) {
browser.getEngine().locationProperty().addListener(new ChangeListener<String>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
if (combo != null) {
if (!"about:blank".equals(newValue)) { //$NON-NLS-1$
combo.setText(newValue);
addToHistory(newValue);
updateHistory();
}
}
}
});
browser.getEngine().titleProperty().addListener(new ChangeListener<String>() {
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
firePropertyChangeEvent(PROPERTY_TITLE, oldValue, newValue);
}
});
}
}
/**
* Add a property change listener to this instance.
*
* @param listener java.beans.PropertyChangeListener
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
if (propertyListeners == null) {
propertyListeners = new ArrayList<PropertyChangeListener>();
}
propertyListeners.add(listener);
}
/**
* Remove a property change listener from this instance.
*
* @param listener java.beans.PropertyChangeListener
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
if (propertyListeners != null) {
propertyListeners.remove(listener);
}
}
/**
* Fire a property change event.
*/
protected void firePropertyChangeEvent(String propertyName, Object oldValue, Object newValue) {
if (propertyListeners == null) {
return;
}
PropertyChangeEvent event = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
try {
int size = propertyListeners.size();
PropertyChangeListener[] pcl = new PropertyChangeListener[size];
propertyListeners.toArray(pcl);
for (int i = 0; i < size; i++) {
try {
pcl[i].propertyChange(event);
}
catch (Exception e) {
// ignore
}
}
}
catch (Exception e) {
// ignore
}
}
/**
* Navigate to the next session history item. Convenience method that calls
* the underlying SWT browser.
*
* @return <code>true</code> if the operation was successful and
* <code>false</code> otherwise
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
* @see #back
*/
public boolean forward() {
if (isForwardEnabled()) {
browser.getEngine().getHistory().go(1);
return true;
}
else {
return false;
}
}
/**
* Navigate to the previous session history item. Convenience method that
* calls the underlying SWT browser.
*
* @return <code>true</code> if the operation was successful and
* <code>false</code> otherwise
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
* @see #forward
*/
public boolean back() {
if (isBackEnabled()) {
browser.getEngine().getHistory().go(-1);
return true;
}
else {
return false;
}
}
/**
* Returns <code>true</code> if the receiver can navigate to the previous
* session history item, and <code>false</code> otherwise. Convenience
* method that calls the underlying SWT browser.
*
* @return the receiver's back command enabled state
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that
* created the receiver</li>
* </ul>
* @see #back
*/
public boolean isBackEnabled() {
return browser.getEngine().getHistory().getCurrentIndex() > 0;
}
/**
* Returns <code>true</code> if the receiver can navigate to the next
* session history item, and <code>false</code> otherwise. Convenience
* method that calls the underlying SWT browser.
*
* @return the receiver's forward command enabled state
* @exception SWTException <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that
* created the receiver</li>
* </ul>
* @see #forward
*/
public boolean isForwardEnabled() {
return browser.getEngine().getHistory().getCurrentIndex() < browser.getEngine().getHistory().getEntries()
.size() - 1;
}
/**
* Stop any loading and rendering activity. Convenience method that calls
* the underlying SWT browser.
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*/
public void stop() {
if (browser != null) {
browser.getEngine().getLoadWorker().cancel();
}
}
/**
* Refresh the current page. Convenience method that calls the underlying
* SWT browser.
*
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
*/
public void refresh() {
if (browser != null) {
browser.getEngine().reload();
try {
Thread.sleep(50);
}
catch (Exception e) {
// ignore
}
}
}
private void setURL(String url, boolean browse) {
if (initialUrl == null) {
this.initialUrl = url;
}
if (url == null) {
goHome();
return;
}
if ("eclipse".equalsIgnoreCase(url)) {
url = "http://www.eclipse.org"; //$NON-NLS-1$
}
else if ("wtp".equalsIgnoreCase(url)) {
url = "http://www.eclipse.org/webtools/"; //$NON-NLS-1$
}
if (browse) {
if (url != null && url.equals(getURL())) {
refresh();
}
else {
if (browser != null) {
browser.getEngine().load(url);
addToHistory(url);
updateHistory();
}
}
}
}
protected void addToHistory(String url) {
if (history == null) {
history = WebBrowserPreference.getInternalWebBrowserHistory();
}
int found = -1;
int size = history.size();
for (int i = 0; i < size; i++) {
String s = history.get(i);
if (s.equals(url)) {
found = i;
break;
}
}
if (found == -1) {
if (size >= MAX_HISTORY) {
history.remove(size - 1);
}
history.add(0, url);
WebBrowserPreference.setInternalWebBrowserHistory(history);
}
else if (found != 0) {
history.remove(found);
history.add(0, url);
WebBrowserPreference.setInternalWebBrowserHistory(history);
}
}
/**
*
*/
@Override
public void dispose() {
super.dispose();
showToolbar = false;
if (busy != null) {
busy.dispose();
}
busy = null;
if (clipboard != null) {
clipboard.dispose();
}
clipboard = null;
}
protected ToolBar createLocationBar(Composite parent) {
combo = new Combo(parent, SWT.DROP_DOWN);
updateHistory();
combo.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent we) {
try {
if (combo.getSelectionIndex() != -1 && !combo.getListVisible()) {
setURL(combo.getItem(combo.getSelectionIndex()));
}
}
catch (Exception e) {
// ignore
}
}
});
combo.addListener(SWT.DefaultSelection, new Listener() {
public void handleEvent(Event e) {
setURL(combo.getText());
}
});
ToolBar toolbar = new ToolBar(parent, SWT.FLAT);
ToolItem go = new ToolItem(toolbar, SWT.NONE);
go.setImage(ImageResource.getImage(ImageResource.IMG_ELCL_NAV_GO));
go.setHotImage(ImageResource.getImage(ImageResource.IMG_CLCL_NAV_GO));
go.setDisabledImage(ImageResource.getImage(ImageResource.IMG_DLCL_NAV_GO));
go.setToolTipText(Messages.actionWebBrowserGo);
go.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
setURL(combo.getText());
}
});
return toolbar;
}
protected ToolBar createToolbar(Composite parent) {
ToolBar toolbar = new ToolBar(parent, SWT.FLAT);
// create 'home' action
ToolItem home = new ToolItem(toolbar, SWT.NONE);
home.setImage(BrowserImages.getImage(BrowserImages.IMG_NAV_HOME));
home.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
goHome();
}
});
// create back and forward actions
back = new ToolItem(toolbar, SWT.NONE);
back.setImage(ImageResource.getImage(ImageResource.IMG_ELCL_NAV_BACKWARD));
back.setHotImage(ImageResource.getImage(ImageResource.IMG_CLCL_NAV_BACKWARD));
back.setDisabledImage(ImageResource.getImage(ImageResource.IMG_DLCL_NAV_BACKWARD));
back.setToolTipText(Messages.actionWebBrowserBack);
back.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
back();
}
});
forward = new ToolItem(toolbar, SWT.NONE);
forward.setImage(ImageResource.getImage(ImageResource.IMG_ELCL_NAV_FORWARD));
forward.setHotImage(ImageResource.getImage(ImageResource.IMG_CLCL_NAV_FORWARD));
forward.setDisabledImage(ImageResource.getImage(ImageResource.IMG_DLCL_NAV_FORWARD));
forward.setToolTipText(Messages.actionWebBrowserForward);
forward.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
forward();
}
});
// create refresh, stop, and print actions
ToolItem stop = new ToolItem(toolbar, SWT.NONE);
stop.setImage(ImageResource.getImage(ImageResource.IMG_ELCL_NAV_STOP));
stop.setHotImage(ImageResource.getImage(ImageResource.IMG_CLCL_NAV_STOP));
stop.setDisabledImage(ImageResource.getImage(ImageResource.IMG_DLCL_NAV_STOP));
stop.setToolTipText(Messages.actionWebBrowserStop);
stop.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
stop();
}
});
ToolItem refresh = new ToolItem(toolbar, SWT.NONE);
refresh.setImage(ImageResource.getImage(ImageResource.IMG_ELCL_NAV_REFRESH));
refresh.setHotImage(ImageResource.getImage(ImageResource.IMG_CLCL_NAV_REFRESH));
refresh.setDisabledImage(ImageResource.getImage(ImageResource.IMG_DLCL_NAV_REFRESH));
refresh.setToolTipText(Messages.actionWebBrowserRefresh);
refresh.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
refresh();
}
});
return toolbar;
}
/**
* Returns the current URL. Convenience method that calls the underlying SWT
* browser.
*
* @return the current URL or an empty <code>String</code> if there is no
* current URL
* @exception SWTException <ul>
* <li>ERROR_THREAD_INVALID_ACCESS when called from the wrong thread</li>
* <li>ERROR_WIDGET_DISPOSED when the widget has been disposed</li>
* </ul>
* @see #setURL(String)
*/
public String getURL() {
if (browser != null) {
return browser.getEngine().locationProperty().get();
}
return initialUrl;
}
@Override
public boolean setFocus() {
if (browser != null) {
browser.requestFocus();
updateHistory();
return true;
}
return super.setFocus();
}
/**
* Update the history list to the global/shared copy.
*/
protected void updateHistory() {
if (combo == null || combo.isDisposed()) {
return;
}
String temp = combo.getText();
if (history == null) {
history = WebBrowserPreference.getInternalWebBrowserHistory();
}
String[] historyList = new String[history.size()];
history.toArray(historyList);
combo.setItems(historyList);
combo.setText(temp);
}
public IBrowserViewerContainer getContainer() {
return container;
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
browser.setVisible(true);
}
public void setContainer(IBrowserViewerContainer container) {
if (container == null && this.container != null) {
IStatusLineManager manager = this.container.getActionBars().getStatusLineManager();
if (manager != null) {
manager.getProgressMonitor().done();
}
}
this.container = container;
}
}