// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.tagging.presets.items; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.Component; import java.awt.GridBagLayout; import java.awt.Insets; import java.text.NumberFormat; import java.text.ParseException; import java.util.Collection; import java.util.Collections; import java.util.List; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.ButtonGroup; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JToggleButton; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Tag; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField; import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; import org.openstreetmap.josm.gui.widgets.JosmComboBox; import org.openstreetmap.josm.gui.widgets.JosmTextField; import org.openstreetmap.josm.tools.GBC; /** * Text field type. */ public class Text extends KeyedItem { private static int auto_increment_selected; // NOSONAR /** The localized version of {@link #text}. */ public String locale_text; // NOSONAR /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable). Defaults to "". */ public String default_; // NOSONAR /** The original value */ public String originalValue; // NOSONAR /** whether the last value is used as default. Using "force" enforces this behaviour also for already tagged objects. Default is "false".*/ public String use_last_as_default = "false"; // NOSONAR /** * May contain a comma separated list of integer increments or decrements, e.g. "-2,-1,+1,+2". * A button will be shown next to the text field for each value, allowing the user to select auto-increment with the given stepping. * Auto-increment only happens if the user selects it. There is also a button to deselect auto-increment. * Default is no auto-increment. Mutually exclusive with {@link #use_last_as_default}. */ public String auto_increment; // NOSONAR /** The length of the text box (number of characters allowed). */ public String length; // NOSONAR /** A comma separated list of alternative keys to use for autocompletion. */ public String alternative_autocomplete_keys; // NOSONAR private JComponent value; @Override public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { // find out if our key is already used in the selection. Usage usage = determineTextUsage(sel, key); AutoCompletingTextField textField = new AutoCompletingTextField(); if (alternative_autocomplete_keys != null) { initAutoCompletionField(textField, (key + ',' + alternative_autocomplete_keys).split(",")); } else { initAutoCompletionField(textField, key); } if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) { textField.setHint(key); } if (length != null && !length.isEmpty()) { textField.setMaxChars(Integer.valueOf(length)); } if (usage.unused()) { if (auto_increment_selected != 0 && auto_increment != null) { try { textField.setText(Integer.toString(Integer.parseInt( LAST_VALUES.get(key)) + auto_increment_selected)); } catch (NumberFormatException ex) { // Ignore - cannot auto-increment if last was non-numeric Main.trace(ex); } } else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { // selected osm primitives are untagged or filling default values feature is enabled if (!presetInitiallyMatches && !"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key)) { textField.setText(LAST_VALUES.get(key)); } else { textField.setText(default_); } } else { // selected osm primitives are tagged and filling default values feature is disabled textField.setText(""); } value = textField; originalValue = null; } else if (usage.hasUniqueValue()) { // all objects use the same value textField.setText(usage.getFirst()); value = textField; originalValue = usage.getFirst(); } else { // the objects have different values JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[usage.values.size()])); comboBox.setEditable(true); comboBox.setEditor(textField); comboBox.getEditor().setItem(DIFFERENT); value = comboBox; originalValue = DIFFERENT; } if (locale_text == null) { locale_text = getLocaleText(text, text_context, null); } // if there's an auto_increment setting, then wrap the text field // into a panel, appending a number of buttons. // auto_increment has a format like -2,-1,1,2 // the text box being the first component in the panel is relied // on in a rather ugly fashion further down. if (auto_increment != null) { ButtonGroup bg = new ButtonGroup(); JPanel pnl = new JPanel(new GridBagLayout()); pnl.add(value, GBC.std().fill(GBC.HORIZONTAL)); // first, one button for each auto_increment value for (final String ai : auto_increment.split(",")) { JToggleButton aibutton = new JToggleButton(ai); aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai)); aibutton.setMargin(new Insets(0, 0, 0, 0)); aibutton.setFocusable(false); saveHorizontalSpace(aibutton); bg.add(aibutton); try { // TODO there must be a better way to parse a number like "+3" than this. final int buttonvalue = (NumberFormat.getIntegerInstance().parse(ai.replace("+", ""))).intValue(); if (auto_increment_selected == buttonvalue) aibutton.setSelected(true); aibutton.addActionListener(e -> auto_increment_selected = buttonvalue); pnl.add(aibutton, GBC.std()); } catch (ParseException ex) { Main.error("Cannot parse auto-increment value of '" + ai + "' into an integer"); } } // an invisible toggle button for "release" of the button group final JToggleButton clearbutton = new JToggleButton("X"); clearbutton.setVisible(false); clearbutton.setFocusable(false); bg.add(clearbutton); // and its visible counterpart. - this mechanism allows us to // have *no* button selected after the X is clicked, instead // of the X remaining selected JButton releasebutton = new JButton("X"); releasebutton.setToolTipText(tr("Cancel auto-increment for this field")); releasebutton.setMargin(new Insets(0, 0, 0, 0)); releasebutton.setFocusable(false); releasebutton.addActionListener(e -> { auto_increment_selected = 0; clearbutton.setSelected(true); }); saveHorizontalSpace(releasebutton); pnl.add(releasebutton, GBC.eol()); value = pnl; } final JLabel label = new JLabel(locale_text + ':'); label.setToolTipText(getKeyTooltipText()); label.setLabelFor(value); p.add(label, GBC.std().insets(0, 0, 10, 0)); p.add(value, GBC.eol().fill(GBC.HORIZONTAL)); value.setToolTipText(getKeyTooltipText()); return true; } private static void saveHorizontalSpace(AbstractButton button) { Insets insets = button.getBorder().getBorderInsets(button); // Ensure the current look&feel does not waste horizontal space (as seen in Nimbus & Aqua) if (insets != null && insets.left+insets.right > insets.top+insets.bottom) { int min = Math.min(insets.top, insets.bottom); button.setBorder(BorderFactory.createEmptyBorder(insets.top, min, insets.bottom, min)); } } private static String getValue(Component comp) { if (comp instanceof JosmComboBox) { return ((JosmComboBox<?>) comp).getEditor().getItem().toString(); } else if (comp instanceof JosmTextField) { return ((JosmTextField) comp).getText(); } else if (comp instanceof JPanel) { return getValue(((JPanel) comp).getComponent(0)); } else { return null; } } @Override public void addCommands(List<Tag> changedTags) { // return if unchanged String v = getValue(value); if (v == null) { Main.error("No 'last value' support for component " + value); return; } v = Tag.removeWhiteSpaces(v); if (!"false".equals(use_last_as_default) || auto_increment != null) { LAST_VALUES.put(key, v); } if (v.equals(originalValue) || (originalValue == null && v.isEmpty())) return; changedTags.add(new Tag(key, v)); AutoCompletionManager.rememberUserInput(key, v, true); } @Override public MatchType getDefaultMatch() { return MatchType.NONE; } @Override public Collection<String> getValues() { if (default_ == null || default_.isEmpty()) return Collections.emptyList(); return Collections.singleton(default_); } }