comparison dwtx/jface/text/DefaultUndoManager.d @ 129:eb30df5ca28b

Added JFace Text sources
author Frank Benoit <benoit@tionex.de>
date Sat, 23 Aug 2008 19:10:48 +0200
parents
children c4fb132a086c
comparison
equal deleted inserted replaced
128:8df1d4193877 129:eb30df5ca28b
1 /*******************************************************************************
2 * Copyright (c) 2000, 2008 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 * Port to the D programming language:
11 * Frank Benoit <benoit@tionex.de>
12 *******************************************************************************/
13
14 module dwtx.jface.text.DefaultUndoManager;
15
16 import dwt.dwthelper.utils;
17
18
19 import java.util.ArrayList;
20 import java.util.List;
21
22 import dwt.DWT;
23 import dwt.custom.StyledText;
24 import dwt.events.KeyEvent;
25 import dwt.events.KeyListener;
26 import dwt.events.MouseEvent;
27 import dwt.events.MouseListener;
28 import dwt.widgets.Display;
29 import dwt.widgets.Shell;
30 import dwtx.core.commands.ExecutionException;
31 import dwtx.core.commands.operations.AbstractOperation;
32 import dwtx.core.commands.operations.IOperationHistory;
33 import dwtx.core.commands.operations.IOperationHistoryListener;
34 import dwtx.core.commands.operations.IUndoContext;
35 import dwtx.core.commands.operations.IUndoableOperation;
36 import dwtx.core.commands.operations.ObjectUndoContext;
37 import dwtx.core.commands.operations.OperationHistoryEvent;
38 import dwtx.core.commands.operations.OperationHistoryFactory;
39 import dwtx.core.runtime.IAdaptable;
40 import dwtx.core.runtime.IProgressMonitor;
41 import dwtx.core.runtime.IStatus;
42 import dwtx.core.runtime.Status;
43 import dwtx.jface.dialogs.MessageDialog;
44
45
46 /**
47 * Standard implementation of {@link dwtx.jface.text.IUndoManager}.
48 * <p>
49 * It registers with the connected text viewer as text input listener and
50 * document listener and logs all changes. It also monitors mouse and keyboard
51 * activities in order to partition the stream of text changes into undo-able
52 * edit commands.
53 * </p>
54 * <p>
55 * Since 3.1 this undo manager is a facade to the global operation history.
56 * </p>
57 * <p>
58 * The usage of {@link dwtx.core.runtime.IAdaptable} in the JFace
59 * layer has been approved by Platform UI, see: https://bugs.eclipse.org/bugs/show_bug.cgi?id=87669#c9
60 * </p>
61 * <p>
62 * This class is not intended to be subclassed.
63 * </p>
64 *
65 * @see dwtx.jface.text.ITextViewer
66 * @see dwtx.jface.text.ITextInputListener
67 * @see dwtx.jface.text.IDocumentListener
68 * @see dwtx.core.commands.operations.IUndoableOperation
69 * @see dwtx.core.commands.operations.IOperationHistory
70 * @see MouseListener
71 * @see KeyListener
72 * @deprecated As of 3.2, replaced by {@link TextViewerUndoManager}
73 * @noextend This class is not intended to be subclassed by clients.
74 */
75 public class DefaultUndoManager : IUndoManager, IUndoManagerExtension {
76
77 /**
78 * Represents an undo-able edit command.
79 * <p>
80 * Since 3.1 this implements the interface for IUndoableOperation.
81 * </p>
82 */
83 class TextCommand : AbstractOperation {
84
85 /** The start index of the replaced text. */
86 protected int fStart= -1;
87 /** The end index of the replaced text. */
88 protected int fEnd= -1;
89 /** The newly inserted text. */
90 protected String fText;
91 /** The replaced text. */
92 protected String fPreservedText;
93
94 /** The undo modification stamp. */
95 protected long fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
96 /** The redo modification stamp. */
97 protected long fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
98
99 /**
100 * Creates a new text command.
101 *
102 * @param context the undo context for this command
103 * @since 3.1
104 */
105 TextCommand(IUndoContext context) {
106 super(JFaceTextMessages.getString("DefaultUndoManager.operationLabel")); //$NON-NLS-1$
107 addContext(context);
108 }
109
110 /**
111 * Re-initializes this text command.
112 */
113 protected void reinitialize() {
114 fStart= fEnd= -1;
115 fText= fPreservedText= null;
116 fUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
117 fRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
118 }
119
120 /**
121 * Sets the start and the end index of this command.
122 *
123 * @param start the start index
124 * @param end the end index
125 */
126 protected void set(int start, int end) {
127 fStart= start;
128 fEnd= end;
129 fText= null;
130 fPreservedText= null;
131 }
132
133 /*
134 * @see dwtx.core.commands.operations.IUndoableOperation#dispose()
135 * @since 3.1
136 */
137 public void dispose() {
138 reinitialize();
139 }
140
141 /**
142 * Undo the change described by this command.
143 *
144 * @since 2.0
145 */
146 protected void undoTextChange() {
147 try {
148 IDocument document= fTextViewer.getDocument();
149 if (document instanceof IDocumentExtension4)
150 ((IDocumentExtension4)document).replace(fStart, fText.length(), fPreservedText, fUndoModificationStamp);
151 else
152 document.replace(fStart, fText.length(), fPreservedText);
153 } catch (BadLocationException x) {
154 }
155 }
156
157 /*
158 * @see dwtx.core.commands.operations.IUndoableOperation#canUndo()
159 * @since 3.1
160 */
161 public bool canUndo() {
162
163 if (isConnected() && isValid()) {
164 IDocument doc= fTextViewer.getDocument();
165 if (doc instanceof IDocumentExtension4) {
166 long docStamp= ((IDocumentExtension4)doc).getModificationStamp();
167
168 // Normal case: an undo is valid if its redo will restore document
169 // to its current modification stamp
170 bool canUndo= docStamp is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP ||
171 docStamp is getRedoModificationStamp();
172
173 /* Special case to check if the answer is false.
174 * If the last document change was empty, then the document's
175 * modification stamp was incremented but nothing was committed.
176 * The operation being queried has an older stamp. In this case only,
177 * the comparison is different. A sequence of document changes that
178 * include an empty change is handled correctly when a valid commit
179 * follows the empty change, but when #canUndo() is queried just after
180 * an empty change, we must special case the check. The check is very
181 * specific to prevent false positives.
182 * see https://bugs.eclipse.org/bugs/show_bug.cgi?id=98245
183 */
184 if (!canUndo &&
185 this is fHistory.getUndoOperation(fUndoContext) && // this is the latest operation
186 this !is fCurrent && // there is a more current operation not on the stack
187 !fCurrent.isValid() && // the current operation is not a valid document modification
188 fCurrent.fUndoModificationStamp !is // the invalid current operation has a document stamp
189 IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) {
190 canUndo= fCurrent.fRedoModificationStamp is docStamp;
191 }
192 /*
193 * When the composite is the current command, it may hold the timestamp
194 * of a no-op change. We check this here rather than in an override of
195 * canUndo() in CompoundTextCommand simply to keep all the special case checks
196 * in one place.
197 */
198 if (!canUndo &&
199 this is fHistory.getUndoOperation(fUndoContext) && // this is the latest operation
200 this instanceof CompoundTextCommand &&
201 this is fCurrent && // this is the current operation
202 this.fStart is -1 && // the current operation text is not valid
203 fCurrent.fRedoModificationStamp !is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP) { // but it has a redo stamp
204 canUndo= fCurrent.fRedoModificationStamp is docStamp;
205 }
206
207 }
208 // if there is no timestamp to check, simply return true per the 3.0.1 behavior
209 return true;
210 }
211 return false;
212 }
213
214 /*
215 * @see dwtx.core.commands.operations.IUndoableOperation#canRedo()
216 * @since 3.1
217 */
218 public bool canRedo() {
219 if (isConnected() && isValid()) {
220 IDocument doc= fTextViewer.getDocument();
221 if (doc instanceof IDocumentExtension4) {
222 long docStamp= ((IDocumentExtension4)doc).getModificationStamp();
223 return docStamp is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP ||
224 docStamp is getUndoModificationStamp();
225 }
226 // if there is no timestamp to check, simply return true per the 3.0.1 behavior
227 return true;
228 }
229 return false;
230 }
231
232 /*
233 * @see dwtx.core.commands.operations.IUndoableOperation#canExecute()
234 * @since 3.1
235 */
236 public bool canExecute() {
237 return isConnected();
238 }
239
240 /*
241 * @see dwtx.core.commands.operations.IUndoableOperation#execute(dwtx.core.runtime.IProgressMonitor, dwtx.core.runtime.IAdaptable)
242 * @since 3.1
243 */
244 public IStatus execute(IProgressMonitor monitor, IAdaptable uiInfo) {
245 // Text commands execute as they are typed, so executing one has no effect.
246 return Status.OK_STATUS;
247 }
248
249 /*
250 * Undo the change described by this command. Also selects and
251 * reveals the change.
252 */
253
254 /**
255 * Undo the change described by this command. Also selects and
256 * reveals the change.
257 *
258 * @param monitor the progress monitor to use if necessary
259 * @param uiInfo an adaptable that can provide UI info if needed
260 * @return the status
261 */
262 public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
263 if (isValid()) {
264 undoTextChange();
265 selectAndReveal(fStart, fPreservedText is null ? 0 : fPreservedText.length());
266 resetProcessChangeSate();
267 return Status.OK_STATUS;
268 }
269 return IOperationHistory.OPERATION_INVALID_STATUS;
270 }
271
272 /**
273 * Re-applies the change described by this command.
274 *
275 * @since 2.0
276 */
277 protected void redoTextChange() {
278 try {
279 IDocument document= fTextViewer.getDocument();
280 if (document instanceof IDocumentExtension4)
281 ((IDocumentExtension4)document).replace(fStart, fEnd - fStart, fText, fRedoModificationStamp);
282 else
283 fTextViewer.getDocument().replace(fStart, fEnd - fStart, fText);
284 } catch (BadLocationException x) {
285 }
286 }
287
288 /**
289 * Re-applies the change described by this command that previously been
290 * rolled back. Also selects and reveals the change.
291 *
292 * @param monitor the progress monitor to use if necessary
293 * @param uiInfo an adaptable that can provide UI info if needed
294 * @return the status
295 */
296 public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
297 if (isValid()) {
298 redoTextChange();
299 resetProcessChangeSate();
300 selectAndReveal(fStart, fText is null ? 0 : fText.length());
301 return Status.OK_STATUS;
302 }
303 return IOperationHistory.OPERATION_INVALID_STATUS;
304 }
305
306 /**
307 * Update the command in response to a commit.
308 *
309 * @since 3.1
310 */
311
312 protected void updateCommand() {
313 fText= fTextBuffer.toString();
314 fTextBuffer.setLength(0);
315 fPreservedText= fPreservedTextBuffer.toString();
316 fPreservedTextBuffer.setLength(0);
317 }
318
319 /**
320 * Creates a new uncommitted text command depending on whether
321 * a compound change is currently being executed.
322 *
323 * @return a new, uncommitted text command or a compound text command
324 */
325 protected TextCommand createCurrent() {
326 return fFoldingIntoCompoundChange ? new CompoundTextCommand(fUndoContext) : new TextCommand(fUndoContext);
327 }
328
329 /**
330 * Commits the current change into this command.
331 */
332 protected void commit() {
333 if (fStart < 0) {
334 if (fFoldingIntoCompoundChange) {
335 fCurrent= createCurrent();
336 } else {
337 reinitialize();
338 }
339 } else {
340 updateCommand();
341 fCurrent= createCurrent();
342 }
343 resetProcessChangeSate();
344 }
345
346 /**
347 * Updates the text from the buffers without resetting
348 * the buffers or adding anything to the stack.
349 *
350 * @since 3.1
351 */
352 protected void pretendCommit() {
353 if (fStart > -1) {
354 fText= fTextBuffer.toString();
355 fPreservedText= fPreservedTextBuffer.toString();
356 }
357 }
358
359 /**
360 * Attempt a commit of this command and answer true if a new
361 * fCurrent was created as a result of the commit.
362 *
363 * @return true if the command was committed and created a
364 * new fCurrent, false if not.
365 * @since 3.1
366 */
367 protected bool attemptCommit() {
368 pretendCommit();
369 if (isValid()) {
370 DefaultUndoManager.this.commit();
371 return true;
372 }
373 return false;
374 }
375
376 /**
377 * Checks whether this text command is valid for undo or redo.
378 *
379 * @return <code>true</code> if the command is valid for undo or redo
380 * @since 3.1
381 */
382 protected bool isValid() {
383 return fStart > -1 &&
384 fEnd > -1 &&
385 fText !is null;
386 }
387
388 /*
389 * @see java.lang.Object#toString()
390 * @since 3.1
391 */
392 public String toString() {
393 String delimiter= ", "; //$NON-NLS-1$
394 StringBuffer text= new StringBuffer(super.toString());
395 text.append("\n"); //$NON-NLS-1$
396 text.append(this.getClass().getName());
397 text.append(" undo modification stamp: "); //$NON-NLS-1$
398 text.append(fUndoModificationStamp);
399 text.append(" redo modification stamp: "); //$NON-NLS-1$
400 text.append(fRedoModificationStamp);
401 text.append(" start: "); //$NON-NLS-1$
402 text.append(fStart);
403 text.append(delimiter);
404 text.append("end: "); //$NON-NLS-1$
405 text.append(fEnd);
406 text.append(delimiter);
407 text.append("text: '"); //$NON-NLS-1$
408 text.append(fText);
409 text.append('\'');
410 text.append(delimiter);
411 text.append("preservedText: '"); //$NON-NLS-1$
412 text.append(fPreservedText);
413 text.append('\'');
414 return text.toString();
415 }
416
417 /**
418 * Return the undo modification stamp
419 *
420 * @return the undo modification stamp for this command
421 * @since 3.1
422 */
423 protected long getUndoModificationStamp() {
424 return fUndoModificationStamp;
425 }
426
427 /**
428 * Return the redo modification stamp
429 *
430 * @return the redo modification stamp for this command
431 * @since 3.1
432 */
433 protected long getRedoModificationStamp() {
434 return fRedoModificationStamp;
435 }
436 }
437
438 /**
439 * Represents an undo-able edit command consisting of several
440 * individual edit commands.
441 */
442 class CompoundTextCommand : TextCommand {
443
444 /** The list of individual commands */
445 private List fCommands= new ArrayList();
446
447 /**
448 * Creates a new compound text command.
449 *
450 * @param context the undo context for this command
451 * @since 3.1
452 */
453 CompoundTextCommand(IUndoContext context) {
454 super(context);
455 }
456
457 /**
458 * Adds a new individual command to this compound command.
459 *
460 * @param command the command to be added
461 */
462 protected void add(TextCommand command) {
463 fCommands.add(command);
464 }
465
466 /*
467 * @see dwtx.jface.text.DefaultUndoManager.TextCommand#undo()
468 */
469 public IStatus undo(IProgressMonitor monitor, IAdaptable uiInfo) {
470 resetProcessChangeSate();
471
472 int size= fCommands.size();
473 if (size > 0) {
474
475 TextCommand c;
476
477 for (int i= size -1; i > 0; --i) {
478 c= (TextCommand) fCommands.get(i);
479 c.undoTextChange();
480 }
481
482 c= (TextCommand) fCommands.get(0);
483 c.undo(monitor, uiInfo);
484 }
485
486 return Status.OK_STATUS;
487 }
488
489 /*
490 * @see dwtx.jface.text.DefaultUndoManager.TextCommand#redo()
491 */
492 public IStatus redo(IProgressMonitor monitor, IAdaptable uiInfo) {
493 resetProcessChangeSate();
494
495 int size= fCommands.size();
496 if (size > 0) {
497
498 TextCommand c;
499
500 for (int i= 0; i < size -1; ++i) {
501 c= (TextCommand) fCommands.get(i);
502 c.redoTextChange();
503 }
504
505 c= (TextCommand) fCommands.get(size -1);
506 c.redo(monitor, uiInfo);
507 }
508 return Status.OK_STATUS;
509 }
510
511 /*
512 * @see TextCommand#updateCommand
513
514 */
515
516 protected void updateCommand() {
517 // first gather the data from the buffers
518 super.updateCommand();
519
520 // the result of the command update is stored as a child command
521 TextCommand c= new TextCommand(fUndoContext);
522 c.fStart= fStart;
523 c.fEnd= fEnd;
524 c.fText= fText;
525 c.fPreservedText= fPreservedText;
526 c.fUndoModificationStamp= fUndoModificationStamp;
527 c.fRedoModificationStamp= fRedoModificationStamp;
528 add(c);
529
530 // clear out all indexes now that the child is added
531 reinitialize();
532 }
533
534 /*
535 * @see TextCommand#createCurrent
536 */
537 protected TextCommand createCurrent() {
538
539 if (!fFoldingIntoCompoundChange)
540 return new TextCommand(fUndoContext);
541
542 reinitialize();
543 return this;
544 }
545
546 /*
547 * @see dwtx.jface.text.DefaultUndoManager.TextCommand#commit()
548 */
549 protected void commit() {
550 // if there is pending data, update the command
551 if (fStart > -1)
552 updateCommand();
553 fCurrent= createCurrent();
554 resetProcessChangeSate();
555 }
556
557 /**
558 * Checks whether the command is valid for undo or redo.
559 *
560 * @return true if the command is valid.
561 * @since 3.1
562 */
563 protected bool isValid() {
564 if (isConnected())
565 return (fStart > -1 || fCommands.size() > 0);
566 return false;
567 }
568
569 /**
570 * Returns the undo modification stamp.
571 *
572 * @return the undo modification stamp
573 * @since 3.1
574 */
575 protected long getUndoModificationStamp() {
576 if (fStart > -1)
577 return super.getUndoModificationStamp();
578 else if (fCommands.size() > 0)
579 return ((TextCommand)fCommands.get(0)).getUndoModificationStamp();
580
581 return fUndoModificationStamp;
582 }
583
584 /**
585 * Returns the redo modification stamp.
586 *
587 * @return the redo modification stamp
588 * @since 3.1
589 */
590 protected long getRedoModificationStamp() {
591 if (fStart > -1)
592 return super.getRedoModificationStamp();
593 else if (fCommands.size() > 0)
594 return ((TextCommand)fCommands.get(fCommands.size()-1)).getRedoModificationStamp();
595
596 return fRedoModificationStamp;
597 }
598 }
599
600 /**
601 * Internal listener to mouse and key events.
602 */
603 class KeyAndMouseListener : MouseListener, KeyListener {
604
605 /*
606 * @see MouseListener#mouseDoubleClick
607 */
608 public void mouseDoubleClick(MouseEvent e) {
609 }
610
611 /*
612 * If the right mouse button is pressed, the current editing command is closed
613 * @see MouseListener#mouseDown
614 */
615 public void mouseDown(MouseEvent e) {
616 if (e.button is 1)
617 commit();
618 }
619
620 /*
621 * @see MouseListener#mouseUp
622 */
623 public void mouseUp(MouseEvent e) {
624 }
625
626 /*
627 * @see KeyListener#keyPressed
628 */
629 public void keyReleased(KeyEvent e) {
630 }
631
632 /*
633 * On cursor keys, the current editing command is closed
634 * @see KeyListener#keyPressed
635 */
636 public void keyPressed(KeyEvent e) {
637 switch (e.keyCode) {
638 case DWT.ARROW_UP:
639 case DWT.ARROW_DOWN:
640 case DWT.ARROW_LEFT:
641 case DWT.ARROW_RIGHT:
642 commit();
643 break;
644 }
645 }
646 }
647
648 /**
649 * Internal listener to document changes.
650 */
651 class DocumentListener : IDocumentListener {
652
653 private String fReplacedText;
654
655 /*
656 * @see dwtx.jface.text.IDocumentListener#documentAboutToBeChanged(dwtx.jface.text.DocumentEvent)
657 */
658 public void documentAboutToBeChanged(DocumentEvent event) {
659 try {
660 fReplacedText= event.getDocument().get(event.getOffset(), event.getLength());
661 fPreservedUndoModificationStamp= event.getModificationStamp();
662 } catch (BadLocationException x) {
663 fReplacedText= null;
664 }
665 }
666
667 /*
668 * @see dwtx.jface.text.IDocumentListener#documentChanged(dwtx.jface.text.DocumentEvent)
669 */
670 public void documentChanged(DocumentEvent event) {
671 fPreservedRedoModificationStamp= event.getModificationStamp();
672
673 // record the current valid state for the top operation in case it remains the
674 // top operation but changes state.
675 IUndoableOperation op= fHistory.getUndoOperation(fUndoContext);
676 bool wasValid= false;
677 if (op !is null)
678 wasValid= op.canUndo();
679 // Process the change, providing the before and after timestamps
680 processChange(event.getOffset(), event.getOffset() + event.getLength(), event.getText(), fReplacedText, fPreservedUndoModificationStamp, fPreservedRedoModificationStamp);
681
682 // now update fCurrent with the latest buffers from the document change.
683 fCurrent.pretendCommit();
684
685 if (op is fCurrent) {
686 // if the document change did not cause a new fCurrent to be created, then we should
687 // notify the history that the current operation changed if its validity has changed.
688 if (wasValid !is fCurrent.isValid())
689 fHistory.operationChanged(op);
690 }
691 else {
692 // if the change created a new fCurrent that we did not yet add to the
693 // stack, do so if it's valid and we are not in the middle of a compound change.
694 if (fCurrent !is fLastAddedCommand && fCurrent.isValid()) {
695 addToCommandStack(fCurrent);
696 }
697 }
698 }
699 }
700
701 /**
702 * Internal text input listener.
703 */
704 class TextInputListener : ITextInputListener {
705
706 /*
707 * @see dwtx.jface.text.ITextInputListener#inputDocumentAboutToBeChanged(dwtx.jface.text.IDocument, dwtx.jface.text.IDocument)
708 */
709 public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) {
710 if (oldInput !is null && fDocumentListener !is null) {
711 oldInput.removeDocumentListener(fDocumentListener);
712 commit();
713 }
714 }
715
716 /*
717 * @see dwtx.jface.text.ITextInputListener#inputDocumentChanged(dwtx.jface.text.IDocument, dwtx.jface.text.IDocument)
718 */
719 public void inputDocumentChanged(IDocument oldInput, IDocument newInput) {
720 if (newInput !is null) {
721 if (fDocumentListener is null)
722 fDocumentListener= new DocumentListener();
723 newInput.addDocumentListener(fDocumentListener);
724 }
725 }
726
727 }
728
729 /*
730 * @see IOperationHistoryListener
731 * @since 3.1
732 */
733 class HistoryListener : IOperationHistoryListener {
734 private IUndoableOperation fOperation;
735
736 public void historyNotification(final OperationHistoryEvent event) {
737 final int type= event.getEventType();
738 switch (type) {
739 case OperationHistoryEvent.ABOUT_TO_UNDO:
740 case OperationHistoryEvent.ABOUT_TO_REDO:
741 // if this is one of our operations
742 if (event.getOperation().hasContext(fUndoContext)) {
743 fTextViewer.getTextWidget().getDisplay().syncExec(new Runnable() {
744 public void run() {
745 // if we are undoing/redoing a command we generated, then ignore
746 // the document changes associated with this undo or redo.
747 if (event.getOperation() instanceof TextCommand) {
748 if (fTextViewer instanceof TextViewer)
749 ((TextViewer)fTextViewer).ignoreAutoEditStrategies(true);
750 listenToTextChanges(false);
751
752 // in the undo case only, make sure compounds are closed
753 if (type is OperationHistoryEvent.ABOUT_TO_UNDO) {
754 if (fFoldingIntoCompoundChange) {
755 endCompoundChange();
756 }
757 }
758 } else {
759 // the undo or redo has our context, but it is not one of
760 // our commands. We will listen to the changes, but will
761 // reset the state that tracks the undo/redo history.
762 commit();
763 fLastAddedCommand= null;
764 }
765 }
766 });
767 fOperation= event.getOperation();
768 }
769 break;
770 case OperationHistoryEvent.UNDONE:
771 case OperationHistoryEvent.REDONE:
772 case OperationHistoryEvent.OPERATION_NOT_OK:
773 if (event.getOperation() is fOperation) {
774 fTextViewer.getTextWidget().getDisplay().syncExec(new Runnable() {
775 public void run() {
776 listenToTextChanges(true);
777 fOperation= null;
778 if (fTextViewer instanceof TextViewer)
779 ((TextViewer)fTextViewer).ignoreAutoEditStrategies(false);
780 }
781 });
782 }
783 break;
784 }
785 }
786
787 }
788
789 /** Text buffer to collect text which is inserted into the viewer */
790 private StringBuffer fTextBuffer= new StringBuffer();
791 /** Text buffer to collect viewer content which has been replaced */
792 private StringBuffer fPreservedTextBuffer= new StringBuffer();
793 /** The document modification stamp for undo. */
794 protected long fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
795 /** The document modification stamp for redo. */
796 protected long fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
797 /** The internal key and mouse event listener */
798 private KeyAndMouseListener fKeyAndMouseListener;
799 /** The internal document listener */
800 private DocumentListener fDocumentListener;
801 /** The internal text input listener */
802 private TextInputListener fTextInputListener;
803
804
805 /** Indicates inserting state */
806 private bool fInserting= false;
807 /** Indicates overwriting state */
808 private bool fOverwriting= false;
809 /** Indicates whether the current change belongs to a compound change */
810 private bool fFoldingIntoCompoundChange= false;
811
812 /** The text viewer the undo manager is connected to */
813 private ITextViewer fTextViewer;
814
815 /** Supported undo level */
816 private int fUndoLevel;
817 /** The currently constructed edit command */
818 private TextCommand fCurrent;
819 /** The last delete edit command */
820 private TextCommand fPreviousDelete;
821
822 /**
823 * The undo context.
824 * @since 3.1
825 */
826 private IOperationHistory fHistory;
827 /**
828 * The operation history.
829 * @since 3.1
830 */
831 private IUndoContext fUndoContext;
832 /**
833 * The operation history listener used for managing undo and redo before
834 * and after the individual commands are performed.
835 * @since 3.1
836 */
837 private IOperationHistoryListener fHistoryListener= new HistoryListener();
838
839 /**
840 * The command last added to the operation history. This must be tracked
841 * internally instead of asking the history, since outside parties may be placing
842 * items on our undo/redo history.
843 */
844 private TextCommand fLastAddedCommand= null;
845
846 /**
847 * Creates a new undo manager who remembers the specified number of edit commands.
848 *
849 * @param undoLevel the length of this manager's history
850 */
851 public DefaultUndoManager(int undoLevel) {
852 fHistory= OperationHistoryFactory.getOperationHistory();
853 setMaximalUndoLevel(undoLevel);
854 }
855
856 /**
857 * Returns whether this undo manager is connected to a text viewer.
858 *
859 * @return <code>true</code> if connected, <code>false</code> otherwise
860 * @since 3.1
861 */
862 private bool isConnected() {
863 return fTextViewer !is null;
864 }
865
866 /*
867 * @see IUndoManager#beginCompoundChange
868 */
869 public void beginCompoundChange() {
870 if (isConnected()) {
871 fFoldingIntoCompoundChange= true;
872 commit();
873 }
874 }
875
876
877 /*
878 * @see IUndoManager#endCompoundChange
879 */
880 public void endCompoundChange() {
881 if (isConnected()) {
882 fFoldingIntoCompoundChange= false;
883 commit();
884 }
885 }
886
887 /**
888 * Registers all necessary listeners with the text viewer.
889 */
890 private void addListeners() {
891 StyledText text= fTextViewer.getTextWidget();
892 if (text !is null) {
893 fKeyAndMouseListener= new KeyAndMouseListener();
894 text.addMouseListener(fKeyAndMouseListener);
895 text.addKeyListener(fKeyAndMouseListener);
896 fTextInputListener= new TextInputListener();
897 fTextViewer.addTextInputListener(fTextInputListener);
898 fHistory.addOperationHistoryListener(fHistoryListener);
899 listenToTextChanges(true);
900 }
901 }
902
903 /**
904 * Unregister all previously installed listeners from the text viewer.
905 */
906 private void removeListeners() {
907 StyledText text= fTextViewer.getTextWidget();
908 if (text !is null) {
909 if (fKeyAndMouseListener !is null) {
910 text.removeMouseListener(fKeyAndMouseListener);
911 text.removeKeyListener(fKeyAndMouseListener);
912 fKeyAndMouseListener= null;
913 }
914 if (fTextInputListener !is null) {
915 fTextViewer.removeTextInputListener(fTextInputListener);
916 fTextInputListener= null;
917 }
918 listenToTextChanges(false);
919 fHistory.removeOperationHistoryListener(fHistoryListener);
920 }
921 }
922
923 /**
924 * Adds the given command to the operation history if it is not part of
925 * a compound change.
926 *
927 * @param command the command to be added
928 * @since 3.1
929 */
930 private void addToCommandStack(TextCommand command){
931 if (!fFoldingIntoCompoundChange || command instanceof CompoundTextCommand) {
932 fHistory.add(command);
933 fLastAddedCommand= command;
934 }
935 }
936
937 /**
938 * Disposes the command stack.
939 *
940 * @since 3.1
941 */
942 private void disposeCommandStack() {
943 fHistory.dispose(fUndoContext, true, true, true);
944 }
945
946 /**
947 * Initializes the command stack.
948 *
949 * @since 3.1
950 */
951 private void initializeCommandStack() {
952 if (fHistory !is null && fUndoContext !is null)
953 fHistory.dispose(fUndoContext, true, true, false);
954
955 }
956
957 /**
958 * Switches the state of whether there is a text listener or not.
959 *
960 * @param listen the state which should be established
961 */
962 private void listenToTextChanges(bool listen) {
963 if (listen) {
964 if (fDocumentListener is null && fTextViewer.getDocument() !is null) {
965 fDocumentListener= new DocumentListener();
966 fTextViewer.getDocument().addDocumentListener(fDocumentListener);
967 }
968 } else if (!listen) {
969 if (fDocumentListener !is null && fTextViewer.getDocument() !is null) {
970 fTextViewer.getDocument().removeDocumentListener(fDocumentListener);
971 fDocumentListener= null;
972 }
973 }
974 }
975
976 /**
977 * Closes the current editing command and opens a new one.
978 */
979 private void commit() {
980 // if fCurrent has never been placed on the command stack, do so now.
981 // this can happen when there are multiple programmatically commits in a single
982 // document change.
983 if (fLastAddedCommand !is fCurrent) {
984 fCurrent.pretendCommit();
985 if (fCurrent.isValid())
986 addToCommandStack(fCurrent);
987 }
988 fCurrent.commit();
989 }
990
991 /**
992 * Reset processChange state.
993 *
994 * @since 3.2
995 */
996 private void resetProcessChangeSate() {
997 fInserting= false;
998 fOverwriting= false;
999 fPreviousDelete.reinitialize();
1000 }
1001
1002 /**
1003 * Checks whether the given text starts with a line delimiter and
1004 * subsequently contains a white space only.
1005 *
1006 * @param text the text to check
1007 * @return <code>true</code> if the text is a line delimiter followed by whitespace, <code>false</code> otherwise
1008 */
1009 private bool isWhitespaceText(String text) {
1010
1011 if (text is null || text.length() is 0)
1012 return false;
1013
1014 String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters();
1015 int index= TextUtilities.startsWith(delimiters, text);
1016 if (index > -1) {
1017 char c;
1018 int length= text.length();
1019 for (int i= delimiters[index].length(); i < length; i++) {
1020 c= text.charAt(i);
1021 if (c !is ' ' && c !is '\t')
1022 return false;
1023 }
1024 return true;
1025 }
1026
1027 return false;
1028 }
1029
1030 private void processChange(int modelStart, int modelEnd, String insertedText, String replacedText, long beforeChangeModificationStamp, long afterChangeModificationStamp) {
1031
1032 if (insertedText is null)
1033 insertedText= ""; //$NON-NLS-1$
1034
1035 if (replacedText is null)
1036 replacedText= ""; //$NON-NLS-1$
1037
1038 int length= insertedText.length();
1039 int diff= modelEnd - modelStart;
1040
1041 if (fCurrent.fUndoModificationStamp is IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP)
1042 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
1043
1044 // normalize
1045 if (diff < 0) {
1046 int tmp= modelEnd;
1047 modelEnd= modelStart;
1048 modelStart= tmp;
1049 }
1050
1051 if (modelStart is modelEnd) {
1052 // text will be inserted
1053 if ((length is 1) || isWhitespaceText(insertedText)) {
1054 // by typing or whitespace
1055 if (!fInserting || (modelStart !is fCurrent.fStart + fTextBuffer.length())) {
1056 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
1057 if (fCurrent.attemptCommit())
1058 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
1059
1060 fInserting= true;
1061 }
1062 if (fCurrent.fStart < 0)
1063 fCurrent.fStart= fCurrent.fEnd= modelStart;
1064 if (length > 0)
1065 fTextBuffer.append(insertedText);
1066 } else if (length >= 0) {
1067 // by pasting or model manipulation
1068 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
1069 if (fCurrent.attemptCommit())
1070 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
1071
1072 fCurrent.fStart= fCurrent.fEnd= modelStart;
1073 fTextBuffer.append(insertedText);
1074 fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
1075 if (fCurrent.attemptCommit())
1076 fCurrent.fUndoModificationStamp= afterChangeModificationStamp;
1077
1078 }
1079 } else {
1080 if (length is 0) {
1081 // text will be deleted by backspace or DEL key or empty clipboard
1082 length= replacedText.length();
1083 String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters();
1084
1085 if ((length is 1) || TextUtilities.equals(delimiters, replacedText) > -1) {
1086
1087 // whereby selection is empty
1088
1089 if (fPreviousDelete.fStart is modelStart && fPreviousDelete.fEnd is modelEnd) {
1090 // repeated DEL
1091
1092 // correct wrong settings of fCurrent
1093 if (fCurrent.fStart is modelEnd && fCurrent.fEnd is modelStart) {
1094 fCurrent.fStart= modelStart;
1095 fCurrent.fEnd= modelEnd;
1096 }
1097 // append to buffer && extend command range
1098 fPreservedTextBuffer.append(replacedText);
1099 ++fCurrent.fEnd;
1100
1101 } else if (fPreviousDelete.fStart is modelEnd) {
1102 // repeated backspace
1103
1104 // insert in buffer and extend command range
1105 fPreservedTextBuffer.insert(0, replacedText);
1106 fCurrent.fStart= modelStart;
1107
1108 } else {
1109 // either DEL or backspace for the first time
1110
1111 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
1112 if (fCurrent.attemptCommit())
1113 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
1114
1115 // as we can not decide whether it was DEL or backspace we initialize for backspace
1116 fPreservedTextBuffer.append(replacedText);
1117 fCurrent.fStart= modelStart;
1118 fCurrent.fEnd= modelEnd;
1119 }
1120
1121 fPreviousDelete.set(modelStart, modelEnd);
1122
1123 } else if (length > 0) {
1124 // whereby selection is not empty
1125 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
1126 if (fCurrent.attemptCommit())
1127 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
1128
1129 fCurrent.fStart= modelStart;
1130 fCurrent.fEnd= modelEnd;
1131 fPreservedTextBuffer.append(replacedText);
1132 }
1133 } else {
1134 // text will be replaced
1135
1136 if (length is 1) {
1137 length= replacedText.length();
1138 String[] delimiters= fTextViewer.getDocument().getLegalLineDelimiters();
1139
1140 if ((length is 1) || TextUtilities.equals(delimiters, replacedText) > -1) {
1141 // because of overwrite mode or model manipulation
1142 if (!fOverwriting || (modelStart !is fCurrent.fStart + fTextBuffer.length())) {
1143 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
1144 if (fCurrent.attemptCommit())
1145 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
1146
1147 fOverwriting= true;
1148 }
1149
1150 if (fCurrent.fStart < 0)
1151 fCurrent.fStart= modelStart;
1152
1153 fCurrent.fEnd= modelEnd;
1154 fTextBuffer.append(insertedText);
1155 fPreservedTextBuffer.append(replacedText);
1156 fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
1157 return;
1158 }
1159 }
1160 // because of typing or pasting whereby selection is not empty
1161 fCurrent.fRedoModificationStamp= beforeChangeModificationStamp;
1162 if (fCurrent.attemptCommit())
1163 fCurrent.fUndoModificationStamp= beforeChangeModificationStamp;
1164
1165 fCurrent.fStart= modelStart;
1166 fCurrent.fEnd= modelEnd;
1167 fTextBuffer.append(insertedText);
1168 fPreservedTextBuffer.append(replacedText);
1169 }
1170 }
1171 // in all cases, the redo modification stamp is updated on the open command
1172 fCurrent.fRedoModificationStamp= afterChangeModificationStamp;
1173 }
1174
1175 /**
1176 * Shows the given exception in an error dialog.
1177 *
1178 * @param title the dialog title
1179 * @param ex the exception
1180 * @since 3.1
1181 */
1182 private void openErrorDialog(final String title, final Exception ex) {
1183 Shell shell= null;
1184 if (isConnected()) {
1185 StyledText st= fTextViewer.getTextWidget();
1186 if (st !is null && !st.isDisposed())
1187 shell= st.getShell();
1188 }
1189 if (Display.getCurrent() !is null)
1190 MessageDialog.openError(shell, title, ex.getLocalizedMessage());
1191 else {
1192 Display display;
1193 final Shell finalShell= shell;
1194 if (finalShell !is null)
1195 display= finalShell.getDisplay();
1196 else
1197 display= Display.getDefault();
1198 display.syncExec(new Runnable() {
1199 public void run() {
1200 MessageDialog.openError(finalShell, title, ex.getLocalizedMessage());
1201 }
1202 });
1203 }
1204 }
1205
1206 /*
1207 * @see dwtx.jface.text.IUndoManager#setMaximalUndoLevel(int)
1208 */
1209 public void setMaximalUndoLevel(int undoLevel) {
1210 fUndoLevel= Math.max(0, undoLevel);
1211 if (isConnected()) {
1212 fHistory.setLimit(fUndoContext, fUndoLevel);
1213 }
1214 }
1215
1216 /*
1217 * @see dwtx.jface.text.IUndoManager#connect(dwtx.jface.text.ITextViewer)
1218 */
1219 public void connect(ITextViewer textViewer) {
1220 if (!isConnected() && textViewer !is null) {
1221 fTextViewer= textViewer;
1222 if (fUndoContext is null)
1223 fUndoContext= new ObjectUndoContext(this);
1224
1225 fHistory.setLimit(fUndoContext, fUndoLevel);
1226
1227 initializeCommandStack();
1228
1229 // open up the current command
1230 fCurrent= new TextCommand(fUndoContext);
1231
1232 fPreviousDelete= new TextCommand(fUndoContext);
1233 addListeners();
1234 }
1235 }
1236
1237 /*
1238 * @see dwtx.jface.text.IUndoManager#disconnect()
1239 */
1240 public void disconnect() {
1241 if (isConnected()) {
1242
1243 removeListeners();
1244
1245 fCurrent= null;
1246 fTextViewer= null;
1247 disposeCommandStack();
1248 fTextBuffer= null;
1249 fPreservedTextBuffer= null;
1250 fUndoContext= null;
1251 }
1252 }
1253
1254 /*
1255 * @see dwtx.jface.text.IUndoManager#reset()
1256 */
1257 public void reset() {
1258 if (isConnected()) {
1259 initializeCommandStack();
1260 fCurrent= new TextCommand(fUndoContext);
1261 fFoldingIntoCompoundChange= false;
1262 fInserting= false;
1263 fOverwriting= false;
1264 fTextBuffer.setLength(0);
1265 fPreservedTextBuffer.setLength(0);
1266 fPreservedUndoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
1267 fPreservedRedoModificationStamp= IDocumentExtension4.UNKNOWN_MODIFICATION_STAMP;
1268 }
1269 }
1270
1271 /*
1272 * @see dwtx.jface.text.IUndoManager#redoable()
1273 */
1274 public bool redoable() {
1275 return fHistory.canRedo(fUndoContext);
1276 }
1277
1278 /*
1279 * @see dwtx.jface.text.IUndoManager#undoable()
1280 */
1281 public bool undoable() {
1282 return fHistory.canUndo(fUndoContext);
1283 }
1284
1285 /*
1286 * @see dwtx.jface.text.IUndoManager#redo()
1287 */
1288 public void redo() {
1289 if (isConnected() && redoable()) {
1290 try {
1291 fHistory.redo(fUndoContext, null, null);
1292 } catch (ExecutionException ex) {
1293 openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.redoFailed.title"), ex); //$NON-NLS-1$
1294 }
1295 }
1296 }
1297
1298 /*
1299 * @see dwtx.jface.text.IUndoManager#undo()
1300 */
1301 public void undo() {
1302 if (isConnected() && undoable()) {
1303 try {
1304 fHistory.undo(fUndoContext, null, null);
1305 } catch (ExecutionException ex) {
1306 openErrorDialog(JFaceTextMessages.getString("DefaultUndoManager.error.undoFailed.title"), ex); //$NON-NLS-1$
1307 }
1308 }
1309 }
1310
1311 /**
1312 * Selects and reveals the specified range.
1313 *
1314 * @param offset the offset of the range
1315 * @param length the length of the range
1316 * @since 3.0
1317 */
1318 protected void selectAndReveal(int offset, int length) {
1319 if (fTextViewer instanceof ITextViewerExtension5) {
1320 ITextViewerExtension5 extension= (ITextViewerExtension5) fTextViewer;
1321 extension.exposeModelRange(new Region(offset, length));
1322 } else if (!fTextViewer.overlapsWithVisibleRegion(offset, length))
1323 fTextViewer.resetVisibleRegion();
1324
1325 fTextViewer.setSelectedRange(offset, length);
1326 fTextViewer.revealRange(offset, length);
1327 }
1328
1329 /*
1330 * @see dwtx.jface.text.IUndoManagerExtension#getUndoContext()
1331 * @since 3.1
1332 */
1333 public IUndoContext getUndoContext() {
1334 return fUndoContext;
1335 }
1336
1337 }