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

Last change on this file since 12574 was 12574, checked in by michael2402, 13 days ago

Apply #15057: Improve the over pass turbo dialog

Adds the ability to add favorites and a new wizard dialog with examples.

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