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