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

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

see #15229 - use Config.getPref() wherever possible

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