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

Last change on this file since 12841 was 12841, checked in by bastiK, 7 years ago

see #15229 - fix deprecations caused by [12840]

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