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

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

see #15182 - introduce Main.getEditDataSet to avoid unneeded GUI dependence in validator tests and tagging presets

  • Property svn:eol-style set to native
File size: 17.1 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.util.ArrayList;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.EnumSet;
14import java.util.HashSet;
15import java.util.Iterator;
16import java.util.List;
17import java.util.Locale;
18import java.util.Objects;
19import java.util.Set;
20
21import javax.swing.AbstractAction;
22import javax.swing.Action;
23import javax.swing.BoxLayout;
24import javax.swing.DefaultListCellRenderer;
25import javax.swing.Icon;
26import javax.swing.JCheckBox;
27import javax.swing.JLabel;
28import javax.swing.JList;
29import javax.swing.JPanel;
30import javax.swing.JPopupMenu;
31import javax.swing.ListCellRenderer;
32import javax.swing.event.ListSelectionEvent;
33import javax.swing.event.ListSelectionListener;
34
35import org.openstreetmap.josm.Main;
36import org.openstreetmap.josm.data.SelectionChangedListener;
37import org.openstreetmap.josm.data.osm.DataSet;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.preferences.BooleanProperty;
40import org.openstreetmap.josm.gui.MainApplication;
41import org.openstreetmap.josm.gui.tagging.presets.items.ComboMultiSelect;
42import org.openstreetmap.josm.gui.tagging.presets.items.Key;
43import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
44import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
45import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
46import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
47import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
48import org.openstreetmap.josm.tools.Utils;
49
50/**
51 * GUI component to select tagging preset: the list with filter and two checkboxes
52 * @since 6068
53 */
54public class TaggingPresetSelector extends SearchTextResultListPanel<TaggingPreset> implements SelectionChangedListener {
55
56 private static final int CLASSIFICATION_IN_FAVORITES = 300;
57 private static final int CLASSIFICATION_NAME_MATCH = 300;
58 private static final int CLASSIFICATION_GROUP_MATCH = 200;
59 private static final int CLASSIFICATION_TAGS_MATCH = 100;
60
61 private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true);
62 private static final BooleanProperty ONLY_APPLICABLE = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true);
63
64 private final JCheckBox ckOnlyApplicable;
65 private final JCheckBox ckSearchInTags;
66 private final Set<TaggingPresetType> typesInSelection = EnumSet.noneOf(TaggingPresetType.class);
67 private boolean typesInSelectionDirty = true;
68 private final transient PresetClassifications classifications = new PresetClassifications();
69
70 private static class ResultListCellRenderer implements ListCellRenderer<TaggingPreset> {
71 private final DefaultListCellRenderer def = new DefaultListCellRenderer();
72 @Override
73 public Component getListCellRendererComponent(JList<? extends TaggingPreset> list, TaggingPreset tp, int index,
74 boolean isSelected, boolean cellHasFocus) {
75 JLabel result = (JLabel) def.getListCellRendererComponent(list, tp, index, isSelected, cellHasFocus);
76 result.setText(tp.getName());
77 result.setIcon((Icon) tp.getValue(Action.SMALL_ICON));
78 return result;
79 }
80 }
81
82 /**
83 * Computes the match ration of a {@link TaggingPreset} wrt. a searchString.
84 */
85 public static class PresetClassification implements Comparable<PresetClassification> {
86 public final TaggingPreset preset;
87 public int classification;
88 public int favoriteIndex;
89 private final Collection<String> groups = new HashSet<>();
90 private final Collection<String> names = new HashSet<>();
91 private final Collection<String> tags = new HashSet<>();
92
93 PresetClassification(TaggingPreset preset) {
94 this.preset = preset;
95 TaggingPreset group = preset.group;
96 while (group != null) {
97 addLocaleNames(groups, group);
98 group = group.group;
99 }
100 addLocaleNames(names, preset);
101 for (TaggingPresetItem item: preset.data) {
102 if (item instanceof KeyedItem) {
103 tags.add(((KeyedItem) item).key);
104 if (item instanceof ComboMultiSelect) {
105 final ComboMultiSelect cms = (ComboMultiSelect) item;
106 if (Boolean.parseBoolean(cms.values_searchable)) {
107 tags.addAll(cms.getDisplayValues());
108 }
109 }
110 if (item instanceof Key && ((Key) item).value != null) {
111 tags.add(((Key) item).value);
112 }
113 } else if (item instanceof Roles) {
114 for (Role role : ((Roles) item).roles) {
115 tags.add(role.key);
116 }
117 }
118 }
119 }
120
121 private static void addLocaleNames(Collection<String> collection, TaggingPreset preset) {
122 String locName = preset.getLocaleName();
123 if (locName != null) {
124 Collections.addAll(collection, locName.toLowerCase(Locale.ENGLISH).split("\\s"));
125 }
126 }
127
128 private static int isMatching(Collection<String> values, String... searchString) {
129 int sum = 0;
130 for (String word: searchString) {
131 boolean found = false;
132 boolean foundFirst = false;
133 for (String value: values) {
134 int index = value.toLowerCase(Locale.ENGLISH).indexOf(word);
135 if (index == 0) {
136 foundFirst = true;
137 break;
138 } else if (index > 0) {
139 found = true;
140 }
141 }
142 if (foundFirst) {
143 sum += 2;
144 } else if (found) {
145 sum += 1;
146 } else
147 return 0;
148 }
149 return sum;
150 }
151
152 int isMatchingGroup(String... words) {
153 return isMatching(groups, words);
154 }
155
156 int isMatchingName(String... words) {
157 return isMatching(names, words);
158 }
159
160 int isMatchingTags(String... words) {
161 return isMatching(tags, words);
162 }
163
164 @Override
165 public int compareTo(PresetClassification o) {
166 int result = o.classification - classification;
167 if (result == 0)
168 return preset.getName().compareTo(o.preset.getName());
169 else
170 return result;
171 }
172
173 @Override
174 public String toString() {
175 return Integer.toString(classification) + ' ' + preset;
176 }
177 }
178
179 /**
180 * Constructs a new {@code TaggingPresetSelector}.
181 * @param displayOnlyApplicable if {@code true} display "Show only applicable to selection" checkbox
182 * @param displaySearchInTags if {@code true} display "Search in tags" checkbox
183 */
184 public TaggingPresetSelector(boolean displayOnlyApplicable, boolean displaySearchInTags) {
185 super();
186 lsResult.setCellRenderer(new ResultListCellRenderer());
187 classifications.loadPresets(TaggingPresets.getTaggingPresets());
188
189 JPanel pnChecks = new JPanel();
190 pnChecks.setLayout(new BoxLayout(pnChecks, BoxLayout.Y_AXIS));
191
192 if (displayOnlyApplicable) {
193 ckOnlyApplicable = new JCheckBox();
194 ckOnlyApplicable.setText(tr("Show only applicable to selection"));
195 pnChecks.add(ckOnlyApplicable);
196 ckOnlyApplicable.addItemListener(e -> filterItems());
197 } else {
198 ckOnlyApplicable = null;
199 }
200
201 if (displaySearchInTags) {
202 ckSearchInTags = new JCheckBox();
203 ckSearchInTags.setText(tr("Search in tags"));
204 ckSearchInTags.setSelected(SEARCH_IN_TAGS.get());
205 ckSearchInTags.addItemListener(e -> filterItems());
206 pnChecks.add(ckSearchInTags);
207 } else {
208 ckSearchInTags = null;
209 }
210
211 add(pnChecks, BorderLayout.SOUTH);
212
213 setPreferredSize(new Dimension(400, 300));
214 filterItems();
215 JPopupMenu popupMenu = new JPopupMenu();
216 popupMenu.add(new AbstractAction(tr("Add toolbar button")) {
217 @Override
218 public void actionPerformed(ActionEvent ae) {
219 final TaggingPreset preset = getSelectedPreset();
220 if (preset != null) {
221 MainApplication.getToolbar().addCustomButton(preset.getToolbarString(), -1, false);
222 }
223 }
224 });
225 lsResult.addMouseListener(new PopupMenuLauncher(popupMenu));
226 }
227
228 /**
229 * Search expression can be in form: "group1/group2/name" where names can contain multiple words
230 */
231 @Override
232 protected synchronized void filterItems() {
233 //TODO Save favorites to file
234 String text = edSearchText.getText().toLowerCase(Locale.ENGLISH);
235 boolean onlyApplicable = ckOnlyApplicable != null && ckOnlyApplicable.isSelected();
236 boolean inTags = ckSearchInTags != null && ckSearchInTags.isSelected();
237
238 DataSet ds = Main.main.getEditDataSet();
239 Collection<OsmPrimitive> selected = (ds == null) ? Collections.<OsmPrimitive>emptyList() : ds.getSelected();
240 final List<PresetClassification> result = classifications.getMatchingPresets(
241 text, onlyApplicable, inTags, getTypesInSelection(), selected);
242
243 final TaggingPreset oldPreset = getSelectedPreset();
244 lsResultModel.setItems(Utils.transform(result, x -> x.preset));
245 final TaggingPreset newPreset = getSelectedPreset();
246 if (!Objects.equals(oldPreset, newPreset)) {
247 int[] indices = lsResult.getSelectedIndices();
248 for (ListSelectionListener listener : listSelectionListeners) {
249 listener.valueChanged(new ListSelectionEvent(lsResult, lsResult.getSelectedIndex(),
250 indices.length > 0 ? indices[indices.length-1] : -1, false));
251 }
252 }
253 }
254
255 /**
256 * A collection of {@link PresetClassification}s with the functionality of filtering wrt. searchString.
257 */
258 public static class PresetClassifications implements Iterable<PresetClassification> {
259
260 private final List<PresetClassification> classifications = new ArrayList<>();
261
262 public List<PresetClassification> getMatchingPresets(String searchText, boolean onlyApplicable, boolean inTags,
263 Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
264 final String[] groupWords;
265 final String[] nameWords;
266
267 if (searchText.contains("/")) {
268 groupWords = searchText.substring(0, searchText.lastIndexOf('/')).split("[\\s/]");
269 nameWords = searchText.substring(searchText.indexOf('/') + 1).split("\\s");
270 } else {
271 groupWords = null;
272 nameWords = searchText.split("\\s");
273 }
274
275 return getMatchingPresets(groupWords, nameWords, onlyApplicable, inTags, presetTypes, selectedPrimitives);
276 }
277
278 public List<PresetClassification> getMatchingPresets(String[] groupWords, String[] nameWords, boolean onlyApplicable,
279 boolean inTags, Set<TaggingPresetType> presetTypes, final Collection<? extends OsmPrimitive> selectedPrimitives) {
280
281 final List<PresetClassification> result = new ArrayList<>();
282 for (PresetClassification presetClassification : classifications) {
283 TaggingPreset preset = presetClassification.preset;
284 presetClassification.classification = 0;
285
286 if (onlyApplicable) {
287 boolean suitable = preset.typeMatches(presetTypes);
288
289 if (!suitable && preset.types.contains(TaggingPresetType.RELATION)
290 && preset.roles != null && !preset.roles.roles.isEmpty()) {
291 suitable = preset.roles.roles.stream().anyMatch(
292 object -> object.memberExpression != null && selectedPrimitives.stream().anyMatch(object.memberExpression));
293 // keep the preset to allow the creation of new relations
294 }
295 if (!suitable) {
296 continue;
297 }
298 }
299
300 if (groupWords != null && presetClassification.isMatchingGroup(groupWords) == 0) {
301 continue;
302 }
303
304 int matchName = presetClassification.isMatchingName(nameWords);
305
306 if (matchName == 0) {
307 if (groupWords == null) {
308 int groupMatch = presetClassification.isMatchingGroup(nameWords);
309 if (groupMatch > 0) {
310 presetClassification.classification = CLASSIFICATION_GROUP_MATCH + groupMatch;
311 }
312 }
313 if (presetClassification.classification == 0 && inTags) {
314 int tagsMatch = presetClassification.isMatchingTags(nameWords);
315 if (tagsMatch > 0) {
316 presetClassification.classification = CLASSIFICATION_TAGS_MATCH + tagsMatch;
317 }
318 }
319 } else {
320 presetClassification.classification = CLASSIFICATION_NAME_MATCH + matchName;
321 }
322
323 if (presetClassification.classification > 0) {
324 presetClassification.classification += presetClassification.favoriteIndex;
325 result.add(presetClassification);
326 }
327 }
328
329 Collections.sort(result);
330 return result;
331
332 }
333
334 public void clear() {
335 classifications.clear();
336 }
337
338 public void loadPresets(Collection<TaggingPreset> presets) {
339 for (TaggingPreset preset : presets) {
340 if (preset instanceof TaggingPresetSeparator || preset instanceof TaggingPresetMenu) {
341 continue;
342 }
343 classifications.add(new PresetClassification(preset));
344 }
345 }
346
347 @Override
348 public Iterator<PresetClassification> iterator() {
349 return classifications.iterator();
350 }
351 }
352
353 private Set<TaggingPresetType> getTypesInSelection() {
354 if (typesInSelectionDirty) {
355 synchronized (typesInSelection) {
356 typesInSelectionDirty = false;
357 typesInSelection.clear();
358 if (Main.main == null || Main.main.getEditDataSet() == null) return typesInSelection;
359 for (OsmPrimitive primitive : Main.main.getEditDataSet().getSelected()) {
360 typesInSelection.add(TaggingPresetType.forPrimitive(primitive));
361 }
362 }
363 }
364 return typesInSelection;
365 }
366
367 @Override
368 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
369 typesInSelectionDirty = true;
370 }
371
372 @Override
373 public synchronized void init() {
374 if (ckOnlyApplicable != null) {
375 ckOnlyApplicable.setEnabled(!getTypesInSelection().isEmpty());
376 ckOnlyApplicable.setSelected(!getTypesInSelection().isEmpty() && ONLY_APPLICABLE.get());
377 }
378 super.init();
379 }
380
381 public void init(Collection<TaggingPreset> presets) {
382 classifications.clear();
383 classifications.loadPresets(presets);
384 init();
385 }
386
387 /**
388 * Save checkbox values in preferences for future reuse
389 */
390 public void savePreferences() {
391 if (ckSearchInTags != null) {
392 SEARCH_IN_TAGS.put(ckSearchInTags.isSelected());
393 }
394 if (ckOnlyApplicable != null && ckOnlyApplicable.isEnabled()) {
395 ONLY_APPLICABLE.put(ckOnlyApplicable.isSelected());
396 }
397 }
398
399 /**
400 * Determines, which preset is selected at the moment.
401 * @return selected preset (as action)
402 */
403 public synchronized TaggingPreset getSelectedPreset() {
404 if (lsResultModel.isEmpty()) return null;
405 int idx = lsResult.getSelectedIndex();
406 if (idx < 0 || idx >= lsResultModel.getSize()) {
407 idx = 0;
408 }
409 return lsResultModel.getElementAt(idx);
410 }
411
412 /**
413 * Determines, which preset is selected at the moment. Updates {@link PresetClassification#favoriteIndex}!
414 * @return selected preset (as action)
415 */
416 public synchronized TaggingPreset getSelectedPresetAndUpdateClassification() {
417 final TaggingPreset preset = getSelectedPreset();
418 for (PresetClassification pc: classifications) {
419 if (pc.preset == preset) {
420 pc.favoriteIndex = CLASSIFICATION_IN_FAVORITES;
421 } else if (pc.favoriteIndex > 0) {
422 pc.favoriteIndex--;
423 }
424 }
425 return preset;
426 }
427
428 public synchronized void setSelectedPreset(TaggingPreset p) {
429 lsResult.setSelectedValue(p, true);
430 }
431}
Note: See TracBrowser for help on using the repository browser.