X-Git-Url: http://secure.phpeclipse.com diff --git a/net.sourceforge.phpeclipse.ui/src/net/sourceforge/phpdt/internal/ui/text/TypingRunDetector.java b/net.sourceforge.phpeclipse.ui/src/net/sourceforge/phpdt/internal/ui/text/TypingRunDetector.java new file mode 100644 index 0000000..d74a9c3 --- /dev/null +++ b/net.sourceforge.phpeclipse.ui/src/net/sourceforge/phpdt/internal/ui/text/TypingRunDetector.java @@ -0,0 +1,491 @@ +/******************************************************************************* + * 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; + +import org.eclipse.jface.text.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 TypingRunDetector observes + * TypingRun events. A typing run is a sequence of similar text + * modifications, such as inserting or deleting single characters. + *

+ * Listeners are informed about the start and end of a TypingRun. + *

+ * + * @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 type. + * + * @param type + * the ChangeType 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 true if the receiver can extend the typing + * range the last change of which is described by change. + * + * @param change + * the last change in a typing run + * @return true if the receiver is a valid extension to + * change,false 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 true if the receiver describes a text + * modification, false if it describes a focus / + * selection change. + * + * @return true 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 null in + * uninstall. + */ + private ITextViewer fViewer; + + /** The text event listener. */ + private final TextListener fTextListener = new TextListener(); + + /** + * The selection listener. Set to null when no run is active. + */ + private SelectionListener fSelectionListener; + + /* state variables */ + + /** The most recently observed change. Never null. */ + private Change fLastChange; + + /** The current run, or null 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. install() + * 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 TypingRun 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 listener is not + * registered with the receiver, nothing happens. + * + * @param listener + * the listener to remove, or null + */ + 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 true if there is an active run, false + * otherwise. + * + * @return true if there is an active run, false + * 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 TypingRun. + * + * @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 TypingRun. + * + * @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); + } + } +}