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

Last change on this file since 7207 was 7207, checked in by akks, 10 years ago

see #10092: preset filtering did not work correctly for closed ways

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