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