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

Last change on this file since 8404 was 8404, checked in by Don-vip, 9 years ago

When doing a String.toLowerCase()/toUpperCase() call, use a Locale. This avoids problems with certain locales, i.e. Lithuanian or Turkish. See PMD UseLocaleWithCaseConversions rule and String.toLowerCase() javadoc.

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