001: /*******************************************************************************
002: * Copyright (c) 2000, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.jdt.internal.ui.text;
011:
012: import java.util.HashMap;
013: import java.util.Iterator;
014: import java.util.Map;
015:
016: import org.eclipse.text.edits.MalformedTreeException;
017: import org.eclipse.text.edits.TextEdit;
018:
019: import org.eclipse.core.runtime.Assert;
020:
021: import org.eclipse.swt.SWT;
022: import org.eclipse.swt.custom.VerifyKeyListener;
023: import org.eclipse.swt.events.VerifyEvent;
024: import org.eclipse.swt.graphics.Point;
025:
026: import org.eclipse.jface.text.BadLocationException;
027: import org.eclipse.jface.text.IDocument;
028: import org.eclipse.jface.text.IRegion;
029: import org.eclipse.jface.text.ITextViewer;
030: import org.eclipse.jface.text.ITextViewerExtension;
031: import org.eclipse.jface.text.TextViewer;
032:
033: import org.eclipse.jdt.internal.ui.text.TypingRun.ChangeType;
034:
035: /**
036: * Installs as a verify key listener on a viewer and overwrites the behavior
037: * of the backspace key. Clients may register undo specifications for certain
038: * offsets in a document. The <code>SmartBackspaceManager</code> will manage the
039: * specifications and execute the contained <code>TextEdit</code>s when backspace
040: * is pressed at the given offset and the specification is still valid.
041: * <p>
042: * Undo specifications are removed after a number of typing runs.
043: * </p>
044: *
045: * @since 3.0
046: */
047: public class SmartBackspaceManager {
048: /* independent of JDT - may be moved to jface.text */
049:
050: /**
051: * An undo specification describes the change that should be executed if
052: * backspace is pressed at its trigger offset.
053: *
054: * @since 3.0
055: */
056: public static final class UndoSpec {
057: private final int triggerOffset;
058: private final IRegion selection;
059: private final TextEdit[] undoEdits;
060: private final UndoSpec child;
061: int lives;
062:
063: /**
064: * Creates a new spec. A specification consists of a number of
065: * <code>TextEdit</code>s that will be executed when backspace is
066: * pressed at <code>triggerOffset</code>. The spec will be removed
067: * when it is executed, or if more than <code>lives</code>
068: * <code>TypingRun</code>s have ended after registering the spec.
069: * <p>
070: * Optionally, a child specification can be registered. After executing
071: * the spec, the child spec will be registered with the manager. This allows
072: * to create chains of <code>UndoSpec</code>s that will be executed upon
073: * repeated pressing of backspace.
074: * </p>
075: *
076: * @param triggerOffset the offset where this spec is active
077: * @param selection the selection after executing the undo spec
078: * @param edits the <code>TextEdit</code>s to perform when executing
079: * the spec
080: * @param lives the number of <code>TypingRun</code>s before removing
081: * the spec
082: * @param child a child specification that will be registered after
083: * executing this spec, or <code>null</code>
084: */
085: public UndoSpec(int triggerOffset, IRegion selection,
086: TextEdit[] edits, int lives, UndoSpec child) {
087: Assert.isLegal(triggerOffset >= 0);
088: Assert.isLegal(selection != null);
089: Assert.isLegal(lives >= 0);
090: Assert.isLegal(edits != null);
091: Assert.isLegal(edits.length > 0);
092: for (int i = 0; i < edits.length; i++) {
093: Assert.isLegal(edits[i] != null);
094: }
095:
096: this .triggerOffset = triggerOffset;
097: this .selection = selection;
098: this .undoEdits = edits;
099: this .lives = lives;
100: this .child = child;
101: }
102: }
103:
104: private class BackspaceListener implements VerifyKeyListener {
105:
106: /*
107: * @see org.eclipse.swt.custom.VerifyKeyListener#verifyKey(org.eclipse.swt.events.VerifyEvent)
108: */
109: public void verifyKey(VerifyEvent event) {
110: if (fViewer != null && isBackspace(event)) {
111: int offset = getCaretOffset();
112: UndoSpec spec = removeEdit(offset);
113: if (spec != null) {
114: try {
115: beginChange();
116: for (int i = 0; i < spec.undoEdits.length; i++) {
117: spec.undoEdits[i].apply(getDocument(),
118: TextEdit.UPDATE_REGIONS);
119: }
120: fViewer.setSelectedRange(spec.selection
121: .getOffset(), spec.selection
122: .getLength());
123: if (spec.child != null)
124: register(spec.child);
125: } catch (MalformedTreeException e) {
126: // fall back to standard bs
127: return;
128: } catch (BadLocationException e) {
129: // fall back to standard bs
130: return;
131: } finally {
132: endChange();
133: }
134: event.doit = false;
135: }
136:
137: }
138: }
139:
140: private void beginChange() {
141: ITextViewer viewer = fViewer;
142: if (viewer instanceof TextViewer) {
143: TextViewer v = (TextViewer) viewer;
144: v.getRewriteTarget().beginCompoundChange();
145: }
146: }
147:
148: private void endChange() {
149: ITextViewer viewer = fViewer;
150: if (viewer instanceof TextViewer) {
151: TextViewer v = (TextViewer) viewer;
152: v.getRewriteTarget().endCompoundChange();
153: }
154: }
155:
156: private boolean isBackspace(VerifyEvent event) {
157: return event.doit == true && event.character == SWT.BS
158: && event.stateMask == 0;
159: }
160:
161: private int getCaretOffset() {
162: ITextViewer viewer = fViewer;
163: Point point = viewer.getSelectedRange();
164: return point.x;
165: }
166:
167: }
168:
169: private ITextViewer fViewer;
170: private BackspaceListener fBackspaceListener;
171: private Map fSpecs;
172: private TypingRunDetector fRunDetector;
173: private ITypingRunListener fRunListener;
174:
175: /**
176: * Registers an undo specification with this manager.
177: *
178: * @param spec the specification to register
179: * @throws IllegalStateException if the manager is not installed
180: */
181: public void register(UndoSpec spec) {
182: if (fViewer == null)
183: throw new IllegalStateException();
184:
185: ensureListenerInstalled();
186: addEdit(spec);
187: }
188:
189: private void addEdit(UndoSpec spec) {
190: Integer i = new Integer(spec.triggerOffset);
191: fSpecs.put(i, spec);
192: }
193:
194: private UndoSpec removeEdit(int offset) {
195: Integer i = new Integer(offset);
196: UndoSpec spec = (UndoSpec) fSpecs.remove(i);
197: return spec;
198: }
199:
200: private void ensureListenerInstalled() {
201: if (fBackspaceListener == null) {
202: fBackspaceListener = new BackspaceListener();
203: ITextViewer viewer = fViewer;
204: if (viewer instanceof ITextViewerExtension)
205: ((ITextViewerExtension) viewer)
206: .prependVerifyKeyListener(fBackspaceListener);
207: else
208: viewer.getTextWidget().addVerifyKeyListener(
209: fBackspaceListener);
210: }
211: }
212:
213: private void ensureListenerRemoved() {
214: if (fBackspaceListener != null) {
215: ITextViewer viewer = fViewer;
216: if (viewer instanceof ITextViewerExtension)
217: ((ITextViewerExtension) viewer)
218: .removeVerifyKeyListener(fBackspaceListener);
219: else
220: viewer.getTextWidget().removeVerifyKeyListener(
221: fBackspaceListener);
222: fBackspaceListener = null;
223: }
224: }
225:
226: private IDocument getDocument() {
227: return fViewer.getDocument();
228: }
229:
230: /**
231: * Installs the receiver on a text viewer.
232: *
233: * @param viewer
234: */
235: public void install(ITextViewer viewer) {
236: Assert.isLegal(viewer != null);
237:
238: fViewer = viewer;
239: fSpecs = new HashMap();
240: fRunDetector = new TypingRunDetector();
241: fRunDetector.install(viewer);
242: fRunListener = new ITypingRunListener() {
243:
244: /*
245: * @see org.eclipse.jface.text.TypingRunDetector.ITypingRunListener#typingRunStarted(org.eclipse.jface.text.TypingRunDetector.TypingRun)
246: */
247: public void typingRunStarted(TypingRun run) {
248: }
249:
250: /*
251: * @see org.eclipse.jface.text.TypingRunDetector.ITypingRunListener#typingRunEnded(org.eclipse.jface.text.TypingRunDetector.TypingRun)
252: */
253: public void typingRunEnded(TypingRun run, ChangeType reason) {
254: if (reason == TypingRun.SELECTION)
255: fSpecs.clear();
256: else
257: prune();
258: }
259: };
260: fRunDetector.addTypingRunListener(fRunListener);
261: }
262:
263: private void prune() {
264: for (Iterator it = fSpecs.values().iterator(); it.hasNext();) {
265: UndoSpec spec = (UndoSpec) it.next();
266: if (--spec.lives < 0)
267: it.remove();
268: }
269: }
270:
271: /**
272: * Uninstalls the receiver. No undo specifications may be registered on an
273: * uninstalled manager.
274: */
275: public void uninstall() {
276: if (fViewer != null) {
277: fRunDetector.removeTypingRunListener(fRunListener);
278: fRunDetector.uninstall();
279: fRunDetector = null;
280: ensureListenerRemoved();
281: fViewer = null;
282: }
283: }
284: }
|