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

Last change on this file since 17318 was 16629, checked in by simon04, 4 years ago

see #19334 - https://errorprone.info/bugpattern/JavaTimeDefaultTimeZone

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.ZoneId;
18import java.time.format.DateTimeFormatter;
19import java.time.format.DateTimeParseException;
20import java.util.ArrayList;
21import java.util.Collection;
22import java.util.Collections;
23import java.util.HashMap;
24import java.util.List;
25import java.util.Locale;
26import java.util.Map;
27import java.util.Objects;
28import java.util.Optional;
29import java.util.stream.Collectors;
30
31import javax.swing.AbstractAction;
32import javax.swing.BorderFactory;
33import javax.swing.JLabel;
34import javax.swing.JList;
35import javax.swing.JOptionPane;
36import javax.swing.JPanel;
37import javax.swing.JPopupMenu;
38import javax.swing.JScrollPane;
39import javax.swing.JTextField;
40import javax.swing.ListCellRenderer;
41import javax.swing.SwingUtilities;
42import javax.swing.border.CompoundBorder;
43import javax.swing.text.JTextComponent;
44
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(ZoneId.systemDefault()).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().toLowerCase(Locale.ENGLISH).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 Config#getPref}.
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 Config#getPref}.
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 if (list.isShowing()) {
327 emptySelectionPopup.show(list, e.getX(), e.getY());
328 }
329 } else {
330 list.setSelectedIndex(index);
331 list.ensureIndexIsVisible(index);
332 if (list.isShowing()) {
333 elementPopup.show(list, e.getX(), e.getY());
334 }
335 }
336 }
337 }
338
339 @Override
340 public void mouseMoved(MouseEvent e) {
341 super.mouseMoved(e);
342 int idx = locationToIndex(e.getPoint());
343 if (idx == -1) {
344 return;
345 }
346
347 SelectorItem item = model.getElementAt(idx);
348 list.setToolTipText("<html><pre style='width:300px;'>" +
349 Utils.escapeReservedCharactersHTML(Utils.restrictStringLines(item.getQuery(), 9)));
350 }
351
352 private void initPopupMenus() {
353 AbstractAction add = new AbstractAction(tr("Add")) {
354 @Override
355 public void actionPerformed(ActionEvent e) {
356 createNewItem();
357 }
358 };
359 AbstractAction edit = new AbstractAction(tr("Edit")) {
360 @Override
361 public void actionPerformed(ActionEvent e) {
362 editSelectedItem();
363 }
364 };
365 AbstractAction remove = new AbstractAction(tr("Remove")) {
366 @Override
367 public void actionPerformed(ActionEvent e) {
368 removeSelectedItem();
369 }
370 };
371 this.emptySelectionPopup.add(add);
372 this.elementPopup.add(add);
373 this.elementPopup.add(edit);
374 this.elementPopup.add(remove);
375 }
376 }
377
378 /**
379 * This class defines the way each element is rendered in the list.
380 */
381 private static class QueryCellRendered extends JLabel implements ListCellRenderer<SelectorItem> {
382
383 QueryCellRendered() {
384 setOpaque(true);
385 }
386
387 @Override
388 public Component getListCellRendererComponent(
389 JList<? extends SelectorItem> list,
390 SelectorItem value,
391 int index,
392 boolean isSelected,
393 boolean cellHasFocus) {
394
395 Font font = list.getFont();
396 if (isSelected) {
397 setFont(new Font(font.getFontName(), Font.BOLD, font.getSize() + 2));
398 setBackground(list.getSelectionBackground());
399 setForeground(list.getSelectionForeground());
400 } else {
401 setFont(new Font(font.getFontName(), Font.PLAIN, font.getSize() + 2));
402 setBackground(list.getBackground());
403 setForeground(list.getForeground());
404 }
405
406 setEnabled(list.isEnabled());
407 setText(value.getKey());
408
409 if (isSelected && cellHasFocus) {
410 setBorder(new CompoundBorder(
411 BorderFactory.createLineBorder(Color.BLACK, 1),
412 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
413 } else {
414 setBorder(new CompoundBorder(
415 null,
416 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
417 }
418
419 return this;
420 }
421 }
422
423 /**
424 * Dialog that provides functionality to add/edit an item from the list.
425 */
426 private final class EditItemDialog extends ExtendedDialog {
427
428 private final JTextField name;
429 private final JosmTextArea query;
430
431 private final transient AbstractTextComponentValidator queryValidator;
432 private final transient AbstractTextComponentValidator nameValidator;
433
434 private static final int SUCCESS_BTN = 0;
435 private static final int CANCEL_BTN = 1;
436
437 private final transient SelectorItem itemToEdit;
438
439 /**
440 * Added/Edited object to be returned. If {@link Optional#empty()} then probably
441 * the user closed the dialog, otherwise {@link SelectorItem} is present.
442 */
443 private transient Optional<SelectorItem> outputItem = Optional.empty();
444
445 EditItemDialog(Component parent, String title, String... buttonTexts) {
446 this(parent, title, null, buttonTexts);
447 }
448
449 EditItemDialog(
450 Component parent,
451 String title,
452 SelectorItem itemToEdit,
453 String... buttonTexts) {
454 super(parent, title, buttonTexts);
455
456 this.itemToEdit = itemToEdit;
457
458 String nameToEdit = itemToEdit == null ? "" : itemToEdit.getKey();
459 String queryToEdit = itemToEdit == null ? "" : itemToEdit.getQuery();
460
461 this.name = new JTextField(nameToEdit);
462 this.query = new JosmTextArea(queryToEdit);
463
464 this.queryValidator = new DefaultTextComponentValidator(this.query, "", tr("Query cannot be empty"));
465 this.nameValidator = new AbstractTextComponentValidator(this.name) {
466 @Override
467 public void validate() {
468 if (isValid()) {
469 feedbackValid(tr("This name can be used for the item"));
470 } else {
471 feedbackInvalid(tr("Item with this name already exists"));
472 }
473 }
474
475 @Override
476 public boolean isValid() {
477 String currentName = name.getText();
478
479 boolean notEmpty = !Utils.isStripEmpty(currentName);
480 boolean exist = !currentName.equals(nameToEdit) &&
481 items.containsKey(currentName);
482
483 return notEmpty && !exist;
484 }
485 };
486
487 this.name.getDocument().addDocumentListener(this.nameValidator);
488 this.query.getDocument().addDocumentListener(this.queryValidator);
489
490 JPanel panel = new JPanel(new GridBagLayout());
491 JScrollPane queryScrollPane = GuiHelper.embedInVerticalScrollPane(this.query);
492 queryScrollPane.getVerticalScrollBar().setUnitIncrement(10); // make scrolling smooth
493
494 GBC constraint = GBC.eol().insets(8, 0, 8, 8).anchor(GBC.CENTER).fill(GBC.HORIZONTAL);
495 constraint.ipady = 250;
496 panel.add(this.name, GBC.eol().insets(5).anchor(GBC.SOUTHEAST).fill(GBC.HORIZONTAL));
497 panel.add(queryScrollPane, constraint);
498
499 setDefaultButton(SUCCESS_BTN + 1);
500 setCancelButton(CANCEL_BTN + 1);
501 setPreferredSize(new Dimension(400, 400));
502 setContent(panel, false);
503 }
504
505 /**
506 * Gets a new {@link SelectorItem} if one was created/modified.
507 * @return A {@link SelectorItem} object created out of the fields of the dialog.
508 */
509 public Optional<SelectorItem> getOutputItem() {
510 return this.outputItem;
511 }
512
513 @Override
514 protected void buttonAction(int buttonIndex, ActionEvent evt) {
515 if (buttonIndex == SUCCESS_BTN) {
516 if (!this.nameValidator.isValid()) {
517 JOptionPane.showMessageDialog(
518 componentParent,
519 tr("The item cannot be created with provided name"),
520 tr("Warning"),
521 JOptionPane.WARNING_MESSAGE);
522
523 return;
524 } else if (!this.queryValidator.isValid()) {
525 JOptionPane.showMessageDialog(
526 componentParent,
527 tr("The item cannot be created with an empty query"),
528 tr("Warning"),
529 JOptionPane.WARNING_MESSAGE);
530
531 return;
532 } else if (this.itemToEdit != null) { // editing the item
533 String newKey = this.name.getText();
534 String newQuery = this.query.getText();
535
536 String itemKey = this.itemToEdit.getKey();
537 String itemQuery = this.itemToEdit.getQuery();
538
539 this.outputItem = Optional.of(new SelectorItem(
540 this.name.getText(),
541 this.query.getText(),
542 !newKey.equals(itemKey) || !newQuery.equals(itemQuery)
543 ? LocalDateTime.now(ZoneId.systemDefault())
544 : this.itemToEdit.getLastEdit()));
545
546 } else { // creating new
547 this.outputItem = Optional.of(new SelectorItem(
548 this.name.getText(),
549 this.query.getText()));
550 }
551 }
552
553 super.buttonAction(buttonIndex, evt);
554 }
555 }
556
557 /**
558 * This class represents an Overpass query used by the user that can be
559 * shown within {@link UserQueryList}.
560 */
561 public static class SelectorItem {
562 private final String itemKey;
563 private final String query;
564 private final LocalDateTime lastEdit;
565
566 /**
567 * Constructs a new {@code SelectorItem}.
568 * @param key The key of this item.
569 * @param query The query of the item.
570 * @exception NullPointerException if any parameter is {@code null}.
571 * @exception IllegalArgumentException if any parameter is empty.
572 */
573 public SelectorItem(String key, String query) {
574 this(key, query, LocalDateTime.now(ZoneId.systemDefault()));
575 }
576
577 /**
578 * Constructs a new {@code SelectorItem}.
579 * @param key The key of this item.
580 * @param query The query of the item.
581 * @param lastEdit The latest when the item was
582 * @exception NullPointerException if any parameter is {@code null}.
583 * @exception IllegalArgumentException if any parameter is empty.
584 */
585 public SelectorItem(String key, String query, LocalDateTime lastEdit) {
586 Objects.requireNonNull(key, "The name of the item cannot be null");
587 Objects.requireNonNull(query, "The query of the item cannot be null");
588 Objects.requireNonNull(lastEdit, "The last edit date time cannot be null");
589
590 if (Utils.isStripEmpty(key)) {
591 throw new IllegalArgumentException("The key of the item cannot be empty");
592 }
593 if (Utils.isStripEmpty(query)) {
594 throw new IllegalArgumentException("The query cannot be empty");
595 }
596
597 this.itemKey = key;
598 this.query = query;
599 this.lastEdit = lastEdit;
600 }
601
602 /**
603 * Gets the key (a string that is displayed in the selector) of this item.
604 * @return A string representing the key of this item.
605 */
606 public String getKey() {
607 return this.itemKey;
608 }
609
610 /**
611 * Gets the query of this item.
612 * @return A string representing the query of this item.
613 */
614 public String getQuery() {
615 return this.query;
616 }
617
618 /**
619 * Gets the latest date time when the item was created/changed.
620 * @return The latest date time when the item was created/changed.
621 */
622 public LocalDateTime getLastEdit() {
623 return lastEdit;
624 }
625
626 @Override
627 public int hashCode() {
628 return Objects.hash(itemKey, query);
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.