001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.widgets; 003 004import java.awt.Color; 005import java.awt.ComponentOrientation; 006import java.awt.Font; 007import java.awt.FontMetrics; 008import java.awt.Graphics; 009import java.awt.Graphics2D; 010import java.awt.Insets; 011import java.awt.Point; 012import java.awt.RenderingHints; 013import java.awt.event.ComponentEvent; 014import java.awt.event.ComponentListener; 015import java.awt.event.FocusEvent; 016import java.awt.event.FocusListener; 017import java.beans.PropertyChangeEvent; 018import java.beans.PropertyChangeListener; 019 020import javax.swing.Icon; 021import javax.swing.JTextField; 022import javax.swing.RepaintManager; 023import javax.swing.JMenuItem; 024import javax.swing.JPopupMenu; 025import javax.swing.UIManager; 026import javax.swing.text.BadLocationException; 027import javax.swing.text.Document; 028 029import org.openstreetmap.josm.gui.MainApplication; 030import org.openstreetmap.josm.gui.MapFrame; 031import org.openstreetmap.josm.tools.Destroyable; 032import org.openstreetmap.josm.tools.Utils; 033 034/** 035 * Subclass of {@link JTextField} that:<ul> 036 * <li>adds a "native" context menu (undo/redo/cut/copy/paste/select all)</li> 037 * <li>adds an optional "hint" displayed when no text has been entered</li> 038 * <li>disables the global advanced key press detector when focused</li> 039 * <li>implements a workaround to <a href="https://bugs.openjdk.java.net/browse/JDK-6322854">JDK bug 6322854</a></li> 040 * </ul><br>This class must be used everywhere in core and plugins instead of {@code JTextField}. 041 * @since 5886 042 */ 043public class JosmTextField extends JTextField implements Destroyable, ComponentListener, FocusListener, PropertyChangeListener { 044 045 private final PopupMenuLauncher launcher; 046 private String hint; 047 private Icon icon; 048 private Point iconPos; 049 private Insets originalMargin; 050 private OrientationAction orientationAction; 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 * @throws IllegalArgumentException if <code>columns</code> < 0 067 */ 068 public JosmTextField(Document doc, String text, int columns) { 069 this(doc, text, columns, true); 070 } 071 072 /** 073 * Constructs a new <code>JosmTextField</code> that uses the given text 074 * storage model and the given number of columns. 075 * This is the constructor through which the other constructors feed. 076 * If the document is <code>null</code>, a default model is created. 077 * 078 * @param doc the text storage to use; if this is <code>null</code>, 079 * a default will be provided by calling the 080 * <code>createDefaultModel</code> method 081 * @param text the initial string to display, or <code>null</code> 082 * @param columns the number of columns to use to calculate 083 * the preferred width >= 0; if <code>columns</code> 084 * is set to zero, the preferred width will be whatever 085 * naturally results from the component implementation 086 * @param undoRedo Enables or not Undo/Redo feature. Not recommended for table cell editors, unless each cell provides its own editor 087 * @throws IllegalArgumentException if <code>columns</code> < 0 088 */ 089 public JosmTextField(Document doc, String text, int columns, boolean undoRedo) { 090 super(doc, text, columns); 091 launcher = TextContextualPopupMenu.enableMenuFor(this, undoRedo); 092 093 // There seems to be a bug in Swing 8 that components with Bidi enabled are smaller than 094 // without. (eg. 23px vs 21px in height, maybe a font thing). Usually Bidi starts disabled 095 // but gets enabled whenever RTL text is loaded. To avoid trashing the layout we enable 096 // Bidi by default. See also {@link #drawHint()}. 097 getDocument().putProperty("i18n", Boolean.TRUE); 098 099 // the menu and hotkey to change text orientation 100 orientationAction = new OrientationAction(this); 101 orientationAction.addPropertyChangeListener(this); 102 JPopupMenu menu = launcher.getMenu(); 103 menu.addSeparator(); 104 menu.add(new JMenuItem(orientationAction)); 105 getInputMap().put(OrientationAction.getShortcutKey(), orientationAction); 106 107 // Fix minimum size when columns are specified 108 if (columns > 0) { 109 setMinimumSize(getPreferredSize()); 110 } 111 addFocusListener(this); 112 addComponentListener(this); 113 // Workaround for Java bug 6322854 114 JosmPasswordField.workaroundJdkBug6322854(this); 115 originalMargin = getMargin(); 116 } 117 118 /** 119 * Constructs a new <code>JosmTextField</code> initialized with the 120 * specified text and columns. A default model is created. 121 * 122 * @param text the text to be displayed, or <code>null</code> 123 * @param columns the number of columns to use to calculate 124 * the preferred width; if columns is set to zero, the 125 * preferred width will be whatever naturally results from 126 * the component implementation 127 */ 128 public JosmTextField(String text, int columns) { 129 this(null, text, columns); 130 } 131 132 /** 133 * Constructs a new <code>JosmTextField</code> initialized with the 134 * specified text. A default model is created and the number of 135 * columns is 0. 136 * 137 * @param text the text to be displayed, or <code>null</code> 138 */ 139 public JosmTextField(String text) { 140 this(null, text, 0); 141 } 142 143 /** 144 * Constructs a new empty <code>JosmTextField</code> with the specified 145 * number of columns. 146 * A default model is created and the initial string is set to 147 * <code>null</code>. 148 * 149 * @param columns the number of columns to use to calculate 150 * the preferred width; if columns is set to zero, the 151 * preferred width will be whatever naturally results from 152 * the component implementation 153 */ 154 public JosmTextField(int columns) { 155 this(null, null, columns); 156 } 157 158 /** 159 * Constructs a new <code>JosmTextField</code>. A default model is created, 160 * the initial string is <code>null</code>, 161 * and the number of columns is set to 0. 162 */ 163 public JosmTextField() { 164 this(null, null, 0); 165 } 166 167 /** 168 * Replies the hint displayed when no text has been entered. 169 * @return the hint 170 * @since 7505 171 */ 172 public final String getHint() { 173 return hint; 174 } 175 176 /** 177 * Sets the hint to display when no text has been entered. 178 * @param hint the hint to set 179 * @return the old hint 180 * @since 18221 (signature) 181 */ 182 public String setHint(String hint) { 183 String old = hint; 184 this.hint = hint; 185 return old; 186 } 187 188 /** 189 * Return true if the textfield should display the hint text. 190 * 191 * @return whether to display the hint text 192 * @since 18221 193 */ 194 public boolean displayHint() { 195 return !Utils.isEmpty(hint) && getText().isEmpty() && !isFocusOwner(); 196 } 197 198 /** 199 * Returns the icon to display 200 * @return the icon to display 201 * @since 17768 202 */ 203 public Icon getIcon() { 204 return icon; 205 } 206 207 /** 208 * Sets the icon to display 209 * @param icon the icon to set 210 * @since 17768 211 */ 212 public void setIcon(Icon icon) { 213 this.icon = icon; 214 if (icon == null) { 215 setMargin(originalMargin); 216 } 217 positionIcon(); 218 } 219 220 private void positionIcon() { 221 if (icon != null) { 222 Insets margin = (Insets) originalMargin.clone(); 223 int hGap = (getHeight() - icon.getIconHeight()) / 2; 224 if (getComponentOrientation() == ComponentOrientation.RIGHT_TO_LEFT) { 225 margin.right += icon.getIconWidth() + 2 * hGap; 226 iconPos = new Point(getWidth() - icon.getIconWidth() - hGap, hGap); 227 } else { 228 margin.left += icon.getIconWidth() + 2 * hGap; 229 iconPos = new Point(hGap, hGap); 230 } 231 setMargin(margin); 232 } 233 } 234 235 @Override 236 public void setComponentOrientation(ComponentOrientation o) { 237 if (o.isLeftToRight() != getComponentOrientation().isLeftToRight()) { 238 super.setComponentOrientation(o); 239 positionIcon(); 240 } 241 } 242 243 /** 244 * Empties the internal undo manager. 245 * @since 14977 246 */ 247 public final void discardAllUndoableEdits() { 248 launcher.discardAllUndoableEdits(); 249 } 250 251 /** 252 * Returns the color for hint texts. 253 * @return the Color for hint texts 254 */ 255 public static Color getHintTextColor() { 256 Color color = UIManager.getColor("TextField[Disabled].textForeground"); // Nimbus? 257 if (color == null) 258 color = UIManager.getColor("TextField.inactiveForeground"); 259 if (color == null) 260 color = Color.GRAY; 261 return color; 262 } 263 264 /** 265 * Returns the font for hint texts. 266 * @return the font for hint texts 267 */ 268 public static Font getHintFont() { 269 return UIManager.getFont("TextField.font"); 270 } 271 272 @Override 273 public void paintComponent(Graphics g) { 274 super.paintComponent(g); 275 if (icon != null) { 276 icon.paintIcon(this, g, iconPos.x, iconPos.y); 277 } 278 if (displayHint()) { 279 // Logging.debug("drawing textfield hint: {0}", getHint()); 280 drawHint(g); 281 } 282 } 283 284 /** 285 * Draws the hint text over the editor component. 286 * 287 * @param g the graphics context 288 */ 289 public void drawHint(Graphics g) { 290 int x; 291 try { 292 x = modelToView(0).x; 293 } catch (BadLocationException exc) { 294 return; // can't happen 295 } 296 // Taken from http://stackoverflow.com/a/24571681/2257172 297 if (g instanceof Graphics2D) { 298 ((Graphics2D) g).setRenderingHint( 299 RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 300 } 301 g.setColor(getHintTextColor()); 302 g.setFont(getHintFont()); 303 if (getComponentOrientation().isLeftToRight()) { 304 g.drawString(getHint(), x, getBaseline(getWidth(), getHeight())); 305 } else { 306 FontMetrics metrics = g.getFontMetrics(g.getFont()); 307 int dx = metrics.stringWidth(getHint()); 308 g.drawString(getHint(), x - dx, getBaseline(getWidth(), getHeight())); 309 } 310 // Needed to avoid endless repaint loop if we accidentally draw over the insets. This may 311 // easily happen because a change in text orientation invalidates the textfield and 312 // following that the preferred size gets smaller. (Bug in Swing?) 313 RepaintManager.currentManager(this).markCompletelyClean(this); 314 } 315 316 @Override 317 public void focusGained(FocusEvent e) { 318 MapFrame map = MainApplication.getMap(); 319 if (map != null) { 320 map.keyDetector.setEnabled(false); 321 } 322 if (e != null && e.getOppositeComponent() != null) { 323 // Select all characters when the change of focus occurs inside JOSM only. 324 // When switching from another application, it is annoying, see #13747 325 selectAll(); 326 } 327 positionIcon(); 328 repaint(); // get rid of hint 329 } 330 331 @Override 332 public void focusLost(FocusEvent e) { 333 MapFrame map = MainApplication.getMap(); 334 if (map != null) { 335 map.keyDetector.setEnabled(true); 336 } 337 repaint(); // paint hint 338 } 339 340 @Override 341 public void destroy() { 342 removeFocusListener(this); 343 TextContextualPopupMenu.disableMenuFor(this, launcher); 344 } 345 346 @Override 347 public void componentResized(ComponentEvent e) { 348 positionIcon(); 349 } 350 351 @Override 352 public void componentMoved(ComponentEvent e) { 353 } 354 355 @Override 356 public void componentShown(ComponentEvent e) { 357 } 358 359 @Override 360 public void componentHidden(ComponentEvent e) { 361 } 362 363 @Override 364 public void propertyChange(PropertyChangeEvent evt) { 365 // command from the menu / shortcut key 366 if ("orientationAction".equals(evt.getPropertyName())) { 367 setComponentOrientation((ComponentOrientation) evt.getNewValue()); 368 } 369 } 370}