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