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

Last change on this file since 12620 was 12620, checked in by Don-vip, 2 months ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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