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