001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.tagging.presets.items;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.GridBagLayout;
008import java.awt.Insets;
009import java.awt.event.ActionEvent;
010import java.awt.event.ActionListener;
011import java.text.NumberFormat;
012import java.text.ParseException;
013import java.util.Collection;
014import java.util.Collections;
015import java.util.List;
016
017import javax.swing.AbstractButton;
018import javax.swing.BorderFactory;
019import javax.swing.ButtonGroup;
020import javax.swing.JButton;
021import javax.swing.JComponent;
022import javax.swing.JLabel;
023import javax.swing.JPanel;
024import javax.swing.JToggleButton;
025
026import org.openstreetmap.josm.Main;
027import org.openstreetmap.josm.data.osm.OsmPrimitive;
028import org.openstreetmap.josm.data.osm.Tag;
029import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
030import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
031import org.openstreetmap.josm.gui.widgets.JosmComboBox;
032import org.openstreetmap.josm.gui.widgets.JosmTextField;
033import org.openstreetmap.josm.tools.GBC;
034
035/**
036 * Text field type.
037 */
038public class Text extends KeyedItem {
039
040    private static int auto_increment_selected; // NOSONAR
041
042    /** The localized version of {@link #text}. */
043    public String locale_text; // NOSONAR
044    /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable). Defaults to "". */
045    public String default_; // NOSONAR
046    /** The original value */
047    public String originalValue; // NOSONAR
048    /** whether the last value is used as default. Using "force" enforces this behaviour also for already tagged objects. Default is "false".*/
049    public String use_last_as_default = "false"; // NOSONAR
050    /**
051     * May contain a comma separated list of integer increments or decrements, e.g. "-2,-1,+1,+2".
052     * A button will be shown next to the text field for each value, allowing the user to select auto-increment with the given stepping.
053     * Auto-increment only happens if the user selects it. There is also a button to deselect auto-increment.
054     * Default is no auto-increment. Mutually exclusive with {@link #use_last_as_default}.
055     */
056    public String auto_increment; // NOSONAR
057    /** The length of the text box (number of characters allowed). */
058    public String length; // NOSONAR
059    /** A comma separated list of alternative keys to use for autocompletion. */
060    public String alternative_autocomplete_keys; // NOSONAR
061
062    private JComponent value;
063
064    @Override
065    public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
066
067        // find out if our key is already used in the selection.
068        Usage usage = determineTextUsage(sel, key);
069        AutoCompletingTextField textField = new AutoCompletingTextField();
070        if (alternative_autocomplete_keys != null) {
071            initAutoCompletionField(textField, (key + ',' + alternative_autocomplete_keys).split(","));
072        } else {
073            initAutoCompletionField(textField, key);
074        }
075        if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) {
076            textField.setHint(key);
077        }
078        if (length != null && !length.isEmpty()) {
079            textField.setMaxChars(Integer.valueOf(length));
080        }
081        if (usage.unused()) {
082            if (auto_increment_selected != 0 && auto_increment != null) {
083                try {
084                    textField.setText(Integer.toString(Integer.parseInt(
085                            LAST_VALUES.get(key)) + auto_increment_selected));
086                } catch (NumberFormatException ex) {
087                    // Ignore - cannot auto-increment if last was non-numeric
088                    if (Main.isTraceEnabled()) {
089                        Main.trace(ex.getMessage());
090                    }
091                }
092            } else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
093                // selected osm primitives are untagged or filling default values feature is enabled
094                if (!"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) {
095                    textField.setText(LAST_VALUES.get(key));
096                } else {
097                    textField.setText(default_);
098                }
099            } else {
100                // selected osm primitives are tagged and filling default values feature is disabled
101                textField.setText("");
102            }
103            value = textField;
104            originalValue = null;
105        } else if (usage.hasUniqueValue()) {
106            // all objects use the same value
107            textField.setText(usage.getFirst());
108            value = textField;
109            originalValue = usage.getFirst();
110        } else {
111            // the objects have different values
112            JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[0]));
113            comboBox.setEditable(true);
114            comboBox.setEditor(textField);
115            comboBox.getEditor().setItem(DIFFERENT);
116            value = comboBox;
117            originalValue = DIFFERENT;
118        }
119        if (locale_text == null) {
120            locale_text = getLocaleText(text, text_context, null);
121        }
122
123        // if there's an auto_increment setting, then wrap the text field
124        // into a panel, appending a number of buttons.
125        // auto_increment has a format like -2,-1,1,2
126        // the text box being the first component in the panel is relied
127        // on in a rather ugly fashion further down.
128        if (auto_increment != null) {
129            ButtonGroup bg = new ButtonGroup();
130            JPanel pnl = new JPanel(new GridBagLayout());
131            pnl.add(value, GBC.std().fill(GBC.HORIZONTAL));
132
133            // first, one button for each auto_increment value
134            for (final String ai : auto_increment.split(",")) {
135                JToggleButton aibutton = new JToggleButton(ai);
136                aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai));
137                aibutton.setMargin(new Insets(0, 0, 0, 0));
138                aibutton.setFocusable(false);
139                saveHorizontalSpace(aibutton);
140                bg.add(aibutton);
141                try {
142                    // TODO there must be a better way to parse a number like "+3" than this.
143                    final int buttonvalue = (NumberFormat.getIntegerInstance().parse(ai.replace("+", ""))).intValue();
144                    if (auto_increment_selected == buttonvalue) aibutton.setSelected(true);
145                    aibutton.addActionListener(new ActionListener() {
146                        @Override
147                        public void actionPerformed(ActionEvent e) {
148                            auto_increment_selected = buttonvalue;
149                        }
150                    });
151                    pnl.add(aibutton, GBC.std());
152                } catch (ParseException x) {
153                    Main.error("Cannot parse auto-increment value of '" + ai + "' into an integer");
154                }
155            }
156
157            // an invisible toggle button for "release" of the button group
158            final JToggleButton clearbutton = new JToggleButton("X");
159            clearbutton.setVisible(false);
160            clearbutton.setFocusable(false);
161            bg.add(clearbutton);
162            // and its visible counterpart. - this mechanism allows us to
163            // have *no* button selected after the X is clicked, instead
164            // of the X remaining selected
165            JButton releasebutton = new JButton("X");
166            releasebutton.setToolTipText(tr("Cancel auto-increment for this field"));
167            releasebutton.setMargin(new Insets(0, 0, 0, 0));
168            releasebutton.setFocusable(false);
169            releasebutton.addActionListener(new ActionListener() {
170                @Override
171                public void actionPerformed(ActionEvent e) {
172                    auto_increment_selected = 0;
173                    clearbutton.setSelected(true);
174                }
175            });
176            saveHorizontalSpace(releasebutton);
177            pnl.add(releasebutton, GBC.eol());
178            value = pnl;
179        }
180        final JLabel label = new JLabel(locale_text + ':');
181        label.setToolTipText(getKeyTooltipText());
182        label.setLabelFor(value);
183        p.add(label, GBC.std().insets(0, 0, 10, 0));
184        p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
185        value.setToolTipText(getKeyTooltipText());
186        return true;
187    }
188
189    private static void saveHorizontalSpace(AbstractButton button) {
190        Insets insets = button.getBorder().getBorderInsets(button);
191        // Ensure the current look&feel does not waste horizontal space (as seen in Nimbus & Aqua)
192        if (insets != null && insets.left+insets.right > insets.top+insets.bottom) {
193            int min = Math.min(insets.top, insets.bottom);
194            button.setBorder(BorderFactory.createEmptyBorder(insets.top, min, insets.bottom, min));
195        }
196    }
197
198    private static String getValue(Component comp) {
199        if (comp instanceof JosmComboBox) {
200            return ((JosmComboBox<?>) comp).getEditor().getItem().toString();
201        } else if (comp instanceof JosmTextField) {
202            return ((JosmTextField) comp).getText();
203        } else if (comp instanceof JPanel) {
204            return getValue(((JPanel) comp).getComponent(0));
205        } else {
206            return null;
207        }
208    }
209
210    @Override
211    public void addCommands(List<Tag> changedTags) {
212
213        // return if unchanged
214        String v = getValue(value);
215        if (v == null) {
216            Main.error("No 'last value' support for component " + value);
217            return;
218        }
219
220        v = Tag.removeWhiteSpaces(v);
221
222        if (!"false".equals(use_last_as_default) || auto_increment != null) {
223            LAST_VALUES.put(key, v);
224        }
225        if (v.equals(originalValue) || (originalValue == null && v.isEmpty()))
226            return;
227
228        changedTags.add(new Tag(key, v));
229        AutoCompletionManager.rememberUserInput(key, v, true);
230    }
231
232    @Override
233    public MatchType getDefaultMatch() {
234        return MatchType.NONE;
235    }
236
237    @Override
238    public Collection<String> getValues() {
239        if (default_ == null || default_.isEmpty())
240            return Collections.emptyList();
241        return Collections.singleton(default_);
242    }
243}