001 /*
002 * LAPIS lightweight structured text processing system
003 *
004 * Copyright (C) 1998-2002 Carnegie Mellon University,
005 * Copyright (C) 2003 Massachusetts Institute of Technology.
006 * All rights reserved.
007 *
008 * This library is free software; you can redistribute it
009 * and/or modify it under the terms of the GNU General
010 * Public License as published by the Free Software
011 * Foundation, version 2.
012 *
013 * LAPIS homepage: http://graphics.lcs.mit.edu/lapis/
014 */
015
016 package lapisx.swing;
017
018 import java.awt.event.*;
019 import javax.swing.text.*;
020 import javax.swing.event.*;
021 import javax.swing.undo.*;
022
023 /**
024 * This class coalesces multiple typed keystrokes into a single undoable event.
025 * To use it, just replace your undo event listener attachment call
026 * <tt>document.addUndoableEditListener (undoManager)</tt> with a call to
027 * <tt>CoalescingUndo.addUndoableEditListener (document, undoManager)</tt>.
028 */
029 public abstract class CoalescingUndo {
030
031 /**
032 * Attaches an undo listener to a document in such a way that
033 * multiple typed keystrokes are coalesced into a single undoable edit.
034 * <b>Only one undo listener may be attached to a document with this call.</b>
035 * @param doc Document generating the edit events
036 * @param undo Listener receiving the edit events, usually an instance of UndoManager
037 */
038 public static void setUndoableEditListener (Document doc, UndoableEditListener undo) {
039 if (!installed)
040 install ();
041 doc.addUndoableEditListener (undo);
042 doc.putProperty (UNDO_LISTENER, undo);
043 }
044
045 public static UndoableEditListener getUndoableEditListener (Document doc) {
046 Object undoObject = doc.getProperty (UNDO_LISTENER);
047 if (undoObject == null
048 || !(undoObject instanceof UndoableEditListener))
049 return null;
050 return (UndoableEditListener) undoObject;
051 }
052
053 /*
054 * Replace the default key-typed action in the default keymap with our own
055 * KeyTypedAction.
056 */
057 protected static boolean installed = false;
058 protected static void install () {
059 Keymap keymap = JTextComponent.getKeymap (JTextComponent.DEFAULT_KEYMAP);
060 keymap.setDefaultAction (new KeyTypedAction ());
061 installed = true;
062 }
063
064 protected static final String UNDO_LISTENER = "lapisx.swing.CoalescingUndo.UndoableEditListener";
065
066 /*
067 * Action when an unmapped key is pressed. If it's a character, we send a KeyTypedEdit edit
068 * to the undo listener which will absorb subsequent typed keystrokes.
069 */
070 protected static class KeyTypedAction extends DefaultEditorKit.DefaultKeyTypedAction {
071
072 public void actionPerformed(ActionEvent e) {
073 KeyTypedEdit edit = coalesce (e);
074 super.actionPerformed (e);
075 if (edit != null)
076 edit.end ();
077 }
078
079 protected KeyTypedEdit coalesce (ActionEvent e) {
080 if (e == null)
081 return null;
082
083 JTextComponent target = getTextComponent(e);
084 if (target == null
085 || !target.isEditable ()
086 || !target.isEnabled ())
087 return null;
088
089 Document doc = target.getDocument ();
090 if (doc == null)
091 return null;
092
093 UndoableEditListener undo = getUndoableEditListener (doc);
094 if (undo == null)
095 return null;
096
097 int mod = e.getModifiers();
098 if ((mod & ActionEvent.ALT_MASK) != 0
099 || (mod & ActionEvent.CTRL_MASK) != 0)
100 return null;
101
102 String content = e.getActionCommand();
103 if (content == null
104 || content.length() == 0)
105 return null;
106
107 char c = content.charAt(0);
108 if (c < 0x20
109 || c == 0x7F)
110 return null;
111
112 // if we're doing pending delete, take care of that first
113 if (target.getSelectionStart () != target.getSelectionEnd ())
114 target.replaceSelection ("");
115
116 KeyTypedEdit edit = new KeyTypedEdit (target);
117 UndoableEditEvent ue = new UndoableEditEvent (target, edit);
118 undo.undoableEditHappened (ue);
119
120 return edit;
121 }
122 }
123
124 /*
125 * Key typed undoable edit. Absorbs multiple typed keystrokes until a non-KeyTypedEdit
126 * edit occurs or until the caret moves.
127 */
128 protected static class KeyTypedEdit extends CompoundEdit implements CaretListener {
129 JTextComponent editor;
130 boolean caretMoved = false;
131
132 public KeyTypedEdit (JTextComponent editor) {
133 this.editor = editor;
134 }
135
136 public boolean replaceEdit (UndoableEdit edit) {
137 if (edit instanceof KeyTypedEdit) {
138 KeyTypedEdit te = (KeyTypedEdit) edit;
139 if (!te.caretMoved) {
140 edits = te.edits;
141 editor.removeCaretListener (te);
142 return true;
143 }
144 }
145
146 return super.replaceEdit (edit);
147 }
148
149 public void end () {
150 editor.addCaretListener (this);
151 super.end ();
152 }
153
154 public void caretUpdate (CaretEvent evt) {
155 caretMoved = true;
156 editor.removeCaretListener (this);
157 }
158 }
159 }
160