//------------------------------------------------------------------------------
// Copyright (c) 2005, 2006 IBM Corporation 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:
// IBM Corporation - initial implementation
//------------------------------------------------------------------------------
package org.eclipse.epf.richtext;

import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Platform;
import org.eclipse.epf.common.html.HTMLFormatter;
import org.eclipse.epf.common.serviceability.Logger;
import org.eclipse.epf.common.utils.FileUtil;
import org.eclipse.epf.common.utils.XMLUtil;
import org.eclipse.epf.common.xml.XSLTProcessor;
import org.eclipse.epf.richtext.actions.CopyAction;
import org.eclipse.epf.richtext.actions.CutAction;
import org.eclipse.epf.richtext.actions.FindReplaceAction;
import org.eclipse.epf.richtext.actions.PasteAction;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.LocationAdapter;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.StatusTextEvent;
import org.eclipse.swt.browser.StatusTextListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.HelpListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.MenuListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.ui.PlatformUI;

/**
 * The default rich text control implementation.
 * <p>
 * The default rich text editor uses XHTML as the underlying markup language for
 * the rich text content. It is implemented using a SWT <code>Browser</code>
 * control and DHTML (HTML, CSS and JavaScript).
 * 
 * @author Kelvin Low
 * @since 1.0
 */
public class RichText implements IRichText {

	// Encoded single quote. All single quotes need to be specially encoded to
	// avoid JavaScript error when calling Browse.executeCommand.
	private static final String ENCODED_SINGLE_QUOTE = "%sq%"; //$NON-NLS-1$

	private static final String ENCODED_NEWLINE = "%EOL%"; //$NON-NLS-1$

	private static final String STATUS_PREFIX = "$$$"; //$NON-NLS-1$

	private static final int STATUS_PREFIX_LENGTH = STATUS_PREFIX.length();

	private static final int STATUS_INITIALIZED = 1;

	private static final int STATUS_MODIFIED = 2;

	private static final int STATUS_GET_TEXT = 3;

	private static final int STATUS_KEY_DOWN = 4;

	private static final int STATUS_KEY_UP = 5;

	private static final int STATUS_SELECT_TEXT = 6;

	private static final int STATUS_SELECT_CONTROL = 7;

	private static final int STATUS_SELECT_NONE = 8;

	private static final int STATUS_EXEC_CMD = 9;

	// The default base path used for resolving links (<href>, <img>, etc.)
	private static final String DEFAULT_BASE_PATH = System
			.getProperty("user.home") //$NON-NLS-1$
			+ System.getProperty("file.separator") + "rte"; //$NON-NLS-1$ //$NON-NLS-2$

	// If true, log debugging info.
	protected boolean debug;

	// The plug-in logger.
	protected Logger logger;

	// The underlying SWT Browser used for loading the JavaScript/DHTML editor.
	protected Browser editor;

	// The underlying OleControlSite for the SWT Browser (Win32 only).
	protected Control editorControl;

	// The context menu associated with this control.
	private Menu contextMenu;

	// The folder that contains the supporting CSS and JavaScript files
	private String rteFolder;

	// The URL that points to the supporting CSS and JavaScript files.
	private String rteURL;

	// The base path used for resolving links (<href>, <img>, etc.)
	private String basePath;

	// The DHTML initialization flag.
	private boolean initialized;

	// The initial focus.
	private boolean initializedWithFocus;

	// The control's initial text. This is used to cache the HTML source passed
	// in via setText()while the DHTML initialization is happening.
	private String initialText;

	// The control's current text.
	private String currentText = ""; //$NON-NLS-1$

	// The control's editable flag.
	private boolean editable = true;

	// The control's modification flag.
	private boolean modified;

	// The control's text and object selection flag.
	private boolean selection;

	// The control's text selection.
	protected String selectedText = ""; //$NON-NLS-1$

	// JavaScript command execution status code.
	private int status = 0;

	// The HTML source formatter.
	private HTMLFormatter htmlFormatter;

	// The SWT event listeners.
	private Map listeners;

	// The modify listeners.
	private List modifyListeners;

	// The control's edit flag.
	private boolean notifyingModifyListeners = false;

	// The controls's focus flag.
	private boolean hasFocus = false;

	/**
	 * Creates a new instance.
	 * 
	 * @param parent
	 *            the parent composite
	 * @param style
	 *            the style for this control
	 * @param basePath
	 *            the path used for resolving links
	 */
	public RichText(Composite parent, int style, String basePath) {
		debug = RichTextPlugin.getDefault().isDebugging();
		logger = RichTextPlugin.getDefault().getLogger();
		rteFolder = RichTextPlugin.getDefault().getInstallPath() + "rte/"; //$NON-NLS-1$		
		rteURL = XMLUtil.escape("file://" + rteFolder); //$NON-NLS-1$
		setBasePath(basePath);

		try {
			editor = new Browser(parent, SWT.NONE);
			if (debug) {
				printDebugMessage("RichText", "basePath=" + basePath); //$NON-NLS-1$ //$NON-NLS-2$
			}
			editor.setLayoutData(new GridData(GridData.FILL_BOTH));
			editor.setData(PROPERTY_NAME, this);
			init(parent, style);
		} catch (Exception e) {
			editor = null;
			String msg = "Failed to create RichText with basePath=" + basePath; //$NON-NLS-1$
			logger.logError(msg, e);
			if (debug) {
				System.out.println(msg);
				e.printStackTrace();
			}
		}
	}

	/**
	 * Creates a new instance.
	 * 
	 * @param parent
	 *            the parent composite
	 * @param style
	 *            the style for this control
	 */
	public RichText(Composite parent, int style) {
		this(parent, style, null);
	}

	/**
	 * Sets the base path for resolving links.
	 */
	protected void setBasePath(String path) {
		if (path != null && path.length() > 0) {
			if (path.startsWith(FileUtil.UNC_PATH_PREFIX)) {
				basePath = FileUtil.UNC_PATH_PREFIX
						+ FileUtil.appendSeparator(path.substring(
								FileUtil.UNC_PATH_PREFIX_LENGTH).replace('\\',
								'/'), "/"); //$NON-NLS-1$
			} else {
				basePath = FileUtil.appendSeparator(path).replace('\\', '/');
			}
		} else {
			basePath = FileUtil.appendSeparator(DEFAULT_BASE_PATH).replace(
					'\\', '/');
		}
	}

	/**
	 * Initializes this control.
	 * 
	 * @param parent
	 *            the parent composite
	 * @param style
	 *            the style for this control
	 * @throws Exception
	 *             when an error has occurred while initialzing this control
	 */
	protected void init(Composite parent, int style) throws Exception {
		try {
			addStatusTextListener();
			if (debug) {
				printDebugMessage("init", "added status text listener"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			String editorHTML = generateEditorHTML();
			if (debug) {
				printDebugMessage("init", "generated editor HTML"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			editor.setText(editorHTML);
			if (debug) {
				printDebugMessage("init", "loaded editor HTML"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			contextMenu = new Menu(parent.getShell(), SWT.POP_UP);
			editor.setMenu(contextMenu);
			fillContextMenu(contextMenu);
			if (debug) {
				printDebugMessage("init", "added context menu"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			addListeners();
			if (debug) {
				printDebugMessage("init", "added listeners"); //$NON-NLS-1$ //$NON-NLS-2$
			}

			htmlFormatter = new HTMLFormatter();
			if (debug) {
				printDebugMessage("init", "instantiated HTMLFormatter"); //$NON-NLS-1$ //$NON-NLS-2$
			}
		} catch (Exception e) {
			editor = null;
			dispose();
			throw e;
		}
	}

	/**
	 * Returns this rich text control.
	 * 
	 * @return this rich text control
	 */
	public Control getControl() {
		return editor;
	}

	/**
	 * Sets the layout data.
	 * 
	 * @param layoutData
	 *            the layout data to set
	 */
	public void setLayoutData(Object layoutData) {
		if (editor != null) {
			editor.setLayoutData(layoutData);
		}
	}

	/**
	 * Returns the layout data.
	 * 
	 * @return this control's layout data
	 */
	public Object getLayoutData() {
		if (editor != null) {
			return editor.getLayoutData();
		}
		return null;
	}

	/**
	 * Sets focus to this control.
	 */
	public void setFocus() {
		if (debug) {
			printDebugMessage("setFocus"); //$NON-NLS-1$
		}
		if (editor != null) {
			if (initialized) {
				if (!Platform.getOS().equals("win32")) { //$NON-NLS-1$
					// Workaround for Mozilla and Firefox rich text editor focus
					// issue.
					editor.setFocus();
				}
				executeCommand(RichTextCommand.SET_FOCUS);
				hasFocus = true;
			} else {
				initializedWithFocus = true;
			}
		}
	}

	/**
	 * Checks whether this control has focus.
	 * 
	 * @return <code>true</code> if this control has the user-interface focus
	 */
	public boolean hasFocus() {
		if (editor != null) {
			return hasFocus;
		}
		return false;
	}

	/**
	 * Returns the base path used for resolving text and image links.
	 * 
	 * @return the base path used for resolving links in this control
	 */
	public String getBasePath() {
		return basePath;
	}

	/**
	 * Returns the editable state.
	 * 
	 * @return <code>true</code> if the content is ediatble
	 */
	public boolean getEditable() {
		return editable;
	}

	/**
	 * Sets the editable state.
	 * 
	 * @param editable
	 *            the editable state
	 */
	public void setEditable(boolean editable) {
		this.editable = editable;
		if (initialized) {
			executeCommand(RichTextCommand.SET_EDITABLE, "" + editable); //$NON-NLS-1$
		}
	}

	/**
	 * Checks whether the content has been modified.
	 * 
	 * @return <code>true</code> if the content has been modified
	 */
	public boolean getModified() {
		return modified;
	}

	/**
	 * Sets the modified state.
	 * 
	 * @param modified
	 *            the modified state
	 */
	public void setModified(boolean modified) {
		this.modified = modified;
	}

	/**
	 * Returns the rich text content.
	 * 
	 * @return the rich text content formatted in a markup language
	 */
	public String getText() {
		if (editor != null) {
			try {
				executeCommand(RichTextCommand.GET_TEXT);
				if (currentText != null && currentText.length() > 0) {
					currentText = currentText.replaceAll(
							"<P>&nbsp;</P>", "<br/>"); //$NON-NLS-1$ //$NON-NLS-2$			
					try {
						// Call JTidy to format the source to XHTML.
						currentText = htmlFormatter.formatHTML(currentText);
					} catch (Exception e) {
						logger.logError(e);
					}
					currentText = tidyText(currentText);
				} else {
					currentText = ""; //$NON-NLS-1$
				}
				if (debug) {
					printDebugMessage("getText", "text=", currentText); //$NON-NLS-1$ //$NON-NLS-2$
				}
				return currentText;
			} catch (Exception e) {
				logger.logError(e);
			}
		}
		return ""; //$NON-NLS-1$
	}

	/**
	 * Sets the rich text content.
	 * 
	 * @param text
	 *            the rich text content formatted in a markup language
	 */
	public void setText(String text) {
		if (editor != null) {
			if (debug) {
				printDebugMessage("setText", "text=", text); //$NON-NLS-1$ //$NON-NLS-2$
			}

			String newText = text;
			if (newText != null && newText.length() > 0) {
				try {
					// Call JTidy to format the source to XHTML.
					newText = htmlFormatter.formatHTML(newText);
				} catch (Exception e) {
					logger.logError(e);
				}
				newText = tidyText(newText);
			} else {
				newText = ""; //$NON-NLS-1$
			}

			modified = !newText.equals(currentText);
			initialText = newText;

			if (debug) {
				printDebugMessage(
						"setText", "modified=" + modified + ", newText=", newText); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
			}

			if (initialized) {
				try {
					executeCommand(RichTextCommand.SET_TEXT, newText);
				} catch (Exception e) {
					logger.logError(e);
				}
			}

			currentText = newText;
		}
	}

	/**
	 * Restores the rich text content back to the initial value.
	 */
	public void restoreText() {
		setText(initialText);
	}

	/**
	 * Returns the currently selected text.
	 * 
	 * @return the selected text or <code>""</code> if there is no selection
	 */
	public String getSelectedText() {
		executeCommand(RichTextCommand.GET_SELECTED_TEXT);
		return selectedText;
	}

	/**
	 * Returns an application specific property value.
	 * 
	 * @param key
	 *            the name of the property
	 * @return the value of the property or <code>null</code> if it has not
	 *         been set
	 */
	public Object getData(String key) {
		if (editor != null) {
			editor.getData(key);
		}
		return null;
	}

	/**
	 * Sets an application specific property name and value.
	 * 
	 * @param key
	 *            the name of the property
	 * @param value
	 *            the property value
	 */
	public void setData(String key, Object value) {
		if (editor != null) {
			editor.setData(key, value);
		}
	}

	/**
	 * Executes the given JavaScript.
	 * 
	 * @param script
	 *            the JavaScript to execute
	 * @return a status code returned by the executed script
	 */
	protected int execute(String script) {
		status = 0;
		if (editor != null && script != null && script.length() > 0) {
			try {
				editor.execute(script);
				if (debug) {
					printDebugMessage("execute", script); //$NON-NLS-1$				
				}
			} catch (Exception e) {
				String msg = "Failed to execute " + script; //$NON-NLS-1$
				logger.logError(msg, e);
				if (debug) {
					printDebugMessage("execute", msg); //$NON-NLS-1$
					e.printStackTrace();
				}
			}
		}
		return status;
	}

	/**
	 * Executes the given rich text command. The supported command strings are
	 * defined in <code>RichTextCommand<code>.
	 * 
	 * @param	command		a rich text command string.
	 * @return	a status code returned by the executed command
	 */
	public int executeCommand(String command) {
		status = 0;
		if (command != null && command.equals(RichTextCommand.CLEAR_CONTENT)) {
			String oldInitialText = initialText;
			setText(""); //$NON-NLS-1$
			initialText = oldInitialText;
			status = 1;
			modified = true;
			notifyModifyListeners();
		} else {
			status = execute(command + "();"); //$NON-NLS-1$
		}
		return status;
	}

	/**
	 * Executes the given rich text command with a single parameter. The
	 * supported command strings are defined in <code>RichTextCommand<code>.
	 * 
	 * @param	command		a rich text command string
	 * @param	param		a parameter for the command or <code>null</code>
	 * @return	a status code returned by the executed command
	 */
	public int executeCommand(String command, String param) {
		if (param == null) {
			return executeCommand(command);
		}
		return execute(command + "('" + formatText(param) + "');"); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Executes the given rich text command with an array of parameters. The
	 * supported command strings are defined in <code>RichTextCommand<code>.
	 * 
	 * @param	command		a rich text command string
	 * @param	params		an array of parameters for the command or <code>null</code>
	 * @return	a status code returned by the executed command
	 */
	public int executeCommand(String command, String[] params) {
		if (params == null || params.length == 0) {
			return executeCommand(command);
		}
		StringBuffer sb = new StringBuffer();
		int paramsLength = params.length;
		for (int i = 0; i < paramsLength; i++) {
			sb.append('\'').append(formatText(params[i])).append('\'');
			if (i < paramsLength - 1) {
				sb.append(',');
			}
		}
		String param = sb.toString();
		return execute(command + "(" + param + ");"); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Disposes the operating system resources allocated by the control.
	 */
	public void dispose() {
		if (contextMenu != null && !contextMenu.isDisposed()) {
			contextMenu.dispose();
			contextMenu = null;
		}
		if (listeners != null) {
			listeners.clear();
			listeners = null;
		}
		if (modifyListeners != null) {
			modifyListeners.clear();
			modifyListeners = null;
		}
		if (htmlFormatter != null) {
			htmlFormatter = null;
		}
	}

	/**
	 * Checks whether this control has been disposed.
	 * 
	 * @return <code>true</code> if this control is disposed successfully
	 */
	public boolean isDisposed() {
		return editor.isDisposed();
	}

	/**
	 * Returns the modify listeners attached to this control.
	 * 
	 * @return an iterator for retrieving the modify listeners
	 */
	public Iterator getModifyListeners() {
		return modifyListeners.iterator();
	}

	/**
	 * Adds a listener to the collection of listeners who will be notified when
	 * keys are pressed and released within this control.
	 * 
	 * @param listener
	 *            the listener which should be notified
	 */
	public void addKeyListener(KeyListener listener) {
		if (editor != null) {
			editor.addKeyListener(listener);
		}
	}

	/**
	 * Removes a listener from the collection of listeners who will be notified
	 * when keys are pressed and released within this control.
	 * 
	 * @param listener
	 *            the listener which should no longer be notified
	 */
	public void removeKeyListener(KeyListener listener) {
		if (editor != null) {
			editor.removeKeyListener(listener);
		}
	}

	/**
	 * Adds a listener to the collection of listeners who will be notified when
	 * the content of this control is modified.
	 * 
	 * @param listener
	 *            the listener which should be notified
	 */
	public void addModifyListener(ModifyListener listener) {
		if (editor != null && listener != null
				&& !modifyListeners.contains(listener)) {
			modifyListeners.add(listener);
		}
	}

	/**
	 * Removes a listener from the collection of listeners who will be notified
	 * when the content of this control is modified.
	 * 
	 * @param listener
	 *            the listener which should no longer be notified
	 */
	public void removeModifyListener(ModifyListener listener) {
		if (editor != null && listener != null
				&& modifyListeners.contains(listener)) {
			modifyListeners.remove(listener);
		}
	}

	/**
	 * Adds the listener to the collection of listeners who will be notifed when
	 * this control is disposed.
	 * 
	 * @param listener
	 *            the listener which should be notified
	 */
	public void addDisposeListener(DisposeListener listener) {
		if (editor != null) {
			editor.addDisposeListener(listener);
		}
	}

	/**
	 * Removes a listener from the collection of listeners who will be notified
	 * when this control is disposed.
	 * 
	 * @param listener
	 *            the listener which should no longer be notified
	 */
	public void removeDisposeListener(DisposeListener listener) {
		if (editor != null) {
			editor.removeDisposeListener(listener);
		}
	}

	/**
	 * Adds a listener to the collection of listeners who will be notified when
	 * help events are generated for this control.
	 * 
	 * @param listener
	 *            the listener which should be notified
	 */
	public void addHelpListener(HelpListener listener) {
		if (editor != null) {
			editor.addHelpListener(listener);
		}
	}

	/**
	 * Removes a listener from the collection of listeners who will be notified
	 * when help events are generated for this control.
	 * 
	 * @param listener
	 *            the listener which should no longer be notified
	 */
	public void removeHelpListener(HelpListener listener) {
		if (editor != null) {
			editor.removeHelpListener(listener);
		}
	}

	/**
	 * Adds the listener to the collection of listeners who will be notifed when
	 * an event of the given type occurs within this control.
	 * 
	 * @param eventType
	 *            the type of event to listen for
	 * @param listener
	 *            the listener which should be notified when the event occurs
	 */
	public void addListener(int eventType, Listener listener) {
		if (editor != null && !listeners.containsKey(listener)) {
			if (editorControl == null
					|| (eventType != SWT.Activate
							&& eventType != SWT.Deactivate
							&& eventType != SWT.FocusIn && eventType != SWT.FocusOut)) {
				editor.addListener(eventType, listener);
			}
			listeners.put(listener, new RichTextListener(eventType, listener));
		}
	}

	/**
	 * Removes the listener from the collection of listeners who will be notifed
	 * when an event of the given type occurs within this control.
	 * 
	 * @param eventType
	 *            the type of event to listen for
	 * @param listener
	 *            the listener which should no longer be notified when the event
	 *            occurs
	 */
	public void removeListener(int eventType, Listener listener) {
		if (editor != null && listeners.containsKey(listener)) {
			if (editorControl == null
					|| (eventType != SWT.Activate
							&& eventType != SWT.Deactivate
							&& eventType != SWT.FocusIn && eventType != SWT.FocusOut)) {
				editor.removeListener(eventType, listener);
			}
			listeners.remove(listener);
		}
	}

	/**
	 * Returns the event listeners attached to this control.
	 * 
	 * @return an iterator for retrieving the event listeners attached to this
	 *         control
	 */
	public Iterator getListeners() {
		return listeners.values().iterator();
	}

	/**
	 * Adds the listener to monitor events and status sent by the underlying
	 * DHTML editor.
	 */
	protected void addStatusTextListener() {
		editor.addStatusTextListener(new StatusTextListener() {
			public void changed(StatusTextEvent event) {
				String eventText = event.text;
				int eventTextLength = eventText.length();
				if (eventText.startsWith(STATUS_PREFIX)
						&& eventTextLength > STATUS_PREFIX_LENGTH) {
					try {
						int endStatusIndex = STATUS_PREFIX_LENGTH + 1;
						if (eventText.length() > STATUS_PREFIX_LENGTH + 1
								&& Character.isDigit(eventText
										.charAt(endStatusIndex))) {
							endStatusIndex++;
						}
						int statusType = Integer.parseInt(eventText.substring(
								STATUS_PREFIX_LENGTH, endStatusIndex));
						switch (statusType) {
						case STATUS_INITIALIZED:
							if (!initialized) {
								initialized = true;
								if (debug) {
									printDebugMessage(
											"statusTextListener", "STATUS_INITIALIZED"); //$NON-NLS-1$ //$NON-NLS-2$
								}
								if (!Platform.getOS().equals("win32")) { //$NON-NLS-1$
									// Workaround Mozilla'a IFRAME height issue.
									executeCommand(RichTextCommand.SET_HEIGHT,
											"" + editor.getBounds().height); //$NON-NLS-1$
								}
								executeCommand(RichTextCommand.SET_TEXT,
										currentText);
								if (initializedWithFocus) {
									setFocus();
								}
								if (!editable) {
									executeCommand(
											RichTextCommand.SET_EDITABLE,
											"" + editable); //$NON-NLS-1$
								}
							}
							break;
						case STATUS_MODIFIED:
							if (debug) {
								printDebugMessage(
										"statusTextListener", "STATUS_MODIFIED"); //$NON-NLS-1$ //$NON-NLS-2$
							}
							checkModify();
							break;
						case STATUS_GET_TEXT:
							if (eventTextLength >= STATUS_PREFIX_LENGTH + 2) {
								currentText = eventText
										.substring(STATUS_PREFIX_LENGTH + 2);
							} else {
								currentText = ""; //$NON-NLS-1$
							}
							if (debug) {
								printDebugMessage(
										"statusTextListener", //$NON-NLS-1$
										"STATUS_GET_TEXT, currentText=", currentText); //$NON-NLS-1$
							}
							break;
						case STATUS_KEY_DOWN:
							if (eventTextLength >= STATUS_PREFIX_LENGTH + 2) {
								String cmd = eventText
										.substring(STATUS_PREFIX_LENGTH + 2);
								if (debug) {
									printDebugMessage("statusTextListener", //$NON-NLS-1$
											"STATUS_KEY_DOWN, cmd=" + cmd); //$NON-NLS-1$
								}
								if (cmd.equals(RichTextCommand.FIND_TEXT)) {
									FindReplaceAction action = new FindReplaceAction();
									action.execute(RichText.this);
								} else if (cmd.equals(RichTextCommand.PASTE)) {
									PasteAction action = new PasteAction();
									action.execute(RichText.this);
								} else if (cmd.equals(RichTextCommand.SAVE)) {
									PlatformUI.getWorkbench()
											.getActiveWorkbenchWindow()
											.getActivePage().getActiveEditor()
											.doSave(null);
								} else if (cmd.equals(RichTextCommand.SAVE_ALL)) {
									PlatformUI.getWorkbench()
											.getActiveWorkbenchWindow()
											.getActivePage().saveAllEditors(
													false);
								}
							}
							break;
						case STATUS_KEY_UP:
							if (debug) {
								printDebugMessage("statusTextListener", //$NON-NLS-1$
										"STATUS_KEY_UP, modified=" + modified); //$NON-NLS-1$
							}
							checkModify();
							break;
						case STATUS_SELECT_TEXT:
							if (eventTextLength >= STATUS_PREFIX_LENGTH + 2) {
								selectedText = eventText
										.substring(STATUS_PREFIX_LENGTH + 2);
								selection = true;
							} else {
								selectedText = ""; //$NON-NLS-1$
								selection = false;
							}
							if (debug) {
								printDebugMessage(
										"statusTextListener", //$NON-NLS-1$
										"STATUS_SELECT_TEXT, selectedText=", selectedText); //$NON-NLS-1$
							}
							break;
						case STATUS_SELECT_CONTROL:
							selection = true;
							break;
						case STATUS_SELECT_NONE:
							selection = false;
							break;
						case STATUS_EXEC_CMD:
							if (eventTextLength >= STATUS_PREFIX_LENGTH + 3) {
								try {
									status = Integer.parseInt(eventText
											.substring(
													STATUS_PREFIX_LENGTH + 2,
													STATUS_PREFIX_LENGTH + 3));
								} catch (Exception e) {
									status = -1;
								}
							}
							if (debug && status != 1) {
								printDebugMessage("statusTextListener", //$NON-NLS-1$
										"STATUS_EXEC_CMD, status=" + status); //$NON-NLS-1$ //$NON-NLS-2$
							}
							break;
						}
					} catch (Exception e) {
					}
				}
			}
		});
	}

	/**
	 * Generates the HTML source for the editor.
	 * 
	 * @return the HTML source for the editor
	 */
	protected String generateEditorHTML() throws Exception {
		String escapedBasePath = basePath;
		if (escapedBasePath.startsWith(FileUtil.UNC_PATH_PREFIX))
			escapedBasePath = escapedBasePath.replaceFirst(
					"^\\\\\\\\", "\\\\\\\\\\\\\\\\"); //$NON-NLS-1$ //$NON-NLS-2$
		escapedBasePath = XMLUtil
				.escape("file://" + escapedBasePath.replaceAll("'", "\\\\'")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
		String escapedRteUTL = rteURL.replaceAll("&apos;", "%27"); //$NON-NLS-1$ //$NON-NLS-2$

		StringBuffer rteXML = new StringBuffer();
		rteXML.append("<rte id=\"").append("rte") //$NON-NLS-1$ //$NON-NLS-2$
				.append("\" css=\"").append(escapedRteUTL + "rte.css") //$NON-NLS-1$ //$NON-NLS-2$
				.append("\" js=\"").append(escapedRteUTL + "rte.js") //$NON-NLS-1$ //$NON-NLS-2$
				.append("\" baseURL=\"").append(escapedBasePath) //$NON-NLS-1$
				.append("\"/>"); //$NON-NLS-1$
		StringWriter result = new StringWriter();
		XSLTProcessor.transform(
				rteFolder + "rte.xsl", rteXML.toString(), result); //$NON-NLS-1$
		return result.toString();
	}

	/**
	 * Fills the context menu with menu items.
	 * 
	 * @param contextMenu
	 *            a context menu containing rich text actions
	 */
	protected void fillContextMenu(Menu contextMenu) {
		final MenuItem cutItem = new MenuItem(contextMenu, SWT.PUSH);
		cutItem.setText(RichTextResources.cutAction_text);
		cutItem.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent event) {
				CutAction action = new CutAction();
				action.execute(RichText.this);
			}
		});
		final MenuItem copyItem = new MenuItem(contextMenu, SWT.PUSH);
		copyItem.setText(RichTextResources.copyAction_text); //$NON-NLS-1$
		copyItem.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent event) {
				CopyAction action = new CopyAction();
				action.execute(RichText.this);
			}
		});
		final MenuItem pasteItem = new MenuItem(contextMenu, SWT.PUSH);
		pasteItem.setText(RichTextResources.pasteAction_text); //$NON-NLS-1$
		pasteItem.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent event) {
				PasteAction action = new PasteAction();
				action.execute(RichText.this);
			}
		});

		contextMenu.addMenuListener(new MenuListener() {
			public void menuHidden(MenuEvent e) {
			}

			public void menuShown(MenuEvent e) {
				getSelectedText();
				cutItem.setEnabled(editable && selection);
				copyItem.setEnabled(selection);
				pasteItem.setEnabled(editable);
			}
		});
	}

	/**
	 * Adds listeners to manage the activation and focus events.
	 */
	protected void addListeners() {
		editorControl = getControlSite(editor);
		if (editorControl != null) {
			if (debug) {
				printDebugMessage(
						"init", "editorControl=" + editorControl.getClass().getName()); //$NON-NLS-1$ //$NON-NLS-2$
			}

			editorControl.addListener(SWT.Activate, new Listener() {
				public void handleEvent(Event event) {
					if (debug) {
						printDebugMessage("activateListener"); //$NON-NLS-1$
					}
					notifyListeners(SWT.Activate, event);
				}
			});

			editorControl.addListener(SWT.Deactivate, new Listener() {
				public void handleEvent(Event event) {
					if (debug) {
						printDebugMessage("deactivateListener"); //$NON-NLS-1$
					}
					notifyListeners(SWT.Deactivate, event);
				}
			});

			editorControl.addListener(SWT.FocusIn, new Listener() {
				public void handleEvent(Event event) {
					if (debug) {
						printDebugMessage("focusInListener"); //$NON-NLS-1$							
					}
					executeCommand("updateSelection"); //$NON-NLS-1$					
					notifyListeners(SWT.FocusIn, event);
				}
			});

			editorControl.addListener(SWT.FocusOut, new Listener() {
				public void handleEvent(Event event) {
					if (debug) {
						printDebugMessage("focusOutListener"); //$NON-NLS-1$							
					}
					notifyListeners(SWT.FocusOut, event);
				}
			});

			editor.addKeyListener(new KeyAdapter() {
				public void keyReleased(KeyEvent e) {
					if ((e.stateMask & SWT.CTRL) > 0
							|| (e.stateMask & SWT.ALT) > 0)
						return;
					if (editable) {
						switch (e.keyCode) {
						case SWT.ARROW_DOWN:
						case SWT.ARROW_LEFT:
						case SWT.ARROW_RIGHT:
						case SWT.ARROW_UP:
						case SWT.END:
						case SWT.HOME:
						case SWT.PAGE_DOWN:
						case SWT.PAGE_UP:
						case SWT.SHIFT:
							break;
						default:
							checkModify();
							break;
						}
					}
				}
			});

			editor.addLocationListener(new LocationAdapter() {
				public void changing(LocationEvent event) {
					// Deactivate the links in the content page in readonly
					// mode.
					event.doit = editable;
				}
			});
		} else {
			editor.addListener(SWT.Activate, new Listener() {
				public void handleEvent(Event event) {
					if (debug) {
						printDebugMessage("activateListener"); //$NON-NLS-1$
					}
					setFocus();
				}
			});

			editor.addKeyListener(new KeyListener() {
				public void keyPressed(KeyEvent e) {
					if (!editable) {
						e.doit = false;
					}
				}

				public void keyReleased(KeyEvent e) {
					if ((e.stateMask & SWT.CTRL) > 0
							|| (e.stateMask & SWT.ALT) > 0)
						return;
					if (editable) {
						switch (e.keyCode) {
						case SWT.ARROW_DOWN:
						case SWT.ARROW_LEFT:
						case SWT.ARROW_RIGHT:
						case SWT.ARROW_UP:
						case SWT.END:
						case SWT.HOME:
						case SWT.PAGE_DOWN:
						case SWT.PAGE_UP:
						case SWT.SHIFT:
							break;
						default:
							checkModify();
							break;
						}
					}
				}
			});
		}

		editor.addDisposeListener(new DisposeListener() {
			public void widgetDisposed(DisposeEvent e) {
				if (debug) {
					printDebugMessage("disposeListener"); //$NON-NLS-1$						
				}
				dispose();
			}
		});

		listeners = new Hashtable();
		modifyListeners = new ArrayList();
	}

	/**
	 * Notifies the rich text event listeners.
	 * 
	 * @param eventType
	 *            the event type
	 * @param event
	 *            the SWT event
	 */
	protected void notifyListeners(int eventType, Event event) {
		if (notifyingModifyListeners) {
			return;
		}

		event.display = Display.getCurrent();
		event.widget = editor;

		for (Iterator i = listeners.values().iterator(); i.hasNext();) {
			RichTextListener listener = (RichTextListener) i.next();
			if (listener.getEventType() == eventType) {
				if (debug) {
					printDebugMessage(
							"notifyListeners", "notifying listener, " + listener + ", eventType=" + eventType); //$NON-NLS-1$ //$NON-NLS-2$	//$NON-NLS-3$	
				}
				listener.getListener().handleEvent(event);
				if (debug) {
					printDebugMessage(
							"notifyListeners", "notified listener, " + listener + ", eventType=" + eventType); //$NON-NLS-1$ //$NON-NLS-2$	//$NON-NLS-3$	
				}
			}
		}
	}

	/**
	 * Notifies the modify listeners that the rich text editor content has
	 * changed.
	 */
	public void notifyModifyListeners() {
		notifyingModifyListeners = true;

		Event event = new Event();
		event.display = Display.getCurrent();
		event.widget = editor;

		for (Iterator i = modifyListeners.iterator(); i.hasNext();) {
			ModifyListener listener = (ModifyListener) i.next();
			if (debug) {
				printDebugMessage(
						"notifyModifyListeners", "notifying listener, " + listener); //$NON-NLS-1$ //$NON-NLS-2$	
			}
			listener.modifyText(new ModifyEvent(event));
			if (debug) {
				printDebugMessage(
						"notifyModifyListeners", "notified listener, " + listener); //$NON-NLS-1$ //$NON-NLS-2$	
			}
		}

		notifyingModifyListeners = false;
	}

	/**
	 * Checks whether the editor content has been modified. If it has been
	 * modified, notify the modify listeners.
	 */
	protected void checkModify() {
		if (modified) {
			notifyModifyListeners();
		} else {
			if (!getText().equals(initialText)) {
				modified = true;
				notifyModifyListeners();
			}
		}
		if (debug) {
			printDebugMessage("checkModify", "modified=" + modified); //$NON-NLS-1$ //$NON-NLS-2$	
		}
	}

	/**
	 * This provides an opportunity for a sub class to tidy up the rich text.
	 * 
	 * @param text
	 *            rich text encoded in HTML format
	 */
	protected String tidyText(String text) {
		return text;
	}

	/**
	 * Formats the text for consumption by the JavaScript/DHTML editor.
	 * 
	 * @param text
	 *            rich text encoded in HTML format
	 */
	protected String formatText(String text) {
		if (text == null || text.length() == 0) {
			return text;
		}
		StringBuffer result = new StringBuffer();
		int textSize = text.length();
		for (int i = 0; i < textSize; i++) {
			char ch = text.charAt(i);
			switch (ch) {
			case '\r':
				break;
			case '\t':
				result.append(' ');
				break;
			case '\n':
				result.append(ENCODED_NEWLINE);
				break;
			case '\'':
				result.append(ENCODED_SINGLE_QUOTE);
				break;
			case '\\':
				result.append("\\\\"); //$NON-NLS-1$
				break;
			default:
				result.append(ch);
			}
		}
		return result.toString();
	}

	/**
	 * Returns the child <code>OleControlSite</code> contained within the
	 * given <code>Composite</code>.
	 * 
	 * @param composite
	 *            a <code>Composite</code> object, presumably a
	 *            <code>Browser</code>
	 * @return an <code>OleControlSite</code> object
	 */
	private Control getControlSite(Composite composite) {
		if (Platform.getOS().equals("win32")) { //$NON-NLS-1$
			Control[] controls = composite.getChildren();
			for (int i = 0; i < controls.length; i++) {
				String controlClass = controls[i].getClass().getName();
				if (controlClass.equals("org.eclipse.swt.browser.WebSite")) { //$NON-NLS-1$
					return controls[i];
				} else if (controls[i] instanceof Composite) {
					return getControlSite((Composite) controls[i]);
				}
			}
		}
		return null;
	}

	/**
	 * Displays the given debug message to the console.
	 */
	private void printDebugMessage(String method, String msg, String text) {
		StringBuffer strBuf = new StringBuffer();
		strBuf.append("RichText[").append(editor.handle).append(']') //$NON-NLS-1$
				.append('.').append(method);
		if (msg != null && msg.length() > 0) {
			strBuf.append(": ").append(msg); //$NON-NLS-1$
		}
		if (text != null && text.length() > 0) {
			strBuf.append('\n').append(text);
		}
		System.out.println(strBuf);
	}

	/**
	 * Displays the given debug message to the console.
	 */
	private void printDebugMessage(String method, String msg) {
		printDebugMessage(method, msg, null);
	}

	/**
	 * Displays the given debug message to the console.
	 */
	private void printDebugMessage(String method) {
		printDebugMessage(method, null);
	}

}