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