source: josm/trunk/src/org/openstreetmap/josm/gui/tagging/TaggingPresetSelector.java@ 7322

Last change on this file since 7322 was 7322, checked in by akks, 10 years ago

fix #10288: tagging-preset-tester NPE without data layer

File size: 20.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.tagging;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.BorderLayout;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.event.ActionEvent;
10import java.awt.event.ActionListener;
11import java.awt.event.ItemEvent;
12import java.awt.event.ItemListener;
13import java.awt.event.KeyAdapter;
14import java.awt.event.KeyEvent;
15import java.awt.event.MouseAdapter;
16import java.awt.event.MouseEvent;
17import java.util.ArrayList;
18import java.util.Collection;
19import java.util.Collections;
20import java.util.EnumSet;
21import java.util.HashSet;
22import java.util.Iterator;
23import java.util.List;
24
25import javax.swing.AbstractAction;
26import javax.swing.AbstractListModel;
27import javax.swing.Action;
28import javax.swing.BoxLayout;
29import javax.swing.DefaultListCellRenderer;
30import javax.swing.Icon;
31import javax.swing.JCheckBox;
32import javax.swing.JLabel;
33import javax.swing.JList;
34import javax.swing.JPanel;
35import javax.swing.JPopupMenu;
36import javax.swing.JScrollPane;
37import javax.swing.ListCellRenderer;
38import javax.swing.event.DocumentEvent;
39import javax.swing.event.DocumentListener;
40import javax.swing.event.ListSelectionEvent;
41import javax.swing.event.ListSelectionListener;
42
43import org.openstreetmap.josm.Main;
44import org.openstreetmap.josm.data.SelectionChangedListener;
45import org.openstreetmap.josm.data.osm.DataSet;
46import org.openstreetmap.josm.data.osm.OsmPrimitive;
47import org.openstreetmap.josm.data.preferences.BooleanProperty;
48import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Key;
49import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.KeyedItem;
50import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role;
51import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Roles;
52import org.openstreetmap.josm.gui.widgets.JosmTextField;
53import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
54import org.openstreetmap.josm.tools.Predicate;
55import org.openstreetmap.josm.tools.Utils;
56
57/**
58 * GUI component to select tagging preset: the list with filter and two checkboxes
59 * @since 6068
60 */
61public class TaggingPresetSelector extends JPanel implements SelectionChangedListener {
62
63 private static final int CLASSIFICATION_IN_FAVORITES = 300;
64 private static final int CLASSIFICATION_NAME_MATCH = 300;
65 private static final int CLASSIFICATION_GROUP_MATCH = 200;
66 private static final int CLASSIFICATION_TAGS_MATCH = 100;
67
68 private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true);
69 private static final BooleanProperty ONLY_APPLICABLE = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true);
70
71 private JosmTextField edSearchText;
72 private JList<TaggingPreset> lsResult;
73 private JCheckBox ckOnlyApplicable;
74 private JCheckBox ckSearchInTags;
75 private final EnumSet<TaggingPresetType> typesInSelection = EnumSet.noneOf(TaggingPresetType.class);
76 private boolean typesInSelectionDirty = true;
77 private final PresetClassifications classifications = new PresetClassifications();
78 private ResultListModel lsResultModel = new ResultListModel();
79
80 private ActionListener dblClickListener;
81 private ActionListener clickListener;
82
83 private static class ResultListCellRenderer implements ListCellRenderer<TaggingPreset> {
84 final DefaultListCellRenderer def = new DefaultListCellRenderer();
85 @Override
86 public Component getListCellRendererComponent(JList<? extends TaggingPreset> list, TaggingPreset tp, int index, boolean isSelected, boolean cellHasFocus) {
87 JLabel result = (JLabel) def.getListCellRendererComponent(list, tp, index, isSelected, cellHasFocus);
88 result.setText(tp.getName());
89 result.setIcon((Icon) tp.getValue(Action.SMALL_ICON));
90 return result;
91 }
92 }
93
94 private static class ResultListModel extends AbstractListModel<TaggingPreset> {
95
96 private List<PresetClassification> presets = new ArrayList<>();
97
98 public void setPresets(List<PresetClassification> presets) {
99 this.presets = presets;
100 fireContentsChanged(this, 0, Integer.MAX_VALUE);
101 }
102
103 public List<PresetClassification> getPresets() {
104 return presets;
105 }
106
107 @Override
108 public TaggingPreset getElementAt(int index) {
109 return presets.get(index).preset;
110 }
111
112 @Override
113 public int getSize() {
114 return presets.size();
115 }
116 }
117
118 /**
119 * Computes the match ration of a {@link TaggingPreset} wrt. a searchString.
120 */
121 static class PresetClassification implements Comparable<PresetClassification> {
122 public final TaggingPreset preset;
123 public int classification;
124 public int favoriteIndex;
125 private final Collection<String> groups = new HashSet<>();
126 private final Collection<String> names = new HashSet<>();
127 private final Collection<String> tags = new HashSet<>();
128
129 PresetClassification(TaggingPreset preset) {
130 this.preset = preset;
131 TaggingPreset group = preset.group;
132 while (group != null) {
133 Collections.addAll(groups, group.getLocaleName().toLowerCase().split("\\s"));
134 group = group.group;
135 }
136 Collections.addAll(names, preset.getLocaleName().toLowerCase().split("\\s"));
137 for (TaggingPresetItem item: preset.data) {
138 if (item instanceof KeyedItem) {
139 tags.add(((KeyedItem) item).key);
140 if (item instanceof TaggingPresetItems.ComboMultiSelect) {
141 final TaggingPresetItems.ComboMultiSelect cms = (TaggingPresetItems.ComboMultiSelect) item;
142 if (Boolean.parseBoolean(cms.values_searchable)) {
143 tags.addAll(cms.getDisplayValues());
144 }
145 }
146 if (item instanceof Key && ((Key) item).value != null) {
147 tags.add(((Key) item).value);
148 }
149 } else if (item instanceof Roles) {
150 for (Role role : ((Roles) item).roles) {
151 tags.add(role.key);
152 }
153 }
154 }
155 }
156
157 private int isMatching(Collection<String> values, String[] searchString) {
158 int sum = 0;
159 for (String word: searchString) {
160 boolean found = false;
161 boolean foundFirst = false;
162 for (String value: values) {
163 int index = value.toLowerCase().indexOf(word);
164 if (index == 0) {
165 foundFirst = true;
166 break;
167 } else if (index > 0) {
168 found = true;
169 }
170 }
171 if (foundFirst) {
172 sum += 2;
173 } else if (found) {
174 sum += 1;
175 } else
176 return 0;
177 }
178 return sum;
179 }
180
181 int isMatchingGroup(String[] words) {
182 return isMatching(groups, words);
183 }
184
185 int isMatchingName(String[] words) {
186 return isMatching(names, words);
187 }
188
189 int isMatchingTags(String[] words) {
190 return isMatching(tags, words);
191 }
192
193 @Override
194 public int compareTo(PresetClassification o) {
195 int result = o.classification - classification;
196 if (result == 0)
197 return preset.getName().compareTo(o.preset.getName());
198 else
199 return result;
200 }
201
202 @Override
203 public String toString() {
204 return classification + " " + preset.toString();
205 }
206 }
207
208 /**
209 * Constructs a new {@code TaggingPresetSelector}.
210 */
211 public TaggingPresetSelector(boolean displayOnlyApplicable, boolean displaySearchInTags) {
212 super(new BorderLayout());
213 classifications.loadPresets(TaggingPresets.getTaggingPresets());
214
215 edSearchText = new JosmTextField();
216 edSearchText.getDocument().addDocumentListener(new DocumentListener() {
217 @Override public void removeUpdate(DocumentEvent e) { filterPresets(); }
218 @Override public void insertUpdate(DocumentEvent e) { filterPresets(); }
219 @Override public void changedUpdate(DocumentEvent e) { filterPresets(); }
220 });
221 edSearchText.addKeyListener(new KeyAdapter() {
222 @Override
223 public void keyPressed(KeyEvent e) {
224 switch (e.getKeyCode()) {
225 case KeyEvent.VK_DOWN:
226 selectPreset(lsResult.getSelectedIndex() + 1);
227 break;
228 case KeyEvent.VK_UP:
229 selectPreset(lsResult.getSelectedIndex() - 1);
230 break;
231 case KeyEvent.VK_PAGE_DOWN:
232 selectPreset(lsResult.getSelectedIndex() + 10);
233 break;
234 case KeyEvent.VK_PAGE_UP:
235 selectPreset(lsResult.getSelectedIndex() - 10);
236 break;
237 case KeyEvent.VK_HOME:
238 selectPreset(0);
239 break;
240 case KeyEvent.VK_END:
241 selectPreset(lsResultModel.getSize());
242 break;
243 }
244 }
245 });
246 add(edSearchText, BorderLayout.NORTH);
247
248 lsResult = new JList<>();
249 lsResult.setModel(lsResultModel);
250 lsResult.setCellRenderer(new ResultListCellRenderer());
251 lsResult.addMouseListener(new MouseAdapter() {
252 @Override
253 public void mouseClicked(MouseEvent e) {
254 if (e.getClickCount()>1) {
255 if (dblClickListener!=null)
256 dblClickListener.actionPerformed(null);
257 } else {
258 if (clickListener!=null)
259 clickListener.actionPerformed(null);
260 }
261 }
262 });
263 add(new JScrollPane(lsResult), BorderLayout.CENTER);
264
265 JPanel pnChecks = new JPanel();
266 pnChecks.setLayout(new BoxLayout(pnChecks, BoxLayout.Y_AXIS));
267
268 if (displayOnlyApplicable) {
269 ckOnlyApplicable = new JCheckBox();
270 ckOnlyApplicable.setText(tr("Show only applicable to selection"));
271 pnChecks.add(ckOnlyApplicable);
272 ckOnlyApplicable.addItemListener(new ItemListener() {
273 @Override
274 public void itemStateChanged(ItemEvent e) {
275 filterPresets();
276 }
277 });
278 }
279
280 if (displaySearchInTags) {
281 ckSearchInTags = new JCheckBox();
282 ckSearchInTags.setText(tr("Search in tags"));
283 ckSearchInTags.setSelected(SEARCH_IN_TAGS.get());
284 ckSearchInTags.addItemListener(new ItemListener() {
285 @Override
286 public void itemStateChanged(ItemEvent e) {
287 filterPresets();
288 }
289 });
290 pnChecks.add(ckSearchInTags);
291 }
292
293 add(pnChecks, BorderLayout.SOUTH);
294
295 setPreferredSize(new Dimension(400, 300));
296 filterPresets();
297 JPopupMenu popupMenu = new JPopupMenu();
298 popupMenu.add(new AbstractAction(tr("Add toolbar button")) {
299 @Override
300 public void actionPerformed(ActionEvent ae) {
301 String res = getSelectedPreset().getToolbarString();
302 Main.toolbar.addCustomButton(res, -1, false);
303 }
304 });
305 lsResult.addMouseListener(new PopupMenuLauncher(popupMenu));
306 }
307
308 private void selectPreset(int newIndex) {
309 if (newIndex < 0) {
310 newIndex = 0;
311 }
312 if (newIndex > lsResultModel.getSize() - 1) {
313 newIndex = lsResultModel.getSize() - 1;
314 }
315 lsResult.setSelectedIndex(newIndex);
316 lsResult.ensureIndexIsVisible(newIndex);
317 }
318
319 /**
320 * Search expression can be in form: "group1/group2/name" where names can contain multiple words
321 */
322 private void filterPresets() {
323 //TODO Save favorites to file
324 String text = edSearchText.getText().toLowerCase();
325 boolean onlyApplicable = ckOnlyApplicable != null && ckOnlyApplicable.isSelected();
326 boolean inTags = ckSearchInTags != null && ckSearchInTags.isSelected();
327
328 DataSet ds = Main.main.getCurrentDataSet();
329 Collection<OsmPrimitive> selected = (ds==null)? Collections.EMPTY_LIST : ds.getSelected();
330 final List<PresetClassification> result = classifications.getMatchingPresets(
331 text, onlyApplicable, inTags, getTypesInSelection(), selected);
332
333 lsResultModel.setPresets(result);
334
335 }
336
337 /**
338 * A collection of {@link PresetClassification}s with the functionality of filtering wrt. searchString.
339 */
340 static class PresetClassifications implements Iterable<PresetClassification> {
341
342 private final List<PresetClassification> classifications = new ArrayList<>();
343
344 public List<PresetClassification> getMatchingPresets(String searchText, boolean onlyApplicable, boolean inTags, EnumSet<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
345 final String[] groupWords;
346 final String[] nameWords;
347
348 if (searchText.contains("/")) {
349 groupWords = searchText.substring(0, searchText.lastIndexOf('/')).split("[\\s/]");
350 nameWords = searchText.substring(searchText.indexOf('/') + 1).split("\\s");
351 } else {
352 groupWords = null;
353 nameWords = searchText.split("\\s");
354 }
355
356 return getMatchingPresets(groupWords, nameWords, onlyApplicable, inTags, presetTypes, selectedPrimitives);
357 }
358
359 public List<PresetClassification> getMatchingPresets(String[] groupWords, String[] nameWords, boolean onlyApplicable, boolean inTags, EnumSet<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
360
361 final List<PresetClassification> result = new ArrayList<>();
362 for (PresetClassification presetClassification : classifications) {
363 TaggingPreset preset = presetClassification.preset;
364 presetClassification.classification = 0;
365
366 if (onlyApplicable) {
367 boolean suitable = preset.typeMatches(presetTypes);
368
369 if (!suitable && preset.types.contains(TaggingPresetType.RELATION) && preset.roles != null && !preset.roles.roles.isEmpty()) {
370 final Predicate<Role> memberExpressionMatchesOnePrimitive = new Predicate<Role>() {
371
372 @Override
373 public boolean evaluate(Role object) {
374 return object.memberExpression != null
375 && Utils.exists(selectedPrimitives, object.memberExpression);
376 }
377 };
378 suitable = Utils.exists(preset.roles.roles, memberExpressionMatchesOnePrimitive);
379 // keep the preset to allow the creation of new relations
380 }
381 if (!suitable) {
382 continue;
383 }
384 }
385
386 if (groupWords != null && presetClassification.isMatchingGroup(groupWords) == 0) {
387 continue;
388 }
389
390 int matchName = presetClassification.isMatchingName(nameWords);
391
392 if (matchName == 0) {
393 if (groupWords == null) {
394 int groupMatch = presetClassification.isMatchingGroup(nameWords);
395 if (groupMatch > 0) {
396 presetClassification.classification = CLASSIFICATION_GROUP_MATCH + groupMatch;
397 }
398 }
399 if (presetClassification.classification == 0 && inTags) {
400 int tagsMatch = presetClassification.isMatchingTags(nameWords);
401 if (tagsMatch > 0) {
402 presetClassification.classification = CLASSIFICATION_TAGS_MATCH + tagsMatch;
403 }
404 }
405 } else {
406 presetClassification.classification = CLASSIFICATION_NAME_MATCH + matchName;
407 }
408
409 if (presetClassification.classification > 0) {
410 presetClassification.classification += presetClassification.favoriteIndex;
411 result.add(presetClassification);
412 }
413 }
414
415 Collections.sort(result);
416 return result;
417
418 }
419
420 public void clear() {
421 classifications.clear();
422 }
423
424 public void loadPresets(Collection<TaggingPreset> presets) {
425 for (TaggingPreset preset : presets) {
426 if (preset instanceof TaggingPresetSeparator || preset instanceof TaggingPresetMenu) {
427 continue;
428 }
429 classifications.add(new PresetClassification(preset));
430 }
431 }
432
433 @Override
434 public Iterator<PresetClassification> iterator() {
435 return classifications.iterator();
436 }
437 }
438
439 private EnumSet<TaggingPresetType> getTypesInSelection() {
440 if (typesInSelectionDirty) {
441 synchronized (typesInSelection) {
442 typesInSelectionDirty = false;
443 typesInSelection.clear();
444 if (Main.main==null || Main.main.getCurrentDataSet() == null) return typesInSelection;
445 for (OsmPrimitive primitive : Main.main.getCurrentDataSet().getSelected()) {
446 typesInSelection.add(TaggingPresetType.forPrimitive(primitive));
447 }
448 }
449 }
450 return typesInSelection;
451 }
452
453 @Override
454 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
455 typesInSelectionDirty = true;
456 }
457
458 public void init() {
459 if (ckOnlyApplicable != null) {
460 ckOnlyApplicable.setEnabled(!getTypesInSelection().isEmpty());
461 ckOnlyApplicable.setSelected(!getTypesInSelection().isEmpty() && ONLY_APPLICABLE.get());
462 }
463 edSearchText.setText("");
464 filterPresets();
465 }
466
467 public void init(Collection<TaggingPreset> presets) {
468 classifications.clear();
469 classifications.loadPresets(presets);
470 init();
471 }
472
473 public void clearSelection() {
474 lsResult.getSelectionModel().clearSelection();
475 }
476
477 /**
478 * Save checkbox values in preferences for future reuse
479 */
480 public void savePreferences() {
481 if (ckSearchInTags != null) {
482 SEARCH_IN_TAGS.put(ckSearchInTags.isSelected());
483 }
484 if (ckOnlyApplicable != null && ckOnlyApplicable.isEnabled()) {
485 ONLY_APPLICABLE.put(ckOnlyApplicable.isSelected());
486 }
487 }
488
489 /**
490 * Determines, which preset is selected at the current moment
491 * @return selected preset (as action)
492 */
493 public TaggingPreset getSelectedPreset() {
494 List<PresetClassification> presets = lsResultModel.getPresets();
495 if (presets.isEmpty()) return null;
496 int idx = lsResult.getSelectedIndex();
497 if (idx == -1) {
498 idx = 0;
499 }
500 TaggingPreset preset = presets.get(idx).preset;
501 for (PresetClassification pc: classifications) {
502 if (pc.preset == preset) {
503 pc.favoriteIndex = CLASSIFICATION_IN_FAVORITES;
504 } else if (pc.favoriteIndex > 0) {
505 pc.favoriteIndex--;
506 }
507 }
508 return preset;
509 }
510
511 public void setSelectedPreset(TaggingPreset p) {
512 lsResult.setSelectedValue(p, true);
513 }
514
515 public int getItemCount() {
516 return lsResultModel.getSize();
517 }
518
519 public void setDblClickListener(ActionListener dblClickListener) {
520 this.dblClickListener = dblClickListener;
521 }
522
523 public void setClickListener(ActionListener clickListener) {
524 this.clickListener = clickListener;
525 }
526
527 public void addSelectionListener(final ActionListener selectListener) {
528 lsResult.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
529 @Override
530 public void valueChanged(ListSelectionEvent e) {
531 if (!e.getValueIsAdjusting())
532 selectListener.actionPerformed(null);
533 }
534 });
535 }
536}
Note: See TracBrowser for help on using the repository browser.