source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPreset.java@ 1743

Last change on this file since 1743 was 1743, checked in by stoecker, 15 years ago

added much improved preferences for external styles and presets

  • Property svn:eol-style set to native
File size: 25.7 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.GridBagLayout;
10import java.awt.Image;
11import java.awt.event.ActionEvent;
12import java.io.BufferedReader;
13import java.io.IOException;
14import java.io.InputStreamReader;
15import java.io.Reader;
16import java.io.UnsupportedEncodingException;
17import java.util.Arrays;
18import java.util.Collection;
19import java.util.HashMap;
20import java.util.HashSet;
21import java.util.LinkedHashMap;
22import java.util.LinkedList;
23import java.util.List;
24import java.util.Set;
25
26import javax.swing.AbstractAction;
27import javax.swing.Action;
28import javax.swing.ImageIcon;
29import javax.swing.JComboBox;
30import javax.swing.JComponent;
31import javax.swing.JLabel;
32import javax.swing.JOptionPane;
33import javax.swing.JPanel;
34import javax.swing.JTextField;
35
36import org.openstreetmap.josm.Main;
37import org.openstreetmap.josm.command.ChangePropertyCommand;
38import org.openstreetmap.josm.command.Command;
39import org.openstreetmap.josm.command.SequenceCommand;
40import org.openstreetmap.josm.data.osm.Node;
41import org.openstreetmap.josm.data.osm.OsmPrimitive;
42import org.openstreetmap.josm.data.osm.OsmUtils;
43import org.openstreetmap.josm.data.osm.Relation;
44import org.openstreetmap.josm.data.osm.Way;
45import org.openstreetmap.josm.gui.ExtendedDialog;
46import org.openstreetmap.josm.gui.QuadStateCheckBox;
47import org.openstreetmap.josm.io.MirroredInputStream;
48import org.openstreetmap.josm.tools.GBC;
49import org.openstreetmap.josm.tools.ImageProvider;
50import org.openstreetmap.josm.tools.UrlLabel;
51import org.openstreetmap.josm.tools.XmlObjectParser;
52import org.xml.sax.SAXException;
53
54/**
55 * This class read encapsulate one tagging preset. A class method can
56 * read in all predefined presets, either shipped with JOSM or that are
57 * in the config directory.
58 *
59 * It is also able to construct dialogs out of preset definitions.
60 */
61public class TaggingPreset extends AbstractAction {
62
63 public TaggingPresetMenu group = null;
64 public String name;
65 public String locale_name;
66
67 public static abstract class Item {
68 public boolean focus = false;
69 abstract void addToPanel(JPanel p, Collection<OsmPrimitive> sel);
70 abstract void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds);
71 boolean requestFocusInWindow() {return false;}
72 }
73
74 public static class Usage {
75 Set<String> values;
76 Boolean hadKeys = false;
77 Boolean hadEmpty = false;
78 public Boolean allSimilar()
79 {
80 return values.size() == 1 && !hadEmpty;
81 }
82 public Boolean unused()
83 {
84 return values.size() == 0;
85 }
86 public String getFirst()
87 {
88 return (String)(values.toArray()[0]);
89 }
90 public Boolean hadKeys()
91 {
92 return hadKeys;
93 }
94 }
95
96 public static final String DIFFERENT = tr("<different>");
97
98 static Usage determineTextUsage(Collection<OsmPrimitive> sel, String key) {
99 Usage returnValue = new Usage();
100 returnValue.values = new HashSet<String>();
101 for (OsmPrimitive s : sel) {
102 String v = s.get(key);
103 if (v != null)
104 returnValue.values.add(v);
105 else
106 returnValue.hadEmpty = true;
107 if(s.keys != null && s.keys.size() > 0)
108 returnValue.hadKeys = true;
109 }
110 return returnValue;
111 }
112
113 static Usage determineBooleanUsage(Collection<OsmPrimitive> sel, String key) {
114
115 Usage returnValue = new Usage();
116 returnValue.values = new HashSet<String>();
117 for (OsmPrimitive s : sel) {
118 returnValue.values.add(OsmUtils.getNamedOsmBoolean(s.get(key)));
119 }
120 return returnValue;
121 }
122
123 public static class Text extends Item {
124
125 public String key;
126 public String text;
127 public String locale_text;
128 public String default_;
129 public String originalValue;
130 public boolean use_last_as_default = false;
131 public boolean delete_if_empty = false;
132
133 private JComponent value;
134
135 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
136
137 // find out if our key is already used in the selection.
138 Usage usage = determineTextUsage(sel, key);
139 if (usage.unused())
140 {
141 value = new JTextField();
142 if (use_last_as_default && lastValue.containsKey(key)) {
143 ((JTextField)value).setText(lastValue.get(key));
144 } else {
145 ((JTextField)value).setText(default_);
146 }
147 originalValue = null;
148 } else if (usage.allSimilar()) {
149 // all objects use the same value
150 value = new JTextField();
151 for (String s : usage.values) ((JTextField) value).setText(s);
152 originalValue = ((JTextField)value).getText();
153 } else {
154 // the objects have different values
155 value = new JComboBox(usage.values.toArray());
156 ((JComboBox)value).setEditable(true);
157 ((JComboBox)value).getEditor().setItem(DIFFERENT);
158 originalValue = DIFFERENT;
159 }
160 if(locale_text == null)
161 locale_text = tr(text);
162 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
163 p.add(value, GBC.eol().fill(GBC.HORIZONTAL));
164 }
165
166 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
167
168 // return if unchanged
169 String v = (value instanceof JComboBox) ?
170 ((JComboBox)value).getEditor().getItem().toString() :
171 ((JTextField)value).getText();
172
173 if (use_last_as_default) lastValue.put(key, v);
174 if (v.equals(originalValue) || (originalValue == null && v.length() == 0)) return;
175
176 if (delete_if_empty && v.length() == 0)
177 v = null;
178 cmds.add(new ChangePropertyCommand(sel, key, v));
179 }
180 @Override boolean requestFocusInWindow() {return value.requestFocusInWindow();}
181 }
182
183 public static class Check extends Item {
184
185 public String key;
186 public String text;
187 public String locale_text;
188 public boolean default_ = false; // only used for tagless objects
189 public boolean use_last_as_default = false;
190
191 private QuadStateCheckBox check;
192 private QuadStateCheckBox.State initialState;
193 private boolean def;
194
195 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
196
197 // find out if our key is already used in the selection.
198 Usage usage = determineBooleanUsage(sel, key);
199 def = default_;
200
201 if(locale_text == null)
202 locale_text = tr(text);
203
204 String oneValue = null;
205 for (String s : usage.values) oneValue = s;
206 if (usage.values.size() < 2 && (oneValue == null || OsmUtils.trueval.equals(oneValue) || OsmUtils.falseval.equals(oneValue))) {
207 if(def)
208 {
209 for (OsmPrimitive s : sel)
210 if(s.keys != null && s.keys.size() > 0) def = false;
211 }
212
213 // all selected objects share the same value which is either true or false or unset,
214 // we can display a standard check box.
215 initialState = OsmUtils.trueval.equals(oneValue) ?
216 QuadStateCheckBox.State.SELECTED :
217 OsmUtils.falseval.equals(oneValue) ?
218 QuadStateCheckBox.State.NOT_SELECTED :
219 def ? QuadStateCheckBox.State.SELECTED
220 : QuadStateCheckBox.State.UNSET;
221 check = new QuadStateCheckBox(locale_text, initialState,
222 new QuadStateCheckBox.State[] {
223 QuadStateCheckBox.State.SELECTED,
224 QuadStateCheckBox.State.NOT_SELECTED,
225 QuadStateCheckBox.State.UNSET });
226 } else {
227 def = false;
228 // the objects have different values, or one or more objects have something
229 // else than true/false. we display a quad-state check box
230 // in "partial" state.
231 initialState = QuadStateCheckBox.State.PARTIAL;
232 check = new QuadStateCheckBox(locale_text, QuadStateCheckBox.State.PARTIAL,
233 new QuadStateCheckBox.State[] {
234 QuadStateCheckBox.State.PARTIAL,
235 QuadStateCheckBox.State.SELECTED,
236 QuadStateCheckBox.State.NOT_SELECTED,
237 QuadStateCheckBox.State.UNSET });
238 }
239 p.add(check, GBC.eol().fill(GBC.HORIZONTAL));
240 }
241
242 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
243 // if the user hasn't changed anything, don't create a command.
244 if (check.getState() == initialState && !def) return;
245
246 // otherwise change things according to the selected value.
247 cmds.add(new ChangePropertyCommand(sel, key,
248 check.getState() == QuadStateCheckBox.State.SELECTED ? OsmUtils.trueval :
249 check.getState() == QuadStateCheckBox.State.NOT_SELECTED ? OsmUtils.falseval :
250 null));
251 }
252 @Override boolean requestFocusInWindow() {return check.requestFocusInWindow();}
253 }
254
255 public static class Combo extends Item {
256
257 public String key;
258 public String text;
259 public String locale_text;
260 public String values;
261 public String display_values;
262 public String locale_display_values;
263 public String default_;
264 public boolean delete_if_empty = false;
265 public boolean editable = true;
266 public boolean use_last_as_default = false;
267
268 private JComboBox combo;
269 private LinkedHashMap<String,String> lhm;
270 private Usage usage;
271 private String originalValue;
272
273 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
274
275 // find out if our key is already used in the selection.
276 usage = determineTextUsage(sel, key);
277
278 String[] value_array = values.split(",");
279 String[] display_array;
280 if(locale_display_values != null)
281 display_array = locale_display_values.split(",");
282 else if(display_values != null)
283 display_array = display_values.split(",");
284 else
285 display_array = value_array;
286
287 lhm = new LinkedHashMap<String,String>();
288 if (!usage.allSimilar() && !usage.unused())
289 {
290 lhm.put(DIFFERENT, DIFFERENT);
291 }
292 for (int i=0; i<value_array.length; i++) {
293 lhm.put(value_array[i],
294 (locale_display_values == null) ?
295 tr(display_array[i]) : display_array[i]);
296 }
297 if(!usage.unused())
298 {
299 for (String s : usage.values) {
300 if (!lhm.containsKey(s)) lhm.put(s, s);
301 }
302 }
303 if (default_ != null && !lhm.containsKey(default_)) lhm.put(default_, default_);
304 if(!lhm.containsKey("")) lhm.put("", "");
305
306 combo = new JComboBox(lhm.values().toArray());
307 combo.setEditable(editable);
308 if (usage.allSimilar() && !usage.unused())
309 {
310 originalValue=usage.getFirst();
311 combo.setSelectedItem(lhm.get(originalValue));
312 }
313 // use default only in case it is a totally new entry
314 else if(default_ != null && !usage.hadKeys())
315 {
316 combo.setSelectedItem(default_);
317 originalValue=DIFFERENT;
318 }
319 else if(usage.unused())
320 {
321 combo.setSelectedItem("");
322 originalValue="";
323 }
324 else
325 {
326 combo.setSelectedItem(DIFFERENT);
327 originalValue=DIFFERENT;
328 }
329
330 if(locale_text == null)
331 locale_text = tr(text);
332 p.add(new JLabel(locale_text+":"), GBC.std().insets(0,0,10,0));
333 p.add(combo, GBC.eol().fill(GBC.HORIZONTAL));
334 }
335 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
336 Object obj = combo.getSelectedItem();
337 String display = (obj == null) ? null : obj.toString();
338 String value = null;
339 if(display == null && combo.isEditable())
340 display = combo.getEditor().getItem().toString();
341
342 if (display != null)
343 {
344 for (String key : lhm.keySet()) {
345 String k = lhm.get(key);
346 if (k != null && k.equals(display)) value=key;
347 }
348 if(value == null)
349 value = display;
350 }
351 else
352 value = "";
353
354 // no change if same as before
355 if (value.equals(originalValue) || (originalValue == null && (value == null || value.length() == 0))) return;
356
357 if (delete_if_empty && value != null && value.length() == 0)
358 value = null;
359 cmds.add(new ChangePropertyCommand(sel, key, value));
360 }
361 @Override boolean requestFocusInWindow() {return combo.requestFocusInWindow();}
362 }
363
364 public static class Label extends Item {
365 public String text;
366 public String locale_text;
367
368 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
369 if(locale_text == null)
370 locale_text = tr(text);
371 p.add(new JLabel(locale_text), GBC.eol());
372 }
373 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
374 }
375
376 public static class Link extends Item {
377 public String href;
378 public String text;
379 public String locale_text;
380 public String locale_href;
381
382 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
383 if(locale_text == null)
384 locale_text = text == null ? tr("More information about this feature") : tr(text);
385 String url = locale_href;
386 if (url == null) {
387 url = href;
388 }
389 if (url != null) {
390 p.add(new UrlLabel(url, locale_text), GBC.eol().anchor(GBC.WEST));
391 }
392 }
393 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
394 }
395
396 public static class Optional extends Item {
397 // TODO: Draw a box around optional stuff
398 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
399 p.add(new JLabel(" "), GBC.eol()); // space
400 p.add(new JLabel(tr("Optional Attributes:")), GBC.eol());
401 p.add(new JLabel(" "), GBC.eol()); // space
402 }
403 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
404 }
405
406 public static class Space extends Item {
407 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) {
408 p.add(new JLabel(" "), GBC.eol()); // space
409 }
410 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {}
411 }
412
413 public static class Key extends Item {
414 public String key;
415 public String value;
416
417 @Override public void addToPanel(JPanel p, Collection<OsmPrimitive> sel) { }
418 @Override public void addCommands(Collection<OsmPrimitive> sel, List<Command> cmds) {
419 cmds.add(new ChangePropertyCommand(sel, key, value != null && !value.equals("") ? value : null));
420 }
421 }
422
423 /**
424 * The types as preparsed collection.
425 */
426 public List<String> types;
427 public List<Item> data = new LinkedList<Item>();
428 private static HashMap<String,String> lastValue = new HashMap<String,String>();
429
430 /**
431 * Create an empty tagging preset. This will not have any items and
432 * will be an empty string as text. createPanel will return null.
433 * Use this as default item for "do not select anything".
434 */
435 public TaggingPreset() {}
436
437 /**
438 * Change the display name without changing the toolbar value.
439 */
440 public void setDisplayName() {
441 putValue(Action.NAME, getName());
442 putValue("toolbar", "tagging_" + getRawName());
443 putValue(SHORT_DESCRIPTION, (group != null ?
444 tr("Use preset ''{0}'' of group ''{1}''", getLocaleName(), group.getName()) :
445 tr("Use preset ''{0}''", getLocaleName())));
446 }
447
448 public String getLocaleName() {
449 if(locale_name == null)
450 locale_name = tr(name);
451 return locale_name;
452 }
453
454 public String getName() {
455 return group != null ? group.getName() + "/" + getLocaleName() : getLocaleName();
456 }
457 public String getRawName() {
458 return group != null ? group.getRawName() + "/" + name : name;
459 }
460 /**
461 * Called from the XML parser to set the icon
462 *
463 * FIXME for Java 1.6 - use 24x24 icons for LARGE_ICON_KEY (button bar)
464 * and the 16x16 icons for SMALL_ICON.
465 */
466 public void setIcon(String iconName) {
467 Collection<String> s = Main.pref.getCollection("taggingpreset.icon.sources", null);
468 ImageIcon icon = ImageProvider.getIfAvailable(s, "presets", null, iconName);
469 if (icon == null)
470 {
471 System.out.println("Could not get presets icon " + iconName);
472 icon = new ImageIcon(iconName);
473 }
474 if (Math.max(icon.getIconHeight(), icon.getIconWidth()) != 16)
475 icon = new ImageIcon(icon.getImage().getScaledInstance(16, 16, Image.SCALE_SMOOTH));
476 putValue(Action.SMALL_ICON, icon);
477 }
478
479 /**
480 * Called from the XML parser to set the types this preset affects
481 */
482 private static Collection<String> allowedtypes = Arrays.asList(new String[]
483 {marktr("way"), marktr("node"), marktr("relation"), marktr("closedway")});
484 public void setType(String types) throws SAXException {
485 this.types = Arrays.asList(types.split(","));
486 for (String type : this.types) {
487 if(!allowedtypes.contains(type))
488 throw new SAXException(tr("Unknown type: {0}", type));
489 }
490 }
491
492 public static List<TaggingPreset> readAll(Reader in) throws SAXException {
493 XmlObjectParser parser = new XmlObjectParser();
494 parser.mapOnStart("item", TaggingPreset.class);
495 parser.mapOnStart("separator", TaggingPresetSeparator.class);
496 parser.mapBoth("group", TaggingPresetMenu.class);
497 parser.map("text", Text.class);
498 parser.map("link", Link.class);
499 parser.mapOnStart("optional", Optional.class);
500 parser.map("check", Check.class);
501 parser.map("combo", Combo.class);
502 parser.map("label", Label.class);
503 parser.map("space", Space.class);
504 parser.map("key", Key.class);
505 LinkedList<TaggingPreset> all = new LinkedList<TaggingPreset>();
506 TaggingPresetMenu lastmenu = null;
507 parser.start(in);
508 while(parser.hasNext()) {
509 Object o = parser.next();
510 if (o instanceof TaggingPresetMenu) {
511 TaggingPresetMenu tp = (TaggingPresetMenu) o;
512 if(tp == lastmenu)
513 lastmenu = tp.group;
514 else
515 {
516 tp.setDisplayName();
517 tp.group = lastmenu;
518 lastmenu = tp;
519 all.add(tp);
520 Main.toolbar.register(tp);
521
522 }
523 } else if (o instanceof TaggingPresetSeparator) {
524 TaggingPresetSeparator tp = (TaggingPresetSeparator) o;
525 tp.group = lastmenu;
526 all.add(tp);
527 } else if (o instanceof TaggingPreset) {
528 TaggingPreset tp = (TaggingPreset) o;
529 tp.group = lastmenu;
530 tp.setDisplayName();
531 all.add(tp);
532 Main.toolbar.register(tp);
533 } else
534 all.getLast().data.add((Item)o);
535 }
536 return all;
537 }
538
539 public static Collection<TaggingPreset> readFromPreferences() {
540 LinkedList<TaggingPreset> allPresets = new LinkedList<TaggingPreset>();
541 LinkedList<String> sources = new LinkedList<String>();
542
543 if(Main.pref.getBoolean("taggingpreset.enable-defaults", true))
544 sources.add("resource://presets/presets.xml");
545 sources.addAll(Main.pref.getCollection("taggingpreset.sources", new LinkedList<String>()));
546
547 for(String source : sources)
548 {
549 try {
550 MirroredInputStream s = new MirroredInputStream(source);
551 InputStreamReader r;
552 try
553 {
554 r = new InputStreamReader(s, "UTF-8");
555 }
556 catch (UnsupportedEncodingException e)
557 {
558 r = new InputStreamReader(s);
559 }
560 allPresets.addAll(TaggingPreset.readAll(new BufferedReader(r)));
561 } catch (IOException e) {
562 e.printStackTrace();
563 JOptionPane.showMessageDialog(Main.parent, tr("Could not read tagging preset source: {0}",source));
564 } catch (SAXException e) {
565 e.printStackTrace();
566 JOptionPane.showMessageDialog(Main.parent, tr("Error parsing {0}: ", source)+e.getMessage());
567 }
568 }
569 return allPresets;
570 }
571
572 public JPanel createPanel(Collection<OsmPrimitive> selected) {
573 if (data == null)
574 return null;
575 JPanel p = new JPanel(new GridBagLayout());
576 LinkedList<Item> l = new LinkedList<Item>();
577 if(types != null)
578 {
579 JPanel pp = new JPanel();
580 for(String t : types)
581 {
582 JLabel la = new JLabel(ImageProvider.get("Mf_" + t));
583 la.setToolTipText(tr("Elements of type {0} are supported.", tr(t)));
584 pp.add(la);
585 }
586 p.add(pp, GBC.eol());
587 }
588
589 for (Item i : data)
590 {
591 if(i instanceof Link)
592 l.add(i);
593 else
594 i.addToPanel(p, selected);
595 }
596 for(Item link : l)
597 link.addToPanel(p, selected);
598 return p;
599 }
600
601 public void actionPerformed(ActionEvent e) {
602 Collection<OsmPrimitive> sel = createSelection(Main.ds.getSelected());
603 JPanel p = createPanel(sel);
604 if (p == null)
605 return;
606
607 int answer = 1;
608 if (p.getComponentCount() != 0) {
609 String title = trn("Change {0} object", "Change {0} objects", sel.size(), sel.size());
610 if(sel.size() == 0) {
611 if(originalSelectionEmpty)
612 title = tr("Nothing selected!");
613 else
614 title = tr("Selection unsuitable!");
615 }
616
617 class PresetDialog extends ExtendedDialog {
618 public PresetDialog(Component content, String title, boolean disableApply) {
619 super(Main.parent,
620 title,
621 new String[] { tr("Apply Preset"), tr("Cancel") },
622 true);
623 contentConstraints = GBC.eol().fill().insets(5,10,5,0);
624 setupDialog(content, new String[] {"ok.png", "cancel.png" });
625 buttons.get(0).setEnabled(!disableApply);
626 buttons.get(0).setToolTipText(title);
627 setVisible(true);
628 }
629 }
630
631 answer = new PresetDialog(p, title, (sel.size() == 0)).getValue();
632 }
633 if (sel.size() != 0 && answer == 1) {
634 Command cmd = createCommand(sel);
635 if (cmd != null)
636 Main.main.undoRedo.add(cmd);
637 }
638 Main.ds.setSelected(Main.ds.getSelected()); // force update
639 }
640
641 /**
642 * True whenever the original selection given into createSelection was empty
643 */
644 private boolean originalSelectionEmpty = false;
645
646 /**
647 * Removes all unsuitable OsmPrimitives from the given list
648 * @param participants List of possibile OsmPrimitives to tag
649 * @return Cleaned list with suitable OsmPrimitives only
650 */
651 private Collection<OsmPrimitive> createSelection(Collection<OsmPrimitive> participants) {
652 originalSelectionEmpty = participants.size() == 0;
653 Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>();
654 for (OsmPrimitive osm : participants)
655 {
656 if (types != null)
657 {
658 if(osm instanceof Relation)
659 {
660 if(!types.contains("relation")) continue;
661 }
662 else if(osm instanceof Node)
663 {
664 if(!types.contains("node")) continue;
665 }
666 else if(osm instanceof Way)
667 {
668 if(!types.contains("way") &&
669 !(types.contains("closedway") && ((Way)osm).isClosed()))
670 continue;
671 }
672 }
673 sel.add(osm);
674 }
675 return sel;
676 }
677
678 private Command createCommand(Collection<OsmPrimitive> sel) {
679 List<Command> cmds = new LinkedList<Command>();
680 for (Item i : data)
681 i.addCommands(sel, cmds);
682 if (cmds.size() == 0)
683 return null;
684 else if (cmds.size() == 1)
685 return cmds.get(0);
686 else
687 return new SequenceCommand(tr("Change Properties"), cmds);
688 }
689}
Note: See TracBrowser for help on using the repository browser.