content: /*******************************************************************************
   * Copyright (c) 2000, 2008 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 API and implementation
   *******************************************************************************/
  package org.eclipse.wst.jsdt.internal.ui.text;
  
  import java.util.ArrayList;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Set;
  
  import org.eclipse.core.runtime.Assert;
  import org.eclipse.jface.text.DocumentEvent;
  import org.eclipse.jface.text.ITextListener;
  import org.eclipse.jface.text.ITextViewer;
  import org.eclipse.jface.text.TextEvent;
  import org.eclipse.swt.SWT;
  import org.eclipse.swt.custom.StyledText;
  import org.eclipse.swt.events.FocusEvent;
  import org.eclipse.swt.events.FocusListener;
  import org.eclipse.swt.events.KeyEvent;
  import org.eclipse.swt.events.KeyListener;
  import org.eclipse.swt.events.MouseEvent;
  import org.eclipse.swt.events.MouseListener;
  import org.eclipse.wst.jsdt.internal.ui.text.TypingRun.ChangeType;
  
  
  /**
   * When connected to a text viewer, a <code>TypingRunDetector</code> observes
   * <code>TypingRun</code> events. A typing run is a sequence of similar text
   * modifications, such as inserting or deleting single characters.
   * <p>
   * Listeners are informed about the start and end of a <code>TypingRun</code>.
   * </p>
   *
   * 
   */
  public class TypingRunDetector {
  	/*
  	 * Implementation note: This class is independent of JDT and may be pulled
  	 * up to jface.text if needed.
  	 */
  
  	/** Debug flag. */
  	private static final boolean DEBUG= false;
  
  	/**
  	 * Instances of this class abstract a text modification into a simple
  	 * description. Typing runs consists of a sequence of one or more modifying
  	 * changes of the same type. Every change records the type of change
  	 * described by a text modification, and an offset it can be followed by
  	 * another change of the same run.
  	 */
  	private static final class Change {
  		private ChangeType fType;
  		private int fNextOffset;
  
  		/**
  		 * Creates a new change of type <code>type</code>.
  		 *
  		 * @param type the <code>ChangeType</code> of the new change
  		 * @param nextOffset the offset of the next change in a typing run
  		 */
  		public Change(ChangeType type, int nextOffset) {
  			fType= type;
  			fNextOffset= nextOffset;
  		}
  
  		/**
  		 * Returns <code>true</code> if the receiver can extend the typing run
  		 * the last change of which is described by <code>change</code>.
  		 *
  		 * @param change the last change in a typing run
  		 * @return <code>true</code> if the receiver is a valid extension to
  		 *         <code>change</code>, <code>false</code> otherwise
  		 */
  		public boolean canFollow(Change change) {
  			if (fType == TypingRun.NO_CHANGE)
  				return true;
  			if (fType.equals(TypingRun.UNKNOWN))
  				return false;
  			if (fType.equals(change.fType)) {
  				if (fType == TypingRun.DELETE)
  					return fNextOffset == change.fNextOffset - 1;
  				else if (fType == TypingRun.INSERT)
  					return fNextOffset == change.fNextOffset + 1;
  				else if (fType == TypingRun.OVERTYPE)
  					return fNextOffset == change.fNextOffset + 1;
  				else if (fType == TypingRun.SELECTION)
  					return true;
  			}
  			return false;
  		}
  
  		/**
  		 * Returns <code>true</code> if the receiver describes a text
  		 * modification, <code>false</code> if it describes a focus /
  		 * selection change.
  		 *
  		 * @return <code>true</code> if the receiver is a text modification
  		 */
  		public boolean isModification() {
  			return fType.isModification();
  		}
  
  		/*
  		 * @see java.lang.Object#toString()
  		 */
  		public String toString() {
  			return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$
  		}
  
  		/**
  		 * Returns the change type of this change.
  		 *
  		 * @return the change type of this change
  		 */
  		public ChangeType getType() {
  			return fType;
  		}
  	}
  
  	/**
  	 * Observes any events that modify the content of the document displayed in
  	 * the editor. Since text events may start a new run, this listener is
  	 * always registered if the detector is connected.
  	 */
  	private class TextListener implements ITextListener {
  
  		/*
  		 * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
  		 */
  		public void textChanged(TextEvent event) {
  			handleTextChanged(event);
  		}
  	}
  
  	/**
  	 * Observes non-modifying events that will end a run, such as clicking into
  	 * the editor, moving the caret, and the editor losing focus. These events
  	 * can never start a run, therefore this listener is only registered if
  	 * there is an ongoing run.
  	 */
  	private class SelectionListener implements MouseListener, KeyListener, FocusListener {
  
  		/*
  		 * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
  		 */
  		public void focusGained(FocusEvent e) {
  			handleSelectionChanged();
  		}
  
  		/*
  		 * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
  		 */
  		public void focusLost(FocusEvent e) {
  		}
  
  		/*
  		 * @see MouseListener#mouseDoubleClick
  		 */
  		public void mouseDoubleClick(MouseEvent e) {
  		}
  
  		/*
  		 * If the right mouse button is pressed, the current editing command is closed
  		 * @see MouseListener#mouseDown
  		 */
  		public void mouseDown(MouseEvent e) {
  			if (e.button == 1)
  				handleSelectionChanged();
  		}
  
  		/*
  		 * @see MouseListener#mouseUp
  		 */
  		public void mouseUp(MouseEvent e) {
  		}
  
  		/*
  		 * @see KeyListener#keyPressed
  		 */
  		public void keyReleased(KeyEvent e) {
  		}
  
  		/*
  		 * On cursor keys, the current editing command is closed
  		 * @see KeyListener#keyPressed
  		 */
  		public void keyPressed(KeyEvent e) {
  			switch (e.keyCode) {
  				case SWT.ARROW_UP:
  				case SWT.ARROW_DOWN:
  				case SWT.ARROW_LEFT:
  				case SWT.ARROW_RIGHT:
  				case SWT.END:
  				case SWT.HOME:
  				case SWT.PAGE_DOWN:
  				case SWT.PAGE_UP:
  					handleSelectionChanged();
  					break;
  			}
  		}
  	}
  
  	/** The listeners. */
  	private final Set fListeners= new HashSet();
  	/**
  	 * The viewer we work upon. Set to <code>null</code> in
  	 * <code>uninstall</code>.
  	 */
  	private ITextViewer fViewer;
  	/** The text event listener. */
  	private final TextListener fTextListener= new TextListener();
  	/**
  	 * The selection listener. Set to <code>null</code> when no run is active.
  	 */
  	private SelectionListener fSelectionListener;
  
  	/* state variables */
  
  	/** The most recently observed change. Never <code>null</code>. */
  	private Change fLastChange;
  	/** The current run, or <code>null</code> if there is none. */
  	private TypingRun fRun;
  
  	/**
  	 * Installs the receiver with a text viewer.
  	 *
  	 * @param viewer the viewer to install on
  	 */
  	public void install(ITextViewer viewer) {
  		Assert.isLegal(viewer != null);
  		fViewer= viewer;
  		connect();
  	}
  
  	/**
  	 * Initializes the state variables and registers any permanent listeners.
  	 */
  	private void connect() {
  		if (fViewer != null) {
  			fLastChange= new Change(TypingRun.UNKNOWN, -1);
  			fRun= null;
  			fSelectionListener= null;
  			fViewer.addTextListener(fTextListener);
  		}
  	}
  
  	/**
  	 * Uninstalls the receiver and removes all listeners. <code>install()</code>
  	 * must be called for events to be generated.
  	 */
  	public void uninstall() {
  		if (fViewer != null) {
  			fListeners.clear();
  			disconnect();
  			fViewer= null;
  		}
  	}
  
  	/**
  	 * Disconnects any registered listeners.
  	 */
  	private void disconnect() {
  		fViewer.removeTextListener(fTextListener);
  		ensureSelectionListenerRemoved();
  	}
  
  	/**
  	 * Adds a listener for <code>TypingRun</code> events. Repeatedly adding
  	 * the same listener instance has no effect. Listeners may be added even
  	 * if the receiver is neither connected nor installed.
  	 *
  	 * @param listener the listener add
  	 */
  	public void addTypingRunListener(ITypingRunListener listener) {
  		Assert.isLegal(listener != null);
  		fListeners.add(listener);
  		if (fListeners.size() == 1)
  			connect();
  	}
  
  	/**
  	 * Removes the listener from this manager. If <code>listener</code> is not
  	 * registered with the receiver, nothing happens.
  	 *
  	 * @param listener the listener to remove, or <code>null</code>
  	 */
  	public void removeTypingRunListener(ITypingRunListener listener) {
  		fListeners.remove(listener);
  		if (fListeners.size() == 0)
  			disconnect();
  	}
  
  	/**
  	 * Handles an incoming text event.
  	 *
  	 * @param event the text event that describes the text modification
  	 */
  	void handleTextChanged(TextEvent event) {
  		Change type= computeChange(event);
  		handleChange(type);
  	}
  
  	/**
  	 * Computes the change abstraction given a text event.
  	 *
  	 * @param event the text event to analyze
  	 * @return a change object describing the event
  	 */
  	private Change computeChange(TextEvent event) {
  		DocumentEvent e= event.getDocumentEvent();
  		if (e == null)
  			return new Change(TypingRun.NO_CHANGE, -1);
  
  		int start= e.getOffset();
  		int end= e.getOffset() + e.getLength();
  		String newText= e.getText();
  		if (newText == null)
  			newText= new String();
  
  		if (start == end) {
  			// no replace / delete / overwrite
  			if (newText.length() == 1)
  				return new Change(TypingRun.INSERT, end + 1);
  		} else if (start == end - 1) {
  			if (newText.length() == 1)
  				return new Change(TypingRun.OVERTYPE, end);
  			if (newText.length() == 0)
  				return new Change(TypingRun.DELETE, start);
  		}
  
  		return new Change(TypingRun.UNKNOWN, -1);
  	}
  
  	/**
  	 * Handles an incoming selection event.
  	 */
  	void handleSelectionChanged() {
  		handleChange(new Change(TypingRun.SELECTION, -1));
  	}
  
  	/**
  	 * State machine. Changes state given the current state and the incoming
  	 * change.
  	 *
  	 * @param change the incoming change
  	 */
  	private void handleChange(Change change) {
  		if (change.getType() == TypingRun.NO_CHANGE)
  			return;
  
  		if (DEBUG)
  			System.err.println("Last change: " + fLastChange); //$NON-NLS-1$
  
  		if (!change.canFollow(fLastChange))
  			endIfStarted(change);
  		fLastChange= change;
  		if (change.isModification())
  			startOrContinue();
  
  		if (DEBUG)
  			System.err.println("New change: " + change); //$NON-NLS-1$
  	}
  
  	/**
  	 * Starts a new run if there is none and informs all listeners. If there
  	 * already is a run, nothing happens.
  	 */
  	private void startOrContinue() {
  		if (!hasRun()) {
  			if (DEBUG)
  				System.err.println("+Start run"); //$NON-NLS-1$
  			fRun= new TypingRun(fLastChange.getType());
  			ensureSelectionListenerAdded();
  			fireRunBegun(fRun);
  		}
  	}
  
  	/**
  	 * Returns <code>true</code> if there is an active run, <code>false</code>
  	 * otherwise.
  	 *
  	 * @return <code>true</code> if there is an active run, <code>false</code>
  	 *         otherwise
  	 */
  	private boolean hasRun() {
  		return fRun != null;
  	}
  
  	/**
  	 * Ends any active run and informs all listeners. If there is none, nothing
  	 * happens.
  	 *
  	 * @param change the change that triggered ending the active run
  	 */
  	private void endIfStarted(Change change) {
  		if (hasRun()) {
  			ensureSelectionListenerRemoved();
  			if (DEBUG)
  				System.err.println("-End run"); //$NON-NLS-1$
  			fireRunEnded(fRun, change.getType());
  			fRun= null;
  		}
  	}
  
  	/**
  	 * Adds the selection listener to the text widget underlying the viewer, if
  	 * not already done.
  	 */
  	private void ensureSelectionListenerAdded() {
  		if (fSelectionListener == null) {
  			fSelectionListener= new SelectionListener();
  			StyledText textWidget= fViewer.getTextWidget();
  			textWidget.addFocusListener(fSelectionListener);
  			textWidget.addKeyListener(fSelectionListener);
  			textWidget.addMouseListener(fSelectionListener);
  		}
  	}
  
  	/**
  	 * If there is a selection listener, it is removed from the text widget
  	 * underlying the viewer.
  	 */
  	private void ensureSelectionListenerRemoved() {
  		if (fSelectionListener != null) {
  			StyledText textWidget= fViewer.getTextWidget();
  			textWidget.removeFocusListener(fSelectionListener);
  			textWidget.removeKeyListener(fSelectionListener);
  			textWidget.removeMouseListener(fSelectionListener);
  			fSelectionListener= null;
  		}
  	}
  
  	/**
  	 * Informs all listeners about a newly started <code>TypingRun</code>.
  	 *
  	 * @param run the new run
  	 */
  	private void fireRunBegun(TypingRun run) {
  		List listeners= new ArrayList(fListeners);
  		for (Iterator it= listeners.iterator(); it.hasNext();) {
  			ITypingRunListener listener= (ITypingRunListener) it.next();
  			listener.typingRunStarted(fRun);
  		}
  	}
  
  	/**
  	 * Informs all listeners about an ended <code>TypingRun</code>.
  	 *
  	 * @param run the previously active run
  	 * @param reason the type of change that caused the run to be ended
  	 */
  	private void fireRunEnded(TypingRun run, ChangeType reason) {
  		List listeners= new ArrayList(fListeners);
  		for (Iterator it= listeners.iterator(); it.hasNext();) {
  			ITypingRunListener listener= (ITypingRunListener) it.next();
  			listener.typingRunEnded(fRun, reason);
  		}
  	}
  }
  