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