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