1 /*******************************************************************************
2 * Copyright (c) 2000, 2011 IBM Corporation and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * http://www.eclipse.org/legal/epl-v10.html
9 * IBM Corporation - initial API and implementation
10 *******************************************************************************/
11 package org.eclipse.jdt.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 org.eclipse.swt.SWT;
20 import org.eclipse.swt.custom.StyledText;
21 import org.eclipse.swt.events.FocusEvent;
22 import org.eclipse.swt.events.FocusListener;
23 import org.eclipse.swt.events.KeyEvent;
24 import org.eclipse.swt.events.KeyListener;
25 import org.eclipse.swt.events.MouseEvent;
26 import org.eclipse.swt.events.MouseListener;
28 import org.eclipse.core.runtime.Assert;
30 import org.eclipse.jface.text.DocumentEvent;
31 import org.eclipse.jface.text.ITextListener;
32 import org.eclipse.jface.text.ITextViewer;
33 import org.eclipse.jface.text.TextEvent;
35 import org.eclipse.jdt.internal.ui.text.TypingRun.ChangeType;
39 * When connected to a text viewer, a <code>TypingRunDetector</code> observes
40 * <code>TypingRun</code> events. A typing run is a sequence of similar text
41 * modifications, such as inserting or deleting single characters.
43 * Listeners are informed about the start and end of a <code>TypingRun</code>.
48 public class TypingRunDetector {
50 * Implementation note: This class is independent of JDT and may be pulled
51 * up to jface.text if needed.
55 private static final boolean DEBUG= false;
58 * Instances of this class abstract a text modification into a simple
59 * description. Typing runs consists of a sequence of one or more modifying
60 * changes of the same type. Every change records the type of change
61 * described by a text modification, and an offset it can be followed by
62 * another change of the same run.
64 private static final class Change {
65 private ChangeType fType;
66 private int fNextOffset;
69 * Creates a new change of type <code>type</code>.
71 * @param type the <code>ChangeType</code> of the new change
72 * @param nextOffset the offset of the next change in a typing run
74 public Change(ChangeType type, int nextOffset) {
76 fNextOffset= nextOffset;
80 * Returns <code>true</code> if the receiver can extend the typing run
81 * the last change of which is described by <code>change</code>.
83 * @param change the last change in a typing run
84 * @return <code>true</code> if the receiver is a valid extension to
85 * <code>change</code>, <code>false</code> otherwise
87 public boolean canFollow(Change change) {
88 if (fType == TypingRun.NO_CHANGE)
90 if (fType.equals(TypingRun.UNKNOWN))
92 if (fType.equals(change.fType)) {
93 if (fType == TypingRun.DELETE)
94 return fNextOffset == change.fNextOffset - 1;
95 else if (fType == TypingRun.INSERT)
96 return fNextOffset == change.fNextOffset + 1;
97 else if (fType == TypingRun.OVERTYPE)
98 return fNextOffset == change.fNextOffset + 1;
99 else if (fType == TypingRun.SELECTION)
106 * Returns <code>true</code> if the receiver describes a text
107 * modification, <code>false</code> if it describes a focus /
110 * @return <code>true</code> if the receiver is a text modification
112 public boolean isModification() {
113 return fType.isModification();
117 * @see java.lang.Object#toString()
120 public String toString() {
121 return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$
125 * Returns the change type of this change.
127 * @return the change type of this change
129 public ChangeType getType() {
135 * Observes any events that modify the content of the document displayed in
136 * the editor. Since text events may start a new run, this listener is
137 * always registered if the detector is connected.
139 private class TextListener implements ITextListener {
142 * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
144 public void textChanged(TextEvent event) {
145 handleTextChanged(event);
150 * Observes non-modifying events that will end a run, such as clicking into
151 * the editor, moving the caret, and the editor losing focus. These events
152 * can never start a run, therefore this listener is only registered if
153 * there is an ongoing run.
155 private class SelectionListener implements MouseListener, KeyListener, FocusListener {
158 * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
160 public void focusGained(FocusEvent e) {
161 handleSelectionChanged();
165 * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
167 public void focusLost(FocusEvent e) {
171 * @see MouseListener#mouseDoubleClick
173 public void mouseDoubleClick(MouseEvent e) {
177 * If the right mouse button is pressed, the current editing command is closed
178 * @see MouseListener#mouseDown
180 public void mouseDown(MouseEvent e) {
182 handleSelectionChanged();
186 * @see MouseListener#mouseUp
188 public void mouseUp(MouseEvent e) {
192 * @see KeyListener#keyPressed
194 public void keyReleased(KeyEvent e) {
198 * On cursor keys, the current editing command is closed
199 * @see KeyListener#keyPressed
201 public void keyPressed(KeyEvent e) {
206 case SWT.ARROW_RIGHT:
211 handleSelectionChanged();
217 /** The listeners. */
218 private final Set<ITypingRunListener> fListeners= new HashSet<ITypingRunListener>();
220 * The viewer we work upon. Set to <code>null</code> in
221 * <code>uninstall</code>.
223 private ITextViewer fViewer;
224 /** The text event listener. */
225 private final TextListener fTextListener= new TextListener();
227 * The selection listener. Set to <code>null</code> when no run is active.
229 private SelectionListener fSelectionListener;
231 /* state variables */
233 /** The most recently observed change. Never <code>null</code>. */
234 private Change fLastChange;
235 /** The current run, or <code>null</code> if there is none. */
236 private TypingRun fRun;
239 * Installs the receiver with a text viewer.
241 * @param viewer the viewer to install on
243 public void install(ITextViewer viewer) {
244 Assert.isLegal(viewer != null);
250 * Initializes the state variables and registers any permanent listeners.
252 private void connect() {
253 if (fViewer != null) {
254 fLastChange= new Change(TypingRun.UNKNOWN, -1);
256 fSelectionListener= null;
257 fViewer.addTextListener(fTextListener);
262 * Uninstalls the receiver and removes all listeners. <code>install()</code>
263 * must be called for events to be generated.
265 public void uninstall() {
266 if (fViewer != null) {
274 * Disconnects any registered listeners.
276 private void disconnect() {
277 fViewer.removeTextListener(fTextListener);
278 ensureSelectionListenerRemoved();
282 * Adds a listener for <code>TypingRun</code> events. Repeatedly adding
283 * the same listener instance has no effect. Listeners may be added even
284 * if the receiver is neither connected nor installed.
286 * @param listener the listener add
288 public void addTypingRunListener(ITypingRunListener listener) {
289 Assert.isLegal(listener != null);
290 fListeners.add(listener);
291 if (fListeners.size() == 1)
296 * Removes the listener from this manager. If <code>listener</code> is not
297 * registered with the receiver, nothing happens.
299 * @param listener the listener to remove, or <code>null</code>
301 public void removeTypingRunListener(ITypingRunListener listener) {
302 fListeners.remove(listener);
303 if (fListeners.size() == 0)
308 * Handles an incoming text event.
310 * @param event the text event that describes the text modification
312 void handleTextChanged(TextEvent event) {
313 Change type= computeChange(event);
318 * Computes the change abstraction given a text event.
320 * @param event the text event to analyze
321 * @return a change object describing the event
323 private Change computeChange(TextEvent event) {
324 DocumentEvent e= event.getDocumentEvent();
326 return new Change(TypingRun.NO_CHANGE, -1);
328 int start= e.getOffset();
329 int end= e.getOffset() + e.getLength();
330 String newText= e.getText();
332 newText= new String();
335 // no replace / delete / overwrite
336 if (newText.length() == 1)
337 return new Change(TypingRun.INSERT, end + 1);
338 } else if (start == end - 1) {
339 if (newText.length() == 1)
340 return new Change(TypingRun.OVERTYPE, end);
341 if (newText.length() == 0)
342 return new Change(TypingRun.DELETE, start);
345 return new Change(TypingRun.UNKNOWN, -1);
349 * Handles an incoming selection event.
351 void handleSelectionChanged() {
352 handleChange(new Change(TypingRun.SELECTION, -1));
356 * State machine. Changes state given the current state and the incoming
359 * @param change the incoming change
361 private void handleChange(Change change) {
362 if (change.getType() == TypingRun.NO_CHANGE)
366 System.err.println("Last change: " + fLastChange); //$NON-NLS-1$
368 if (!change.canFollow(fLastChange))
369 endIfStarted(change);
371 if (change.isModification())
375 System.err.println("New change: " + change); //$NON-NLS-1$
379 * Starts a new run if there is none and informs all listeners. If there
380 * already is a run, nothing happens.
382 private void startOrContinue() {
385 System.err.println("+Start run"); //$NON-NLS-1$
386 fRun= new TypingRun(fLastChange.getType());
387 ensureSelectionListenerAdded();
393 * Returns <code>true</code> if there is an active run, <code>false</code>
396 * @return <code>true</code> if there is an active run, <code>false</code>
399 private boolean hasRun() {
404 * Ends any active run and informs all listeners. If there is none, nothing
407 * @param change the change that triggered ending the active run
409 private void endIfStarted(Change change) {
411 ensureSelectionListenerRemoved();
413 System.err.println("-End run"); //$NON-NLS-1$
414 fireRunEnded(fRun, change.getType());
420 * Adds the selection listener to the text widget underlying the viewer, if
423 private void ensureSelectionListenerAdded() {
424 if (fSelectionListener == null) {
425 fSelectionListener= new SelectionListener();
426 StyledText textWidget= fViewer.getTextWidget();
427 textWidget.addFocusListener(fSelectionListener);
428 textWidget.addKeyListener(fSelectionListener);
429 textWidget.addMouseListener(fSelectionListener);
434 * If there is a selection listener, it is removed from the text widget
435 * underlying the viewer.
437 private void ensureSelectionListenerRemoved() {
438 if (fSelectionListener != null) {
439 StyledText textWidget= fViewer.getTextWidget();
440 textWidget.removeFocusListener(fSelectionListener);
441 textWidget.removeKeyListener(fSelectionListener);
442 textWidget.removeMouseListener(fSelectionListener);
443 fSelectionListener= null;
448 * Informs all listeners about a newly started <code>TypingRun</code>.
450 * @param run the new run
452 private void fireRunBegun(TypingRun run) {
453 List<ITypingRunListener> listeners= new ArrayList<ITypingRunListener>(fListeners);
454 for (Iterator<ITypingRunListener> it= listeners.iterator(); it.hasNext();) {
455 ITypingRunListener listener= it.next();
456 listener.typingRunStarted(fRun);
461 * Informs all listeners about an ended <code>TypingRun</code>.
463 * @param run the previously active run
464 * @param reason the type of change that caused the run to be ended
466 private void fireRunEnded(TypingRun run, ChangeType reason) {
467 List<ITypingRunListener> listeners= new ArrayList<ITypingRunListener>(fListeners);
468 for (Iterator<ITypingRunListener> it= listeners.iterator(); it.hasNext();) {
469 ITypingRunListener listener= it.next();
470 listener.typingRunEnded(fRun, reason);