/*******************************************************************************
 * Copyright (c) 2004, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    IBM Corporation - Initial API and implementation
 *    Jacek Pospychala - jacek.pospychala@pl.ibm.com - fix for bug 224887
 *    Ian Pun & Lucas Bullen (Red Hat Inc.) - Bug 508508
 *******************************************************************************/
package org.eclipse.ui.internal.browser;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.IStatusLineManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationAdapter;
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.swt.browser.VisibilityWindowAdapter;
import org.eclipse.swt.browser.WindowEvent;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
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.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.PlatformUI;
import org.eclipse.urischeme.IUriSchemeProcessor;

/**
 * A Web browser widget. It extends the Eclipse SWT Browser widget by adding an
 * optional toolbar complete with a URL combo box, history, back &amp; 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>
 *
 * @since 1.0
 */
public class BrowserViewer extends Composite {
	/**
	 * 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 MenuItem autoRefresh;

	protected BusyIndicator busy;

	protected boolean loading;

	protected static java.util.List<String> history;

	protected Browser browser;

	protected BrowserText text;

	protected boolean newWindow;

	protected IBrowserViewerContainer container;

	protected String title;

	protected int progressWorked = 0;

	protected WatchService watcher;

	protected List<PropertyChangeListener> propertyListeners;

	/**
	 * Under development - do not use
	 */
	public static interface ILocationListener {
		public void locationChanged(String url);

		public void historyChanged(String[] history2);
	}

	public ILocationListener locationListener;

	/**
	 * Under development - do not use
	 */
	public static interface IBackNextListener {
		public void updateBackNextBusy();
	}

	public IBackNextListener backNextListener;

	private ImageResourceManager imageManager;

	private static final String URL_PREFIX = "$nl$/icons/"; //$NON-NLS-1$
	private static final String URL_ELCL = URL_PREFIX + "elcl16/"; //$NON-NLS-1$
	private static final String URL_CLCL = URL_PREFIX + "clcl16/"; //$NON-NLS-1$
	private static final String URL_DLCL = URL_PREFIX + "dlcl16/"; //$NON-NLS-1$

	/**
	 * 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 BrowserViewer(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);
		setLayoutData(new GridData(GridData.FILL_BOTH));
		clipboard = new Clipboard(parent.getDisplay());

		if (showToolbar || showURLbar) {
			Composite toolbarComp = new Composite(this, SWT.NONE);
			imageManager = new ImageResourceManager(toolbarComp);
			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 MouseAdapter() {
					@Override
					public void mouseDown(MouseEvent e) {
						setURL("http://www.eclipse.org"); //$NON-NLS-1$
					}
				});
			}
			PlatformUI.getWorkbench().getHelpSystem().setHelp(this, ContextIds.WEB_BROWSER);
		}

		// create a new SWT Web browser widget, checking once again to make sure
		// we can use it in this environment
		//if (WebBrowserUtil.canUseInternalWebBrowser())
		try {
			this.browser = new Browser(this, SWT.NONE);
			this.browser.addLocationListener(LocationListener.changingAdapter(event -> {
					URI uri = URI.create(event.location);
					if (!(uri.getScheme().equals("http") || uri.getScheme().equals("https") //$NON-NLS-1$ //$NON-NLS-2$
							|| uri.getScheme().equals("file"))) //$NON-NLS-1$
						try {
							if (IUriSchemeProcessor.INSTANCE.canHandle(uri)) {
								IUriSchemeProcessor.INSTANCE.handleUri(uri);
								event.doit = false;
							}
						} catch (CoreException e) {
							WebBrowserUIPlugin.logError(e.getMessage(), e);
						}

				}));
		}
		catch (SWTError e) {
			if (e.code!=SWT.ERROR_NO_HANDLES) {
				WebBrowserUtil.openError(Messages.errorCouldNotLaunchInternalWebBrowser);
				return;
			}
			text = new BrowserText(this, this, e);
		}

		if (showURLbar)
			updateHistory();
		if (showToolbar)
			updateBackNextBusy();

		if (browser!=null) {
			browser.setLayoutData(new GridData(GridData.FILL_BOTH));
			PlatformUI.getWorkbench().getHelpSystem().setHelp(browser,
					ContextIds.WEB_BROWSER);
		}
		else
			text.getControl().setLayoutData(new GridData(GridData.FILL_BOTH));

		addBrowserListeners();
		addDisposeListener(this::dispose);
	}

	/**
	 * Returns the underlying SWT browser widget.
	 *
	 * @return the underlying browser
	 */
	public Browser getBrowser() {
		return browser;
	}

	/**
	 * Navigate to the home URL.
	 */
	public void home() {
		browser.setText(""); //$NON-NLS-1$
	}

	/**
	 * Loads a URL.
	 *
	 * @param url
	 *            the URL to be loaded
	 * @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();
	}

	protected void updateLocation() {
		if (locationListener != null)
			locationListener.historyChanged(null);

		if (locationListener != null)
			locationListener.locationChanged(null);
	}

	/**
	 *
	 */
	private void addBrowserListeners() {
		if (browser==null) return;
		// respond to ExternalBrowserInstance StatusTextEvents events by
		// updating the status line
		browser.addStatusTextListener(event -> {
			// System.out.println("status: " + event.text); //$NON-NLS-1$
			if (container != null) {
				IStatusLineManager status = container.getActionBars().getStatusLineManager();
				status.setMessage(event.text);
			}
		});

		// 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.addOpenWindowListener(event -> {
			Shell shell2 = new Shell(getShell(), SWT.SHELL_TRIM);
			shell2.setLayout(new FillLayout());
			shell2.setText(Messages.viewWebBrowserTitle);
			shell2.setImage(getShell().getImage());
			if (event.location != null)
				shell2.setLocation(event.location);
			if (event.size != null)
				shell2.setSize(event.size);
			int style = 0;
			if (showURLbar)
				style += LOCATION_BAR;
			if (showToolbar)
				style += BUTTON_BAR;
			BrowserViewer browser2 = new BrowserViewer(shell2, style);
			browser2.newWindow = true;
			event.browser = browser2.browser;
		});

		browser.addVisibilityWindowListener(new VisibilityWindowAdapter() {
			@Override
			public void show(WindowEvent e) {
				Browser browser2 = (Browser) e.widget;
				if (browser2.getParent().getParent() instanceof Shell) {
					Shell shell = (Shell) browser2.getParent().getParent();
					if (e.location != null)
						shell.setLocation(e.location);
					if (e.size != null)
						shell.setSize(shell.computeSize(e.size.x, e.size.y));
					shell.open();
				}
			}
		});

		browser.addCloseWindowListener(event -> {
			// if shell is not null, it must be a secondary popup window,
			// else its an editor window
			if (newWindow)
				getShell().dispose();
			else
				container.close();
		});

		browser.addProgressListener(new ProgressListener() {
			@Override
			public void changed(ProgressEvent event) {
					//System.out.println("progress: " + event.current + ", " + event.total); //$NON-NLS-1$ //$NON-NLS-2$
				if (event.total == 0)
					return;

				boolean done = (event.current == event.total);

				int percentProgress = event.current * 100 / event.total;
				if (container != null) {
					IProgressMonitor monitor = container.getActionBars()
							.getStatusLineManager().getProgressMonitor();
					if (done) {
						monitor.done();
						progressWorked = 0;
					} else if (progressWorked == 0) {
						monitor.beginTask("", event.total); //$NON-NLS-1$
						progressWorked = percentProgress;
					} else {
						monitor.worked(event.current - progressWorked);
						progressWorked = event.current;
					}
				}

				if (showToolbar) {
					if (!busy.isBusy() && !done)
						loading = true;
					else if (busy.isBusy() && done) // once the progress hits
						// 100 percent, done, set
						// busy to false
						loading = false;

					//System.out.println("loading: " + loading); //$NON-NLS-1$
					updateBackNextBusy();
					updateHistory();
				}
			}

			@Override
			public void completed(ProgressEvent event) {
				if (container != null) {
					IProgressMonitor monitor = container.getActionBars()
							.getStatusLineManager().getProgressMonitor();
					monitor.done();
				}
				if (showToolbar) {
					loading = false;
					updateBackNextBusy();
					updateHistory();
				}
			}
		});

		if (showURLbar) {
			browser.addLocationListener(new LocationAdapter() {
				@Override
				public void changed(LocationEvent event) {
					if (!event.top)
						return;
					if (combo != null) {
						if (!"about:blank".equals(event.location)) { //$NON-NLS-1$
							combo.setText(event.location);
							addToHistory(event.location);
							updateHistory();
						}// else
						//    combo.setText(""); //$NON-NLS-1$
					}
					if (showToolbar) {
						// enable auto-refresh button if URL is a file
						File temp = getFile(browser.getUrl());
						if (temp != null && temp.exists()) {
							autoRefresh.setEnabled(true);
							if (autoRefresh.getSelection()) {
								fileChangedWatchService(temp);
							}
						} else {
							autoRefresh.setSelection(false);
							toggleAutoRefresh();
							autoRefresh.setEnabled(false);
						}
					}
				}
			});
		}

		browser.addTitleListener(event -> {
			String oldTitle = title;
			title = event.title;
			firePropertyChangeEvent(PROPERTY_TITLE, oldTitle, title);
		});
	}

	 /**
		 * Add a property change listener to this instance.
		 *
		 * @param listener java.beans.PropertyChangeListener
		 */
		public void addPropertyChangeListener(PropertyChangeListener listener) {
			if (propertyListeners == null)
				propertyListeners = new ArrayList<>();
			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);
			//Trace.trace("Firing: " + event + " " + oldValue);
			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 (browser==null)
			return false;
		return browser.forward();
	}

	/**
	 * 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 (browser==null)
			return false;
		return browser.back();
	}

	/**
	 * 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() {
		if (browser==null)
			return false;
		return browser.isBackEnabled();
	}

	/**
	 * 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() {
		if (browser==null)
			return false;
		return browser.isForwardEnabled();
	}

	/**
	 * 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.stop();
	}

	/**
	 *
	 */
	private boolean navigate(String url) {
		Trace.trace(Trace.FINER, "Navigate: " + url); //$NON-NLS-1$
		if (url != null && url.equals(getURL())) {
			refresh();
			return true;
		}
		if (browser!=null)
			return browser.setUrl(url, null, new String[] {"Cache-Control: no-cache"}); //$NON-NLS-1$
		return text.setUrl(url);
	}

	/**
	 * 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.refresh();
		else
			text.refresh();
		try {
			Thread.sleep(50);
		} catch (Exception e) {
			// ignore
		}
	}

	private void toggleAutoRefresh() {
		File temp = getFile(browser.getUrl());
		if (temp != null && temp.exists() && autoRefresh.getSelection()) {
			refresh();
			fileChangedWatchService(temp);
		} else if (watcher != null) {
			try {
				watcher.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private void setURL(String url, boolean browse) {
		Trace.trace(Trace.FINEST, "setURL: " + url + " " + browse); //$NON-NLS-1$ //$NON-NLS-2$
		if (url == null) {
			home();
			return;
		}

		if ("eclipse".equalsIgnoreCase(url)) //$NON-NLS-1$
			url = "http://www.eclipse.org"; //$NON-NLS-1$
		else if ("wtp".equalsIgnoreCase(url)) //$NON-NLS-1$
			url = "http://www.eclipse.org/webtools/"; //$NON-NLS-1$

		if (browse) {
			navigate(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);
		}
	}

	/**
	 *
	 */
	private void dispose(DisposeEvent event) {
		showToolbar = false;

		if (busy != null)
			busy.dispose();
		busy = null;

		browser = null;
		text = null;
		if (clipboard!=null)
			clipboard.dispose();
		clipboard=null;

		if (watcher != null) {
			try {
				watcher.close();
			} catch (IOException e) {
				WebBrowserUIPlugin.logError(e.getMessage(), e);
			}
		}
		watcher = null;

		removeSynchronizationListener();
	}

	private ToolBar createLocationBar(Composite parent) {
		combo = new Combo(parent, SWT.DROP_DOWN);

		updateHistory();

		combo.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
			try {
				if (combo.getSelectionIndex() != -1 && !combo.getListVisible()) {
					setURL(combo.getItem(combo.getSelectionIndex()));
				}
			} catch (Exception e1) {
				// ignore
			}
		}));
		combo.addListener(SWT.DefaultSelection, e -> setURL(combo.getText()));

		ToolBar toolbar = new ToolBar(parent, SWT.FLAT);

		ToolItem go = new ToolItem(toolbar, SWT.NONE);
		go.setImage(getImage(URL_ELCL, "nav_go.png")); //$NON-NLS-1$
		go.setHotImage(getImage(URL_CLCL, "nav_go.png")); //$NON-NLS-1$
		go.setDisabledImage(getImage(URL_DLCL, "nav_go.png")); //$NON-NLS-1$
		go.setToolTipText(Messages.actionWebBrowserGo);
		go.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> setURL(combo.getText())));

		return toolbar;
	}

	private ToolBar createToolbar(Composite parent) {
			ToolBar toolbar = new ToolBar(parent, SWT.FLAT);

		// create back and forward actions
		back = new ToolItem(toolbar, SWT.NONE);
		back.setImage(getImage(URL_ELCL, "nav_backward.png")); //$NON-NLS-1$
		back.setHotImage(getImage(URL_CLCL, "nav_backward.png")); //$NON-NLS-1$
		back.setDisabledImage(getImage(URL_DLCL, "nav_backward.png")); //$NON-NLS-1$
		back.setToolTipText(Messages.actionWebBrowserBack);
		back.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> back()));

		forward = new ToolItem(toolbar, SWT.NONE);
		forward.setImage(getImage(URL_ELCL, "nav_forward.png")); //$NON-NLS-1$
		forward.setHotImage(getImage(URL_CLCL, "nav_forward.png")); //$NON-NLS-1$
		forward.setDisabledImage(getImage(URL_DLCL, "nav_forward.png")); //$NON-NLS-1$
		forward.setToolTipText(Messages.actionWebBrowserForward);
		forward.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> forward()));

		// create refresh, stop, and print actions
		ToolItem stop = new ToolItem(toolbar, SWT.NONE);
		stop.setImage(getImage(URL_ELCL, "nav_stop.png")); //$NON-NLS-1$
		stop.setHotImage(getImage(URL_CLCL, "nav_stop.png")); //$NON-NLS-1$
		stop.setDisabledImage(getImage(URL_DLCL, "nav_stop.png")); //$NON-NLS-1$
		stop.setToolTipText(Messages.actionWebBrowserStop);
		stop.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> stop()));

		ToolItem refresh = new ToolItem(toolbar, SWT.DROP_DOWN);
		refresh.setImage(getImage(URL_ELCL, "nav_refresh.png")); //$NON-NLS-1$
		refresh.setHotImage(getImage(URL_CLCL, "nav_refresh.png")); //$NON-NLS-1$
		refresh.setDisabledImage(getImage(URL_DLCL, "nav_refresh.png")); //$NON-NLS-1$
		refresh.setToolTipText(Messages.actionWebBrowserRefresh);

		// create auto-refresh action
		Menu refreshMenu = new Menu(getShell(), SWT.POP_UP);
		autoRefresh = new MenuItem(refreshMenu, SWT.CHECK);
		autoRefresh.setText(Messages.actionWebBrowserAutoRefresh);
		autoRefresh.setEnabled(false);
		refresh.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> {
			if (e.detail == SWT.ARROW) {
				refreshMenu.setVisible(true);
			} else {
				refresh();
			}
		}));
		autoRefresh.addSelectionListener(SelectionListener.widgetSelectedAdapter(e -> toggleAutoRefresh()));

		return toolbar;
	}

	private Image getImage(String folder, String name) {
		ImageDescriptor id = ImageResourceManager.getImageDescriptor(folder + name);
		return imageManager.getImage(id);
	}

	/**
	 * 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.getUrl();
		return text.getUrl();
	}

	@Override
	public boolean setFocus() {
		if (browser!=null) {
			browser.setFocus();
			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;
	}

	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;
	}

	protected File file;
	protected long timestamp;
	protected Thread fileListenerThread;
	protected LocationListener locationListener2;
	protected Object syncObject = new Object();

	protected void addSynchronizationListener() {
		if (fileListenerThread != null)
			return;

		fileListenerThread = new Thread("Browser file synchronization") { //$NON-NLS-1$
			@Override
			public void run() {
				while (fileListenerThread != null) {
					try {
						Thread.sleep(2000);
					} catch (Exception e) {
						// ignore
					}
					synchronized (syncObject) {
						if (file != null && file.lastModified() != timestamp) {
							timestamp = file.lastModified();
							Display.getDefault().syncExec(() -> refresh());
						}
					}
				}
			}
		};
		fileListenerThread.setDaemon(true);
		fileListenerThread.setPriority(Thread.MIN_PRIORITY);

		locationListener2 = new LocationAdapter() {
			@Override
			public void changed(LocationEvent event) {
				File temp = getFile(event.location);
				if (temp != null && temp.exists()) {
					synchronized (syncObject) {
						file = temp;
						timestamp = file.lastModified();
					}
				} else
					file = null;
			}
		};
		browser.addLocationListener(locationListener2);

		File temp = getFile(browser.getUrl());
		if (temp != null && temp.exists()) {
			file = temp;
			timestamp = file.lastModified();
		}
		fileListenerThread.start();
	}

	protected static File getFile(String location) {
		if (location == null)
			return null;
		if (location.startsWith("file:/")) //$NON-NLS-1$
			location = location.substring(6);

		return new File(location);
	}

	protected void removeSynchronizationListener() {
		if (fileListenerThread == null)
			return;

		fileListenerThread = null;
		browser.removeLocationListener(locationListener2);
		locationListener2 = null;
	}

	/*
	 * Start the WatchService so that it monitors the local file system for any
	 * changes. This is used by the auto-refresh action as it will monitor the file
	 * being displayed and refresh the browser when there are changes.
	 */
	private void fileChangedWatchService(File file) {
		while (file.isFile()) {
			// get the directory as that is the requirement of WatchService
			file = file.getParentFile();
		}
		try {
			if (watcher != null) {
				watcher.close();
			}
			watcher = FileSystems.getDefault().newWatchService();
			final Path path = FileSystems.getDefault().getPath(file.getAbsolutePath());
			path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY);
			new Thread(() -> {
				try {
					WatchKey key = watcher.take();
					while (key != null) {
						for (WatchEvent<?> event : key.pollEvents()) {
							final Path changedPath = (Path) event.context();
							Display.getDefault().asyncExec(() -> {
								if (browser.getUrl().endsWith(changedPath.toString())) {
									browser.refresh();
								}
							});
						}
						key.reset();
						key = watcher.take();
					}
				} catch (InterruptedException | ClosedWatchServiceException e) {
					// catch and continue to abort the thread
				}
			}).start();

		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}
