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}