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