001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trc; 006 007import java.awt.Component; 008import java.awt.Dimension; 009import java.awt.Font; 010import java.awt.GridBagLayout; 011import java.awt.GridLayout; 012import java.awt.event.ActionEvent; 013import java.awt.event.ActionListener; 014import java.awt.event.MouseAdapter; 015import java.awt.event.MouseEvent; 016import java.io.File; 017import java.lang.reflect.Method; 018import java.lang.reflect.Modifier; 019import java.text.NumberFormat; 020import java.text.ParseException; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.EnumSet; 026import java.util.HashMap; 027import java.util.LinkedHashMap; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.TreeSet; 032 033import javax.swing.ButtonGroup; 034import javax.swing.Icon; 035import javax.swing.ImageIcon; 036import javax.swing.JButton; 037import javax.swing.JComponent; 038import javax.swing.JLabel; 039import javax.swing.JList; 040import javax.swing.JPanel; 041import javax.swing.JScrollPane; 042import javax.swing.JSeparator; 043import javax.swing.JToggleButton; 044import javax.swing.ListCellRenderer; 045import javax.swing.ListModel; 046 047import org.openstreetmap.josm.Main; 048import org.openstreetmap.josm.actions.search.SearchCompiler; 049import org.openstreetmap.josm.data.osm.OsmPrimitive; 050import org.openstreetmap.josm.data.osm.OsmUtils; 051import org.openstreetmap.josm.data.osm.Tag; 052import org.openstreetmap.josm.data.preferences.BooleanProperty; 053import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField; 054import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionItemPriority; 055import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList; 056import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager; 057import org.openstreetmap.josm.gui.widgets.JosmComboBox; 058import org.openstreetmap.josm.gui.widgets.JosmTextField; 059import org.openstreetmap.josm.gui.widgets.QuadStateCheckBox; 060import org.openstreetmap.josm.gui.widgets.UrlLabel; 061import org.openstreetmap.josm.tools.GBC; 062import org.openstreetmap.josm.tools.ImageProvider; 063import org.openstreetmap.josm.tools.Predicate; 064import org.openstreetmap.josm.tools.Utils; 065import org.xml.sax.SAXException; 066 067/** 068 * Class that contains all subtypes of TaggingPresetItem, static supplementary data, types and methods 069 * @since 6068 070 */ 071public final class TaggingPresetItems { 072 private TaggingPresetItems() { 073 } 074 075 private static int auto_increment_selected = 0; 076 /** Translatation of "<different>". Use in combo boxes to display en entry matching several different values. */ 077 public static final String DIFFERENT = tr("<different>"); 078 079 private static final BooleanProperty PROP_FILL_DEFAULT = new BooleanProperty("taggingpreset.fill-default-for-tagged-primitives", false); 080 081 // cache the parsing of types using a LRU cache (http://java-planet.blogspot.com/2005/08/how-to-set-up-simple-lru-cache-using.html) 082 private static final Map<String,EnumSet<TaggingPresetType>> TYPE_CACHE = new LinkedHashMap<>(16, 1.1f, true); 083 084 /** 085 * Last value of each key used in presets, used for prefilling corresponding fields 086 */ 087 private static final Map<String,String> LAST_VALUES = new HashMap<>(); 088 089 public static class PresetListEntry { 090 public String value; 091 /** The context used for translating {@link #value} */ 092 public String value_context; 093 public String display_value; 094 public String short_description; 095 /** The location of icon file to display */ 096 public String icon; 097 /** The size of displayed icon. If not set, default is size from icon file */ 098 public String icon_size; 099 /** The localized version of {@link #display_value}. */ 100 public String locale_display_value; 101 /** The localized version of {@link #short_description}. */ 102 public String locale_short_description; 103 private final File zipIcons = TaggingPresetReader.getZipIcons(); 104 105 // Cached size (currently only for Combo) to speed up preset dialog initialization 106 private int prefferedWidth = -1; 107 private int prefferedHeight = -1; 108 109 public String getListDisplay() { 110 if (value.equals(DIFFERENT)) 111 return "<b>"+DIFFERENT.replaceAll("<", "<").replaceAll(">", ">")+"</b>"; 112 113 if (value.isEmpty()) 114 return " "; 115 116 final StringBuilder res = new StringBuilder("<b>"); 117 res.append(getDisplayValue(true).replaceAll("<", "<").replaceAll(">", ">")); 118 res.append("</b>"); 119 if (getShortDescription(true) != null) { 120 // wrap in table to restrict the text width 121 res.append("<div style=\"width:300px; padding:0 0 5px 5px\">"); 122 res.append(getShortDescription(true)); 123 res.append("</div>"); 124 } 125 return res.toString(); 126 } 127 128 /** 129 * Returns the entry icon, if any. 130 * @return the entry icon, or {@code null} 131 */ 132 public ImageIcon getIcon() { 133 return icon == null ? null : loadImageIcon(icon, zipIcons, parseInteger(icon_size)); 134 } 135 136 /** 137 * Construxts a new {@code PresetListEntry}, uninitialized. 138 */ 139 public PresetListEntry() { 140 } 141 142 public PresetListEntry(String value) { 143 this.value = value; 144 } 145 146 public String getDisplayValue(boolean translated) { 147 return translated 148 ? Utils.firstNonNull(locale_display_value, tr(display_value), trc(value_context, value)) 149 : Utils.firstNonNull(display_value, value); 150 } 151 152 public String getShortDescription(boolean translated) { 153 return translated 154 ? Utils.firstNonNull(locale_short_description, tr(short_description)) 155 : short_description; 156 } 157 158 // toString is mainly used to initialize the Editor 159 @Override 160 public String toString() { 161 if (value.equals(DIFFERENT)) 162 return DIFFERENT; 163 return getDisplayValue(true).replaceAll("<.*>", ""); // remove additional markup, e.g. <br> 164 } 165 } 166 167 public static class Role { 168 public EnumSet<TaggingPresetType> types; 169 public String key; 170 /** The text to display */ 171 public String text; 172 /** The context used for translating {@link #text} */ 173 public String text_context; 174 /** The localized version of {@link #text}. */ 175 public String locale_text; 176 public SearchCompiler.Match memberExpression; 177 178 public boolean required = false; 179 private long count = 0; 180 181 public void setType(String types) throws SAXException { 182 this.types = getType(types); 183 } 184 185 public void setRequisite(String str) throws SAXException { 186 if("required".equals(str)) { 187 required = true; 188 } else if(!"optional".equals(str)) 189 throw new SAXException(tr("Unknown requisite: {0}", str)); 190 } 191 192 public void setMember_expression(String member_expression) throws SAXException { 193 try { 194 this.memberExpression = SearchCompiler.compile(member_expression, true, true); 195 } catch (SearchCompiler.ParseError ex) { 196 throw new SAXException(tr("Illegal member expression: {0}", ex.getMessage()), ex); 197 } 198 } 199 200 public void setCount(String count) { 201 this.count = Long.parseLong(count); 202 } 203 204 /** 205 * Return either argument, the highest possible value or the lowest allowed value 206 */ 207 public long getValidCount(long c) { 208 if (count > 0 && !required) 209 return c != 0 ? count : 0; 210 else if (count > 0) 211 return count; 212 else if (!required) 213 return c != 0 ? c : 0; 214 else 215 return c != 0 ? c : 1; 216 } 217 218 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel) { 219 String cstring; 220 if (count > 0 && !required) { 221 cstring = "0,"+count; 222 } else if(count > 0) { 223 cstring = String.valueOf(count); 224 } else if(!required) { 225 cstring = "0-..."; 226 } else { 227 cstring = "1-..."; 228 } 229 if (locale_text == null) { 230 locale_text = getLocaleText(text, text_context, null); 231 } 232 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0)); 233 p.add(new JLabel(key), GBC.std().insets(0,0,10,0)); 234 p.add(new JLabel(cstring), types == null ? GBC.eol() : GBC.std().insets(0,0,10,0)); 235 if (types != null) { 236 JPanel pp = new JPanel(); 237 for(TaggingPresetType t : types) { 238 pp.add(new JLabel(ImageProvider.get(t.getIconName()))); 239 } 240 p.add(pp, GBC.eol()); 241 } 242 return true; 243 } 244 } 245 246 /** 247 * Enum denoting how a match (see {@link TaggingPresetItem#matches}) is performed. 248 */ 249 public static enum MatchType { 250 251 /** Neutral, i.e., do not consider this item for matching. */ 252 NONE("none"), 253 /** Positive if key matches, neutral otherwise. */ 254 KEY("key"), 255 /** Positive if key matches, negative otherwise. */ 256 KEY_REQUIRED("key!"), 257 /** Positive if key and value matches, neutral otherwise. */ 258 KEY_VALUE("keyvalue"), 259 /** Positive if key and value matches, negative otherwise. */ 260 KEY_VALUE_REQUIRED("keyvalue!"); 261 262 private final String value; 263 264 private MatchType(String value) { 265 this.value = value; 266 } 267 268 /** 269 * Replies the associated textual value. 270 * @return the associated textual value 271 */ 272 public String getValue() { 273 return value; 274 } 275 276 /** 277 * Determines the {@code MatchType} for the given textual value. 278 * @param type the textual value 279 * @return the {@code MatchType} for the given textual value 280 */ 281 public static MatchType ofString(String type) { 282 for (MatchType i : EnumSet.allOf(MatchType.class)) { 283 if (i.getValue().equals(type)) 284 return i; 285 } 286 throw new IllegalArgumentException(type + " is not allowed"); 287 } 288 } 289 290 public static class Usage { 291 TreeSet<String> values; 292 boolean hadKeys = false; 293 boolean hadEmpty = false; 294 295 public boolean hasUniqueValue() { 296 return values.size() == 1 && !hadEmpty; 297 } 298 299 public boolean unused() { 300 return values.isEmpty(); 301 } 302 303 public String getFirst() { 304 return values.first(); 305 } 306 307 public boolean hadKeys() { 308 return hadKeys; 309 } 310 } 311 312 /** 313 * A tagging preset item displaying a localizable text. 314 * @since 6190 315 */ 316 public abstract static class TaggingPresetTextItem extends TaggingPresetItem { 317 318 /** The text to display */ 319 public String text; 320 321 /** The context used for translating {@link #text} */ 322 public String text_context; 323 324 /** The localized version of {@link #text} */ 325 public String locale_text; 326 327 protected final void initializeLocaleText(String defaultText) { 328 if (locale_text == null) { 329 locale_text = getLocaleText(text, text_context, defaultText); 330 } 331 } 332 333 @Override 334 void addCommands(List<Tag> changedTags) { 335 } 336 337 protected String fieldsToString() { 338 return (text != null ? "text=" + text + ", " : "") 339 + (text_context != null ? "text_context=" + text_context + ", " : "") 340 + (locale_text != null ? "locale_text=" + locale_text : ""); 341 } 342 343 @Override 344 public String toString() { 345 return getClass().getSimpleName() + " [" + fieldsToString() + "]"; 346 } 347 } 348 349 /** 350 * Label type. 351 */ 352 public static class Label extends TaggingPresetTextItem { 353 354 /** The location of icon file to display (optional) */ 355 public String icon; 356 /** The size of displayed icon. If not set, default is 16px */ 357 public String icon_size; 358 359 @Override 360 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 361 initializeLocaleText(null); 362 addLabel(p, getIcon(), locale_text); 363 return true; 364 } 365 366 /** 367 * Adds a new {@code JLabel} to the given panel. 368 * @param p The panel 369 * @param icon the icon (optional, can be null) 370 * @param label The text label 371 */ 372 public static void addLabel(JPanel p, Icon icon, String label) { 373 p.add(new JLabel(label, icon, JLabel.LEADING), GBC.eol().fill(GBC.HORIZONTAL)); 374 } 375 376 /** 377 * Returns the label icon, if any. 378 * @return the label icon, or {@code null} 379 */ 380 public ImageIcon getIcon() { 381 Integer size = parseInteger(icon_size); 382 return icon == null ? null : loadImageIcon(icon, TaggingPresetReader.getZipIcons(), size != null ? size : 16); 383 } 384 } 385 386 /** 387 * Hyperlink type. 388 */ 389 public static class Link extends TaggingPresetTextItem { 390 391 /** The link to display. */ 392 public String href; 393 394 /** The localized version of {@link #href}. */ 395 public String locale_href; 396 397 @Override 398 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 399 initializeLocaleText(tr("More information about this feature")); 400 String url = locale_href; 401 if (url == null) { 402 url = href; 403 } 404 if (url != null) { 405 p.add(new UrlLabel(url, locale_text, 2), GBC.eol().insets(0, 10, 0, 0).fill(GBC.HORIZONTAL)); 406 } 407 return false; 408 } 409 410 @Override 411 protected String fieldsToString() { 412 return super.fieldsToString() 413 + (href != null ? "href=" + href + ", " : "") 414 + (locale_href != null ? "locale_href=" + locale_href + ", " : ""); 415 } 416 } 417 418 public static class PresetLink extends TaggingPresetItem { 419 420 public String preset_name = ""; 421 422 @Override 423 boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 424 final String presetName = preset_name; 425 final TaggingPreset t = Utils.filter(TaggingPresets.getTaggingPresets(), new Predicate<TaggingPreset>() { 426 @Override 427 public boolean evaluate(TaggingPreset object) { 428 return presetName.equals(object.name); 429 } 430 }).iterator().next(); 431 if (t == null) return false; 432 JLabel lbl = new PresetLabel(t); 433 lbl.addMouseListener(new MouseAdapter() { 434 @Override 435 public void mouseClicked(MouseEvent arg0) { 436 t.actionPerformed(null); 437 } 438 }); 439 p.add(lbl, GBC.eol().fill(GBC.HORIZONTAL)); 440 return false; 441 } 442 443 @Override 444 void addCommands(List<Tag> changedTags) { 445 } 446 } 447 448 public static class Roles extends TaggingPresetItem { 449 450 public final List<Role> roles = new LinkedList<>(); 451 452 @Override 453 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 454 p.add(new JLabel(" "), GBC.eol()); // space 455 if (!roles.isEmpty()) { 456 JPanel proles = new JPanel(new GridBagLayout()); 457 proles.add(new JLabel(tr("Available roles")), GBC.std().insets(0, 0, 10, 0)); 458 proles.add(new JLabel(tr("role")), GBC.std().insets(0, 0, 10, 0)); 459 proles.add(new JLabel(tr("count")), GBC.std().insets(0, 0, 10, 0)); 460 proles.add(new JLabel(tr("elements")), GBC.eol()); 461 for (Role i : roles) { 462 i.addToPanel(proles, sel); 463 } 464 p.add(proles, GBC.eol()); 465 } 466 return false; 467 } 468 469 @Override 470 public void addCommands(List<Tag> changedTags) { 471 } 472 } 473 474 public static class Optional extends TaggingPresetTextItem { 475 476 // TODO: Draw a box around optional stuff 477 @Override 478 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 479 initializeLocaleText(tr("Optional Attributes:")); 480 p.add(new JLabel(" "), GBC.eol()); // space 481 p.add(new JLabel(locale_text), GBC.eol()); 482 p.add(new JLabel(" "), GBC.eol()); // space 483 return false; 484 } 485 } 486 487 /** 488 * Horizontal separator type. 489 */ 490 public static class Space extends TaggingPresetItem { 491 492 @Override 493 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 494 p.add(new JLabel(" "), GBC.eol()); // space 495 return false; 496 } 497 498 @Override 499 public void addCommands(List<Tag> changedTags) { 500 } 501 502 @Override 503 public String toString() { 504 return "Space"; 505 } 506 } 507 508 /** 509 * Class used to represent a {@link JSeparator} inside tagging preset window. 510 * @since 6198 511 */ 512 public static class ItemSeparator extends TaggingPresetItem { 513 514 @Override 515 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 516 p.add(new JSeparator(), GBC.eol().fill(GBC.HORIZONTAL).insets(0, 5, 0, 5)); 517 return false; 518 } 519 520 @Override 521 public void addCommands(List<Tag> changedTags) { 522 } 523 524 @Override 525 public String toString() { 526 return "ItemSeparator"; 527 } 528 } 529 530 /** 531 * Preset item associated to an OSM key. 532 */ 533 public abstract static class KeyedItem extends TaggingPresetItem { 534 535 public String key; 536 /** The text to display */ 537 public String text; 538 /** The context used for translating {@link #text} */ 539 public String text_context; 540 public String match = getDefaultMatch().getValue(); 541 542 public abstract MatchType getDefaultMatch(); 543 public abstract Collection<String> getValues(); 544 545 @Override 546 Boolean matches(Map<String, String> tags) { 547 switch (MatchType.ofString(match)) { 548 case NONE: 549 return null; 550 case KEY: 551 return tags.containsKey(key) ? true : null; 552 case KEY_REQUIRED: 553 return tags.containsKey(key); 554 case KEY_VALUE: 555 return tags.containsKey(key) && getValues().contains(tags.get(key)) ? true : null; 556 case KEY_VALUE_REQUIRED: 557 return tags.containsKey(key) && getValues().contains(tags.get(key)); 558 default: 559 throw new IllegalStateException(); 560 } 561 } 562 563 @Override 564 public String toString() { 565 return "KeyedItem [key=" + key + ", text=" + text 566 + ", text_context=" + text_context + ", match=" + match 567 + "]"; 568 } 569 } 570 571 /** 572 * Invisible type allowing to hardcode an OSM key/value from the preset definition. 573 */ 574 public static class Key extends KeyedItem { 575 576 /** The hardcoded value for key */ 577 public String value; 578 579 @Override 580 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 581 return false; 582 } 583 584 @Override 585 public void addCommands(List<Tag> changedTags) { 586 changedTags.add(new Tag(key, value)); 587 } 588 589 @Override 590 public MatchType getDefaultMatch() { 591 return MatchType.KEY_VALUE_REQUIRED; 592 } 593 594 @Override 595 public Collection<String> getValues() { 596 return Collections.singleton(value); 597 } 598 599 @Override 600 public String toString() { 601 return "Key [key=" + key + ", value=" + value + ", text=" + text 602 + ", text_context=" + text_context + ", match=" + match 603 + "]"; 604 } 605 } 606 607 /** 608 * Text field type. 609 */ 610 public static class Text extends KeyedItem { 611 612 /** The localized version of {@link #text}. */ 613 public String locale_text; 614 public String default_; 615 public String originalValue; 616 public String use_last_as_default = "false"; 617 public String auto_increment; 618 public String length; 619 public String alternative_autocomplete_keys; 620 621 private JComponent value; 622 623 @Override 624 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 625 626 // find out if our key is already used in the selection. 627 Usage usage = determineTextUsage(sel, key); 628 AutoCompletingTextField textField = new AutoCompletingTextField(); 629 if (alternative_autocomplete_keys != null) { 630 initAutoCompletionField(textField, (key + "," + alternative_autocomplete_keys).split(",")); 631 } else { 632 initAutoCompletionField(textField, key); 633 } 634 if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) { 635 textField.setHint(key); 636 } 637 if (length != null && !length.isEmpty()) { 638 textField.setMaxChars(Integer.valueOf(length)); 639 } 640 if (usage.unused()){ 641 if (auto_increment_selected != 0 && auto_increment != null) { 642 try { 643 textField.setText(Integer.toString(Integer.parseInt(LAST_VALUES.get(key)) + auto_increment_selected)); 644 } catch (NumberFormatException ex) { 645 // Ignore - cannot auto-increment if last was non-numeric 646 } 647 } 648 else if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 649 // selected osm primitives are untagged or filling default values feature is enabled 650 if (!"false".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) { 651 textField.setText(LAST_VALUES.get(key)); 652 } else { 653 textField.setText(default_); 654 } 655 } else { 656 // selected osm primitives are tagged and filling default values feature is disabled 657 textField.setText(""); 658 } 659 value = textField; 660 originalValue = null; 661 } else if (usage.hasUniqueValue()) { 662 // all objects use the same value 663 textField.setText(usage.getFirst()); 664 value = textField; 665 originalValue = usage.getFirst(); 666 } else { 667 // the objects have different values 668 JosmComboBox<String> comboBox = new JosmComboBox<>(usage.values.toArray(new String[0])); 669 comboBox.setEditable(true); 670 comboBox.setEditor(textField); 671 comboBox.getEditor().setItem(DIFFERENT); 672 value=comboBox; 673 originalValue = DIFFERENT; 674 } 675 if (locale_text == null) { 676 locale_text = getLocaleText(text, text_context, null); 677 } 678 679 // if there's an auto_increment setting, then wrap the text field 680 // into a panel, appending a number of buttons. 681 // auto_increment has a format like -2,-1,1,2 682 // the text box being the first component in the panel is relied 683 // on in a rather ugly fashion further down. 684 if (auto_increment != null) { 685 ButtonGroup bg = new ButtonGroup(); 686 JPanel pnl = new JPanel(new GridBagLayout()); 687 pnl.add(value, GBC.std().fill(GBC.HORIZONTAL)); 688 689 // first, one button for each auto_increment value 690 for (final String ai : auto_increment.split(",")) { 691 JToggleButton aibutton = new JToggleButton(ai); 692 aibutton.setToolTipText(tr("Select auto-increment of {0} for this field", ai)); 693 aibutton.setMargin(new java.awt.Insets(0,0,0,0)); 694 aibutton.setFocusable(false); 695 bg.add(aibutton); 696 try { 697 // TODO there must be a better way to parse a number like "+3" than this. 698 final int buttonvalue = (NumberFormat.getIntegerInstance().parse(ai.replace("+", ""))).intValue(); 699 if (auto_increment_selected == buttonvalue) aibutton.setSelected(true); 700 aibutton.addActionListener(new ActionListener() { 701 @Override 702 public void actionPerformed(ActionEvent e) { 703 auto_increment_selected = buttonvalue; 704 } 705 }); 706 pnl.add(aibutton, GBC.std()); 707 } catch (ParseException x) { 708 Main.error("Cannot parse auto-increment value of '" + ai + "' into an integer"); 709 } 710 } 711 712 // an invisible toggle button for "release" of the button group 713 final JToggleButton clearbutton = new JToggleButton("X"); 714 clearbutton.setVisible(false); 715 clearbutton.setFocusable(false); 716 bg.add(clearbutton); 717 // and its visible counterpart. - this mechanism allows us to 718 // have *no* button selected after the X is clicked, instead 719 // of the X remaining selected 720 JButton releasebutton = new JButton("X"); 721 releasebutton.setToolTipText(tr("Cancel auto-increment for this field")); 722 releasebutton.setMargin(new java.awt.Insets(0,0,0,0)); 723 releasebutton.setFocusable(false); 724 releasebutton.addActionListener(new ActionListener() { 725 @Override 726 public void actionPerformed(ActionEvent e) { 727 auto_increment_selected = 0; 728 clearbutton.setSelected(true); 729 } 730 }); 731 pnl.add(releasebutton, GBC.eol()); 732 value = pnl; 733 } 734 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0)); 735 p.add(value, GBC.eol().fill(GBC.HORIZONTAL)); 736 return true; 737 } 738 739 private static String getValue(Component comp) { 740 if (comp instanceof JosmComboBox) { 741 return ((JosmComboBox<?>) comp).getEditor().getItem().toString(); 742 } else if (comp instanceof JosmTextField) { 743 return ((JosmTextField) comp).getText(); 744 } else if (comp instanceof JPanel) { 745 return getValue(((JPanel)comp).getComponent(0)); 746 } else { 747 return null; 748 } 749 } 750 751 @Override 752 public void addCommands(List<Tag> changedTags) { 753 754 // return if unchanged 755 String v = getValue(value); 756 if (v == null) { 757 Main.error("No 'last value' support for component " + value); 758 return; 759 } 760 761 v = Tag.removeWhiteSpaces(v); 762 763 if (!"false".equals(use_last_as_default) || auto_increment != null) { 764 LAST_VALUES.put(key, v); 765 } 766 if (v.equals(originalValue) || (originalValue == null && v.length() == 0)) 767 return; 768 769 changedTags.add(new Tag(key, v)); 770 AutoCompletionManager.rememberUserInput(key, v, true); 771 } 772 773 @Override 774 boolean requestFocusInWindow() { 775 return value.requestFocusInWindow(); 776 } 777 778 @Override 779 public MatchType getDefaultMatch() { 780 return MatchType.NONE; 781 } 782 783 @Override 784 public Collection<String> getValues() { 785 if (default_ == null || default_.isEmpty()) 786 return Collections.emptyList(); 787 return Collections.singleton(default_); 788 } 789 } 790 791 /** 792 * A group of {@link Check}s. 793 * @since 6114 794 */ 795 public static class CheckGroup extends TaggingPresetItem { 796 797 /** 798 * Number of columns (positive integer) 799 */ 800 public String columns; 801 802 /** 803 * List of checkboxes 804 */ 805 public final List<Check> checks = new LinkedList<>(); 806 807 @Override 808 boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 809 Integer cols = Integer.valueOf(columns); 810 int rows = (int) Math.ceil(checks.size()/cols.doubleValue()); 811 JPanel panel = new JPanel(new GridLayout(rows, cols)); 812 813 for (Check check : checks) { 814 check.addToPanel(panel, sel, presetInitiallyMatches); 815 } 816 817 p.add(panel, GBC.eol()); 818 return false; 819 } 820 821 @Override 822 void addCommands(List<Tag> changedTags) { 823 for (Check check : checks) { 824 check.addCommands(changedTags); 825 } 826 } 827 828 @Override 829 public String toString() { 830 return "CheckGroup [columns=" + columns + "]"; 831 } 832 } 833 834 /** 835 * Checkbox type. 836 */ 837 public static class Check extends KeyedItem { 838 839 /** The localized version of {@link #text}. */ 840 public String locale_text; 841 /** the value to set when checked (default is "yes") */ 842 public String value_on = OsmUtils.trueval; 843 /** the value to set when unchecked (default is "no") */ 844 public String value_off = OsmUtils.falseval; 845 /** whether the off value is disabled in the dialog, i.e., only unset or yes are provided */ 846 public boolean disable_off = false; 847 /** ticked on/off (default is "off") */ 848 public boolean default_ = false; // only used for tagless objects 849 850 private QuadStateCheckBox check; 851 private QuadStateCheckBox.State initialState; 852 private boolean def; 853 854 @Override 855 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 856 857 // find out if our key is already used in the selection. 858 final Usage usage = determineBooleanUsage(sel, key); 859 final String oneValue = usage.values.isEmpty() ? null : usage.values.last(); 860 def = default_; 861 862 if (locale_text == null) { 863 locale_text = getLocaleText(text, text_context, null); 864 } 865 866 if (usage.values.size() < 2 && (oneValue == null || value_on.equals(oneValue) || value_off.equals(oneValue))) { 867 if (def && !PROP_FILL_DEFAULT.get()) { 868 // default is set and filling default values feature is disabled - check if all primitives are untagged 869 for (OsmPrimitive s : sel) 870 if (s.hasKeys()) { 871 def = false; 872 } 873 } 874 875 // all selected objects share the same value which is either true or false or unset, 876 // we can display a standard check box. 877 initialState = value_on.equals(oneValue) 878 ? QuadStateCheckBox.State.SELECTED 879 : value_off.equals(oneValue) 880 ? QuadStateCheckBox.State.NOT_SELECTED 881 : def 882 ? QuadStateCheckBox.State.SELECTED 883 : QuadStateCheckBox.State.UNSET; 884 } else { 885 def = false; 886 // the objects have different values, or one or more objects have something 887 // else than true/false. we display a quad-state check box 888 // in "partial" state. 889 initialState = QuadStateCheckBox.State.PARTIAL; 890 } 891 892 final List<QuadStateCheckBox.State> allowedStates = new ArrayList<>(4); 893 if (QuadStateCheckBox.State.PARTIAL.equals(initialState)) 894 allowedStates.add(QuadStateCheckBox.State.PARTIAL); 895 allowedStates.add(QuadStateCheckBox.State.SELECTED); 896 if (!disable_off || value_off.equals(oneValue)) 897 allowedStates.add(QuadStateCheckBox.State.NOT_SELECTED); 898 allowedStates.add(QuadStateCheckBox.State.UNSET); 899 check = new QuadStateCheckBox(locale_text, initialState, 900 allowedStates.toArray(new QuadStateCheckBox.State[allowedStates.size()])); 901 902 p.add(check, GBC.eol().fill(GBC.HORIZONTAL)); 903 return true; 904 } 905 906 @Override 907 public void addCommands(List<Tag> changedTags) { 908 // if the user hasn't changed anything, don't create a command. 909 if (check.getState() == initialState && !def) return; 910 911 // otherwise change things according to the selected value. 912 changedTags.add(new Tag(key, 913 check.getState() == QuadStateCheckBox.State.SELECTED ? value_on : 914 check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? value_off : 915 null)); 916 } 917 918 @Override 919 boolean requestFocusInWindow() {return check.requestFocusInWindow();} 920 921 @Override 922 public MatchType getDefaultMatch() { 923 return MatchType.NONE; 924 } 925 926 @Override 927 public Collection<String> getValues() { 928 return disable_off ? Arrays.asList(value_on) : Arrays.asList(value_on, value_off); 929 } 930 931 @Override 932 public String toString() { 933 return "Check [" 934 + (locale_text != null ? "locale_text=" + locale_text + ", " : "") 935 + (value_on != null ? "value_on=" + value_on + ", " : "") 936 + (value_off != null ? "value_off=" + value_off + ", " : "") 937 + "default_=" + default_ + ", " 938 + (check != null ? "check=" + check + ", " : "") 939 + (initialState != null ? "initialState=" + initialState 940 + ", " : "") + "def=" + def + "]"; 941 } 942 } 943 944 /** 945 * Abstract superclass for combo box and multi-select list types. 946 */ 947 public abstract static class ComboMultiSelect extends KeyedItem { 948 949 /** The localized version of {@link #text}. */ 950 public String locale_text; 951 public String values; 952 public String values_from; 953 /** The context used for translating {@link #values} */ 954 public String values_context; 955 public String display_values; 956 /** The localized version of {@link #display_values}. */ 957 public String locale_display_values; 958 public String short_descriptions; 959 /** The localized version of {@link #short_descriptions}. */ 960 public String locale_short_descriptions; 961 public String default_; 962 public String delimiter = ";"; 963 public String use_last_as_default = "false"; 964 /** whether to use values for search via {@link TaggingPresetSelector} */ 965 public String values_searchable = "false"; 966 967 protected JComponent component; 968 protected final Map<String, PresetListEntry> lhm = new LinkedHashMap<>(); 969 private boolean initialized = false; 970 protected Usage usage; 971 protected Object originalValue; 972 973 protected abstract Object getSelectedItem(); 974 protected abstract void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches); 975 976 protected char getDelChar() { 977 return delimiter.isEmpty() ? ';' : delimiter.charAt(0); 978 } 979 980 @Override 981 public Collection<String> getValues() { 982 initListEntries(); 983 return lhm.keySet(); 984 } 985 986 public Collection<String> getDisplayValues() { 987 initListEntries(); 988 return Utils.transform(lhm.values(), new Utils.Function<PresetListEntry, String>() { 989 @Override 990 public String apply(PresetListEntry x) { 991 return x.getDisplayValue(true); 992 } 993 }); 994 } 995 996 @Override 997 public boolean addToPanel(JPanel p, Collection<OsmPrimitive> sel, boolean presetInitiallyMatches) { 998 999 initListEntries(); 1000 1001 // find out if our key is already used in the selection. 1002 usage = determineTextUsage(sel, key); 1003 if (!usage.hasUniqueValue() && !usage.unused()) { 1004 lhm.put(DIFFERENT, new PresetListEntry(DIFFERENT)); 1005 } 1006 1007 p.add(new JLabel(tr("{0}:", locale_text)), GBC.std().insets(0, 0, 10, 0)); 1008 addToPanelAnchor(p, default_, presetInitiallyMatches); 1009 1010 return true; 1011 1012 } 1013 1014 private void initListEntries() { 1015 if (initialized) { 1016 lhm.remove(DIFFERENT); // possibly added in #addToPanel 1017 return; 1018 } else if (lhm.isEmpty()) { 1019 initListEntriesFromAttributes(); 1020 } else { 1021 if (values != null) { 1022 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": " 1023 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.", 1024 key, text, "values", "list_entry")); 1025 } 1026 if (display_values != null || locale_display_values != null) { 1027 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": " 1028 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.", 1029 key, text, "display_values", "list_entry")); 1030 } 1031 if (short_descriptions != null || locale_short_descriptions != null) { 1032 Main.warn(tr("Warning in tagging preset \"{0}-{1}\": " 1033 + "Ignoring ''{2}'' attribute as ''{3}'' elements are given.", 1034 key, text, "short_descriptions", "list_entry")); 1035 } 1036 for (PresetListEntry e : lhm.values()) { 1037 if (e.value_context == null) { 1038 e.value_context = values_context; 1039 } 1040 } 1041 } 1042 if (locale_text == null) { 1043 locale_text = getLocaleText(text, text_context, null); 1044 } 1045 initialized = true; 1046 } 1047 1048 private String[] initListEntriesFromAttributes() { 1049 char delChar = getDelChar(); 1050 1051 String[] value_array = null; 1052 1053 if (values_from != null) { 1054 String[] class_method = values_from.split("#"); 1055 if (class_method != null && class_method.length == 2) { 1056 try { 1057 Method method = Class.forName(class_method[0]).getMethod(class_method[1]); 1058 // Check method is public static String[] methodName() 1059 int mod = method.getModifiers(); 1060 if (Modifier.isPublic(mod) && Modifier.isStatic(mod) 1061 && method.getReturnType().equals(String[].class) && method.getParameterTypes().length == 0) { 1062 value_array = (String[]) method.invoke(null); 1063 } else { 1064 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' is not \"{2}\"", key, text, 1065 "public static String[] methodName()")); 1066 } 1067 } catch (Exception e) { 1068 Main.error(tr("Broken tagging preset \"{0}-{1}\" - Java method given in ''values_from'' threw {2} ({3})", key, text, 1069 e.getClass().getName(), e.getMessage())); 1070 } 1071 } 1072 } 1073 1074 if (value_array == null) { 1075 value_array = splitEscaped(delChar, values); 1076 } 1077 1078 final String displ = Utils.firstNonNull(locale_display_values, display_values); 1079 String[] display_array = displ == null ? value_array : splitEscaped(delChar, displ); 1080 1081 final String descr = Utils.firstNonNull(locale_short_descriptions, short_descriptions); 1082 String[] short_descriptions_array = descr == null ? null : splitEscaped(delChar, descr); 1083 1084 if (display_array.length != value_array.length) { 1085 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''display_values'' must be the same as in ''values''", key, text)); 1086 display_array = value_array; 1087 } 1088 1089 if (short_descriptions_array != null && short_descriptions_array.length != value_array.length) { 1090 Main.error(tr("Broken tagging preset \"{0}-{1}\" - number of items in ''short_descriptions'' must be the same as in ''values''", key, text)); 1091 short_descriptions_array = null; 1092 } 1093 1094 for (int i = 0; i < value_array.length; i++) { 1095 final PresetListEntry e = new PresetListEntry(value_array[i]); 1096 e.locale_display_value = locale_display_values != null 1097 ? display_array[i] 1098 : trc(values_context, fixPresetString(display_array[i])); 1099 if (short_descriptions_array != null) { 1100 e.locale_short_description = locale_short_descriptions != null 1101 ? short_descriptions_array[i] 1102 : tr(fixPresetString(short_descriptions_array[i])); 1103 } 1104 lhm.put(value_array[i], e); 1105 display_array[i] = e.getDisplayValue(true); 1106 } 1107 1108 return display_array; 1109 } 1110 1111 protected String getDisplayIfNull() { 1112 return null; 1113 } 1114 1115 @Override 1116 public void addCommands(List<Tag> changedTags) { 1117 Object obj = getSelectedItem(); 1118 String display = (obj == null) ? null : obj.toString(); 1119 String value = null; 1120 if (display == null) { 1121 display = getDisplayIfNull(); 1122 } 1123 1124 if (display != null) { 1125 for (String val : lhm.keySet()) { 1126 String k = lhm.get(val).toString(); 1127 if (k != null && k.equals(display)) { 1128 value = val; 1129 break; 1130 } 1131 } 1132 if (value == null) { 1133 value = display; 1134 } 1135 } else { 1136 value = ""; 1137 } 1138 value = Tag.removeWhiteSpaces(value); 1139 1140 // no change if same as before 1141 if (originalValue == null) { 1142 if (value.length() == 0) 1143 return; 1144 } else if (value.equals(originalValue.toString())) 1145 return; 1146 1147 if (!"false".equals(use_last_as_default)) { 1148 LAST_VALUES.put(key, value); 1149 } 1150 changedTags.add(new Tag(key, value)); 1151 } 1152 1153 public void addListEntry(PresetListEntry e) { 1154 lhm.put(e.value, e); 1155 } 1156 1157 public void addListEntries(Collection<PresetListEntry> e) { 1158 for (PresetListEntry i : e) { 1159 addListEntry(i); 1160 } 1161 } 1162 1163 @Override 1164 boolean requestFocusInWindow() { 1165 return component.requestFocusInWindow(); 1166 } 1167 1168 private static final ListCellRenderer<PresetListEntry> RENDERER = new ListCellRenderer<PresetListEntry>() { 1169 1170 JLabel lbl = new JLabel(); 1171 1172 @Override 1173 public Component getListCellRendererComponent( 1174 JList<? extends PresetListEntry> list, 1175 PresetListEntry item, 1176 int index, 1177 boolean isSelected, 1178 boolean cellHasFocus) { 1179 1180 // Only return cached size, item is not shown 1181 if (!list.isShowing() && item.prefferedWidth != -1 && item.prefferedHeight != -1) { 1182 if (index == -1) { 1183 lbl.setPreferredSize(new Dimension(item.prefferedWidth, 10)); 1184 } else { 1185 lbl.setPreferredSize(new Dimension(item.prefferedWidth, item.prefferedHeight)); 1186 } 1187 return lbl; 1188 } 1189 1190 lbl.setPreferredSize(null); 1191 1192 1193 if (isSelected) { 1194 lbl.setBackground(list.getSelectionBackground()); 1195 lbl.setForeground(list.getSelectionForeground()); 1196 } else { 1197 lbl.setBackground(list.getBackground()); 1198 lbl.setForeground(list.getForeground()); 1199 } 1200 1201 lbl.setOpaque(true); 1202 lbl.setFont(lbl.getFont().deriveFont(Font.PLAIN)); 1203 lbl.setText("<html>" + item.getListDisplay() + "</html>"); 1204 lbl.setIcon(item.getIcon()); 1205 lbl.setEnabled(list.isEnabled()); 1206 1207 // Cache size 1208 item.prefferedWidth = lbl.getPreferredSize().width; 1209 item.prefferedHeight = lbl.getPreferredSize().height; 1210 1211 // We do not want the editor to have the maximum height of all 1212 // entries. Return a dummy with bogus height. 1213 if (index == -1) { 1214 lbl.setPreferredSize(new Dimension(lbl.getPreferredSize().width, 10)); 1215 } 1216 return lbl; 1217 } 1218 }; 1219 1220 protected ListCellRenderer<PresetListEntry> getListCellRenderer() { 1221 return RENDERER; 1222 } 1223 1224 @Override 1225 public MatchType getDefaultMatch() { 1226 return MatchType.NONE; 1227 } 1228 } 1229 1230 /** 1231 * Combobox type. 1232 */ 1233 public static class Combo extends ComboMultiSelect { 1234 1235 public boolean editable = true; 1236 protected JosmComboBox<PresetListEntry> combo; 1237 public String length; 1238 1239 /** 1240 * Constructs a new {@code Combo}. 1241 */ 1242 public Combo() { 1243 delimiter = ","; 1244 } 1245 1246 @Override 1247 protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) { 1248 if (!usage.unused()) { 1249 for (String s : usage.values) { 1250 if (!lhm.containsKey(s)) { 1251 lhm.put(s, new PresetListEntry(s)); 1252 } 1253 } 1254 } 1255 if (def != null && !lhm.containsKey(def)) { 1256 lhm.put(def, new PresetListEntry(def)); 1257 } 1258 lhm.put("", new PresetListEntry("")); 1259 1260 combo = new JosmComboBox<>(lhm.values().toArray(new PresetListEntry[0])); 1261 component = combo; 1262 combo.setRenderer(getListCellRenderer()); 1263 combo.setEditable(editable); 1264 combo.reinitialize(lhm.values()); 1265 AutoCompletingTextField tf = new AutoCompletingTextField(); 1266 initAutoCompletionField(tf, key); 1267 if (Main.pref.getBoolean("taggingpreset.display-keys-as-hint", true)) { 1268 tf.setHint(key); 1269 } 1270 if (length != null && !length.isEmpty()) { 1271 tf.setMaxChars(Integer.valueOf(length)); 1272 } 1273 AutoCompletionList acList = tf.getAutoCompletionList(); 1274 if (acList != null) { 1275 acList.add(getDisplayValues(), AutoCompletionItemPriority.IS_IN_STANDARD); 1276 } 1277 combo.setEditor(tf); 1278 1279 if (usage.hasUniqueValue()) { 1280 // all items have the same value (and there were no unset items) 1281 originalValue = lhm.get(usage.getFirst()); 1282 combo.setSelectedItem(originalValue); 1283 } else if (def != null && usage.unused()) { 1284 // default is set and all items were unset 1285 if (!usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 1286 // selected osm primitives are untagged or filling default feature is enabled 1287 combo.setSelectedItem(lhm.get(def).getDisplayValue(true)); 1288 } else { 1289 // selected osm primitives are tagged and filling default feature is disabled 1290 combo.setSelectedItem(""); 1291 } 1292 originalValue = lhm.get(DIFFERENT); 1293 } else if (usage.unused()) { 1294 // all items were unset (and so is default) 1295 originalValue = lhm.get(""); 1296 if ("force".equals(use_last_as_default) && LAST_VALUES.containsKey(key) && !presetInitiallyMatches) { 1297 combo.setSelectedItem(lhm.get(LAST_VALUES.get(key))); 1298 } else { 1299 combo.setSelectedItem(originalValue); 1300 } 1301 } else { 1302 originalValue = lhm.get(DIFFERENT); 1303 combo.setSelectedItem(originalValue); 1304 } 1305 p.add(combo, GBC.eol().fill(GBC.HORIZONTAL)); 1306 1307 } 1308 1309 @Override 1310 protected Object getSelectedItem() { 1311 return combo.getSelectedItem(); 1312 1313 } 1314 1315 @Override 1316 protected String getDisplayIfNull() { 1317 if (combo.isEditable()) 1318 return combo.getEditor().getItem().toString(); 1319 else 1320 return null; 1321 } 1322 } 1323 1324 /** 1325 * Multi-select list type. 1326 */ 1327 public static class MultiSelect extends ComboMultiSelect { 1328 1329 public long rows = -1; 1330 protected ConcatenatingJList list; 1331 1332 @Override 1333 protected void addToPanelAnchor(JPanel p, String def, boolean presetInitiallyMatches) { 1334 list = new ConcatenatingJList(delimiter, lhm.values().toArray(new PresetListEntry[0])); 1335 component = list; 1336 ListCellRenderer<PresetListEntry> renderer = getListCellRenderer(); 1337 list.setCellRenderer(renderer); 1338 1339 if (usage.hasUniqueValue() && !usage.unused()) { 1340 originalValue = usage.getFirst(); 1341 list.setSelectedItem(originalValue); 1342 } else if (def != null && !usage.hadKeys() || PROP_FILL_DEFAULT.get() || "force".equals(use_last_as_default)) { 1343 originalValue = DIFFERENT; 1344 list.setSelectedItem(def); 1345 } else if (usage.unused()) { 1346 originalValue = null; 1347 list.setSelectedItem(originalValue); 1348 } else { 1349 originalValue = DIFFERENT; 1350 list.setSelectedItem(originalValue); 1351 } 1352 1353 JScrollPane sp = new JScrollPane(list); 1354 // if a number of rows has been specified in the preset, 1355 // modify preferred height of scroll pane to match that row count. 1356 if (rows != -1) { 1357 double height = renderer.getListCellRendererComponent(list, 1358 new PresetListEntry("x"), 0, false, false).getPreferredSize().getHeight() * rows; 1359 sp.setPreferredSize(new Dimension((int) sp.getPreferredSize().getWidth(), (int) height)); 1360 } 1361 p.add(sp, GBC.eol().fill(GBC.HORIZONTAL)); 1362 } 1363 1364 @Override 1365 protected Object getSelectedItem() { 1366 return list.getSelectedItem(); 1367 } 1368 1369 @Override 1370 public void addCommands(List<Tag> changedTags) { 1371 // Do not create any commands if list has been disabled because of an unknown value (fix #8605) 1372 if (list.isEnabled()) { 1373 super.addCommands(changedTags); 1374 } 1375 } 1376 } 1377 1378 /** 1379 * Class that allows list values to be assigned and retrieved as a comma-delimited 1380 * string (extracted from TaggingPreset) 1381 */ 1382 private static class ConcatenatingJList extends JList<PresetListEntry> { 1383 private String delimiter; 1384 public ConcatenatingJList(String del, PresetListEntry[] o) { 1385 super(o); 1386 delimiter = del; 1387 } 1388 1389 public void setSelectedItem(Object o) { 1390 if (o == null) { 1391 clearSelection(); 1392 } else { 1393 String s = o.toString(); 1394 TreeSet<String> parts = new TreeSet<>(Arrays.asList(s.split(delimiter))); 1395 ListModel<PresetListEntry> lm = getModel(); 1396 int[] intParts = new int[lm.getSize()]; 1397 int j = 0; 1398 for (int i = 0; i < lm.getSize(); i++) { 1399 final String value = lm.getElementAt(i).value; 1400 if (parts.contains(value)) { 1401 intParts[j++]=i; 1402 parts.remove(value); 1403 } 1404 } 1405 setSelectedIndices(Arrays.copyOf(intParts, j)); 1406 // check if we have actually managed to represent the full 1407 // value with our presets. if not, cop out; we will not offer 1408 // a selection list that threatens to ruin the value. 1409 setEnabled(parts.isEmpty()); 1410 } 1411 } 1412 1413 public String getSelectedItem() { 1414 ListModel<PresetListEntry> lm = getModel(); 1415 int[] si = getSelectedIndices(); 1416 StringBuilder builder = new StringBuilder(); 1417 for (int i=0; i<si.length; i++) { 1418 if (i>0) { 1419 builder.append(delimiter); 1420 } 1421 builder.append(lm.getElementAt(si[i]).value); 1422 } 1423 return builder.toString(); 1424 } 1425 } 1426 1427 public static EnumSet<TaggingPresetType> getType(String types) throws SAXException { 1428 if (TYPE_CACHE.containsKey(types)) 1429 return TYPE_CACHE.get(types); 1430 EnumSet<TaggingPresetType> result = EnumSet.noneOf(TaggingPresetType.class); 1431 for (String type : Arrays.asList(types.split(","))) { 1432 try { 1433 TaggingPresetType presetType = TaggingPresetType.fromString(type); 1434 result.add(presetType); 1435 } catch (IllegalArgumentException e) { 1436 throw new SAXException(tr("Unknown type: {0}", type), e); 1437 } 1438 } 1439 TYPE_CACHE.put(types, result); 1440 return result; 1441 } 1442 1443 static String fixPresetString(String s) { 1444 return s == null ? s : s.replaceAll("'","''"); 1445 } 1446 1447 private static String getLocaleText(String text, String text_context, String defaultText) { 1448 if (text == null) { 1449 return defaultText; 1450 } else if (text_context != null) { 1451 return trc(text_context, fixPresetString(text)); 1452 } else { 1453 return tr(fixPresetString(text)); 1454 } 1455 } 1456 1457 /** 1458 * allow escaped comma in comma separated list: 1459 * "A\, B\, C,one\, two" --> ["A, B, C", "one, two"] 1460 * @param delimiter the delimiter, e.g. a comma. separates the entries and 1461 * must be escaped within one entry 1462 * @param s the string 1463 */ 1464 private static String[] splitEscaped(char delimiter, String s) { 1465 if (s == null) 1466 return new String[0]; 1467 List<String> result = new ArrayList<>(); 1468 boolean backslash = false; 1469 StringBuilder item = new StringBuilder(); 1470 for (int i=0; i<s.length(); i++) { 1471 char ch = s.charAt(i); 1472 if (backslash) { 1473 item.append(ch); 1474 backslash = false; 1475 } else if (ch == '\\') { 1476 backslash = true; 1477 } else if (ch == delimiter) { 1478 result.add(item.toString()); 1479 item.setLength(0); 1480 } else { 1481 item.append(ch); 1482 } 1483 } 1484 if (item.length() > 0) { 1485 result.add(item.toString()); 1486 } 1487 return result.toArray(new String[result.size()]); 1488 } 1489 1490 static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) { 1491 Usage returnValue = new Usage(); 1492 returnValue.values = new TreeSet<>(); 1493 for (OsmPrimitive s : sel) { 1494 String v = s.get(key); 1495 if (v != null) { 1496 returnValue.values.add(v); 1497 } else { 1498 returnValue.hadEmpty = true; 1499 } 1500 if(s.hasKeys()) { 1501 returnValue.hadKeys = true; 1502 } 1503 } 1504 return returnValue; 1505 } 1506 1507 static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) { 1508 1509 Usage returnValue = new Usage(); 1510 returnValue.values = new TreeSet<>(); 1511 for (OsmPrimitive s : sel) { 1512 String booleanValue = OsmUtils.getNamedOsmBoolean(s.get(key)); 1513 if (booleanValue != null) { 1514 returnValue.values.add(booleanValue); 1515 } 1516 } 1517 return returnValue; 1518 } 1519 1520 protected static ImageIcon loadImageIcon(String iconName, File zipIcons, Integer maxSize) { 1521 final Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null); 1522 ImageProvider imgProv = new ImageProvider(iconName).setDirs(s).setId("presets").setArchive(zipIcons).setOptional(true); 1523 if (maxSize != null) { 1524 imgProv.setMaxSize(maxSize); 1525 } 1526 return imgProv.get(); 1527 } 1528 1529 protected static Integer parseInteger(String str) { 1530 if (str == null || str.isEmpty()) 1531 return null; 1532 try { 1533 return Integer.parseInt(str); 1534 } catch (Exception e) { 1535 if (Main.isTraceEnabled()) { 1536 Main.trace(e.getMessage()); 1537 } 1538 } 1539 return null; 1540 } 1541}