import javax.swing.text.DefaultEditorKit;
import javax.swing.text.JTextComponent;
import javax.swing.text.TextAction;
import javax.swing.text.Document;
import javax.swing.text.BadLocationException;
import javax.swing.text.StyledEditorKit;
import javax.swing.Action;
import java.awt.event.ActionEvent;


import javax.swing.*;


// EditorKit with override mode (toggled by INSERT)
// somewhat complicated by handling Unicode surrogates
// (just because the standard deletePrev/NextChar actions
// also do (but probably not much of the rest)).




public class OverrideEditorKit
    extends StyledEditorKit
{
    static
    {
        KeymapFix.init();
    }



    private boolean overrideMode;




    public Action[] getActions()
    {
        return TextAction.augmentList(super.getActions(),
        new Action[] { new OverrideTypedAction(),
                       new ToggleOverrideModeAction() });
    }

    public void install(JEditorPane p)
    {
        super.install(p);

        p.getInputMap().put(KeyStroke.getKeyStroke("INSERT"), "toggle-override-mode");
    }

    public void deinstall(JEditorPane p)
    {
        super.deinstall(p);

        p.getInputMap().remove(KeyStroke.getKeyStroke("INSERT"));
    }



    /** Is there a replaced item at 'position'?
        'c' is the character there.
        This implementation is always false.
    */
    protected boolean isReplaced(Document d, int position, char c)
    {
        return false;
    }


    private class ToggleOverrideModeAction
        extends TextAction
    {
        public ToggleOverrideModeAction()
        {
            super("toggle-override-mode");
        }

        public void actionPerformed(ActionEvent e)
        {
            JTextComponent t = getTextComponent(e);

            if (t == null || !t.isEnabled() || !t.isEditable())
                return;

            if (!(t instanceof JEditorPane))
                return;

            JEditorPane p = (JEditorPane)t;

            if (p.getEditorKit() == OverrideEditorKit.this)
                overrideMode = !overrideMode;
        }
    }


    protected class OverrideTypedAction
        extends DefaultKeyTypedAction
    {
        public OverrideTypedAction()
        {
        }


        // for surrogate handling, see Unicode Standard 3.7, 5.4
        // Problems with inconsistent requests are not fixed:
        // * the content to insert may contain surrogate halfs
        // * the insertion place is in the middle of a surrogate pair
        public void actionPerformed(ActionEvent e)
        {
            JTextComponent target = getTextComponent(e);

            if (target == null)
                return;
                    
            if (!overrideMode || target.getSelectionStart() != target.getSelectionEnd())
            {
                super.actionPerformed(e);
                return;
            }

            if (!(target.isEditable() && target.isEnabled()))
                return;

            // superclass logic when not to insert cannot be used.

            if ((e.getModifiers() & ActionEvent.ALT_MASK) != (e.getModifiers() & ActionEvent.CTRL_MASK))
                return;

            String content = e.getActionCommand();

            if (content == null)
                return;

            int rawContentLength = content.length();

            if (rawContentLength == 0 || content.charAt(0) < 0x20 || content.charAt(0) == 0x7F)
                return;

            int contentLength = rawContentLength;

            // count surrogates only as 1 for replacement

            for (int i = 0; i < rawContentLength; i++)
            {
                char c = content.charAt(i);

                if (isHighSurrogate(c) && i < rawContentLength - 1)
                {
                    if (isLowSurrogate(content.charAt(i + 1)))
                       --contentLength;

                    i++;
                }
            }
            

            
            Document d = target.getDocument();

            int position = target.getCaretPosition();

            int removeMax = Math.min(contentLength, d.getLength() - position);


            // remove surrogate pairs only as one "scalar value"
            // therefore (if every character is), twice the length
            // needs to be inspected

            int inspectLength = Math.min(2 * contentLength, d.getLength() - position);
           
            String old;

            try
            {
                old = d.getText(position, inspectLength);
            }
            catch (BadLocationException xxx)
            {
                throw new IndexOutOfBoundsException(xxx.getMessage());
            }

            int removed = 0;      // number of "scalar values" to remove
            int removeLength = 0; // number of chars to remove


            while (removeLength < inspectLength && removed < removeMax)
            {
                char c = old.charAt(removeLength);

                if (c == '\n') // stop replacing at paragraph level
                    break;

                // replacing to stop at "replaced" characters
                // (embedded images etc.) as well. There is no standard
                // way to detect them.
                if (isReplaced(d, position + removeLength, c))
                    break;
                
                if (isHighSurrogate(c) && removeLength < inspectLength - 1)
                {
                    if (isLowSurrogate(old.charAt(removeLength + 1)))
                        removeLength++;
                }

                removed++;
                removeLength++;
            }

            target.setSelectionEnd(position + removeLength);
            // Problem if NavigationFilter vetoes this, but replace-
            // Selection should be used
            target.replaceSelection(content);
        }

        public final boolean isHighSurrogate(char c)
        {
            return c >= '\uD800' && c <= '\uDBFF';
        }

        public final boolean isLowSurrogate(char c)
        {
            return c >= '\uDC00' && c <= '\uDFFF';
        }
    }





    public static void main(String[] args)
    {
        JEditorPane p = new JEditorPane();
        
        p.setEditorKit(new OverrideEditorKit());
        p.setText("\uD800\uDC00");
        JFrame f = new JFrame();

        f.getContentPane().add(p);

        f.pack(); f.show();
    }
}



// global change!
// instead of always executing DefaultEditorKit.DefaultKeyTypedAction,
// the action named "default-typed" (=DefaultEditorKit.defaultKeyTypedAction)
// of the ActionMap is used if it exists.
// Call init (from custom editor kits) if you want custom key typed actions
// there.

class KeymapFix
{
    private static boolean inited;

    
    
    public static synchronized void init()
    {
        if (inited)
            return;

        JTextComponent.getKeymap(JTextComponent.DEFAULT_KEYMAP).setDefaultAction(new ActionMapTypedAction());
        
        inited = true;
    }


    private static class ActionMapTypedAction
        extends DefaultEditorKit.DefaultKeyTypedAction
    {
        public void actionPerformed(ActionEvent e)
        {
            JTextComponent t = getTextComponent(e);
            
            if (t != null)
            {
                Action a = t.getActionMap().get(DefaultEditorKit.defaultKeyTypedAction);
                
                // really 'this' should never end up in an ActionMap
                if (a != null && a != this)
                {
                    a.actionPerformed(e);
                    return;
                }
            }

            super.actionPerformed(e);
        }
    }
}



