--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2000, 2003 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Common Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/cpl-v10.html
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package net.sourceforge.phpdt.internal.ui.text;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import net.sourceforge.phpdt.internal.ui.text.TypingRun.ChangeType;
+
+//incastrix
+//import org.eclipse.jface.text.Assert;
+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;
+
+/**
+ * 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>
+ *
+ * @since 3.0
+ */
+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
+ * range 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;
+ else 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);
+ }
+ }
+}