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