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

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

fix some Sonar issues

File size: 22.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.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,
187 tr("Save"));
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 private 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(), new SelectorItem(i.getKey(), i.getQuery()));
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
225 super.lsResultModel.setItems(this.items.values().stream()
226 .filter(item -> item.getKey().contains(text))
227 .collect(Collectors.toList()));
228 }
229
230 private void doubleClickEvent() {
231 Optional<SelectorItem> selectedItem = this.getSelectedItem();
232
233 if (!selectedItem.isPresent()) {
234 return;
235 }
236
237 SelectorItem item = selectedItem.get();
238 this.target.setText(item.getQuery());
239 }
240
241 /**
242 * Saves all elements from the list to {@link Main#pref}.
243 */
244 private void savePreferences() {
245 Collection<Map<String, String>> toSave = new ArrayList<>(this.items.size());
246 for (SelectorItem item : this.items.values()) {
247 Map<String, String> it = new HashMap<>();
248 it.put(KEY_KEY, item.getKey());
249 it.put(QUERY_KEY, item.getQuery());
250 it.put(USE_COUNT_KEY, Integer.toString(item.getUsageCount()));
251
252 toSave.add(it);
253 }
254
255 Main.pref.putListOfStructs(PREFERENCE_ITEMS, toSave);
256 }
257
258 /**
259 * Loads the user saved items from {@link Main#pref}.
260 * @return A set of the user saved items.
261 */
262 private static Map<String, SelectorItem> restorePreferences() {
263 Collection<Map<String, String>> toRetrieve =
264 Main.pref.getListOfStructs(PREFERENCE_ITEMS, Collections.emptyList());
265 Map<String, SelectorItem> result = new HashMap<>();
266
267 for (Map<String, String> entry : toRetrieve) {
268 String key = entry.get(KEY_KEY);
269 String query = entry.get(QUERY_KEY);
270 int usageCount = Integer.parseInt(entry.get(USE_COUNT_KEY));
271
272 result.put(key, new SelectorItem(key, query, usageCount));
273 }
274
275 return result;
276 }
277
278 private class OverpassQueryListMouseAdapter extends MouseAdapter {
279
280 private final JList<SelectorItem> list;
281 private final ResultListModel<SelectorItem> model;
282 private final JPopupMenu emptySelectionPopup = new JPopupMenu();
283 private final JPopupMenu elementPopup = new JPopupMenu();
284
285 OverpassQueryListMouseAdapter(JList<SelectorItem> list, ResultListModel<SelectorItem> listModel) {
286 this.list = list;
287 this.model = listModel;
288
289 this.initPopupMenus();
290 }
291
292 /*
293 * Do not select the closest element if the user clicked on
294 * an empty area within the list.
295 */
296 private int locationToIndex(Point p) {
297 int idx = list.locationToIndex(p);
298
299 if (idx != -1 && !list.getCellBounds(idx, idx).contains(p)) {
300 return -1;
301 } else {
302 return idx;
303 }
304 }
305
306 @Override
307 public void mouseClicked(MouseEvent e) {
308 super.mouseClicked(e);
309 if (SwingUtilities.isRightMouseButton(e)) {
310 int index = locationToIndex(e.getPoint());
311
312 if (model.getSize() == 0 || index == -1) {
313 list.clearSelection();
314 emptySelectionPopup.show(list, e.getX(), e.getY());
315 } else {
316 list.setSelectedIndex(index);
317 list.ensureIndexIsVisible(index);
318 elementPopup.show(list, e.getX(), e.getY());
319 }
320 }
321 }
322
323 @Override
324 public void mouseMoved(MouseEvent e) {
325 super.mouseMoved(e);
326 int idx = locationToIndex(e.getPoint());
327 if (idx == -1) {
328 return;
329 }
330
331 SelectorItem item = model.getElementAt(idx);
332 list.setToolTipText("<html><pre style='width:300px;'>" +
333 Utils.escapeReservedCharactersHTML(Utils.restrictStringLines(item.getQuery(), 9)));
334 }
335
336 private void initPopupMenus() {
337 AbstractAction add = new AbstractAction(tr("Add")) {
338 @Override
339 public void actionPerformed(ActionEvent e) {
340 createNewItem();
341 }
342 };
343 AbstractAction edit = new AbstractAction(tr("Edit")) {
344 @Override
345 public void actionPerformed(ActionEvent e) {
346 editSelectedItem();
347 }
348 };
349 AbstractAction remove = new AbstractAction(tr("Remove")) {
350 @Override
351 public void actionPerformed(ActionEvent e) {
352 removeSelectedItem();
353 }
354 };
355 this.emptySelectionPopup.add(add);
356 this.elementPopup.add(add);
357 this.elementPopup.add(edit);
358 this.elementPopup.add(remove);
359 }
360 }
361
362 /**
363 * This class defines the way each element is rendered in the list.
364 */
365 private static class OverpassQueryCellRendered extends JLabel implements ListCellRenderer<SelectorItem> {
366
367 OverpassQueryCellRendered() {
368 setOpaque(true);
369 }
370
371 @Override
372 public Component getListCellRendererComponent(
373 JList<? extends SelectorItem> list,
374 SelectorItem value,
375 int index,
376 boolean isSelected,
377 boolean cellHasFocus) {
378
379 Font font = list.getFont();
380 if (isSelected) {
381 setFont(new Font(font.getFontName(), Font.BOLD, font.getSize() + 2));
382 setBackground(list.getSelectionBackground());
383 setForeground(list.getSelectionForeground());
384 } else {
385 setFont(new Font(font.getFontName(), Font.PLAIN, font.getSize() + 2));
386 setBackground(list.getBackground());
387 setForeground(list.getForeground());
388 }
389
390 setEnabled(list.isEnabled());
391 setText(value.getKey());
392
393 if (isSelected && cellHasFocus) {
394 setBorder(new CompoundBorder(
395 BorderFactory.createLineBorder(Color.BLACK, 1),
396 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
397 } else {
398 setBorder(new CompoundBorder(
399 null,
400 BorderFactory.createEmptyBorder(2, 0, 2, 0)));
401 }
402
403 return this;
404 }
405 }
406
407 /**
408 * Dialog that provides functionality to add/edit an item from the list.
409 */
410 private final class EditItemDialog extends ExtendedDialog {
411
412 private final JTextField name;
413 private final JosmTextArea query;
414 private final int initialNameHash;
415
416 private final transient AbstractTextComponentValidator queryValidator;
417 private final transient AbstractTextComponentValidator nameValidator;
418
419 private static final int SUCCESS_BTN = 0;
420 private static final int CANCEL_BTN = 1;
421
422 /**
423 * Added/Edited object to be returned. If {@link Optional#empty()} then probably
424 * the user closed the dialog, otherwise {@link SelectorItem} is present.
425 */
426 private transient Optional<SelectorItem> outputItem = Optional.empty();
427
428 EditItemDialog(Component parent, String title, String... buttonTexts) {
429 this(parent, title, null, buttonTexts);
430 }
431
432 EditItemDialog(
433 Component parent,
434 String title,
435 SelectorItem itemToEdit,
436 String... buttonTexts) {
437 super(parent, title, buttonTexts);
438
439 String nameToEdit = itemToEdit != null ? itemToEdit.getKey() : "";
440 String queryToEdit = itemToEdit != null ? itemToEdit.getQuery() : "";
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.