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}