1 /*******************************************************************************
2 * Copyright (c) 2000, 2003 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Common Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/cpl-v10.html
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.text;
13 import java.util.ArrayList;
14 import java.util.HashSet;
15 import java.util.Iterator;
16 import java.util.List;
19 import net.sourceforge.phpdt.internal.ui.text.TypingRun.ChangeType;
21 import org.eclipse.jface.text.Assert;
22 import org.eclipse.jface.text.DocumentEvent;
23 import org.eclipse.jface.text.ITextListener;
24 import org.eclipse.jface.text.ITextViewer;
25 import org.eclipse.jface.text.TextEvent;
26 import org.eclipse.swt.SWT;
27 import org.eclipse.swt.custom.StyledText;
28 import org.eclipse.swt.events.FocusEvent;
29 import org.eclipse.swt.events.FocusListener;
30 import org.eclipse.swt.events.KeyEvent;
31 import org.eclipse.swt.events.KeyListener;
32 import org.eclipse.swt.events.MouseEvent;
33 import org.eclipse.swt.events.MouseListener;
37 * When connected to a text viewer, a <code>TypingRunDetector</code> observes
38 * <code>TypingRun</code> events. A typing run is a sequence of similar text
39 * modifications, such as inserting or deleting single characters.
41 * Listeners are informed about the start and end of a <code>TypingRun</code>.
46 public class TypingRunDetector {
48 * Implementation note: This class is independent of JDT and may be pulled
49 * up to jface.text if needed.
53 private static final boolean DEBUG= false;
56 * Instances of this class abstract a text modification into a simple
57 * description. Typing runs consists of a sequence of one or more modifying
58 * changes of the same type. Every change records the type of change
59 * described by a text modification, and an offset it can be followed by
60 * another change of the same run.
62 private static final class Change {
63 private ChangeType fType;
64 private int fNextOffset;
67 * Creates a new change of type <code>type</code>.
69 * @param type the <code>ChangeType</code> of the new change
70 * @param nextOffset the offset of the next change in a typing run
72 public Change(ChangeType type, int nextOffset) {
74 fNextOffset= nextOffset;
78 * Returns <code>true</code> if the receiver can extend the typing
79 * range the last change of which is described by <code>change</code>.
81 * @param change the last change in a typing run
82 * @return <code>true</code> if the receiver is a valid extension to
83 * <code>change</code>,<code>false</code> otherwise
85 public boolean canFollow(Change change) {
86 if (fType == TypingRun.NO_CHANGE)
88 else if (fType.equals(TypingRun.UNKNOWN))
90 if (fType.equals(change.fType)) {
91 if (fType == TypingRun.DELETE)
92 return fNextOffset == change.fNextOffset - 1;
93 else if (fType == TypingRun.INSERT)
94 return fNextOffset == change.fNextOffset + 1;
95 else if (fType == TypingRun.OVERTYPE)
96 return fNextOffset == change.fNextOffset + 1;
97 else if (fType == TypingRun.SELECTION)
104 * Returns <code>true</code> if the receiver describes a text
105 * modification, <code>false</code> if it describes a focus /
108 * @return <code>true</code> if the receiver is a text modification
110 public boolean isModification() {
111 return fType.isModification();
115 * @see java.lang.Object#toString()
117 public String toString() {
118 return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$
122 * Returns the change type of this change.
124 * @return the change type of this change
126 public ChangeType getType() {
132 * Observes any events that modify the content of the document displayed in
133 * the editor. Since text events may start a new run, this listener is
134 * always registered if the detector is connected.
136 private class TextListener implements ITextListener {
139 * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
141 public void textChanged(TextEvent event) {
142 handleTextChanged(event);
147 * Observes non-modifying events that will end a run, such as clicking into
148 * the editor, moving the caret, and the editor losing focus. These events
149 * can never start a run, therefore this listener is only registered if
150 * there is an ongoing run.
152 private class SelectionListener implements MouseListener, KeyListener, FocusListener {
155 * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
157 public void focusGained(FocusEvent e) {
158 handleSelectionChanged();
162 * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
164 public void focusLost(FocusEvent e) {
168 * @see MouseListener#mouseDoubleClick
170 public void mouseDoubleClick(MouseEvent e) {
174 * If the right mouse button is pressed, the current editing command is closed
175 * @see MouseListener#mouseDown
177 public void mouseDown(MouseEvent e) {
179 handleSelectionChanged();
183 * @see MouseListener#mouseUp
185 public void mouseUp(MouseEvent e) {
189 * @see KeyListener#keyPressed
191 public void keyReleased(KeyEvent e) {
195 * On cursor keys, the current editing command is closed
196 * @see KeyListener#keyPressed
198 public void keyPressed(KeyEvent e) {
203 case SWT.ARROW_RIGHT:
208 handleSelectionChanged();
214 /** The listeners. */
215 private final Set fListeners= new HashSet();
217 * The viewer we work upon. Set to <code>null</code> in
218 * <code>uninstall</code>.
220 private ITextViewer fViewer;
221 /** The text event listener. */
222 private final TextListener fTextListener= new TextListener();
224 * The selection listener. Set to <code>null</code> when no run is active.
226 private SelectionListener fSelectionListener;
228 /* state variables */
230 /** The most recently observed change. Never <code>null</code>. */
231 private Change fLastChange;
232 /** The current run, or <code>null</code> if there is none. */
233 private TypingRun fRun;
236 * Installs the receiver with a text viewer.
238 * @param viewer the viewer to install on
240 public void install(ITextViewer viewer) {
241 Assert.isLegal(viewer != null);
247 * Initializes the state variables and registers any permanent listeners.
249 private void connect() {
250 if (fViewer != null) {
251 fLastChange= new Change(TypingRun.UNKNOWN, -1);
253 fSelectionListener= null;
254 fViewer.addTextListener(fTextListener);
259 * Uninstalls the receiver and removes all listeners. <code>install()</code>
260 * must be called for events to be generated.
262 public void uninstall() {
263 if (fViewer != null) {
271 * Disconnects any registered listeners.
273 private void disconnect() {
274 fViewer.removeTextListener(fTextListener);
275 ensureSelectionListenerRemoved();
279 * Adds a listener for <code>TypingRun</code> events. Repeatedly adding
280 * the same listener instance has no effect. Listeners may be added even
281 * if the receiver is neither connected nor installed.
283 * @param listener the listener add
285 public void addTypingRunListener(ITypingRunListener listener) {
286 Assert.isLegal(listener != null);
287 fListeners.add(listener);
288 if (fListeners.size() == 1)
293 * Removes the listener from this manager. If <code>listener</code> is not
294 * registered with the receiver, nothing happens.
296 * @param listener the listener to remove, or <code>null</code>
298 public void removeTypingRunListener(ITypingRunListener listener) {
299 fListeners.remove(listener);
300 if (fListeners.size() == 0)
305 * Handles an incoming text event.
307 * @param event the text event that describes the text modification
309 void handleTextChanged(TextEvent event) {
310 Change type= computeChange(event);
315 * Computes the change abstraction given a text event.
317 * @param event the text event to analyze
318 * @return a change object describing the event
320 private Change computeChange(TextEvent event) {
321 DocumentEvent e= event.getDocumentEvent();
323 return new Change(TypingRun.NO_CHANGE, -1);
325 int start= e.getOffset();
326 int end= e.getOffset() + e.getLength();
327 String newText= e.getText();
329 newText= new String();
332 // no replace / delete / overwrite
333 if (newText.length() == 1)
334 return new Change(TypingRun.INSERT, end + 1);
335 } else if (start == end - 1) {
336 if (newText.length() == 1)
337 return new Change(TypingRun.OVERTYPE, end);
338 if (newText.length() == 0)
339 return new Change(TypingRun.DELETE, start);
342 return new Change(TypingRun.UNKNOWN, -1);
346 * Handles an incoming selection event.
348 void handleSelectionChanged() {
349 handleChange(new Change(TypingRun.SELECTION, -1));
353 * State machine. Changes state given the current state and the incoming
356 * @param change the incoming change
358 private void handleChange(Change change) {
359 if (change.getType() == TypingRun.NO_CHANGE)
363 System.err.println("Last change: " + fLastChange); //$NON-NLS-1$
365 if (!change.canFollow(fLastChange))
366 endIfStarted(change);
368 if (change.isModification())
372 System.err.println("New change: " + change); //$NON-NLS-1$
376 * Starts a new run if there is none and informs all listeners. If there
377 * already is a run, nothing happens.
379 private void startOrContinue() {
382 System.err.println("+Start run"); //$NON-NLS-1$
383 fRun= new TypingRun(fLastChange.getType());
384 ensureSelectionListenerAdded();
390 * Returns <code>true</code> if there is an active run, <code>false</code>
393 * @return <code>true</code> if there is an active run, <code>false</code>
396 private boolean hasRun() {
401 * Ends any active run and informs all listeners. If there is none, nothing
404 * @param change the change that triggered ending the active run
406 private void endIfStarted(Change change) {
408 ensureSelectionListenerRemoved();
410 System.err.println("-End run"); //$NON-NLS-1$
411 fireRunEnded(fRun, change.getType());
417 * Adds the selection listener to the text widget underlying the viewer, if
420 private void ensureSelectionListenerAdded() {
421 if (fSelectionListener == null) {
422 fSelectionListener= new SelectionListener();
423 StyledText textWidget= fViewer.getTextWidget();
424 textWidget.addFocusListener(fSelectionListener);
425 textWidget.addKeyListener(fSelectionListener);
426 textWidget.addMouseListener(fSelectionListener);
431 * If there is a selection listener, it is removed from the text widget
432 * underlying the viewer.
434 private void ensureSelectionListenerRemoved() {
435 if (fSelectionListener != null) {
436 StyledText textWidget= fViewer.getTextWidget();
437 textWidget.removeFocusListener(fSelectionListener);
438 textWidget.removeKeyListener(fSelectionListener);
439 textWidget.removeMouseListener(fSelectionListener);
440 fSelectionListener= null;
445 * Informs all listeners about a newly started <code>TypingRun</code>.
447 * @param run the new run
449 private void fireRunBegun(TypingRun run) {
450 List listeners= new ArrayList(fListeners);
451 for (Iterator it= listeners.iterator(); it.hasNext();) {
452 ITypingRunListener listener= (ITypingRunListener) it.next();
453 listener.typingRunStarted(fRun);
458 * Informs all listeners about an ended <code>TypingRun</code>.
460 * @param run the previously active run
461 * @param reason the type of change that caused the run to be ended
463 private void fireRunEnded(TypingRun run, ChangeType reason) {
464 List listeners= new ArrayList(fListeners);
465 for (Iterator it= listeners.iterator(); it.hasNext();) {
466 ITypingRunListener listener= (ITypingRunListener) it.next();
467 listener.typingRunEnded(fRun, reason);