| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.gui.tagging.presets.items;
|
|---|
| 3 |
|
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 5 |
|
|---|
| 6 | import java.awt.Component;
|
|---|
| 7 | import java.awt.GridBagLayout;
|
|---|
| 8 | import java.awt.Insets;
|
|---|
| 9 | import java.text.NumberFormat;
|
|---|
| 10 | import java.text.ParseException;
|
|---|
| 11 | import java.util.Collection;
|
|---|
| 12 | import java.util.Collections;
|
|---|
| 13 | import java.util.List;
|
|---|
| 14 |
|
|---|
| 15 | import javax.swing.AbstractButton;
|
|---|
| 16 | import javax.swing.BorderFactory;
|
|---|
| 17 | import javax.swing.ButtonGroup;
|
|---|
| 18 | import javax.swing.JButton;
|
|---|
| 19 | import javax.swing.JComponent;
|
|---|
| 20 | import javax.swing.JLabel;
|
|---|
| 21 | import javax.swing.JPanel;
|
|---|
| 22 | import javax.swing.JToggleButton;
|
|---|
| 23 |
|
|---|
| 24 | import org.openstreetmap.josm.Main;
|
|---|
| 25 | import org.openstreetmap.josm.data.osm.OsmPrimitive;
|
|---|
| 26 | import org.openstreetmap.josm.data.osm.Tag;
|
|---|
| 27 | import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
|
|---|
| 28 | import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
|
|---|
| 29 | import org.openstreetmap.josm.gui.widgets.JosmComboBox;
|
|---|
| 30 | import org.openstreetmap.josm.gui.widgets.JosmTextField;
|
|---|
| 31 | import org.openstreetmap.josm.tools.GBC;
|
|---|
| 32 | import org.openstreetmap.josm.tools.Logging;
|
|---|
| 33 |
|
|---|
| 34 | /**
|
|---|
| 35 | * Text field type.
|
|---|
| 36 | */
|
|---|
| 37 | public class Text extends KeyedItem {
|
|---|
| 38 |
|
|---|
| 39 | private static int auto_increment_selected; // NOSONAR
|
|---|
| 40 |
|
|---|
| 41 | /** The localized version of {@link #text}. */
|
|---|
| 42 | public String locale_text; // NOSONAR
|
|---|
| 43 | /** The default value for the item. If not specified, the current value of the key is chosen as default (if applicable). Defaults to "". */
|
|---|
| 44 | public String default_; // NOSONAR
|
|---|
| 45 | /** The original value */
|
|---|
| 46 | public String originalValue; // NOSONAR
|
|---|
| 47 | /** whether the last value is used as default. Using "force" enforces this behaviour also for already tagged objects. Default is "false".*/
|
|---|
| 48 | public String use_last_as_default = "false"; // NOSONAR
|
|---|
| 49 | /**
|
|---|
| 50 | * May contain a comma separated list of integer increments or decrements, e.g. "-2,-1,+1,+2".
|
|---|
| 51 | * A button will be shown next to the text field for each value, allowing the user to select auto-increment with the given stepping.
|
|---|
| 52 | * Auto-increment only happens if the user selects it. There is also a button to deselect auto-increment.
|
|---|
| 53 | * Default is no auto-increment. Mutually exclusive with {@link #use_last_as_default}.
|
|---|
| 54 | */
|
|---|
| 55 | public String auto_increment; // NOSONAR
|
|---|
| 56 | /** The length of the text box (number of characters allowed). */
|
|---|
| 57 | public String length; // NOSONAR
|
|---|
| 58 | /** A comma separated list of alternative keys to use for autocompletion. */
|
|---|
| 59 | public String alternative_autocomplete_keys; // NOSONAR
|
|---|
| 60 |
|
|---|
| 61 | private JComponent value;
|
|---|
| 62 |
|
|---|
| 63 | @Override
|
|---|
| 64 | public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) {
|
|---|
| 65 |
|
|---|
| 66 | // find out if our key is already used in the selection.
|
|---|
| 67 | Usage usage = determineTextUsage(sel, key);
|
|---|
| 68 | AutoCompletingTextField textField = new AutoCompletingTextField();
|
|---|
| 69 | if (alternative_autocomplete_keys != null) {
|
|---|
| 70 | initAutoCompletionField(textField, (key + ',' + alternative_autocomplete_keys).split(","));
|
|---|
| 71 | } else {
|
|---|
| 72 | initAutoCompletionField(textField, key);
|
|---|
| 73 | }
|
|---|
| 74 | if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) {
|
|---|
| 75 | textField.setHint(key);
|
|---|
| 76 | }
|
|---|
| 77 | if (length != null && !length.isEmpty()) {
|
|---|
| 78 | textField.setMaxChars(Integer.valueOf(length));
|
|---|
| 79 | }
|
|---|
| 80 | if (usage.unused()) {
|
|---|
| 81 | if (auto_increment_selected != 0 && auto_increment != null) {
|
|---|
| 82 | try {
|
|---|
| 83 | textField.setText(Integer.toString(Integer.parseInt(
|
|---|
| 84 | LAST_VALUES.get(key)) + auto_increment_selected));
|
|---|
| 85 | } catch (NumberFormatException ex) {
|
|---|
| 86 | // Ignore - cannot auto-increment if last was non-numeric
|
|---|
| 87 | Logging.trace(ex);
|
|---|
| 88 | }
|
|---|
| 89 | } else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) {
|
|---|
| 90 | // selected osm primitives are untagged or filling default values feature is enabled
|
|---|
| 91 | if (!presetInitiallyMatches && !"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key)) {
|
|---|
| 92 | textField.setText(LAST_VALUES.get(key));
|
|---|
| 93 | } else {
|
|---|
| 94 | textField.setText(default_);
|
|---|
| 95 | }
|
|---|
| 96 | } else {
|
|---|
| 97 | // selected osm primitives are tagged and filling default values feature is disabled
|
|---|
| 98 | textField.setText("");
|
|---|
| 99 | }
|
|---|
| 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[usage.values.size()]));
|
|---|
| 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 | }
|
|---|