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