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