001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.tagging; 003 004import java.awt.BorderLayout; 005import java.awt.Component; 006import java.awt.Dimension; 007import java.awt.event.ActionListener; 008import java.awt.event.ItemEvent; 009import java.awt.event.ItemListener; 010import java.awt.event.KeyAdapter; 011import java.awt.event.KeyEvent; 012import java.awt.event.MouseAdapter; 013import java.awt.event.MouseEvent; 014import java.util.ArrayList; 015import java.util.Collection; 016import java.util.Collections; 017import java.util.EnumSet; 018import java.util.HashSet; 019import java.util.List; 020import javax.swing.AbstractListModel; 021import javax.swing.Action; 022import javax.swing.BoxLayout; 023import javax.swing.DefaultListCellRenderer; 024import javax.swing.Icon; 025import javax.swing.JCheckBox; 026import javax.swing.JLabel; 027import javax.swing.JList; 028import javax.swing.JPanel; 029import javax.swing.JScrollPane; 030import javax.swing.event.DocumentEvent; 031import javax.swing.event.DocumentListener; 032import javax.swing.event.ListSelectionEvent; 033import javax.swing.event.ListSelectionListener; 034import org.openstreetmap.josm.Main; 035import org.openstreetmap.josm.data.SelectionChangedListener; 036import org.openstreetmap.josm.data.osm.Node; 037import org.openstreetmap.josm.data.osm.OsmPrimitive; 038import org.openstreetmap.josm.data.osm.Relation; 039import org.openstreetmap.josm.data.osm.Way; 040import org.openstreetmap.josm.data.preferences.BooleanProperty; 041import org.openstreetmap.josm.gui.preferences.map.TaggingPresetPreference; 042import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Key; 043import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.KeyedItem; 044import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role; 045import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Roles; 046import org.openstreetmap.josm.gui.widgets.JosmTextField; 047import static org.openstreetmap.josm.tools.I18n.tr; 048 049/** 050 * GUI component to select tagging preset: the list with filter and two checkboxes 051 * @since 6068 052 */ 053public class TaggingPresetSelector extends JPanel implements SelectionChangedListener { 054 055 private static final int CLASSIFICATION_IN_FAVORITES = 300; 056 private static final int CLASSIFICATION_NAME_MATCH = 300; 057 private static final int CLASSIFICATION_GROUP_MATCH = 200; 058 private static final int CLASSIFICATION_TAGS_MATCH = 100; 059 060 private static final BooleanProperty SEARCH_IN_TAGS = new BooleanProperty("taggingpreset.dialog.search-in-tags", true); 061 private static final BooleanProperty ONLY_APPLICABLE = new BooleanProperty("taggingpreset.dialog.only-applicable-to-selection", true); 062 063 064 private JosmTextField edSearchText; 065 private JList lsResult; 066 private JCheckBox ckOnlyApplicable; 067 private JCheckBox ckSearchInTags; 068 private final EnumSet<TaggingPresetType> typesInSelection = EnumSet.noneOf(TaggingPresetType.class); 069 private boolean typesInSelectionDirty = true; 070 private final List<PresetClassification> classifications = new ArrayList<PresetClassification>(); 071 private ResultListModel lsResultModel = new ResultListModel(); 072 073 private ActionListener dblClickListener; 074 private ActionListener clickListener; 075 076 private static class ResultListCellRenderer extends DefaultListCellRenderer { 077 @Override 078 public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, 079 boolean cellHasFocus) { 080 JLabel result = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); 081 TaggingPreset tp = (TaggingPreset)value; 082 result.setText(tp.getName()); 083 result.setIcon((Icon) tp.getValue(Action.SMALL_ICON)); 084 return result; 085 } 086 } 087 088 private static class ResultListModel extends AbstractListModel { 089 090 private List<PresetClassification> presets = new ArrayList<PresetClassification>(); 091 092 public void setPresets(List<PresetClassification> presets) { 093 this.presets = presets; 094 fireContentsChanged(this, 0, Integer.MAX_VALUE); 095 } 096 097 public List<PresetClassification> getPresets() { 098 return presets; 099 } 100 101 @Override 102 public Object getElementAt(int index) { 103 return presets.get(index).preset; 104 } 105 106 @Override 107 public int getSize() { 108 return presets.size(); 109 } 110 111 } 112 113 private static class PresetClassification implements Comparable<PresetClassification> { 114 public final TaggingPreset preset; 115 public int classification; 116 public int favoriteIndex; 117 private final Collection<String> groups = new HashSet<String>(); 118 private final Collection<String> names = new HashSet<String>(); 119 private final Collection<String> tags = new HashSet<String>(); 120 121 PresetClassification(TaggingPreset preset) { 122 this.preset = preset; 123 TaggingPreset group = preset.group; 124 while (group != null) { 125 Collections.addAll(groups, group.getLocaleName().toLowerCase().split("\\s")); 126 group = group.group; 127 } 128 Collections.addAll(names, preset.getLocaleName().toLowerCase().split("\\s")); 129 for (TaggingPresetItem item: preset.data) { 130 if (item instanceof KeyedItem) { 131 tags.add(((KeyedItem) item).key); 132 if (item instanceof TaggingPresetItems.ComboMultiSelect) { 133 final TaggingPresetItems.ComboMultiSelect cms = (TaggingPresetItems.ComboMultiSelect) item; 134 if (Boolean.parseBoolean(cms.values_searchable)) { 135 tags.addAll(cms.getDisplayValues()); 136 } 137 } 138 if (item instanceof Key && ((Key) item).value != null) { 139 tags.add(((Key) item).value); 140 } 141 } else if (item instanceof Roles) { 142 for (Role role : ((Roles) item).roles) { 143 tags.add(role.key); 144 } 145 } 146 } 147 } 148 149 private int isMatching(Collection<String> values, String[] searchString) { 150 int sum = 0; 151 for (String word: searchString) { 152 boolean found = false; 153 boolean foundFirst = false; 154 for (String value: values) { 155 int index = value.toLowerCase().indexOf(word); 156 if (index == 0) { 157 foundFirst = true; 158 break; 159 } else if (index > 0) { 160 found = true; 161 } 162 } 163 if (foundFirst) { 164 sum += 2; 165 } else if (found) { 166 sum += 1; 167 } else 168 return 0; 169 } 170 return sum; 171 } 172 173 int isMatchingGroup(String[] words) { 174 return isMatching(groups, words); 175 } 176 177 int isMatchingName(String[] words) { 178 return isMatching(names, words); 179 } 180 181 int isMatchingTags(String[] words) { 182 return isMatching(tags, words); 183 } 184 185 @Override 186 public int compareTo(PresetClassification o) { 187 int result = o.classification - classification; 188 if (result == 0) 189 return preset.getName().compareTo(o.preset.getName()); 190 else 191 return result; 192 } 193 194 @Override 195 public String toString() { 196 return classification + " " + preset.toString(); 197 } 198 } 199 200 /** 201 * Constructs a new {@code TaggingPresetSelector}. 202 */ 203 public TaggingPresetSelector() { 204 super(new BorderLayout()); 205 if (TaggingPresetPreference.taggingPresets!=null) { 206 loadPresets(TaggingPresetPreference.taggingPresets); 207 } 208 209 edSearchText = new JosmTextField(); 210 edSearchText.getDocument().addDocumentListener(new DocumentListener() { 211 @Override public void removeUpdate(DocumentEvent e) { filterPresets(); } 212 @Override public void insertUpdate(DocumentEvent e) { filterPresets(); } 213 @Override public void changedUpdate(DocumentEvent e) { filterPresets(); } 214 }); 215 edSearchText.addKeyListener(new KeyAdapter() { 216 @Override 217 public void keyPressed(KeyEvent e) { 218 switch (e.getKeyCode()) { 219 case KeyEvent.VK_DOWN: 220 selectPreset(lsResult.getSelectedIndex() + 1); 221 break; 222 case KeyEvent.VK_UP: 223 selectPreset(lsResult.getSelectedIndex() - 1); 224 break; 225 case KeyEvent.VK_PAGE_DOWN: 226 selectPreset(lsResult.getSelectedIndex() + 10); 227 break; 228 case KeyEvent.VK_PAGE_UP: 229 selectPreset(lsResult.getSelectedIndex() - 10); 230 break; 231 case KeyEvent.VK_HOME: 232 selectPreset(0); 233 break; 234 case KeyEvent.VK_END: 235 selectPreset(lsResultModel.getSize()); 236 break; 237 } 238 } 239 }); 240 add(edSearchText, BorderLayout.NORTH); 241 242 lsResult = new JList(); 243 lsResult.setModel(lsResultModel); 244 lsResult.setCellRenderer(new ResultListCellRenderer()); 245 lsResult.addMouseListener(new MouseAdapter() { 246 @Override 247 public void mouseClicked(MouseEvent e) { 248 if (e.getClickCount()>1) { 249 if (dblClickListener!=null) 250 dblClickListener.actionPerformed(null); 251 } else { 252 if (clickListener!=null) 253 clickListener.actionPerformed(null); 254 } 255 } 256 }); 257 add(new JScrollPane(lsResult), BorderLayout.CENTER); 258 259 JPanel pnChecks = new JPanel(); 260 pnChecks.setLayout(new BoxLayout(pnChecks, BoxLayout.Y_AXIS)); 261 262 ckOnlyApplicable = new JCheckBox(); 263 ckOnlyApplicable.setText(tr("Show only applicable to selection")); 264 pnChecks.add(ckOnlyApplicable); 265 ckOnlyApplicable.addItemListener(new ItemListener() { 266 @Override 267 public void itemStateChanged(ItemEvent e) { 268 filterPresets(); 269 } 270 }); 271 272 ckSearchInTags = new JCheckBox(); 273 ckSearchInTags.setText(tr("Search in tags")); 274 ckSearchInTags.setSelected(SEARCH_IN_TAGS.get()); 275 ckSearchInTags.addItemListener(new ItemListener() { 276 @Override 277 public void itemStateChanged(ItemEvent e) { 278 filterPresets(); 279 } 280 }); 281 pnChecks.add(ckSearchInTags); 282 283 add(pnChecks, BorderLayout.SOUTH); 284 285 setPreferredSize(new Dimension(400, 300)); 286 287 filterPresets(); 288 } 289 290 private void selectPreset(int newIndex) { 291 if (newIndex < 0) { 292 newIndex = 0; 293 } 294 if (newIndex > lsResultModel.getSize() - 1) { 295 newIndex = lsResultModel.getSize() - 1; 296 } 297 lsResult.setSelectedIndex(newIndex); 298 lsResult.ensureIndexIsVisible(newIndex); 299 } 300 301 /** 302 * Search expression can be in form: "group1/group2/name" where names can contain multiple words 303 */ 304 private void filterPresets() { 305 //TODO Save favorites to file 306 String text = edSearchText.getText().toLowerCase(); 307 308 String[] groupWords; 309 String[] nameWords; 310 311 if (text.contains("/")) { 312 groupWords = text.substring(0, text.lastIndexOf('/')).split("[\\s/]"); 313 nameWords = text.substring(text.indexOf('/') + 1).split("\\s"); 314 } else { 315 groupWords = null; 316 nameWords = text.split("\\s"); 317 } 318 319 boolean onlyApplicable = ckOnlyApplicable.isSelected(); 320 boolean inTags = ckSearchInTags.isSelected(); 321 322 List<PresetClassification> result = new ArrayList<PresetClassification>(); 323 PRESET_LOOP: 324 for (PresetClassification presetClasification: classifications) { 325 TaggingPreset preset = presetClasification.preset; 326 presetClasification.classification = 0; 327 328 if (onlyApplicable && preset.types != null) { 329 boolean found = false; 330 for (TaggingPresetType type: preset.types) { 331 if (getTypesInSelection().contains(type)) { 332 found = true; 333 break; 334 } 335 } 336 if (!found) { 337 continue; 338 } 339 } 340 341 if (groupWords != null && presetClasification.isMatchingGroup(groupWords) == 0) { 342 continue PRESET_LOOP; 343 } 344 345 int matchName = presetClasification.isMatchingName(nameWords); 346 347 if (matchName == 0) { 348 if (groupWords == null) { 349 int groupMatch = presetClasification.isMatchingGroup(nameWords); 350 if (groupMatch > 0) { 351 presetClasification.classification = CLASSIFICATION_GROUP_MATCH + groupMatch; 352 } 353 } 354 if (presetClasification.classification == 0 && inTags) { 355 int tagsMatch = presetClasification.isMatchingTags(nameWords); 356 if (tagsMatch > 0) { 357 presetClasification.classification = CLASSIFICATION_TAGS_MATCH + tagsMatch; 358 } 359 } 360 } else { 361 presetClasification.classification = CLASSIFICATION_NAME_MATCH + matchName; 362 } 363 364 if (presetClasification.classification > 0) { 365 presetClasification.classification += presetClasification.favoriteIndex; 366 result.add(presetClasification); 367 } 368 } 369 370 Collections.sort(result); 371 lsResultModel.setPresets(result); 372 373 } 374 375 private EnumSet<TaggingPresetType> getTypesInSelection() { 376 if (typesInSelectionDirty) { 377 synchronized (typesInSelection) { 378 typesInSelectionDirty = false; 379 typesInSelection.clear(); 380 if (Main.main==null || Main.main.getCurrentDataSet() == null) return typesInSelection; 381 for (OsmPrimitive primitive : Main.main.getCurrentDataSet().getSelected()) { 382 if (primitive instanceof Node) { 383 typesInSelection.add(TaggingPresetType.NODE); 384 } else if (primitive instanceof Way) { 385 typesInSelection.add(TaggingPresetType.WAY); 386 if (((Way) primitive).isClosed()) { 387 typesInSelection.add(TaggingPresetType.CLOSEDWAY); 388 } 389 } else if (primitive instanceof Relation) { 390 typesInSelection.add(TaggingPresetType.RELATION); 391 } 392 } 393 } 394 } 395 return typesInSelection; 396 } 397 398 @Override 399 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 400 typesInSelectionDirty = true; 401 } 402 403 public void init() { 404 ckOnlyApplicable.setEnabled(!getTypesInSelection().isEmpty()); 405 ckOnlyApplicable.setSelected(!getTypesInSelection().isEmpty() && ONLY_APPLICABLE.get()); 406 edSearchText.setText(""); 407 filterPresets(); 408 } 409 410 public void init(Collection<TaggingPreset> presets) { 411 classifications.clear(); 412 loadPresets(presets); 413 init(); 414 } 415 416 417 public void clearSelection() { 418 lsResult.getSelectionModel().clearSelection(); 419 } 420 421 /** 422 * Save checkbox values in preferences for future reuse 423 */ 424 public void savePreferences() { 425 SEARCH_IN_TAGS.put(ckSearchInTags.isSelected()); 426 if (ckOnlyApplicable.isEnabled()) { 427 ONLY_APPLICABLE.put(ckOnlyApplicable.isSelected()); 428 } 429 } 430 431 /** 432 * Determines, which preset is selected at the current moment 433 * @return selected preset (as action) 434 */ 435 public TaggingPreset getSelectedPreset() { 436 List<PresetClassification> presets = lsResultModel.getPresets(); 437 if (presets.isEmpty()) return null; 438 int idx = lsResult.getSelectedIndex(); 439 if (idx == -1) { 440 idx = 0; 441 } 442 TaggingPreset preset = presets.get(idx).preset; 443 for (PresetClassification pc: classifications) { 444 if (pc.preset == preset) { 445 pc.favoriteIndex = CLASSIFICATION_IN_FAVORITES; 446 } else if (pc.favoriteIndex > 0) { 447 pc.favoriteIndex--; 448 } 449 } 450 return preset; 451 } 452 453 private void loadPresets(Collection<TaggingPreset> presets) { 454 for (TaggingPreset preset: presets) { 455 if (preset instanceof TaggingPresetSeparator || preset instanceof TaggingPresetMenu) { 456 continue; 457 } 458 classifications.add(new PresetClassification(preset)); 459 } 460 } 461 462 public void setSelectedPreset(TaggingPreset p) { 463 lsResult.setSelectedValue(p, true); 464 } 465 466 public int getItemCount() { 467 return lsResultModel.getSize(); 468 } 469 470 public void setDblClickListener(ActionListener dblClickListener) { 471 this.dblClickListener = dblClickListener; 472 } 473 474 public void setClickListener(ActionListener clickListener) { 475 this.clickListener = clickListener; 476 } 477 478 public void addSelectionListener(final ActionListener selectListener) { 479 lsResult.getSelectionModel().addListSelectionListener(new ListSelectionListener() { 480 @Override 481 public void valueChanged(ListSelectionEvent e) { 482 if (!e.getValueIsAdjusting()) 483 selectListener.actionPerformed(null); 484 } 485 }); 486 } 487}