/*
* Copyright (c) 2011, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.ui.internal.text.functions;
import com.google.dart.tools.ui.internal.text.functions.TypingRun.ChangeType;
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 java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* 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.
*/
/**
* 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 the change type of this change.
*
* @return the change type of this change
*/
public ChangeType getType() {
return fType;
}
/**
* 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()
*/
@Override
public String toString() {
return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$
}
}
/**
* 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)
*/
@Override
public void focusGained(FocusEvent e) {
handleSelectionChanged();
}
/*
* @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events .FocusEvent)
*/
@Override
public void focusLost(FocusEvent e) {
}
/*
* On cursor keys, the current editing command is closed
*
* @see KeyListener#keyPressed
*/
@Override
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;
}
}
/*
* @see KeyListener#keyPressed
*/
@Override
public void keyReleased(KeyEvent e) {
}
/*
* @see MouseListener#mouseDoubleClick
*/
@Override
public void mouseDoubleClick(MouseEvent e) {
}
/*
* If the right mouse button is pressed, the current editing command is closed
*
* @see MouseListener#mouseDown
*/
@Override
public void mouseDown(MouseEvent e) {
if (e.button == 1) {
handleSelectionChanged();
}
}
/*
* @see MouseListener#mouseUp
*/
@Override
public void mouseUp(MouseEvent e) {
}
}
/**
* 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)
*/
@Override
public void textChanged(TextEvent event) {
handleTextChanged(event);
}
}
/** Debug flag. */
private static final boolean DEBUG = false;
/** The listeners. */
private final Set<ITypingRunListener> fListeners = new HashSet<ITypingRunListener>();
/**
* 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;
/**
* 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();
}
}
/**
* 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();
}
/**
* 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();
}
}
/**
* 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;
}
}
/**
* Handles an incoming selection event.
*/
void handleSelectionChanged() {
handleChange(new Change(TypingRun.SELECTION, -1));
}
/**
* 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);
}
/**
* 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);
}
}
/**
* Disconnects any registered listeners.
*/
private void disconnect() {
fViewer.removeTextListener(fTextListener);
ensureSelectionListenerRemoved();
}
/**
* 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<ITypingRunListener> listeners = new ArrayList<ITypingRunListener>(fListeners);
for (Iterator<ITypingRunListener> it = listeners.iterator(); it.hasNext();) {
ITypingRunListener listener = 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<ITypingRunListener> listeners = new ArrayList<ITypingRunListener>(fListeners);
for (Iterator<ITypingRunListener> it = listeners.iterator(); it.hasNext();) {
ITypingRunListener listener = it.next();
listener.typingRunEnded(fRun, reason);
}
}
/**
* 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$
}
}
/**
* 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;
}
/**
* 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);
}
}
}