/*******************************************************************************
* Copyright � 2009, 2011 Florian Pirchner and others.
* 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:
* Florian Pirchner � initial API and implementation (based on other ridgets of
* compeople AG)
* compeople AG - adjustments for Riena v1.2 - 3.0
*******************************************************************************/
package org.eclipse.riena.internal.ui.ridgets.swt;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.runtime.Assert;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.riena.core.util.ListenerList;
import org.eclipse.riena.core.util.StringUtils;
import org.eclipse.riena.ui.ridgets.AbstractMarkerSupport;
import org.eclipse.riena.ui.ridgets.IBrowserRidget;
import org.eclipse.riena.ui.ridgets.listener.ILocationListener;
import org.eclipse.riena.ui.ridgets.listener.IProgressListener;
import org.eclipse.riena.ui.ridgets.swt.AbstractValueRidget;
import org.eclipse.riena.ui.ridgets.swt.BasicMarkerSupport;
import org.eclipse.riena.ui.swt.facades.BrowserFacade;
/**
* Ridget for an SWT {@link Browser} widget.
* <p>
* Implementation note: because of SWT <a href="http://bugs.eclipse.org/84532">Bug 84532</a> the {@link #setFocusable(boolean)} methods has no effect.
*
* @since 1.2
*/
public class BrowserRidget extends AbstractValueRidget implements IBrowserRidget {
private static final String ABOUT_BLANK = "about:blank"; //$NON-NLS-1$
/**
* This property is used by the databinding to sync ridget and model. It is always fired before its sibling {@link IBrowserRidget#PROPERTY_URL} to ensure
* that the model is updated before any listeners try accessing it.
* <p>
* This property is not API. Do not use in client code.
*/
private static final String PROPERTY_URL_INTERNAL = "urlInternal"; //$NON-NLS-1$
private final InternalLocationListener internalLocationListener;
private final InternalProgressListener internalProgressListener;
private final Map<String, IBrowserRidgetFunction> scriptFunctionMappings;
private String url;
private String text;
private final Map<String, BrowserFunction> browserFunctions;
public BrowserRidget() {
internalLocationListener = new InternalLocationListener();
internalProgressListener = new InternalProgressListener();
scriptFunctionMappings = Collections.synchronizedMap(new HashMap<String, IBrowserRidgetFunction>());
browserFunctions = new HashMap<String, BrowserFunction>();
}
@Override
protected void checkUIControl(final Object uiControl) {
checkType(uiControl, Browser.class);
}
@Override
protected void bindUIControl() {
final Browser control = getUIControl();
if (control != null) {
updateUIControl();
control.addLocationListener(internalLocationListener);
control.addProgressListener(internalProgressListener);
for (final Entry<String, IBrowserRidgetFunction> mapping : scriptFunctionMappings.entrySet()) {
final BrowserFunction swtBrowerFunction = addSWTBrowerFunction(mapping.getKey(), mapping.getValue());
browserFunctions.put(mapping.getKey(), swtBrowerFunction);
}
}
}
@Override
protected void unbindUIControl() {
final Browser control = getUIControl();
if (control != null) {
control.removeLocationListener(internalLocationListener);
control.removeProgressListener(internalProgressListener);
}
disposeBrowserFunctions();
super.unbindUIControl();
}
private void disposeBrowserFunctions() {
for (final BrowserFunction browserFunction : browserFunctions.values()) {
browserFunction.dispose();
}
browserFunctions.clear();
}
@Override
protected AbstractMarkerSupport createMarkerSupport() {
return new BasicMarkerSupport(this, propertyChangeSupport);
}
@Override
protected IObservableValue getRidgetObservable() {
return BeansObservables.observeValue(this, PROPERTY_URL_INTERNAL);
}
public void addLocationListener(final ILocationListener listener) {
internalLocationListener.addLocationListener(listener);
}
public void addProgressListener(final IProgressListener listener) {
internalProgressListener.addProgressListener(listener);
}
@Override
public Browser getUIControl() {
return (Browser) super.getUIControl();
}
public String getText() {
return text;
}
public String getUrl() {
return url;
}
/**
* This method is not API. Do not use in client code.
*
* @noreference This method is not intended to be referenced by clients.
*/
public final String getUrlInternal() {
return getUrl();
}
/**
* Always returns true because mandatory markers do not make sense for this ridget.
*/
@Override
public boolean isDisableMandatoryMarker() {
return true;
}
public void removeLocationListener(final ILocationListener listener) {
internalLocationListener.removeLocationListener(listener);
}
public void removeProgressListener(final IProgressListener listener) {
internalProgressListener.removeProgressListener(listener);
}
public void setText(final String text) {
if (!StringUtils.equals(this.text, text)) {
this.text = text;
final String oldUrl = this.url;
this.url = null;
updateUIControl();
firePropertyChange(PROPERTY_URL_INTERNAL, oldUrl, this.url);
firePropertyChange(IBrowserRidget.PROPERTY_URL, oldUrl, this.url);
}
}
public void setUrl(final String url) {
if (!StringUtils.equals(this.url, url)) {
final String oldUrl = this.getUrl();
this.text = null;
this.url = url;
updateUIControl();
firePropertyChange(PROPERTY_URL_INTERNAL, oldUrl, this.url);
firePropertyChange(IBrowserRidget.PROPERTY_URL, oldUrl, this.url);
}
}
/**
* This method is not API. Do not use in client code.
* <p>
* Do not remove - used by the data binding.
*
* @noreference This method is not intended to be referenced by clients.
*/
public final void setUrlInternal(final String url) {
setUrl(url);
}
// helping methods
//////////////////
/**
* Note that this method does not guarantee the result validity. If an invalid (URL) and non-empty parameter is passed, then it will simply be returned and
* remain invalid.
*/
private String convertBlankToValid(final String string) {
return StringUtils.isDeepEmpty(string) ? ABOUT_BLANK : string;
}
private void updateUIControl() {
final Browser browser = getUIControl();
if (browser != null) {
if (text != null) {
final String browserText = BrowserFacade.getDefault().getText(browser);
if (!text.equals(browserText)) {
internalLocationListener.unblock();
browser.setText(text);
}
} else {
final String urlToSet = convertBlankToValid(url);
if (!urlToSet.equals(browser.getUrl())) {
internalLocationListener.unblock();
browser.setUrl(urlToSet);
}
}
}
}
// helping classes
//////////////////
/**
* Listens to location changes in the Browser widget and update's the Ridget's URL if necessary.
*/
private final class InternalLocationListener implements LocationListener {
private ListenerList<ILocationListener> listeners;
private boolean canBlock;
InternalLocationListener() {
canBlock = true;
}
void addLocationListener(final ILocationListener listener) {
Assert.isNotNull(listener);
if (listeners == null) {
listeners = new ListenerList<ILocationListener>(ILocationListener.class);
}
listeners.add(listener);
}
void removeLocationListener(final ILocationListener listener) {
if (listeners != null) {
listeners.remove(listener);
}
}
/**
* Allow the next url-change, even if output-only marker is set.
* <p>
* This is used by updateUIControl() to permit updating a widget on rebind, setText, setUrl.
* <p>
* Implementation notes: {@link #changing(LocationEvent)} is invoked an undefined time after {@link #unblock()}, since the page load happens
* asynchronously. Currently there is no synchronisation build in - we simply allow the next change. This is not likely to cause problems, however it
* could allow another change to happen, if it is processed before the intended LocationEvent. The event.location value is formatted by the browser and
* may have things added (parameters, http://www prefix) so checking for BrowserRidget.url equality is not an option for identifying which url to
* unblock.
*/
void unblock() {
canBlock = false;
}
public void changing(final LocationEvent event) {
if (canBlock) {
if (isOutputOnly()) {
event.doit = false;
}
if (listeners != null && event.doit) {
for (final ILocationListener listener : listeners) {
final org.eclipse.riena.ui.ridgets.listener.LocationEvent locEvent = new org.eclipse.riena.ui.ridgets.listener.LocationEvent(
event.location, event.doit, event.top);
event.doit &= listener.locationChanging(locEvent);
}
}
}
canBlock = true;
}
public void changed(final LocationEvent event) {
if (event.top && !isNullOrAboutBlank(event.location)) {
if (!StringUtils.equals(url, event.location)) {
setUrl(event.location);
if (listeners != null) {
final org.eclipse.riena.ui.ridgets.listener.LocationEvent locEvent = new org.eclipse.riena.ui.ridgets.listener.LocationEvent(
event.location, event.doit, event.top);
for (final ILocationListener listener : listeners) {
listener.locationChanged(locEvent);
}
}
}
}
}
private boolean isNullOrAboutBlank(final String url) {
return url == null || "about:blank".equals(url); //$NON-NLS-1$
}
}
/**
* Listens to progress changes in the Browser widget.
*/
private final class InternalProgressListener implements ProgressListener {
private ListenerList<IProgressListener> listeners;
public void addProgressListener(final IProgressListener listener) {
Assert.isNotNull(listener);
if (listeners == null) {
listeners = new ListenerList<IProgressListener>(IProgressListener.class);
}
listeners.add(listener);
}
public void removeProgressListener(final IProgressListener listener) {
if (listeners != null) {
listeners.remove(listener);
}
}
public void changed(final ProgressEvent event) {
if (listeners != null) {
for (final IProgressListener listener : listeners) {
final org.eclipse.riena.ui.ridgets.listener.ProgressEvent progressEvent = new org.eclipse.riena.ui.ridgets.listener.ProgressEvent(
event.current, event.total);
listener.progressChanged(progressEvent);
}
}
}
public void completed(final ProgressEvent event) {
if (listeners != null) {
for (final IProgressListener listener : listeners) {
final org.eclipse.riena.ui.ridgets.listener.ProgressEvent progressEvent = new org.eclipse.riena.ui.ridgets.listener.ProgressEvent(
event.current, event.total);
listener.progressCompleted(progressEvent);
}
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.riena.ui.ridgets.IBrowserRidget#execute(java.lang.String)
*/
@Override
public boolean execute(final String script) {
if (getUIControl() != null) {
return getUIControl().execute(script);
}
return false;
}
/*
* (non-Javadoc)
*
* @see org.eclipse.riena.ui.ridgets.IBrowserRidget#bindScriptFunction(java.lang.String, java.lang.Object)
*/
@Override
public void mapScriptFunction(final String functionName, final IBrowserRidgetFunction function) {
scriptFunctionMappings.put(functionName, function);
if (getUIControl() != null) {
final BrowserFunction swtBrowerFunction = addSWTBrowerFunction(functionName, function);
browserFunctions.put(functionName, swtBrowerFunction);
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.riena.ui.ridgets.IBrowserRidget#unmapScriptFunction(java.lang.String)
*/
@Override
public void unmapScriptFunction(final String functionName) {
if (scriptFunctionMappings.remove(functionName) != null && getUIControl() != null) {
browserFunctions.remove(functionName).dispose();
}
}
private BrowserFunction addSWTBrowerFunction(final String functionName, final IBrowserRidgetFunction controller) {
return new BrowserFunction(getUIControl(), functionName) {
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.browser.BrowserFunction#function(java.lang.Object[])
*/
@Override
public Object function(final Object[] arguments) {
return controller.execute(arguments);
}
};
}
}