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