001package jmri.util.swing;
002
003import java.awt.*;
004import java.awt.event.*;
005import javax.swing.*;
006import javax.swing.text.*;
007import javax.swing.plaf.*;
008import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
009
010/**
011 * A JTextField where the Insert key switches operation to and from
012 * overwrite mode.  In overwrite mode, the cursor is a line under the
013 * next character that will be replaced by typing.
014 * 
015 * @see <a href="https:coderanch.com/t/742171/java/Fixing-JTextComponent-modelToView-deprecations">original source</a>
016 */ 
017public class OvertypeTextArea extends JTextField {
018
019    private static boolean isOvertypeMode;
020
021    private Caret defaultCaret;
022    private Caret overtypeCaret;
023
024    public OvertypeTextArea(int length) {
025        super(length);
026        setCaretColor( Color.red );
027        defaultCaret = getCaret();
028        overtypeCaret = new OvertypeCaret();
029        overtypeCaret.setBlinkRate( defaultCaret.getBlinkRate() );
030        setOvertypeMode( false );  // fields start in regular `insert` mode
031
032        addFocusListener(new FocusListener() {
033            // Install a listener that will set the visible cursor to the 
034            // correct type when entering a field.
035            
036            // With Java 11 on Mac, the first time there's a change of 
037            // focus with isOvertypeMode true can cause an NPE in the L&F at:
038            //      com.apple.laf.AquaCaret.focusGained(AquaCaret.java:104)
039            // This is not present in Java 17, nor nn other platforms.
040            // The exception is benign to the extent that the operations still work.
041            @Override
042            public void focusGained(FocusEvent e) {
043                setOvertypeMode(isOvertypeMode()); // set caret
044            }
045
046            @Override public void focusLost(FocusEvent e) {}
047        });
048    }
049    
050    /*
051     *  Return the overtype/insert mode
052     */
053    public boolean isOvertypeMode() {
054        return OvertypeTextArea.isOvertypeMode;
055    }
056
057    /*
058     *  Set the caret to use depending on overtype/insert mode
059     */
060    @SuppressFBWarnings(value = "ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD", justification = "GUI ease of use")
061    public void setOvertypeMode(boolean isOvertypeModeArg) {
062        OvertypeTextArea.isOvertypeMode = isOvertypeModeArg;
063        int pos = getCaretPosition();
064
065        if ( isOvertypeMode() ) {
066            setCaret( overtypeCaret );
067        } else {
068            setCaret( defaultCaret );
069        }
070
071        setCaretPosition( pos );
072    }
073
074    /*
075     *  Override method from JComponent to do insert vs overwrite
076     */
077    @Override
078    public void replaceSelection(String text) {
079        //  Implement overtype mode by selecting the character at the current
080        //  caret position
081
082        if ( isOvertypeMode() ) {
083            int pos = getCaretPosition();
084
085            if (getSelectedText() == null
086                    &&  pos < getDocument().getLength()) {
087                moveCaretPosition( pos + 1);
088            }
089        }
090
091        super.replaceSelection(text);
092    }
093
094    /*
095     *  Override method from JComponent to check for INSERT key and handle
096     */
097    @Override
098    protected void processKeyEvent(KeyEvent e) {
099        super.processKeyEvent(e);
100
101        //  Handle release of Insert key to toggle overtype/insert mode
102
103        // The Mac apparently cannot provide a VK_INSERT, even if 
104        // the keyboard has a key labelled `insert`.  There's no
105        // consensus on a replacement key or key sequence either.
106        // As a result, this probably won't work on macOS.
107        if (e.getID() == KeyEvent.KEY_RELEASED
108                    &&  e.getKeyCode() == KeyEvent.VK_INSERT) {
109            setOvertypeMode( ! isOvertypeMode() );
110        }
111    }
112
113    /*
114     *  Paint a horizontal line the width of a column and 1 pixel high
115     */
116    private static class OvertypeCaret extends DefaultCaret {
117        /*
118         *  The overtype caret will simply be a horizontal line one pixel high
119         *  (once we determine where to paint it)
120         */
121        @SuppressWarnings("deprecation") // TextUI#modelToView replaced by modelToView2D
122        @Override
123        public void paint(Graphics g) {
124            if (isVisible()) {
125                try {
126                    JTextComponent component = getComponent();
127                    TextUI mapper = component.getUI();
128                    var r = mapper.modelToView(component, getDot());
129                    g.setColor(component.getCaretColor());
130                    // ((Graphics2D) g).setStroke(new BasicStroke(2));
131                    int width = g.getFontMetrics().charWidth( 'w' );
132                    int y = r.y + r.height - 2;
133                    g.drawLine(r.x, y, r.x + width - 2, y);
134                }
135                catch (BadLocationException e) {}
136            }
137        }
138
139        /*
140         *  Damage must be overridden whenever the paint method is overridden
141         *  (The damaged area is the area the caret is painted in. We must
142         *  consider the area for the default caret and this caret)
143         */
144        @Override
145        protected synchronized void damage(Rectangle r) {
146            if (r != null) {
147                JTextComponent component = getComponent();
148                x = r.x;
149                y = r.y;
150                width = component.getFontMetrics( component.getFont() ).charWidth( 'w' );
151                height = r.height;
152                repaint();
153            }
154        }
155    }
156}