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;
36 * When connected to a text viewer, a <code>TypingRunDetector</code> observes
37 * <code>TypingRun</code> events. A typing run is a sequence of similar text
38 * modifications, such as inserting or deleting single characters.
40 * Listeners are informed about the start and end of a <code>TypingRun</code>.
45 public class TypingRunDetector {
47 * Implementation note: This class is independent of JDT and may be pulled
48 * up to jface.text if needed.
52 private static final boolean DEBUG = false;
55 * Instances of this class abstract a text modification into a simple
56 * description. Typing runs consists of a sequence of one or more modifying
57 * changes of the same type. Every change records the type of change
58 * described by a text modification, and an offset it can be followed by
59 * another change of the same run.
61 private static final class Change {
62 private ChangeType fType;
64 private int fNextOffset;
67 * Creates a new change of type <code>type</code>.
70 * the <code>ChangeType</code> of the new change
72 * 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
81 * range the last change of which is described by <code>change</code>.
84 * the last change in a typing run
85 * @return <code>true</code> if the receiver is a valid extension to
86 * <code>change</code>,<code>false</code> otherwise
88 public boolean canFollow(Change change) {
89 if (fType == TypingRun.NO_CHANGE)
91 else if (fType.equals(TypingRun.UNKNOWN))
93 if (fType.equals(change.fType)) {
94 if (fType == TypingRun.DELETE)
95 return fNextOffset == change.fNextOffset - 1;
96 else if (fType == TypingRun.INSERT)
97 return fNextOffset == change.fNextOffset + 1;
98 else if (fType == TypingRun.OVERTYPE)
99 return fNextOffset == change.fNextOffset + 1;
100 else if (fType == TypingRun.SELECTION)
107 * Returns <code>true</code> if the receiver describes a text
108 * modification, <code>false</code> if it describes a focus /
111 * @return <code>true</code> if the receiver is a text modification
113 public boolean isModification() {
114 return fType.isModification();
118 * @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,
159 * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
161 public void focusGained(FocusEvent e) {
162 handleSelectionChanged();
166 * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
168 public void focusLost(FocusEvent e) {
172 * @see MouseListener#mouseDoubleClick
174 public void mouseDoubleClick(MouseEvent e) {
178 * If the right mouse button is pressed, the current editing command is
181 * @see MouseListener#mouseDown
183 public void mouseDown(MouseEvent e) {
185 handleSelectionChanged();
189 * @see MouseListener#mouseUp
191 public void mouseUp(MouseEvent e) {
195 * @see KeyListener#keyPressed
197 public void keyReleased(KeyEvent e) {
201 * On cursor keys, the current editing command is closed
203 * @see KeyListener#keyPressed
205 public void keyPressed(KeyEvent e) {
210 case SWT.ARROW_RIGHT:
215 handleSelectionChanged();
221 /** The listeners. */
222 private final Set fListeners = new HashSet();
225 * The viewer we work upon. Set to <code>null</code> in
226 * <code>uninstall</code>.
228 private ITextViewer fViewer;
230 /** The text event listener. */
231 private final TextListener fTextListener = new TextListener();
234 * The selection listener. Set to <code>null</code> when no run is active.
236 private SelectionListener fSelectionListener;
238 /* state variables */
240 /** The most recently observed change. Never <code>null</code>. */
241 private Change fLastChange;
243 /** The current run, or <code>null</code> if there is none. */
244 private TypingRun fRun;
247 * Installs the receiver with a text viewer.
250 * the viewer to install on
252 public void install(ITextViewer viewer) {
253 Assert.isLegal(viewer != null);
259 * Initializes the state variables and registers any permanent listeners.
261 private void connect() {
262 if (fViewer != null) {
263 fLastChange = new Change(TypingRun.UNKNOWN, -1);
265 fSelectionListener = null;
266 fViewer.addTextListener(fTextListener);
271 * Uninstalls the receiver and removes all listeners. <code>install()</code>
272 * must be called for events to be generated.
274 public void uninstall() {
275 if (fViewer != null) {
283 * Disconnects any registered listeners.
285 private void disconnect() {
286 fViewer.removeTextListener(fTextListener);
287 ensureSelectionListenerRemoved();
291 * Adds a listener for <code>TypingRun</code> events. Repeatedly adding
292 * the same listener instance has no effect. Listeners may be added even if
293 * the receiver is neither connected nor installed.
298 public void addTypingRunListener(ITypingRunListener listener) {
299 Assert.isLegal(listener != null);
300 fListeners.add(listener);
301 if (fListeners.size() == 1)
306 * Removes the listener from this manager. If <code>listener</code> is not
307 * registered with the receiver, nothing happens.
310 * the listener to remove, or <code>null</code>
312 public void removeTypingRunListener(ITypingRunListener listener) {
313 fListeners.remove(listener);
314 if (fListeners.size() == 0)
319 * Handles an incoming text event.
322 * the text event that describes the text modification
324 void handleTextChanged(TextEvent event) {
325 Change type = computeChange(event);
330 * Computes the change abstraction given a text event.
333 * the text event to analyze
334 * @return a change object describing the event
336 private Change computeChange(TextEvent event) {
337 DocumentEvent e = event.getDocumentEvent();
339 return new Change(TypingRun.NO_CHANGE, -1);
341 int start = e.getOffset();
342 int end = e.getOffset() + e.getLength();
343 String newText = e.getText();
345 newText = new String();
348 // no replace / delete / overwrite
349 if (newText.length() == 1)
350 return new Change(TypingRun.INSERT, end + 1);
351 } else if (start == end - 1) {
352 if (newText.length() == 1)
353 return new Change(TypingRun.OVERTYPE, end);
354 if (newText.length() == 0)
355 return new Change(TypingRun.DELETE, start);
358 return new Change(TypingRun.UNKNOWN, -1);
362 * Handles an incoming selection event.
364 void handleSelectionChanged() {
365 handleChange(new Change(TypingRun.SELECTION, -1));
369 * State machine. Changes state given the current state and the incoming
373 * the incoming change
375 private void handleChange(Change change) {
376 if (change.getType() == TypingRun.NO_CHANGE)
380 System.err.println("Last change: " + fLastChange); //$NON-NLS-1$
382 if (!change.canFollow(fLastChange))
383 endIfStarted(change);
384 fLastChange = change;
385 if (change.isModification())
389 System.err.println("New change: " + change); //$NON-NLS-1$
393 * Starts a new run if there is none and informs all listeners. If there
394 * already is a run, nothing happens.
396 private void startOrContinue() {
399 System.err.println("+Start run"); //$NON-NLS-1$
400 fRun = new TypingRun(fLastChange.getType());
401 ensureSelectionListenerAdded();
407 * Returns <code>true</code> if there is an active run, <code>false</code>
410 * @return <code>true</code> if there is an active run, <code>false</code>
413 private boolean hasRun() {
418 * Ends any active run and informs all listeners. If there is none, nothing
422 * the change that triggered ending the active run
424 private void endIfStarted(Change change) {
426 ensureSelectionListenerRemoved();
428 System.err.println("-End run"); //$NON-NLS-1$
429 fireRunEnded(fRun, change.getType());
435 * Adds the selection listener to the text widget underlying the viewer, if
438 private void ensureSelectionListenerAdded() {
439 if (fSelectionListener == null) {
440 fSelectionListener = new SelectionListener();
441 StyledText textWidget = fViewer.getTextWidget();
442 textWidget.addFocusListener(fSelectionListener);
443 textWidget.addKeyListener(fSelectionListener);
444 textWidget.addMouseListener(fSelectionListener);
449 * If there is a selection listener, it is removed from the text widget
450 * underlying the viewer.
452 private void ensureSelectionListenerRemoved() {
453 if (fSelectionListener != null) {
454 StyledText textWidget = fViewer.getTextWidget();
455 textWidget.removeFocusListener(fSelectionListener);
456 textWidget.removeKeyListener(fSelectionListener);
457 textWidget.removeMouseListener(fSelectionListener);
458 fSelectionListener = null;
463 * Informs all listeners about a newly started <code>TypingRun</code>.
468 private void fireRunBegun(TypingRun run) {
469 List listeners = new ArrayList(fListeners);
470 for (Iterator it = listeners.iterator(); it.hasNext();) {
471 ITypingRunListener listener = (ITypingRunListener) it.next();
472 listener.typingRunStarted(fRun);
477 * Informs all listeners about an ended <code>TypingRun</code>.
480 * the previously active run
482 * the type of change that caused the run to be ended
484 private void fireRunEnded(TypingRun run, ChangeType reason) {
485 List listeners = new ArrayList(fListeners);
486 for (Iterator it = listeners.iterator(); it.hasNext();) {
487 ITypingRunListener listener = (ITypingRunListener) it.next();
488 listener.typingRunEnded(fRun, reason);