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

Last change on this file since 9304 was 9304, checked in by simon04, 8 years ago

fix #12237 - Preset search: JOSM remembers presets not used

Regression of r7412

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