| /******************************************************************************* |
| * Copyright (c) 2000, 2011 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.ui.text; |
| |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.events.FocusEvent; |
| import org.eclipse.swt.events.FocusListener; |
| import org.eclipse.swt.events.KeyEvent; |
| import org.eclipse.swt.events.KeyListener; |
| import org.eclipse.swt.events.MouseEvent; |
| import org.eclipse.swt.events.MouseListener; |
| |
| import org.eclipse.core.runtime.Assert; |
| |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.ITextListener; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.TextEvent; |
| |
| import org.eclipse.jdt.internal.ui.text.TypingRun.ChangeType; |
| |
| |
| /** |
| * When connected to a text viewer, a <code>TypingRunDetector</code> observes |
| * <code>TypingRun</code> events. A typing run is a sequence of similar text |
| * modifications, such as inserting or deleting single characters. |
| * <p> |
| * Listeners are informed about the start and end of a <code>TypingRun</code>. |
| * </p> |
| * |
| * @since 3.0 |
| */ |
| public class TypingRunDetector { |
| /* |
| * Implementation note: This class is independent of JDT and may be pulled |
| * up to jface.text if needed. |
| */ |
| |
| /** Debug flag. */ |
| private static final boolean DEBUG= false; |
| |
| /** |
| * Instances of this class abstract a text modification into a simple |
| * description. Typing runs consists of a sequence of one or more modifying |
| * changes of the same type. Every change records the type of change |
| * described by a text modification, and an offset it can be followed by |
| * another change of the same run. |
| */ |
| private static final class Change { |
| private ChangeType fType; |
| private int fNextOffset; |
| |
| /** |
| * Creates a new change of type <code>type</code>. |
| * |
| * @param type the <code>ChangeType</code> of the new change |
| * @param nextOffset the offset of the next change in a typing run |
| */ |
| public Change(ChangeType type, int nextOffset) { |
| fType= type; |
| fNextOffset= nextOffset; |
| } |
| |
| /** |
| * Returns <code>true</code> if the receiver can extend the typing run |
| * the last change of which is described by <code>change</code>. |
| * |
| * @param change the last change in a typing run |
| * @return <code>true</code> if the receiver is a valid extension to |
| * <code>change</code>, <code>false</code> otherwise |
| */ |
| public boolean canFollow(Change change) { |
| if (fType == TypingRun.NO_CHANGE) |
| return true; |
| if (fType.equals(TypingRun.UNKNOWN)) |
| return false; |
| if (fType.equals(change.fType)) { |
| if (fType == TypingRun.DELETE) |
| return fNextOffset == change.fNextOffset - 1; |
| else if (fType == TypingRun.INSERT) |
| return fNextOffset == change.fNextOffset + 1; |
| else if (fType == TypingRun.OVERTYPE) |
| return fNextOffset == change.fNextOffset + 1; |
| else if (fType == TypingRun.SELECTION) |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Returns <code>true</code> if the receiver describes a text |
| * modification, <code>false</code> if it describes a focus / |
| * selection change. |
| * |
| * @return <code>true</code> if the receiver is a text modification |
| */ |
| public boolean isModification() { |
| return fType.isModification(); |
| } |
| |
| /* |
| * @see java.lang.Object#toString() |
| */ |
| @Override |
| public String toString() { |
| return fType.toString() + "@" + fNextOffset; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Returns the change type of this change. |
| * |
| * @return the change type of this change |
| */ |
| public ChangeType getType() { |
| return fType; |
| } |
| } |
| |
| /** |
| * Observes any events that modify the content of the document displayed in |
| * the editor. Since text events may start a new run, this listener is |
| * always registered if the detector is connected. |
| */ |
| private class TextListener implements ITextListener { |
| |
| /* |
| * @see org.eclipse.jface.text.ITextListener#textChanged(org.eclipse.jface.text.TextEvent) |
| */ |
| @Override |
| public void textChanged(TextEvent event) { |
| handleTextChanged(event); |
| } |
| } |
| |
| /** |
| * Observes non-modifying events that will end a run, such as clicking into |
| * the editor, moving the caret, and the editor losing focus. These events |
| * can never start a run, therefore this listener is only registered if |
| * there is an ongoing run. |
| */ |
| private class SelectionListener implements MouseListener, KeyListener, FocusListener { |
| |
| /* |
| * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent) |
| */ |
| @Override |
| public void focusGained(FocusEvent e) { |
| handleSelectionChanged(); |
| } |
| |
| /* |
| * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent) |
| */ |
| @Override |
| public void focusLost(FocusEvent e) { |
| } |
| |
| /* |
| * @see MouseListener#mouseDoubleClick |
| */ |
| @Override |
| public void mouseDoubleClick(MouseEvent e) { |
| } |
| |
| /* |
| * If the right mouse button is pressed, the current editing command is closed |
| * @see MouseListener#mouseDown |
| */ |
| @Override |
| public void mouseDown(MouseEvent e) { |
| if (e.button == 1) |
| handleSelectionChanged(); |
| } |
| |
| /* |
| * @see MouseListener#mouseUp |
| */ |
| @Override |
| public void mouseUp(MouseEvent e) { |
| } |
| |
| /* |
| * @see KeyListener#keyPressed |
| */ |
| @Override |
| public void keyReleased(KeyEvent e) { |
| } |
| |
| /* |
| * On cursor keys, the current editing command is closed |
| * @see KeyListener#keyPressed |
| */ |
| @Override |
| public void keyPressed(KeyEvent e) { |
| switch (e.keyCode) { |
| case SWT.ARROW_UP: |
| case SWT.ARROW_DOWN: |
| case SWT.ARROW_LEFT: |
| case SWT.ARROW_RIGHT: |
| case SWT.END: |
| case SWT.HOME: |
| case SWT.PAGE_DOWN: |
| case SWT.PAGE_UP: |
| handleSelectionChanged(); |
| break; |
| } |
| } |
| } |
| |
| /** The listeners. */ |
| private final Set<ITypingRunListener> fListeners= new HashSet<>(); |
| /** |
| * The viewer we work upon. Set to <code>null</code> in |
| * <code>uninstall</code>. |
| */ |
| private ITextViewer fViewer; |
| /** The text event listener. */ |
| private final TextListener fTextListener= new TextListener(); |
| /** |
| * The selection listener. Set to <code>null</code> when no run is active. |
| */ |
| private SelectionListener fSelectionListener; |
| |
| /* state variables */ |
| |
| /** The most recently observed change. Never <code>null</code>. */ |
| private Change fLastChange; |
| /** The current run, or <code>null</code> if there is none. */ |
| private TypingRun fRun; |
| |
| /** |
| * Installs the receiver with a text viewer. |
| * |
| * @param viewer the viewer to install on |
| */ |
| public void install(ITextViewer viewer) { |
| Assert.isLegal(viewer != null); |
| fViewer= viewer; |
| connect(); |
| } |
| |
| /** |
| * Initializes the state variables and registers any permanent listeners. |
| */ |
| private void connect() { |
| if (fViewer != null) { |
| fLastChange= new Change(TypingRun.UNKNOWN, -1); |
| fRun= null; |
| fSelectionListener= null; |
| fViewer.addTextListener(fTextListener); |
| } |
| } |
| |
| /** |
| * Uninstalls the receiver and removes all listeners. <code>install()</code> |
| * must be called for events to be generated. |
| */ |
| public void uninstall() { |
| if (fViewer != null) { |
| fListeners.clear(); |
| disconnect(); |
| fViewer= null; |
| } |
| } |
| |
| /** |
| * Disconnects any registered listeners. |
| */ |
| private void disconnect() { |
| fViewer.removeTextListener(fTextListener); |
| ensureSelectionListenerRemoved(); |
| } |
| |
| /** |
| * Adds a listener for <code>TypingRun</code> events. Repeatedly adding |
| * the same listener instance has no effect. Listeners may be added even |
| * if the receiver is neither connected nor installed. |
| * |
| * @param listener the listener add |
| */ |
| public void addTypingRunListener(ITypingRunListener listener) { |
| Assert.isLegal(listener != null); |
| fListeners.add(listener); |
| if (fListeners.size() == 1) |
| connect(); |
| } |
| |
| /** |
| * Removes the listener from this manager. If <code>listener</code> is not |
| * registered with the receiver, nothing happens. |
| * |
| * @param listener the listener to remove, or <code>null</code> |
| */ |
| public void removeTypingRunListener(ITypingRunListener listener) { |
| fListeners.remove(listener); |
| if (fListeners.size() == 0) |
| disconnect(); |
| } |
| |
| /** |
| * Handles an incoming text event. |
| * |
| * @param event the text event that describes the text modification |
| */ |
| void handleTextChanged(TextEvent event) { |
| Change type= computeChange(event); |
| handleChange(type); |
| } |
| |
| /** |
| * Computes the change abstraction given a text event. |
| * |
| * @param event the text event to analyze |
| * @return a change object describing the event |
| */ |
| private Change computeChange(TextEvent event) { |
| DocumentEvent e= event.getDocumentEvent(); |
| if (e == null) |
| return new Change(TypingRun.NO_CHANGE, -1); |
| |
| int start= e.getOffset(); |
| int end= e.getOffset() + e.getLength(); |
| String newText= e.getText(); |
| if (newText == null) |
| newText= new String(); |
| |
| if (start == end) { |
| // no replace / delete / overwrite |
| if (newText.length() == 1) |
| return new Change(TypingRun.INSERT, end + 1); |
| } else if (start == end - 1) { |
| if (newText.length() == 1) |
| return new Change(TypingRun.OVERTYPE, end); |
| if (newText.length() == 0) |
| return new Change(TypingRun.DELETE, start); |
| } |
| |
| return new Change(TypingRun.UNKNOWN, -1); |
| } |
| |
| /** |
| * Handles an incoming selection event. |
| */ |
| void handleSelectionChanged() { |
| handleChange(new Change(TypingRun.SELECTION, -1)); |
| } |
| |
| /** |
| * State machine. Changes state given the current state and the incoming |
| * change. |
| * |
| * @param change the incoming change |
| */ |
| private void handleChange(Change change) { |
| if (change.getType() == TypingRun.NO_CHANGE) |
| return; |
| |
| if (DEBUG) |
| System.err.println("Last change: " + fLastChange); //$NON-NLS-1$ |
| |
| if (!change.canFollow(fLastChange)) |
| endIfStarted(change); |
| fLastChange= change; |
| if (change.isModification()) |
| startOrContinue(); |
| |
| if (DEBUG) |
| System.err.println("New change: " + change); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Starts a new run if there is none and informs all listeners. If there |
| * already is a run, nothing happens. |
| */ |
| private void startOrContinue() { |
| if (!hasRun()) { |
| if (DEBUG) |
| System.err.println("+Start run"); //$NON-NLS-1$ |
| fRun= new TypingRun(fLastChange.getType()); |
| ensureSelectionListenerAdded(); |
| fireRunBegun(fRun); |
| } |
| } |
| |
| /** |
| * Returns <code>true</code> if there is an active run, <code>false</code> |
| * otherwise. |
| * |
| * @return <code>true</code> if there is an active run, <code>false</code> |
| * otherwise |
| */ |
| private boolean hasRun() { |
| return fRun != null; |
| } |
| |
| /** |
| * Ends any active run and informs all listeners. If there is none, nothing |
| * happens. |
| * |
| * @param change the change that triggered ending the active run |
| */ |
| private void endIfStarted(Change change) { |
| if (hasRun()) { |
| ensureSelectionListenerRemoved(); |
| if (DEBUG) |
| System.err.println("-End run"); //$NON-NLS-1$ |
| fireRunEnded(fRun, change.getType()); |
| fRun= null; |
| } |
| } |
| |
| /** |
| * Adds the selection listener to the text widget underlying the viewer, if |
| * not already done. |
| */ |
| private void ensureSelectionListenerAdded() { |
| if (fSelectionListener == null) { |
| fSelectionListener= new SelectionListener(); |
| StyledText textWidget= fViewer.getTextWidget(); |
| textWidget.addFocusListener(fSelectionListener); |
| textWidget.addKeyListener(fSelectionListener); |
| textWidget.addMouseListener(fSelectionListener); |
| } |
| } |
| |
| /** |
| * If there is a selection listener, it is removed from the text widget |
| * underlying the viewer. |
| */ |
| private void ensureSelectionListenerRemoved() { |
| if (fSelectionListener != null) { |
| StyledText textWidget= fViewer.getTextWidget(); |
| textWidget.removeFocusListener(fSelectionListener); |
| textWidget.removeKeyListener(fSelectionListener); |
| textWidget.removeMouseListener(fSelectionListener); |
| fSelectionListener= null; |
| } |
| } |
| |
| /** |
| * Informs all listeners about a newly started <code>TypingRun</code>. |
| * |
| * @param run the new run |
| */ |
| private void fireRunBegun(TypingRun run) { |
| List<ITypingRunListener> listeners= new ArrayList<>(fListeners); |
| for (Iterator<ITypingRunListener> it= listeners.iterator(); it.hasNext();) { |
| ITypingRunListener listener= it.next(); |
| listener.typingRunStarted(fRun); |
| } |
| } |
| |
| /** |
| * Informs all listeners about an ended <code>TypingRun</code>. |
| * |
| * @param run the previously active run |
| * @param reason the type of change that caused the run to be ended |
| */ |
| private void fireRunEnded(TypingRun run, ChangeType reason) { |
| List<ITypingRunListener> listeners= new ArrayList<>(fListeners); |
| for (Iterator<ITypingRunListener> it= listeners.iterator(); it.hasNext();) { |
| ITypingRunListener listener= it.next(); |
| listener.typingRunEnded(fRun, reason); |
| } |
| } |
| } |