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.Color; 007import java.awt.Component; 008import java.awt.GridBagLayout; 009import java.awt.Insets; 010import java.text.NumberFormat; 011import java.text.ParseException; 012import java.util.ArrayList; 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.data.osm.Tag; 027import org.openstreetmap.josm.data.tagging.ac.AutoCompletionItem; 028import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxEditor; 029import org.openstreetmap.josm.gui.tagging.ac.AutoCompComboBoxModel; 030import org.openstreetmap.josm.gui.tagging.ac.AutoCompTextField; 031import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; 032import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem; 033import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItemGuiSupport; 034import org.openstreetmap.josm.gui.util.DocumentAdapter; 035import org.openstreetmap.josm.gui.widgets.JosmComboBox; 036import org.openstreetmap.josm.gui.widgets.JosmTextField; 037import org.openstreetmap.josm.gui.widgets.OrientationAction; 038import org.openstreetmap.josm.tools.GBC; 039import org.openstreetmap.josm.tools.Logging; 040import org.openstreetmap.josm.tools.Utils; 041import org.openstreetmap.josm.tools.template_engine.ParseError; 042import org.openstreetmap.josm.tools.template_engine.TemplateEntry; 043import org.openstreetmap.josm.tools.template_engine.TemplateParser; 044import org.xml.sax.SAXException; 045 046/** 047 * Text field type. 048 */ 049public class Text extends KeyedItem { 050 051 private static int auto_increment_selected; // NOSONAR 052 053 /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable). Defaults to "". */ 054 public String default_; // NOSONAR 055 /** The original value */ 056 public String originalValue; // NOSONAR 057 /** whether the last value is used as default. Using "force" enforces this behaviour also for already tagged objects. Default is "false".*/ 058 public String use_last_as_default = "false"; // NOSONAR 059 /** 060 * May contain a comma separated list of integer increments or decrements, e.g. "-2,-1,+1,+2". 061 * A button will be shown next to the text field for each value, allowing the user to select auto-increment with the given stepping. 062 * Auto-increment only happens if the user selects it. There is also a button to deselect auto-increment. 063 * Default is no auto-increment. Mutually exclusive with {@link #use_last_as_default}. 064 */ 065 public String auto_increment; // NOSONAR 066 /** The length of the text box (number of characters allowed). */ 067 public short length; // NOSONAR 068 /** A comma separated list of alternative keys to use for autocompletion. */ 069 public String alternative_autocomplete_keys; // NOSONAR 070 071 private JComponent value; 072 private transient TemplateEntry valueTemplate; 073 074 @Override 075 public boolean addToPanel(JPanel p, TaggingPresetItemGuiSupport support) { 076 077 AutoCompComboBoxModel<AutoCompletionItem> model = new AutoCompComboBoxModel<>(); 078 List<String> keys = new ArrayList<>(); 079 keys.add(key); 080 if (alternative_autocomplete_keys != null) { 081 for (String k : alternative_autocomplete_keys.split(",", -1)) { 082 keys.add(k); 083 } 084 } 085 getAllForKeys(keys).forEach(model::addElement); 086 087 AutoCompTextField<AutoCompletionItem> textField; 088 AutoCompComboBoxEditor<AutoCompletionItem> editor = null; 089 090 // find out if our key is already used in the selection. 091 Usage usage = determineTextUsage(support.getSelected(), key); 092 093 if (usage.unused() || usage.hasUniqueValue()) { 094 textField = new AutoCompTextField<>(); 095 } else { 096 editor = new AutoCompComboBoxEditor<>(); 097 textField = editor.getEditorComponent(); 098 } 099 textField.setModel(model); 100 value = textField; 101 102 if (length > 0) { 103 textField.setMaxTextLength(length); 104 } 105 if (TaggingPresetItem.DISPLAY_KEYS_AS_HINT.get()) { 106 textField.setHint(key); 107 } 108 if (usage.unused()) { 109 if (auto_increment_selected != 0 && auto_increment != null) { 110 try { 111 textField.setText(Integer.toString(Integer.parseInt( 112 LAST_VALUES.get(key)) + auto_increment_selected)); 113 } catch (NumberFormatException ex) { 114 // Ignore - cannot auto-increment if last was non-numeric 115 Logging.trace(ex); 116 } 117 } else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 118 // selected osm primitives are untagged or filling default values feature is enabled 119 if (!support.isPresetInitiallyMatches() && !"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key)) { 120 textField.setText(LAST_VALUES.get(key)); 121 } else { 122 textField.setText(default_); 123 } 124 } else { 125 // selected osm primitives are tagged and filling default values feature is disabled 126 textField.setText(""); 127 } 128 value = textField; 129 originalValue = null; 130 } else if (usage.hasUniqueValue()) { 131 // all objects use the same value 132 textField.setText(usage.getFirst()); 133 value = textField; 134 originalValue = usage.getFirst(); 135 } 136 if (editor != null) { 137 // The selected primitives have different values for this key. <b>Note:</b> this 138 // cannot be an AutoCompComboBox because the values in the dropdown are different from 139 // those we autocomplete on. 140 JosmComboBox<String> comboBox = new JosmComboBox<>(); 141 comboBox.getModel().addAllElements(usage.map.keySet()); 142 comboBox.setEditable(true); 143 comboBox.setEditor(editor); 144 comboBox.getEditor().setItem(DIFFERENT_I18N); 145 value = comboBox; 146 originalValue = DIFFERENT_I18N; 147 } 148 initializeLocaleText(null); 149 150 setupListeners(textField, support); 151 152 // if there's an auto_increment setting, then wrap the text field 153 // into a panel, appending a number of buttons. 154 // auto_increment has a format like -2,-1,1,2 155 // the text box being the first component in the panel is relied 156 // on in a rather ugly fashion further down. 157 if (auto_increment != null) { 158 ButtonGroup bg = new ButtonGroup(); 159 JPanel pnl = new JPanel(new GridBagLayout()); 160 pnl.add(value, GBC.std().fill(GBC.HORIZONTAL)); 161 162 // first, one button for each auto_increment value 163 for (final String ai : auto_increment.split(",", -1)) { 164 JToggleButton aibutton = new JToggleButton(ai); 165 aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai)); 166 aibutton.setMargin(new Insets(0, 0, 0, 0)); 167 aibutton.setFocusable(false); 168 saveHorizontalSpace(aibutton); 169 bg.add(aibutton); 170 try { 171 // TODO there must be a better way to parse a number like "+3" than this. 172 final int buttonvalue = NumberFormat.getIntegerInstance().parse(ai.replace("+", "")).intValue(); 173 if (auto_increment_selected == buttonvalue) aibutton.setSelected(true); 174 aibutton.addActionListener(e -> auto_increment_selected = buttonvalue); 175 pnl.add(aibutton, GBC.std()); 176 } catch (ParseException ex) { 177 Logging.error("Cannot parse auto-increment value of '" + ai + "' into an integer"); 178 } 179 } 180 181 // an invisible toggle button for "release" of the button group 182 final JToggleButton clearbutton = new JToggleButton("X"); 183 clearbutton.setVisible(false); 184 clearbutton.setFocusable(false); 185 bg.add(clearbutton); 186 // and its visible counterpart. - this mechanism allows us to 187 // have *no* button selected after the X is clicked, instead 188 // of the X remaining selected 189 JButton releasebutton = new JButton("X"); 190 releasebutton.setToolTipText(tr("Cancel auto-increment for this field")); 191 releasebutton.setMargin(new Insets(0, 0, 0, 0)); 192 releasebutton.setFocusable(false); 193 releasebutton.addActionListener(e -> { 194 auto_increment_selected = 0; 195 clearbutton.setSelected(true); 196 }); 197 saveHorizontalSpace(releasebutton); 198 pnl.add(releasebutton, GBC.eol()); 199 value = pnl; 200 } 201 final JLabel label = new JLabel(tr("{0}:", locale_text)); 202 addIcon(label); 203 label.setToolTipText(getKeyTooltipText()); 204 label.setComponentPopupMenu(getPopupMenu()); 205 label.setLabelFor(value); 206 p.add(label, GBC.std().insets(0, 0, 10, 0)); 207 p.add(value, GBC.eol().fill(GBC.HORIZONTAL)); 208 label.applyComponentOrientation(support.getDefaultComponentOrientation()); 209 value.setToolTipText(getKeyTooltipText()); 210 value.applyComponentOrientation(OrientationAction.getNamelikeOrientation(key)); 211 return true; 212 } 213 214 private static void saveHorizontalSpace(AbstractButton button) { 215 Insets insets = button.getBorder().getBorderInsets(button); 216 // Ensure the current look&feel does not waste horizontal space (as seen in Nimbus & Aqua) 217 if (insets != null && insets.left+insets.right > insets.top+insets.bottom) { 218 int min = Math.min(insets.top, insets.bottom); 219 button.setBorder(BorderFactory.createEmptyBorder(insets.top, min, insets.bottom, min)); 220 } 221 } 222 223 private static String getValue(Component comp) { 224 if (comp instanceof JosmComboBox) { 225 return ((JosmComboBox<?>) comp).getEditorItemAsString(); 226 } else if (comp instanceof JosmTextField) { 227 return ((JosmTextField) comp).getText(); 228 } else if (comp instanceof JPanel) { 229 return getValue(((JPanel) comp).getComponent(0)); 230 } else { 231 return null; 232 } 233 } 234 235 @Override 236 public void addCommands(List<Tag> changedTags) { 237 238 // return if unchanged 239 String v = getValue(value); 240 if (v == null) { 241 Logging.error("No 'last value' support for component " + value); 242 return; 243 } 244 245 v = Utils.removeWhiteSpaces(v); 246 247 if (!"false".equals(use_last_as_default) || auto_increment != null) { 248 LAST_VALUES.put(key, v); 249 } 250 if (v.equals(originalValue) || (originalValue == null && v.isEmpty())) 251 return; 252 253 changedTags.add(new Tag(key, v)); 254 AutoCompletionManager.rememberUserInput(key, v, true); 255 } 256 257 @Override 258 public MatchType getDefaultMatch() { 259 return MatchType.NONE; 260 } 261 262 @Override 263 public Collection<String> getValues() { 264 if (Utils.isEmpty(default_)) 265 return Collections.emptyList(); 266 return Collections.singleton(default_); 267 } 268 269 /** 270 * Set the value template. 271 * @param pattern The value_template pattern. 272 * @throws SAXException If an error occured while parsing. 273 */ 274 public void setValue_template(String pattern) throws SAXException { // NOPMD 275 try { 276 this.valueTemplate = new TemplateParser(pattern).parse(); 277 } catch (ParseError e) { 278 Logging.error("Error while parsing " + pattern + ": " + e.getMessage()); 279 throw new SAXException(e); 280 } 281 } 282 283 private void setupListeners(AutoCompTextField<AutoCompletionItem> textField, TaggingPresetItemGuiSupport support) { 284 // value_templates don't work well with multiple selected items because, 285 // as the command queue is currently implemented, we can only save 286 // the same value to all selected primitives, which is probably not 287 // what you want. 288 if (valueTemplate == null || support.getSelected().size() > 1) { // only fire on normal fields 289 textField.getDocument().addDocumentListener(DocumentAdapter.create(ignore -> 290 support.fireItemValueModified(this, key, textField.getText()))); 291 } else { // only listen on calculated fields 292 support.addListener((source, key, newValue) -> { 293 String valueTemplateText = valueTemplate.getText(support); 294 Logging.trace("Evaluating value_template {0} for key {1} from {2} with new value {3} => {4}", 295 valueTemplate, key, source, newValue, valueTemplateText); 296 textField.setText(valueTemplateText); 297 if (originalValue != null && !originalValue.equals(valueTemplateText)) { 298 textField.setForeground(Color.RED); 299 } else { 300 textField.setForeground(Color.BLUE); 301 } 302 }); 303 } 304 } 305}