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

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

fix squid:S1319 - Declarations should use Java collection interfaces rather than specific implementation classes

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