aa63874190b57a6282dc813b3751d2d46e40f825
[phpeclipse.git] / net.sourceforge.phpeclipse.ui / src / net / sourceforge / phpdt / internal / ui / text / TypingRunDetector.java
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
7  *
8  * Contributors:
9  *     IBM Corporation - initial API and implementation
10  *******************************************************************************/
11 package net.sourceforge.phpdt.internal.ui.text;
12
13 import java.util.ArrayList;
14 import java.util.HashSet;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Set;
18
19 import net.sourceforge.phpdt.internal.ui.text.TypingRun.ChangeType;
20
21 //incastrix
22 //import org.eclipse.jface.text.Assert;
23 import org.eclipse.core.runtime.Assert;
24 import org.eclipse.jface.text.DocumentEvent;
25 import org.eclipse.jface.text.ITextListener;
26 import org.eclipse.jface.text.ITextViewer;
27 import org.eclipse.jface.text.TextEvent;
28 import org.eclipse.swt.SWT;
29 import org.eclipse.swt.custom.StyledText;
30 import org.eclipse.swt.events.FocusEvent;
31 import org.eclipse.swt.events.FocusListener;
32 import org.eclipse.swt.events.KeyEvent;
33 import org.eclipse.swt.events.KeyListener;
34 import org.eclipse.swt.events.MouseEvent;
35 import org.eclipse.swt.events.MouseListener;
36
37 /**
38  * When connected to a text viewer, a <code>TypingRunDetector</code> observes
39  * <code>TypingRun</code> events. A typing run is a sequence of similar text
40  * modifications, such as inserting or deleting single characters.
41  * <p>
42  * Listeners are informed about the start and end of a <code>TypingRun</code>.
43  * </p>
44  * 
45  * @since 3.0
46  */
47 public class TypingRunDetector {
48         /*
49          * Implementation note: This class is independent of JDT and may be pulled
50          * up to jface.text if needed.
51          */
52
53         /** Debug flag. */
54         private static final boolean DEBUG = false;
55
56         /**
57          * Instances of this class abstract a text modification into a simple
58          * description. Typing runs consists of a sequence of one or more modifying
59          * changes of the same type. Every change records the type of change
60          * described by a text modification, and an offset it can be followed by
61          * another change of the same run.
62          */
63         private static final class Change {
64                 private ChangeType fType;
65
66                 private int fNextOffset;
67
68                 /**
69                  * Creates a new change of type <code>type</code>.
70                  * 
71                  * @param type
72                  *            the <code>ChangeType</code> of the new change
73                  * @param nextOffset
74                  *            the offset of the next change in a typing run
75                  */
76                 public Change(ChangeType type, int nextOffset) {
77                         fType = type;
78                         fNextOffset = nextOffset;
79                 }
80
81                 /**
82                  * Returns <code>true</code> if the receiver can extend the typing
83                  * range the last change of which is described by <code>change</code>.
84                  * 
85                  * @param change
86                  *            the last change in a typing run
87                  * @return <code>true</code> if the receiver is a valid extension to
88                  *         <code>change</code>,<code>false</code> otherwise
89                  */
90                 public boolean canFollow(Change change) {
91                         if (fType == TypingRun.NO_CHANGE)
92                                 return true;
93                         else if (fType.equals(TypingRun.UNKNOWN))
94                                 return false;
95                         if (fType.equals(change.fType)) {
96                                 if (fType == TypingRun.DELETE)
97                                         return fNextOffset == change.fNextOffset - 1;
98                                 else if (fType == TypingRun.INSERT)
99                                         return fNextOffset == change.fNextOffset + 1;
100                                 else if (fType == TypingRun.OVERTYPE)
101                                         return fNextOffset == change.fNextOffset + 1;
102                                 else if (fType == TypingRun.SELECTION)
103                                         return true;
104                         }
105                         return false;
106                 }
107
108                 /**
109                  * Returns <code>true</code> if the receiver describes a text
110                  * modification, <code>false</code> if it describes a focus /
111                  * selection change.
112                  * 
113                  * @return <code>true</code> if the receiver is a text modification
114                  */
115                 public boolean isModification() {
116                         return fType.isModification();
117                 }
118
119                 /*
120                  * @see java.lang.Object#toString()
121                  */
122                 public String toString() {
123                         return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$
124                 }
125
126                 /**
127                  * Returns the change type of this change.
128                  * 
129                  * @return the change type of this change
130                  */
131                 public ChangeType getType() {
132                         return fType;
133                 }
134         }
135
136         /**
137          * Observes any events that modify the content of the document displayed in
138          * the editor. Since text events may start a new run, this listener is
139          * always registered if the detector is connected.
140          */
141         private class TextListener implements ITextListener {
142
143                 /*
144                  * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent)
145                  */
146                 public void textChanged(TextEvent event) {
147                         handleTextChanged(event);
148                 }
149         }
150
151         /**
152          * Observes non-modifying events that will end a run, such as clicking into
153          * the editor, moving the caret, and the editor losing focus. These events
154          * can never start a run, therefore this listener is only registered if
155          * there is an ongoing run.
156          */
157         private class SelectionListener implements MouseListener, KeyListener,
158                         FocusListener {
159
160                 /*
161                  * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent)
162                  */
163                 public void focusGained(FocusEvent e) {
164                         handleSelectionChanged();
165                 }
166
167                 /*
168                  * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent)
169                  */
170                 public void focusLost(FocusEvent e) {
171                 }
172
173                 /*
174                  * @see MouseListener#mouseDoubleClick
175                  */
176                 public void mouseDoubleClick(MouseEvent e) {
177                 }
178
179                 /*
180                  * If the right mouse button is pressed, the current editing command is
181                  * closed
182                  * 
183                  * @see MouseListener#mouseDown
184                  */
185                 public void mouseDown(MouseEvent e) {
186                         if (e.button == 1)
187                                 handleSelectionChanged();
188                 }
189
190                 /*
191                  * @see MouseListener#mouseUp
192                  */
193                 public void mouseUp(MouseEvent e) {
194                 }
195
196                 /*
197                  * @see KeyListener#keyPressed
198                  */
199                 public void keyReleased(KeyEvent e) {
200                 }
201
202                 /*
203                  * On cursor keys, the current editing command is closed
204                  * 
205                  * @see KeyListener#keyPressed
206                  */
207                 public void keyPressed(KeyEvent e) {
208                         switch (e.keyCode) {
209                         case SWT.ARROW_UP:
210                         case SWT.ARROW_DOWN:
211                         case SWT.ARROW_LEFT:
212                         case SWT.ARROW_RIGHT:
213                         case SWT.END:
214                         case SWT.HOME:
215                         case SWT.PAGE_DOWN:
216                         case SWT.PAGE_UP:
217                                 handleSelectionChanged();
218                                 break;
219                         }
220                 }
221         }
222
223         /** The listeners. */
224         private final Set fListeners = new HashSet();
225
226         /**
227          * The viewer we work upon. Set to <code>null</code> in
228          * <code>uninstall</code>.
229          */
230         private ITextViewer fViewer;
231
232         /** The text event listener. */
233         private final TextListener fTextListener = new TextListener();
234
235         /**
236          * The selection listener. Set to <code>null</code> when no run is active.
237          */
238         private SelectionListener fSelectionListener;
239
240         /* state variables */
241
242         /** The most recently observed change. Never <code>null</code>. */
243         private Change fLastChange;
244
245         /** The current run, or <code>null</code> if there is none. */
246         private TypingRun fRun;
247
248         /**
249          * Installs the receiver with a text viewer.
250          * 
251          * @param viewer
252          *            the viewer to install on
253          */
254         public void install(ITextViewer viewer) {
255                 Assert.isLegal(viewer != null);
256                 fViewer = viewer;
257                 connect();
258         }
259
260         /**
261          * Initializes the state variables and registers any permanent listeners.
262          */
263         private void connect() {
264                 if (fViewer != null) {
265                         fLastChange = new Change(TypingRun.UNKNOWN, -1);
266                         fRun = null;
267                         fSelectionListener = null;
268                         fViewer.addTextListener(fTextListener);
269                 }
270         }
271
272         /**
273          * Uninstalls the receiver and removes all listeners. <code>install()</code>
274          * must be called for events to be generated.
275          */
276         public void uninstall() {
277                 if (fViewer != null) {
278                         fListeners.clear();
279                         disconnect();
280                         fViewer = null;
281                 }
282         }
283
284         /**
285          * Disconnects any registered listeners.
286          */
287         private void disconnect() {
288                 fViewer.removeTextListener(fTextListener);
289                 ensureSelectionListenerRemoved();
290         }
291
292         /**
293          * Adds a listener for <code>TypingRun</code> events. Repeatedly adding
294          * the same listener instance has no effect. Listeners may be added even if
295          * the receiver is neither connected nor installed.
296          * 
297          * @param listener
298          *            the listener add
299          */
300         public void addTypingRunListener(ITypingRunListener listener) {
301                 Assert.isLegal(listener != null);
302                 fListeners.add(listener);
303                 if (fListeners.size() == 1)
304                         connect();
305         }
306
307         /**
308          * Removes the listener from this manager. If <code>listener</code> is not
309          * registered with the receiver, nothing happens.
310          * 
311          * @param listener
312          *            the listener to remove, or <code>null</code>
313          */
314         public void removeTypingRunListener(ITypingRunListener listener) {
315                 fListeners.remove(listener);
316                 if (fListeners.size() == 0)
317                         disconnect();
318         }
319
320         /**
321          * Handles an incoming text event.
322          * 
323          * @param event
324          *            the text event that describes the text modification
325          */
326         void handleTextChanged(TextEvent event) {
327                 Change type = computeChange(event);
328                 handleChange(type);
329         }
330
331         /**
332          * Computes the change abstraction given a text event.
333          * 
334          * @param event
335          *            the text event to analyze
336          * @return a change object describing the event
337          */
338         private Change computeChange(TextEvent event) {
339                 DocumentEvent e = event.getDocumentEvent();
340                 if (e == null)
341                         return new Change(TypingRun.NO_CHANGE, -1);
342
343                 int start = e.getOffset();
344                 int end = e.getOffset() + e.getLength();
345                 String newText = e.getText();
346                 if (newText == null)
347                         newText = new String();
348
349                 if (start == end) {
350                         // no replace / delete / overwrite
351                         if (newText.length() == 1)
352                                 return new Change(TypingRun.INSERT, end + 1);
353                 } else if (start == end - 1) {
354                         if (newText.length() == 1)
355                                 return new Change(TypingRun.OVERTYPE, end);
356                         if (newText.length() == 0)
357                                 return new Change(TypingRun.DELETE, start);
358                 }
359
360                 return new Change(TypingRun.UNKNOWN, -1);
361         }
362
363         /**
364          * Handles an incoming selection event.
365          */
366         void handleSelectionChanged() {
367                 handleChange(new Change(TypingRun.SELECTION, -1));
368         }
369
370         /**
371          * State machine. Changes state given the current state and the incoming
372          * change.
373          * 
374          * @param change
375          *            the incoming change
376          */
377         private void handleChange(Change change) {
378                 if (change.getType() == TypingRun.NO_CHANGE)
379                         return;
380
381                 if (DEBUG)
382                         System.err.println("Last change: " + fLastChange); //$NON-NLS-1$
383
384                 if (!change.canFollow(fLastChange))
385                         endIfStarted(change);
386                 fLastChange = change;
387                 if (change.isModification())
388                         startOrContinue();
389
390                 if (DEBUG)
391                         System.err.println("New change: " + change); //$NON-NLS-1$
392         }
393
394         /**
395          * Starts a new run if there is none and informs all listeners. If there
396          * already is a run, nothing happens.
397          */
398         private void startOrContinue() {
399                 if (!hasRun()) {
400                         if (DEBUG)
401                                 System.err.println("+Start run"); //$NON-NLS-1$
402                         fRun = new TypingRun(fLastChange.getType());
403                         ensureSelectionListenerAdded();
404                         fireRunBegun(fRun);
405                 }
406         }
407
408         /**
409          * Returns <code>true</code> if there is an active run, <code>false</code>
410          * otherwise.
411          * 
412          * @return <code>true</code> if there is an active run, <code>false</code>
413          *         otherwise
414          */
415         private boolean hasRun() {
416                 return fRun != null;
417         }
418
419         /**
420          * Ends any active run and informs all listeners. If there is none, nothing
421          * happens.
422          * 
423          * @param change
424          *            the change that triggered ending the active run
425          */
426         private void endIfStarted(Change change) {
427                 if (hasRun()) {
428                         ensureSelectionListenerRemoved();
429                         if (DEBUG)
430                                 System.err.println("-End run"); //$NON-NLS-1$
431                         fireRunEnded(fRun, change.getType());
432                         fRun = null;
433                 }
434         }
435
436         /**
437          * Adds the selection listener to the text widget underlying the viewer, if
438          * not already done.
439          */
440         private void ensureSelectionListenerAdded() {
441                 if (fSelectionListener == null) {
442                         fSelectionListener = new SelectionListener();
443                         StyledText textWidget = fViewer.getTextWidget();
444                         textWidget.addFocusListener(fSelectionListener);
445                         textWidget.addKeyListener(fSelectionListener);
446                         textWidget.addMouseListener(fSelectionListener);
447                 }
448         }
449
450         /**
451          * If there is a selection listener, it is removed from the text widget
452          * underlying the viewer.
453          */
454         private void ensureSelectionListenerRemoved() {
455                 if (fSelectionListener != null) {
456                         StyledText textWidget = fViewer.getTextWidget();
457                         textWidget.removeFocusListener(fSelectionListener);
458                         textWidget.removeKeyListener(fSelectionListener);
459                         textWidget.removeMouseListener(fSelectionListener);
460                         fSelectionListener = null;
461                 }
462         }
463
464         /**
465          * Informs all listeners about a newly started <code>TypingRun</code>.
466          * 
467          * @param run
468          *            the new run
469          */
470         private void fireRunBegun(TypingRun run) {
471                 List listeners = new ArrayList(fListeners);
472                 for (Iterator it = listeners.iterator(); it.hasNext();) {
473                         ITypingRunListener listener = (ITypingRunListener) it.next();
474                         listener.typingRunStarted(fRun);
475                 }
476         }
477
478         /**
479          * Informs all listeners about an ended <code>TypingRun</code>.
480          * 
481          * @param run
482          *            the previously active run
483          * @param reason
484          *            the type of change that caused the run to be ended
485          */
486         private void fireRunEnded(TypingRun run, ChangeType reason) {
487                 List listeners = new ArrayList(fListeners);
488                 for (Iterator it = listeners.iterator(); it.hasNext();) {
489                         ITypingRunListener listener = (ITypingRunListener) it.next();
490                         listener.typingRunEnded(fRun, reason);
491                 }
492         }
493 }