001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.widgets; 003 004import java.awt.Color; 005import java.awt.FontMetrics; 006import java.awt.Graphics; 007import java.awt.Graphics2D; 008import java.awt.Insets; 009import java.awt.RenderingHints; 010import java.awt.event.FocusEvent; 011import java.awt.event.FocusListener; 012 013import javax.swing.JTextField; 014import javax.swing.text.Document; 015 016import org.openstreetmap.josm.gui.MainApplication; 017import org.openstreetmap.josm.gui.MapFrame; 018import org.openstreetmap.josm.tools.Destroyable; 019 020/** 021 * Subclass of {@link JTextField} that:<ul> 022 * <li>adds a "native" context menu (undo/redo/cut/copy/paste/select all)</li> 023 * <li>adds an optional "hint" displayed when no text has been entered</li> 024 * <li>disables the global advanced key press detector when focused</li> 025 * <li>implements a workaround to <a href="https://bugs.openjdk.java.net/browse/JDK-6322854">JDK bug 6322854</a></li> 026 * </ul><br>This class must be used everywhere in core and plugins instead of {@code JTextField}. 027 * @since 5886 028 */ 029public class JosmTextField extends JTextField implements Destroyable, FocusListener { 030 031 private final PopupMenuLauncher launcher; 032 private String hint; 033 034 /** 035 * Constructs a new <code>JosmTextField</code> that uses the given text 036 * storage model and the given number of columns. 037 * This is the constructor through which the other constructors feed. 038 * If the document is <code>null</code>, a default model is created. 039 * 040 * @param doc the text storage to use; if this is <code>null</code>, 041 * a default will be provided by calling the 042 * <code>createDefaultModel</code> method 043 * @param text the initial string to display, or <code>null</code> 044 * @param columns the number of columns to use to calculate 045 * the preferred width >= 0; if <code>columns</code> 046 * is set to zero, the preferred width will be whatever 047 * naturally results from the component implementation 048 * @throws IllegalArgumentException if <code>columns</code> < 0 049 */ 050 public JosmTextField(Document doc, String text, int columns) { 051 this(doc, text, columns, true); 052 } 053 054 /** 055 * Constructs a new <code>JosmTextField</code> that uses the given text 056 * storage model and the given number of columns. 057 * This is the constructor through which the other constructors feed. 058 * If the document is <code>null</code>, a default model is created. 059 * 060 * @param doc the text storage to use; if this is <code>null</code>, 061 * a default will be provided by calling the 062 * <code>createDefaultModel</code> method 063 * @param text the initial string to display, or <code>null</code> 064 * @param columns the number of columns to use to calculate 065 * the preferred width >= 0; if <code>columns</code> 066 * is set to zero, the preferred width will be whatever 067 * naturally results from the component implementation 068 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor 069 * @throws IllegalArgumentException if <code>columns</code> < 0 070 */ 071 public JosmTextField(Document doc, String text, int columns, boolean undoRedo) { 072 super(doc, text, columns); 073 launcher = TextContextualPopupMenu.enableMenuFor(this, undoRedo); 074 // Fix minimum size when columns are specified 075 if (columns > 0) { 076 setMinimumSize(getPreferredSize()); 077 } 078 addFocusListener(this); 079 // Workaround for Java bug 6322854 080 JosmPasswordField.workaroundJdkBug6322854(this); 081 } 082 083 /** 084 * Constructs a new <code>JosmTextField</code> initialized with the 085 * specified text and columns. A default model is created. 086 * 087 * @param text the text to be displayed, or <code>null</code> 088 * @param columns the number of columns to use to calculate 089 * the preferred width; if columns is set to zero, the 090 * preferred width will be whatever naturally results from 091 * the component implementation 092 */ 093 public JosmTextField(String text, int columns) { 094 this(null, text, columns); 095 } 096 097 /** 098 * Constructs a new <code>JosmTextField</code> initialized with the 099 * specified text. A default model is created and the number of 100 * columns is 0. 101 * 102 * @param text the text to be displayed, or <code>null</code> 103 */ 104 public JosmTextField(String text) { 105 this(null, text, 0); 106 } 107 108 /** 109 * Constructs a new empty <code>JosmTextField</code> with the specified 110 * number of columns. 111 * A default model is created and the initial string is set to 112 * <code>null</code>. 113 * 114 * @param columns the number of columns to use to calculate 115 * the preferred width; if columns is set to zero, the 116 * preferred width will be whatever naturally results from 117 * the component implementation 118 */ 119 public JosmTextField(int columns) { 120 this(null, null, columns); 121 } 122 123 /** 124 * Constructs a new <code>JosmTextField</code>. A default model is created, 125 * the initial string is <code>null</code>, 126 * and the number of columns is set to 0. 127 */ 128 public JosmTextField() { 129 this(null, null, 0); 130 } 131 132 /** 133 * Replies the hint displayed when no text has been entered. 134 * @return the hint 135 * @since 7505 136 */ 137 public final String getHint() { 138 return hint; 139 } 140 141 /** 142 * Sets the hint to display when no text has been entered. 143 * @param hint the hint to set 144 * @since 7505 145 */ 146 public final void setHint(String hint) { 147 this.hint = hint; 148 } 149 150 /** 151 * Empties the internal undo manager. 152 * @since 14977 153 */ 154 public final void discardAllUndoableEdits() { 155 launcher.discardAllUndoableEdits(); 156 } 157 158 @Override 159 public void paint(Graphics g) { 160 super.paint(g); 161 if (hint != null && !hint.isEmpty() && getText().isEmpty() && !isFocusOwner()) { 162 // Taken from http://stackoverflow.com/a/24571681/2257172 163 int h = getHeight(); 164 if (g instanceof Graphics2D) { 165 ((Graphics2D) g).setRenderingHint( 166 RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 167 } 168 Insets ins = getInsets(); 169 FontMetrics fm = g.getFontMetrics(); 170 int c0 = getBackground().getRGB(); 171 int c1 = getForeground().getRGB(); 172 int m = 0xfefefefe; 173 int c2 = ((c0 & m) >>> 1) + ((c1 & m) >>> 1); 174 g.setColor(new Color(c2, true)); 175 g.drawString(hint, ins.left, h / 2 + fm.getAscent() / 2 - 2); 176 } 177 } 178 179 @Override 180 public void focusGained(FocusEvent e) { 181 MapFrame map = MainApplication.getMap(); 182 if (map != null) { 183 map.keyDetector.setEnabled(false); 184 } 185 repaint(); 186 } 187 188 @Override 189 public void focusLost(FocusEvent e) { 190 MapFrame map = MainApplication.getMap(); 191 if (map != null) { 192 map.keyDetector.setEnabled(true); 193 } 194 repaint(); 195 } 196 197 @Override 198 public void destroy() { 199 removeFocusListener(this); 200 TextContextualPopupMenu.disableMenuFor(this, launcher); 201 } 202}