source: josm/trunk/src/org/openstreetmap/josm/gui/download/OverpassQueryList.java@ 12574

Last change on this file since 12574 was 12574, checked in by michael2402, 7 years ago

Apply #15057: Improve the over pass turbo dialog

Adds the ability to add favorites and a new wizard dialog with examples.

File size: 21.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.download;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Component;
8import java.awt.Dimension;
9import java.awt.Font;
10import java.awt.GridBagLayout;
11import java.awt.Point;
12import java.awt.event.ActionEvent;
13import java.awt.event.ActionListener;
14import java.awt.event.MouseAdapter;
15import java.awt.event.MouseEvent;
16import java.time.LocalDateTime;
17import java.time.format.DateTimeFormatter;
18import java.util.ArrayList;
19import java.util.Collection;
20import java.util.Collections;
21import java.util.HashMap;
22import java.util.Locale;
23import java.util.Map;
24import java.util.Objects;
25import java.util.Optional;
26import java.util.stream.Collectors;
27
28import javax.swing.AbstractAction;
29import javax.swing.BorderFactory;
30import javax.swing.JLabel;
31import javax.swing.JList;
32import javax.swing.JOptionPane;
33import javax.swing.JPanel;
34import javax.swing.JPopupMenu;
35import javax.swing.JScrollPane;
36import javax.swing.JTextField;
37import javax.swing.ListCellRenderer;
38import javax.swing.SwingUtilities;
39import javax.swing.border.CompoundBorder;
40import javax.swing.text.JTextComponent;
41
42import org.openstreetmap.josm.Main;
43import org.openstreetmap.josm.gui.ExtendedDialog;
44import org.openstreetmap.josm.gui.util.GuiHelper;
45import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
46import org.openstreetmap.josm.gui.widgets.DefaultTextComponentValidator;
47import org.openstreetmap.josm.gui.widgets.JosmTextArea;
48import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
49import org.openstreetmap.josm.tools.GBC;
50import org.openstreetmap.josm.tools.Utils;
51
52/**
53 * A component to select user saved Overpass queries.
54 * @since 12574
55 */
56public final class OverpassQueryList extends SearchTextResultListPanel<OverpassQueryList.SelectorItem> {
57
58 private final DateTimeFormatter format = DateTimeFormatter.ofPattern("HH:mm:ss, dd-MM-yyyy");
59
60 /*
61 * GUI elements
62 */
63 private final JTextComponent target;
64 private final Component componentParent;
65
66 /*
67 * All loaded elements within the list.
68 */
69 private final transient Map<String, SelectorItem> items;
70
71 /*
72 * Preferences
73 */
74 private static final String KEY_KEY = "key";
75 private static final String QUERY_KEY = "query";
76 private static final String USE_COUNT_KEY = "useCount";
77 private static final String PREFERENCE_ITEMS = "download.overpass.query";
78
79 /**
80 * Constructs a new {@code OverpassQueryList}.
81 * @param parent The parent of this component.
82 * @param target The text component to which the queries must be added.
83 */
84 public OverpassQueryList(Component parent, JTextComponent target) {
85 this.target = target;
86 this.componentParent = parent;
87 this.items = this.restorePreferences();
88
89 OverpassQueryListMouseAdapter mouseHandler = new OverpassQueryListMouseAdapter(lsResult, lsResultModel);
90 super.lsResult.setCellRenderer(new OverpassQueryCellRendered());
91 super.setDblClickListener(this::getDblClickListener);
92 super.lsResult.addMouseListener(mouseHandler);
93 super.lsResult.addMouseMotionListener(mouseHandler);
94
95 filterItems();
96 }
97
98 /**
99 * Returns currently selected element from the list.
100 * @return An {@link Optional#empty()} if nothing is selected, otherwise
101 * the idem is returned.
102 */
103 public synchronized Optional<SelectorItem> getSelectedItem() {
104 int idx = lsResult.getSelectedIndex();
105 if (lsResultModel.getSize() == 0 || idx == -1) {
106 return Optional.empty();
107 }
108
109 SelectorItem item = lsResultModel.getElementAt(idx);
110 item.increaseUsageCount();
111
112 this.items.values().stream()
113 .filter(it -> !it.getKey().equals(item.getKey()))
114 .forEach(SelectorItem::decreaseUsageCount);
115
116 filterItems();
117
118 return Optional.of(item);
119 }
120
121 /**
122 * Adds a new historic item to the list. The key has form 'history {current date}'.
123 * Note, the item is not saved if there is already a historic item with the same query.
124 * @param query The query of the item.
125 * @exception IllegalArgumentException if the query is empty.
126 * @exception NullPointerException if the query is {@code null}.
127 */
128 public synchronized void saveHistoricItem(String query) {
129 boolean historicExist = this.items.values().stream()
130 .filter(it -> it.getKey().contains("history"))
131 .map(SelectorItem::getQuery)
132 .anyMatch(q -> q.equals(query));
133
134 if (!historicExist) {
135 SelectorItem item = new SelectorItem(
136 "history " + LocalDateTime.now().format(this.format),
137 query);
138
139 this.items.put(item.getKey(), item);
140
141 savePreferences();
142 filterItems();
143 }
144 }
145
146 /**
147 * Removes currently selected item, saves the current state to preferences and
148 * updates the view.
149 */
150 private synchronized void removeSelectedItem() {
151 Optional<SelectorItem> it = this.getSelectedItem();
152
153 if (!it.isPresent()) {
154 JOptionPane.showMessageDialog(
155 componentParent,
156 tr("Please select an item first"));
157 return;
158 }
159
160 SelectorItem item = it.get();
161 if (this.items.remove(item.getKey(), item)) {
162 savePreferences();
163 filterItems();
164 }
165 }
166
167 /**
168 * Opens {@link EditItemDialog} for the selected item, saves the current state
169 * to preferences and updates the view.
170 */
171 private synchronized void editSelectedItem() {
172 Optional<SelectorItem> it = this.getSelectedItem();
173
174 if (!it.isPresent()) {
175 JOptionPane.showMessageDialog(
176 componentParent,
177 tr("Please select an item first"));
178 return;
179 }
180
181 SelectorItem item = it.get();
182
183 EditItemDialog dialog = new EditItemDialog(
184 componentParent,
185 tr("Edit item"),
186 item.getKey(),
187 item.getQuery(),
188 new String[] {tr("Save")});
189 dialog.showDialog();
190
191 Optional<SelectorItem> editedItem = dialog.getOutputItem();
192 editedItem.ifPresent(i -> {
193 this.items.remove(item.getKey(), item);
194 this.items.put(i.getKey(), i);
195
196 savePreferences();
197 filterItems();
198 });
199 }
200
201 /**
202 * Opens {@link EditItemDialog}, saves the state to preferences if a new item is added
203 * and updates the view.
204 */
205 private synchronized void createNewItem() {
206 EditItemDialog dialog = new EditItemDialog(componentParent, tr("Add snippet"), tr("Add"));
207 dialog.showDialog();
208
209 Optional<SelectorItem> newItem = dialog.getOutputItem();
210 newItem.ifPresent(i -> {
211 items.put(i.getKey(), new SelectorItem(i.getKey(), i.getQuery()));
212 savePreferences();
213 filterItems();
214 });
215 }
216
217 @Override
218 public void setDblClickListener(ActionListener dblClickListener) {
219 // this listener is already set within this class
220 }
221
222 @Override
223 protected void filterItems() {
224 String text = edSearchText.getText().toLowerCase(Locale.ENGLISH);
225
226 super.lsResultModel.setItems(this.items.values().stream()
227 .filter(item -> item.getKey().contains(text))
228 .collect(Collectors.toList()));
229 }
230
231 private void getDblClickListener(ActionEvent e) {
232 Optional<SelectorItem> selectedItem = this.getSelectedItem();
233
234 if (!selectedItem.isPresent()) {
235 return;
236 }
237
238 SelectorItem item = selectedItem.get();
239 this.target.setText(item.getQuery());
240 }
241
242 /**
243 * Saves all elements from the list to {@link Main#pref}.
244 */
245 private void savePreferences() {
246 Collection<Map<String, String>> toSave = new ArrayList<>(this.items.size());
247 for (SelectorItem item : this.items.values()) {
248 Map<String, String> it = new HashMap<>();
249 it.put(KEY_KEY, item.getKey());
250 it.put(QUERY_KEY, item.getQuery());
251 it.put(USE_COUNT_KEY, Integer.toString(item.getUsageCount()));
252
253 toSave.add(it);
254 }
255
256 Main.pref.putListOfStructs(PREFERENCE_ITEMS, toSave);
257 }
258
259 /**
260 * Loads the user saved items from {@link Main#pref}.
261 * @return A set of the user saved items.
262 */
263 private Map<String, SelectorItem> restorePreferences() {
264 Collection<Map<String, String>> toRetrieve =
265 Main.pref.getListOfStructs(PREFERENCE_ITEMS, Collections.emptyList());
266 Map<String, SelectorItem> result = new HashMap<>();
267
268 for (Map<String, String> entry : toRetrieve) {
269 String key = entry.get(KEY_KEY);
270 String query = entry.get(QUERY_KEY);
271 int usageCount = Integer.parseInt(entry.get(USE_COUNT_KEY));
272
273 result.put(key, new SelectorItem(key, query, usageCount));
274 }
275
276 return result;
277 }
278
279 private class OverpassQueryListMouseAdapter extends MouseAdapter {
280
281 private final JList list;
282 private final ResultListModel model;
283 private final JPopupMenu emptySelectionPopup = new JPopupMenu();
284 private final JPopupMenu elementPopup = new JPopupMenu();
285 private final JPopupMenu queryLookup = new JPopupMenu();
286
287 OverpassQueryListMouseAdapter(JList list, ResultListModel listModel) {
288 this.list = list;
289 this.model = listModel;
290
291 this.initPopupMenus();
292 }
293
294 /*
295 * Do not select the closest element if the user clicked on
296 * an empty area within the list.
297 */
298 private int locationToIndex(Point p) {
299 int idx = list.locationToIndex(p);
300
301 if (idx != -1 && !list.getCellBounds(idx, idx).contains(p)) {
302 return -1;
303 } else {
304 return idx;
305 }
306 }
307
308 @Override
309 public void mouseClicked(MouseEvent e) {
310 super.mouseClicked(e);
311 if (SwingUtilities.isRightMouseButton(e)) {
312 int index = locationToIndex(e.getPoint());
313
314 if (model.getSize() == 0 || index == -1) {
315 list.clearSelection();
316 emptySelectionPopup.show(list, e.getX(), e.getY());
317 } else {
318 list.setSelectedIndex(index);
319 list.ensureIndexIsVisible(index);
320 elementPopup.show(list, e.getX(), e.getY());
321 }
322 }
323 }
324
325 @Override
326 public void mouseMoved(MouseEvent e) {
327 super.mouseMoved(e);
328 int idx = locationToIndex(e.getPoint());
329 if (idx == -1) {
330 return;
331 }
332
333 SelectorItem item = (SelectorItem) model.getElementAt(idx);
334 list.setToolTipText("<html><pre style='width:300px;'>" +
335 Utils.escapeReservedCharactersHTML(Utils.restrictStringLines(item.getQuery(), 9)));
336 }
337
338 private void initPopupMenus() {
339 AbstractAction add = new AbstractAction(tr("Add")) {
340 @Override
341 public void actionPerformed(ActionEvent e) {
342 createNewItem();
343 }
344 };
345 AbstractAction edit = new AbstractAction(tr("Edit")) {
346 @Override
347 public void actionPerformed(ActionEvent e) {
348 editSelectedItem();
349 }
350 };
351 AbstractAction remove = new AbstractAction(tr("Remove")) {
352 @Override
353 public void actionPerformed(ActionEvent e) {
354 removeSelectedItem();
355 }
356 };
357 this.emptySelectionPopup.add(add);
358 this.elementPopup.add(add);
359 this.elementPopup.add(edit);
360 this.elementPopup.add(remove);
361 }
362 }
363
364 /**
365 * This class defines the way each element is rendered in the list.
366 */
367 private static class OverpassQueryCellRendered extends JLabel implements ListCellRenderer<SelectorItem> {
368
369 OverpassQueryCellRendered() {
370 setOpaque(true);
371 }
372
373 @Override
374 public Component getListCellRendererComponent(
375 JList<? extends SelectorItem> list,
376 SelectorItem value,
377 int index,
378 boolean isSelected,
379 boolean cellHasFocus) {
380
381 Font font = list.getFont();
382 if (isSelected) {
383 setFont(new Font(font.getFontName(), Font.BOLD, font.getSize() + 2));
384 setBackground(list.getSelectionBackground());
385 setForeground(list.getSelectionForeground());
386 } else {
387 setFont(new Font(font.getFontName(), Font.PLAIN, font.getSize() + 2));
388 setBackground(list.getBackground());
389 setForeground(list.getForeground());
390 }
391
392 setEnabled(list.isEnabled());
393 setText(value.getKey());
394
395 if (isSelected && cellHasFocus) {
396 setBorder(new CompoundBorder(
397 BorderFactory.createLineBorder(Color.BLACK, 1),
398 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
399 } else {
400 setBorder(new CompoundBorder(
401 null,
402 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
403 }
404
405 return this;
406 }
407 }
408
409 /**
410 * Dialog that provides functionality to add/edit an item from the list.
411 */
412 private final class EditItemDialog extends ExtendedDialog {
413
414 private final JTextField name;
415 private final JosmTextArea query;
416 private final int initialNameHash;
417
418 private final transient AbstractTextComponentValidator queryValidator;
419 private final transient AbstractTextComponentValidator nameValidator;
420
421 private static final int SUCCESS_BTN = 0;
422 private static final int CANCEL_BTN = 1;
423
424 /**
425 * Added/Edited object to be returned. If {@link Optional#empty()} then probably
426 * the user closed the dialog, otherwise {@link SelectorItem} is present.
427 */
428 private transient Optional<SelectorItem> outputItem = Optional.empty();
429
430 EditItemDialog(Component parent, String title, String... buttonTexts) {
431 this(parent, title, "", "", buttonTexts);
432 }
433
434 EditItemDialog(
435 Component parent,
436 String title,
437 String nameToEdit,
438 String queryToEdit,
439 String... buttonTexts) {
440 super(parent, title, buttonTexts);
441
442 this.initialNameHash = nameToEdit.hashCode();
443
444 this.name = new JTextField(nameToEdit);
445 this.query = new JosmTextArea(queryToEdit);
446
447 this.queryValidator = new DefaultTextComponentValidator(this.query, "", tr("Query cannot be empty"));
448 this.nameValidator = new AbstractTextComponentValidator(this.name) {
449 @Override
450 public void validate() {
451 if (isValid()) {
452 feedbackValid(tr("This name can be used for the item"));
453 } else {
454 feedbackInvalid(tr("Item with this name already exists"));
455 }
456 }
457
458 @Override
459 public boolean isValid() {
460 String currentName = name.getText();
461 int currentHash = currentName.hashCode();
462
463 return !Utils.isStripEmpty(currentName) &&
464 !(currentHash != initialNameHash &&
465 items.containsKey(currentName));
466 }
467 };
468
469 this.name.getDocument().addDocumentListener(this.nameValidator);
470 this.query.getDocument().addDocumentListener(this.queryValidator);
471
472 JPanel panel = new JPanel(new GridBagLayout());
473 JScrollPane queryScrollPane = GuiHelper.embedInVerticalScrollPane(this.query);
474 queryScrollPane.getVerticalScrollBar().setUnitIncrement(10); // make scrolling smooth
475
476 GBC constraint = GBC.eol().insets(8, 0, 8, 8).anchor(GBC.CENTER).fill(GBC.HORIZONTAL);
477 constraint.ipady = 250;
478 panel.add(this.name, GBC.eol().insets(5).anchor(GBC.SOUTHEAST).fill(GBC.HORIZONTAL));
479 panel.add(queryScrollPane, constraint);
480
481 setDefaultButton(SUCCESS_BTN);
482 setCancelButton(CANCEL_BTN);
483 setPreferredSize(new Dimension(400, 400));
484 setContent(panel, false);
485 }
486
487 /**
488 * Gets a new {@link SelectorItem} if one was created/modified.
489 * @return A {@link SelectorItem} object created out of the fields of the dialog.
490 */
491 public Optional<SelectorItem> getOutputItem() {
492 return this.outputItem;
493 }
494
495 @Override
496 protected void buttonAction(int buttonIndex, ActionEvent evt) {
497 if (buttonIndex == SUCCESS_BTN) {
498 if (!this.nameValidator.isValid()) {
499 JOptionPane.showMessageDialog(
500 componentParent,
501 tr("The item cannot be created with provided name"),
502 tr("Warning"),
503 JOptionPane.WARNING_MESSAGE);
504 } else if (!this.queryValidator.isValid()) {
505 JOptionPane.showMessageDialog(
506 componentParent,
507 tr("The item cannot be created with an empty query"),
508 tr("Warning"),
509 JOptionPane.WARNING_MESSAGE);
510 } else {
511 this.outputItem = Optional.of(new SelectorItem(this.name.getText(), this.query.getText()));
512 super.buttonAction(buttonIndex, evt);
513 }
514 } else {
515 super.buttonAction(buttonIndex, evt);
516 }
517 }
518 }
519
520 /**
521 * This class represents an Overpass query used by the user that can be
522 * shown within {@link OverpassQueryList}.
523 */
524 public static class SelectorItem {
525 private final String itemKey;
526 private final String query;
527 private int usageCount;
528
529 /**
530 * Constructs a new {@code SelectorItem}.
531 * @param key The key of this item.
532 * @param query The query of the item.
533 * @exception NullPointerException if any parameter is {@code null}.
534 * @exception IllegalArgumentException if any parameter is empty.
535 */
536 public SelectorItem(String key, String query) {
537 this(key, query, 1);
538 }
539
540 /**
541 * Constructs a new {@code SelectorItem}.
542 * @param key The key of this item.
543 * @param query The query of the item.
544 * @param usageCount The number of times this query was used.
545 * @exception NullPointerException if any parameter is {@code null}.
546 * @exception IllegalArgumentException if any parameter is empty.
547 */
548 public SelectorItem(String key, String query, int usageCount) {
549 Objects.requireNonNull(key);
550 Objects.requireNonNull(query);
551
552 if (Utils.isStripEmpty(key)) {
553 throw new IllegalArgumentException("The key of the item cannot be empty");
554 }
555 if (Utils.isStripEmpty(query)) {
556 throw new IllegalArgumentException("The query cannot be empty");
557 }
558
559 this.itemKey = key;
560 this.query = query;
561 this.usageCount = usageCount;
562 }
563
564 /**
565 * Gets the key (a string that is displayed in the selector) of this item.
566 * @return A string representing the key of this item.
567 */
568 public String getKey() {
569 return this.itemKey;
570 }
571
572 /**
573 * Gets the overpass query of this item.
574 * @return A string representing the overpass query of this item.
575 */
576 public String getQuery() {
577 return this.query;
578 }
579
580 /**
581 * Gets the number of times the query was used by the user.
582 * @return The usage count of this item.
583 */
584 public int getUsageCount() {
585 return this.usageCount;
586 }
587
588 /**
589 * Increments the {@link SelectorItem#usageCount} by one till
590 * it reaches {@link Integer#MAX_VALUE}.
591 */
592 public void increaseUsageCount() {
593 if (this.usageCount < Integer.MAX_VALUE) {
594 this.usageCount++;
595 }
596 }
597
598 /**
599 * Decrements the {@link SelectorItem#usageCount} ny one till
600 * it reaches 0.
601 */
602 public void decreaseUsageCount() {
603 if (this.usageCount > 0) {
604 this.usageCount--;
605 }
606 }
607
608 @Override
609 public boolean equals(Object o) {
610 if (this == o) return true;
611 if (!(o instanceof SelectorItem)) return false;
612
613 SelectorItem that = (SelectorItem) o;
614
615 return itemKey.equals(that.itemKey) &&
616 query.equals(that.getKey());
617 }
618
619 @Override
620 public int hashCode() {
621 return itemKey.hashCode();
622 }
623 }
624}
Note: See TracBrowser for help on using the repository browser.