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

Last change on this file since 7937 was 7937, checked in by bastiK, 9 years ago

add subversion property svn:eol=native

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