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

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

fix remaining checkstyle issues

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