source: josm/trunk/src/org/openstreetmap/josm/gui/download/UserQueryList.java@ 13161

Last change on this file since 13161 was 12880, checked in by simon04, 7 years ago

see #15057, see #15264 - Rename OverpassQueryList to UserQueryList

This allows to use it also in the wikipedia plugin without name confusion.

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