KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > eclipse > jdt > internal > ui > text > TypingRunDetector


1 /*******************************************************************************
2  * Copyright (c) 2000, 2006 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
7  *
8  * Contributors:
9  * IBM Corporation - initial API and implementation
10  *******************************************************************************/

11 package org.eclipse.jdt.internal.ui.text;
12
13 import java.util.ArrayList JavaDoc;
14 import java.util.HashSet JavaDoc;
15 import java.util.Iterator JavaDoc;
16 import java.util.List JavaDoc;
17 import java.util.Set JavaDoc;
18
19 import org.eclipse.core.runtime.Assert;
20
21 import org.eclipse.swt.SWT;
22 import org.eclipse.swt.custom.StyledText;
23 import org.eclipse.swt.events.FocusEvent;
24 import org.eclipse.swt.events.FocusListener;
25 import org.eclipse.swt.events.KeyEvent;
26 import org.eclipse.swt.events.KeyListener;
27 import org.eclipse.swt.events.MouseEvent;
28 import org.eclipse.swt.events.MouseListener;
29
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;
34
35 import org.eclipse.jdt.internal.ui.text.TypingRun.ChangeType;
36
37
38 /**
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.
42  * <p>
43  * Listeners are informed about the start and end of a <code>TypingRun</code>.
44  * </p>
45  *
46  * @since 3.0
47  */

48 public class TypingRunDetector {
49     /*
50      * Implementation note: This class is independent of JDT and may be pulled
51      * up to jface.text if needed.
52      */

53
54     /** Debug flag. */
55     private static final boolean DEBUG= false;
56
57     /**
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.
63      */

64     private static final class Change {
65         private ChangeType fType;
66         private int fNextOffset;
67
68         /**
69          * Creates a new change of type <code>type</code>.
70          *
71          * @param type the <code>ChangeType</code> of the new change
72          * @param nextOffset the offset of the next change in a typing run
73          */

74         public Change(ChangeType type, int nextOffset) {
75             fType= type;
76             fNextOffset= nextOffset;
77         }
78
79         /**
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>.
82          *
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
86          */

87         public boolean canFollow(Change change) {
88             if (fType == TypingRun.NO_CHANGE)
89                 return true;
90             if (fType.equals(TypingRun.UNKNOWN))
91                 return false;
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)
100                     return true;
101             }
102             return false;
103         }
104
105         /**
106          * Returns <code>true</code> if the receiver describes a text
107          * modification, <code>false</code> if it describes a focus /
108          * selection change.
109          *
110          * @return <code>true</code> if the receiver is a text modification
111          */

112         public boolean isModification() {
113             return fType.isModification();
114         }
115
116         /*
117          * @see java.lang.Object#toString()
118          */

119         public String JavaDoc toString() {
120             return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$
121
}
122
123         /**
124          * Returns the change type of this change.
125          *
126          * @return the change type of this change
127          */

128         public ChangeType getType() {
129             return fType;
130         }
131     }
132
133     /**
134      * Observes any events that modify the content of the document displayed in
135      * the editor. Since text events may start a new run, this listener is
136      * always registered if the detector is connected.
137      */

138     private class TextListener implements ITextListener {
139
140         /*
141          * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
142          */

143         public void textChanged(TextEvent event) {
144             handleTextChanged(event);
145         }
146     }
147
148     /**
149      * Observes non-modifying events that will end a run, such as clicking into
150      * the editor, moving the caret, and the editor losing focus. These events
151      * can never start a run, therefore this listener is only registered if
152      * there is an ongoing run.
153      */

154     private class SelectionListener implements MouseListener, KeyListener, FocusListener {
155
156         /*
157          * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
158          */

159         public void focusGained(FocusEvent e) {
160             handleSelectionChanged();
161         }
162
163         /*
164          * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
165          */

166         public void focusLost(FocusEvent e) {
167         }
168
169         /*
170          * @see MouseListener#mouseDoubleClick
171          */

172         public void mouseDoubleClick(MouseEvent e) {
173         }
174
175         /*
176          * If the right mouse button is pressed, the current editing command is closed
177          * @see MouseListener#mouseDown
178          */

179         public void mouseDown(MouseEvent e) {
180             if (e.button == 1)
181                 handleSelectionChanged();
182         }
183
184         /*
185          * @see MouseListener#mouseUp
186          */

187         public void mouseUp(MouseEvent e) {
188         }
189
190         /*
191          * @see KeyListener#keyPressed
192          */

193         public void keyReleased(KeyEvent e) {
194         }
195
196         /*
197          * On cursor keys, the current editing command is closed
198          * @see KeyListener#keyPressed
199          */

200         public void keyPressed(KeyEvent e) {
201             switch (e.keyCode) {
202                 case SWT.ARROW_UP:
203                 case SWT.ARROW_DOWN:
204                 case SWT.ARROW_LEFT:
205                 case SWT.ARROW_RIGHT:
206                 case SWT.END:
207                 case SWT.HOME:
208                 case SWT.PAGE_DOWN:
209                 case SWT.PAGE_UP:
210                     handleSelectionChanged();
211                     break;
212             }
213         }
214     }
215
216     /** The listeners. */
217     private final Set JavaDoc fListeners= new HashSet JavaDoc();
218     /**
219      * The viewer we work upon. Set to <code>null</code> in
220      * <code>uninstall</code>.
221      */

222     private ITextViewer fViewer;
223     /** The text event listener. */
224     private final TextListener fTextListener= new TextListener();
225     /**
226      * The selection listener. Set to <code>null</code> when no run is active.
227      */

228     private SelectionListener fSelectionListener;
229
230     /* state variables */
231
232     /** The most recently observed change. Never <code>null</code>. */
233     private Change fLastChange;
234     /** The current run, or <code>null</code> if there is none. */
235     private TypingRun fRun;
236
237     /**
238      * Installs the receiver with a text viewer.
239      *
240      * @param viewer the viewer to install on
241      */

242     public void install(ITextViewer viewer) {
243         Assert.isLegal(viewer != null);
244         fViewer= viewer;
245         connect();
246     }
247
248     /**
249      * Initializes the state variables and registers any permanent listeners.
250      */

251     private void connect() {
252         if (fViewer != null) {
253             fLastChange= new Change(TypingRun.UNKNOWN, -1);
254             fRun= null;
255             fSelectionListener= null;
256             fViewer.addTextListener(fTextListener);
257         }
258     }
259
260     /**
261      * Uninstalls the receiver and removes all listeners. <code>install()</code>
262      * must be called for events to be generated.
263      */

264     public void uninstall() {
265         if (fViewer != null) {
266             fListeners.clear();
267             disconnect();
268             fViewer= null;
269         }
270     }
271
272     /**
273      * Disconnects any registered listeners.
274      */

275     private void disconnect() {
276         fViewer.removeTextListener(fTextListener);
277         ensureSelectionListenerRemoved();
278     }
279
280     /**
281      * Adds a listener for <code>TypingRun</code> events. Repeatedly adding
282      * the same listener instance has no effect. Listeners may be added even
283      * if the receiver is neither connected nor installed.
284      *
285      * @param listener the listener add
286      */

287     public void addTypingRunListener(ITypingRunListener listener) {
288         Assert.isLegal(listener != null);
289         fListeners.add(listener);
290         if (fListeners.size() == 1)
291             connect();
292     }
293
294     /**
295      * Removes the listener from this manager. If <code>listener</code> is not
296      * registered with the receiver, nothing happens.
297      *
298      * @param listener the listener to remove, or <code>null</code>
299      */

300     public void removeTypingRunListener(ITypingRunListener listener) {
301         fListeners.remove(listener);
302         if (fListeners.size() == 0)
303             disconnect();
304     }
305
306     /**
307      * Handles an incoming text event.
308      *
309      * @param event the text event that describes the text modification
310      */

311     void handleTextChanged(TextEvent event) {
312         Change type= computeChange(event);
313         handleChange(type);
314     }
315
316     /**
317      * Computes the change abstraction given a text event.
318      *
319      * @param event the text event to analyze
320      * @return a change object describing the event
321      */

322     private Change computeChange(TextEvent event) {
323         DocumentEvent e= event.getDocumentEvent();
324         if (e == null)
325             return new Change(TypingRun.NO_CHANGE, -1);
326
327         int start= e.getOffset();
328         int end= e.getOffset() + e.getLength();
329         String JavaDoc newText= e.getText();
330         if (newText == null)
331             newText= new String JavaDoc();
332
333         if (start == end) {
334             // no replace / delete / overwrite
335
if (newText.length() == 1)
336                 return new Change(TypingRun.INSERT, end + 1);
337         } else if (start == end - 1) {
338             if (newText.length() == 1)
339                 return new Change(TypingRun.OVERTYPE, end);
340             if (newText.length() == 0)
341                 return new Change(TypingRun.DELETE, start);
342         }
343
344         return new Change(TypingRun.UNKNOWN, -1);
345     }
346
347     /**
348      * Handles an incoming selection event.
349      */

350     void handleSelectionChanged() {
351         handleChange(new Change(TypingRun.SELECTION, -1));
352     }
353
354     /**
355      * State machine. Changes state given the current state and the incoming
356      * change.
357      *
358      * @param change the incoming change
359      */

360     private void handleChange(Change change) {
361         if (change.getType() == TypingRun.NO_CHANGE)
362             return;
363
364         if (DEBUG)
365             System.err.println("Last change: " + fLastChange); //$NON-NLS-1$
366

367         if (!change.canFollow(fLastChange))
368             endIfStarted(change);
369         fLastChange= change;
370         if (change.isModification())
371             startOrContinue();
372
373         if (DEBUG)
374             System.err.println("New change: " + change); //$NON-NLS-1$
375
}
376
377     /**
378      * Starts a new run if there is none and informs all listeners. If there
379      * already is a run, nothing happens.
380      */

381     private void startOrContinue() {
382         if (!hasRun()) {
383             if (DEBUG)
384                 System.err.println("+Start run"); //$NON-NLS-1$
385
fRun= new TypingRun(fLastChange.getType());
386             ensureSelectionListenerAdded();
387             fireRunBegun(fRun);
388         }
389     }
390
391     /**
392      * Returns <code>true</code> if there is an active run, <code>false</code>
393      * otherwise.
394      *
395      * @return <code>true</code> if there is an active run, <code>false</code>
396      * otherwise
397      */

398     private boolean hasRun() {
399         return fRun != null;
400     }
401
402     /**
403      * Ends any active run and informs all listeners. If there is none, nothing
404      * happens.
405      *
406      * @param change the change that triggered ending the active run
407      */

408     private void endIfStarted(Change change) {
409         if (hasRun()) {
410             ensureSelectionListenerRemoved();
411             if (DEBUG)
412                 System.err.println("-End run"); //$NON-NLS-1$
413
fireRunEnded(fRun, change.getType());
414             fRun= null;
415         }
416     }
417
418     /**
419      * Adds the selection listener to the text widget underlying the viewer, if
420      * not already done.
421      */

422     private void ensureSelectionListenerAdded() {
423         if (fSelectionListener == null) {
424             fSelectionListener= new SelectionListener();
425             StyledText textWidget= fViewer.getTextWidget();
426             textWidget.addFocusListener(fSelectionListener);
427             textWidget.addKeyListener(fSelectionListener);
428             textWidget.addMouseListener(fSelectionListener);
429         }
430     }
431
432     /**
433      * If there is a selection listener, it is removed from the text widget
434      * underlying the viewer.
435      */

436     private void ensureSelectionListenerRemoved() {
437         if (fSelectionListener != null) {
438             StyledText textWidget= fViewer.getTextWidget();
439             textWidget.removeFocusListener(fSelectionListener);
440             textWidget.removeKeyListener(fSelectionListener);
441             textWidget.removeMouseListener(fSelectionListener);
442             fSelectionListener= null;
443         }
444     }
445
446     /**
447      * Informs all listeners about a newly started <code>TypingRun</code>.
448      *
449      * @param run the new run
450      */

451     private void fireRunBegun(TypingRun run) {
452         List JavaDoc listeners= new ArrayList JavaDoc(fListeners);
453         for (Iterator JavaDoc it= listeners.iterator(); it.hasNext();) {
454             ITypingRunListener listener= (ITypingRunListener) it.next();
455             listener.typingRunStarted(fRun);
456         }
457     }
458
459     /**
460      * Informs all listeners about an ended <code>TypingRun</code>.
461      *
462      * @param run the previously active run
463      * @param reason the type of change that caused the run to be ended
464      */

465     private void fireRunEnded(TypingRun run, ChangeType reason) {
466         List JavaDoc listeners= new ArrayList JavaDoc(fListeners);
467         for (Iterator JavaDoc it= listeners.iterator(); it.hasNext();) {
468             ITypingRunListener listener= (ITypingRunListener) it.next();
469             listener.typingRunEnded(fRun, reason);
470         }
471     }
472 }
473
Popular Tags