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

Last change on this file since 12577 was 12577, checked in by Don-vip, 7 years ago

see #15057 - fix PMD issue

File size: 21.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.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 static 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 = restorePreferences();
88
89 OverpassQueryListMouseAdapter mouseHandler = new OverpassQueryListMouseAdapter(lsResult, lsResultModel);
90 super.lsResult.setCellRenderer(new OverpassQueryCellRendered());
91 super.setDblClickListener(e -> doubleClickEvent());
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(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 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 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 static 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<SelectorItem> list;
282 private final ResultListModel<SelectorItem> model;
283 private final JPopupMenu emptySelectionPopup = new JPopupMenu();
284 private final JPopupMenu elementPopup = new JPopupMenu();
285
286 OverpassQueryListMouseAdapter(JList<SelectorItem> list, ResultListModel<SelectorItem> listModel) {
287 this.list = list;
288 this.model = listModel;
289
290 this.initPopupMenus();
291 }
292
293 /*
294 * Do not select the closest element if the user clicked on
295 * an empty area within the list.
296 */
297 private int locationToIndex(Point p) {
298 int idx = list.locationToIndex(p);
299
300 if (idx != -1 && !list.getCellBounds(idx, idx).contains(p)) {
301 return -1;
302 } else {
303 return idx;
304 }
305 }
306
307 @Override
308 public void mouseClicked(MouseEvent e) {
309 super.mouseClicked(e);
310 if (SwingUtilities.isRightMouseButton(e)) {
311 int index = locationToIndex(e.getPoint());
312
313 if (model.getSize() == 0 || index == -1) {
314 list.clearSelection();
315 emptySelectionPopup.show(list, e.getX(), e.getY());
316 } else {
317 list.setSelectedIndex(index);
318 list.ensureIndexIsVisible(index);
319 elementPopup.show(list, e.getX(), e.getY());
320 }
321 }
322 }
323
324 @Override
325 public void mouseMoved(MouseEvent e) {
326 super.mouseMoved(e);
327 int idx = locationToIndex(e.getPoint());
328 if (idx == -1) {
329 return;
330 }
331
332 SelectorItem item = model.getElementAt(idx);
333 list.setToolTipText("<html><pre style='width:300px;'>" +
334 Utils.escapeReservedCharactersHTML(Utils.restrictStringLines(item.getQuery(), 9)));
335 }
336
337 private void initPopupMenus() {
338 AbstractAction add = new AbstractAction(tr("Add")) {
339 @Override
340 public void actionPerformed(ActionEvent e) {
341 createNewItem();
342 }
343 };
344 AbstractAction edit = new AbstractAction(tr("Edit")) {
345 @Override
346 public void actionPerformed(ActionEvent e) {
347 editSelectedItem();
348 }
349 };
350 AbstractAction remove = new AbstractAction(tr("Remove")) {
351 @Override
352 public void actionPerformed(ActionEvent e) {
353 removeSelectedItem();
354 }
355 };
356 this.emptySelectionPopup.add(add);
357 this.elementPopup.add(add);
358 this.elementPopup.add(edit);
359 this.elementPopup.add(remove);
360 }
361 }
362
363 /**
364 * This class defines the way each element is rendered in the list.
365 */
366 private static class OverpassQueryCellRendered extends JLabel implements ListCellRenderer<SelectorItem> {
367
368 OverpassQueryCellRendered() {
369 setOpaque(true);
370 }
371
372 @Override
373 public Component getListCellRendererComponent(
374 JList<? extends SelectorItem> list,
375 SelectorItem value,
376 int index,
377 boolean isSelected,
378 boolean cellHasFocus) {
379
380 Font font = list.getFont();
381 if (isSelected) {
382 setFont(new Font(font.getFontName(), Font.BOLD, font.getSize() + 2));
383 setBackground(list.getSelectionBackground());
384 setForeground(list.getSelectionForeground());
385 } else {
386 setFont(new Font(font.getFontName(), Font.PLAIN, font.getSize() + 2));
387 setBackground(list.getBackground());
388 setForeground(list.getForeground());
389 }
390
391 setEnabled(list.isEnabled());
392 setText(value.getKey());
393
394 if (isSelected && cellHasFocus) {
395 setBorder(new CompoundBorder(
396 BorderFactory.createLineBorder(Color.BLACK, 1),
397 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
398 } else {
399 setBorder(new CompoundBorder(
400 null,
401 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
402 }
403
404 return this;
405 }
406 }
407
408 /**
409 * Dialog that provides functionality to add/edit an item from the list.
410 */
411 private final class EditItemDialog extends ExtendedDialog {
412
413 private final JTextField name;
414 private final JosmTextArea query;
415 private final int initialNameHash;
416
417 private final transient AbstractTextComponentValidator queryValidator;
418 private final transient AbstractTextComponentValidator nameValidator;
419
420 private static final int SUCCESS_BTN = 0;
421 private static final int CANCEL_BTN = 1;
422
423 /**
424 * Added/Edited object to be returned. If {@link Optional#empty()} then probably
425 * the user closed the dialog, otherwise {@link SelectorItem} is present.
426 */
427 private transient Optional<SelectorItem> outputItem = Optional.empty();
428
429 EditItemDialog(Component parent, String title, String... buttonTexts) {
430 this(parent, title, "", "", buttonTexts);
431 }
432
433 EditItemDialog(
434 Component parent,
435 String title,
436 String nameToEdit,
437 String queryToEdit,
438 String... buttonTexts) {
439 super(parent, title, buttonTexts);
440
441 this.initialNameHash = nameToEdit.hashCode();
442
443 this.name = new JTextField(nameToEdit);
444 this.query = new JosmTextArea(queryToEdit);
445
446 this.queryValidator = new DefaultTextComponentValidator(this.query, "", tr("Query cannot be empty"));
447 this.nameValidator = new AbstractTextComponentValidator(this.name) {
448 @Override
449 public void validate() {
450 if (isValid()) {
451 feedbackValid(tr("This name can be used for the item"));
452 } else {
453 feedbackInvalid(tr("Item with this name already exists"));
454 }
455 }
456
457 @Override
458 public boolean isValid() {
459 String currentName = name.getText();
460 int currentHash = currentName.hashCode();
461
462 return !Utils.isStripEmpty(currentName) &&
463 !(currentHash != initialNameHash &&
464 items.containsKey(currentName));
465 }
466 };
467
468 this.name.getDocument().addDocumentListener(this.nameValidator);
469 this.query.getDocument().addDocumentListener(this.queryValidator);
470
471 JPanel panel = new JPanel(new GridBagLayout());
472 JScrollPane queryScrollPane = GuiHelper.embedInVerticalScrollPane(this.query);
473 queryScrollPane.getVerticalScrollBar().setUnitIncrement(10); // make scrolling smooth
474
475 GBC constraint = GBC.eol().insets(8, 0, 8, 8).anchor(GBC.CENTER).fill(GBC.HORIZONTAL);
476 constraint.ipady = 250;
477 panel.add(this.name, GBC.eol().insets(5).anchor(GBC.SOUTHEAST).fill(GBC.HORIZONTAL));
478 panel.add(queryScrollPane, constraint);
479
480 setDefaultButton(SUCCESS_BTN);
481 setCancelButton(CANCEL_BTN);
482 setPreferredSize(new Dimension(400, 400));
483 setContent(panel, false);
484 }
485
486 /**
487 * Gets a new {@link SelectorItem} if one was created/modified.
488 * @return A {@link SelectorItem} object created out of the fields of the dialog.
489 */
490 public Optional<SelectorItem> getOutputItem() {
491 return this.outputItem;
492 }
493
494 @Override
495 protected void buttonAction(int buttonIndex, ActionEvent evt) {
496 if (buttonIndex == SUCCESS_BTN) {
497 if (!this.nameValidator.isValid()) {
498 JOptionPane.showMessageDialog(
499 componentParent,
500 tr("The item cannot be created with provided name"),
501 tr("Warning"),
502 JOptionPane.WARNING_MESSAGE);
503 } else if (!this.queryValidator.isValid()) {
504 JOptionPane.showMessageDialog(
505 componentParent,
506 tr("The item cannot be created with an empty query"),
507 tr("Warning"),
508 JOptionPane.WARNING_MESSAGE);
509 } else {
510 this.outputItem = Optional.of(new SelectorItem(this.name.getText(), this.query.getText()));
511 super.buttonAction(buttonIndex, evt);
512 }
513 } else {
514 super.buttonAction(buttonIndex, evt);
515 }
516 }
517 }
518
519 /**
520 * This class represents an Overpass query used by the user that can be
521 * shown within {@link OverpassQueryList}.
522 */
523 public static class SelectorItem {
524 private final String itemKey;
525 private final String query;
526 private int usageCount;
527
528 /**
529 * Constructs a new {@code SelectorItem}.
530 * @param key The key of this item.
531 * @param query The query of the item.
532 * @exception NullPointerException if any parameter is {@code null}.
533 * @exception IllegalArgumentException if any parameter is empty.
534 */
535 public SelectorItem(String key, String query) {
536 this(key, query, 1);
537 }
538
539 /**
540 * Constructs a new {@code SelectorItem}.
541 * @param key The key of this item.
542 * @param query The query of the item.
543 * @param usageCount The number of times this query was used.
544 * @exception NullPointerException if any parameter is {@code null}.
545 * @exception IllegalArgumentException if any parameter is empty.
546 */
547 public SelectorItem(String key, String query, int usageCount) {
548 Objects.requireNonNull(key);
549 Objects.requireNonNull(query);
550
551 if (Utils.isStripEmpty(key)) {
552 throw new IllegalArgumentException("The key of the item cannot be empty");
553 }
554 if (Utils.isStripEmpty(query)) {
555 throw new IllegalArgumentException("The query cannot be empty");
556 }
557
558 this.itemKey = key;
559 this.query = query;
560 this.usageCount = usageCount;
561 }
562
563 /**
564 * Gets the key (a string that is displayed in the selector) of this item.
565 * @return A string representing the key of this item.
566 */
567 public String getKey() {
568 return this.itemKey;
569 }
570
571 /**
572 * Gets the overpass query of this item.
573 * @return A string representing the overpass query of this item.
574 */
575 public String getQuery() {
576 return this.query;
577 }
578
579 /**
580 * Gets the number of times the query was used by the user.
581 * @return The usage count of this item.
582 */
583 public int getUsageCount() {
584 return this.usageCount;
585 }
586
587 /**
588 * Increments the {@link SelectorItem#usageCount} by one till
589 * it reaches {@link Integer#MAX_VALUE}.
590 */
591 public void increaseUsageCount() {
592 if (this.usageCount < Integer.MAX_VALUE) {
593 this.usageCount++;
594 }
595 }
596
597 /**
598 * Decrements the {@link SelectorItem#usageCount} ny one till
599 * it reaches 0.
600 */
601 public void decreaseUsageCount() {
602 if (this.usageCount > 0) {
603 this.usageCount--;
604 }
605 }
606
607 @Override
608 public int hashCode() {
609 final int prime = 31;
610 int result = 1;
611 result = prime * result + ((itemKey == null) ? 0 : itemKey.hashCode());
612 result = prime * result + ((query == null) ? 0 : query.hashCode());
613 return result;
614 }
615
616 @Override
617 public boolean equals(Object obj) {
618 if (this == obj) {
619 return true;
620 }
621 if (obj == null) {
622 return false;
623 }
624 if (getClass() != obj.getClass()) {
625 return false;
626 }
627 SelectorItem other = (SelectorItem) obj;
628 if (itemKey == null) {
629 if (other.itemKey != null) {
630 return false;
631 }
632 } else if (!itemKey.equals(other.itemKey)) {
633 return false;
634 }
635 if (query == null) {
636 if (other.query != null) {
637 return false;
638 }
639 } else if (!query.equals(other.query)) {
640 return false;
641 }
642 return true;
643 }
644 }
645}
Note: See TracBrowser for help on using the repository browser.