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}