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 &gt;= 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> &lt; 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 &gt;= 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> &lt; 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}