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 @Override 151 public void paint(Graphics g) { 152 super.paint(g); 153 if (hint != null && !hint.isEmpty() && getText().isEmpty() && !isFocusOwner()) { 154 // Taken from http://stackoverflow.com/a/24571681/2257172 155 int h = getHeight(); 156 if (g instanceof Graphics2D) { 157 ((Graphics2D) g).setRenderingHint( 158 RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 159 } 160 Insets ins = getInsets(); 161 FontMetrics fm = g.getFontMetrics(); 162 int c0 = getBackground().getRGB(); 163 int c1 = getForeground().getRGB(); 164 int m = 0xfefefefe; 165 int c2 = ((c0 & m) >>> 1) + ((c1 & m) >>> 1); 166 g.setColor(new Color(c2, true)); 167 g.drawString(hint, ins.left, h / 2 + fm.getAscent() / 2 - 2); 168 } 169 } 170 171 @Override 172 public void focusGained(FocusEvent e) { 173 MapFrame map = MainApplication.getMap(); 174 if (map != null) { 175 map.keyDetector.setEnabled(false); 176 } 177 repaint(); 178 } 179 180 @Override 181 public void focusLost(FocusEvent e) { 182 MapFrame map = MainApplication.getMap(); 183 if (map != null) { 184 map.keyDetector.setEnabled(true); 185 } 186 repaint(); 187 } 188 189 @Override 190 public void destroy() { 191 removeFocusListener(this); 192 TextContextualPopupMenu.disableMenuFor(this, launcher); 193 } 194}