001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Component;
008import java.awt.Dimension;
009import java.awt.Font;
010import java.awt.GridBagConstraints;
011import java.awt.GridBagLayout;
012import java.awt.Insets;
013import java.awt.Rectangle;
014import java.awt.event.ActionEvent;
015import java.awt.event.FocusAdapter;
016import java.awt.event.FocusEvent;
017import java.awt.event.KeyEvent;
018import java.awt.event.MouseAdapter;
019import java.awt.event.MouseEvent;
020import java.io.BufferedReader;
021import java.io.File;
022import java.io.IOException;
023import java.net.MalformedURLException;
024import java.net.URL;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Collection;
028import java.util.Collections;
029import java.util.EventObject;
030import java.util.HashMap;
031import java.util.Iterator;
032import java.util.LinkedHashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.Set;
037import java.util.concurrent.CopyOnWriteArrayList;
038import java.util.regex.Matcher;
039import java.util.regex.Pattern;
040
041import javax.swing.AbstractAction;
042import javax.swing.BorderFactory;
043import javax.swing.Box;
044import javax.swing.DefaultListModel;
045import javax.swing.DefaultListSelectionModel;
046import javax.swing.Icon;
047import javax.swing.ImageIcon;
048import javax.swing.JButton;
049import javax.swing.JCheckBox;
050import javax.swing.JComponent;
051import javax.swing.JFileChooser;
052import javax.swing.JLabel;
053import javax.swing.JList;
054import javax.swing.JOptionPane;
055import javax.swing.JPanel;
056import javax.swing.JScrollPane;
057import javax.swing.JSeparator;
058import javax.swing.JTable;
059import javax.swing.JToolBar;
060import javax.swing.KeyStroke;
061import javax.swing.ListCellRenderer;
062import javax.swing.ListSelectionModel;
063import javax.swing.event.CellEditorListener;
064import javax.swing.event.ChangeEvent;
065import javax.swing.event.DocumentEvent;
066import javax.swing.event.DocumentListener;
067import javax.swing.event.ListSelectionEvent;
068import javax.swing.event.ListSelectionListener;
069import javax.swing.event.TableModelEvent;
070import javax.swing.event.TableModelListener;
071import javax.swing.filechooser.FileFilter;
072import javax.swing.table.AbstractTableModel;
073import javax.swing.table.DefaultTableCellRenderer;
074import javax.swing.table.TableCellEditor;
075
076import org.openstreetmap.josm.Main;
077import org.openstreetmap.josm.actions.ExtensionFileFilter;
078import org.openstreetmap.josm.data.Version;
079import org.openstreetmap.josm.gui.ExtendedDialog;
080import org.openstreetmap.josm.gui.HelpAwareOptionPane;
081import org.openstreetmap.josm.gui.PleaseWaitRunnable;
082import org.openstreetmap.josm.gui.util.FileFilterAllFiles;
083import org.openstreetmap.josm.gui.util.GuiHelper;
084import org.openstreetmap.josm.gui.util.TableHelper;
085import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
086import org.openstreetmap.josm.gui.widgets.FileChooserManager;
087import org.openstreetmap.josm.gui.widgets.JosmTextField;
088import org.openstreetmap.josm.io.CachedFile;
089import org.openstreetmap.josm.io.OnlineResource;
090import org.openstreetmap.josm.io.OsmTransferException;
091import org.openstreetmap.josm.tools.GBC;
092import org.openstreetmap.josm.tools.ImageOverlay;
093import org.openstreetmap.josm.tools.ImageProvider;
094import org.openstreetmap.josm.tools.ImageProvider.ImageSizes;
095import org.openstreetmap.josm.tools.LanguageInfo;
096import org.openstreetmap.josm.tools.Utils;
097import org.xml.sax.SAXException;
098
099public abstract class SourceEditor extends JPanel {
100
101    protected final SourceType sourceType;
102    protected final boolean canEnable;
103
104    protected final JTable tblActiveSources;
105    protected final ActiveSourcesModel activeSourcesModel;
106    protected final JList<ExtendedSourceEntry> lstAvailableSources;
107    protected final AvailableSourcesListModel availableSourcesModel;
108    protected final String availableSourcesUrl;
109    protected final transient List<SourceProvider> sourceProviders;
110
111    private JTable tblIconPaths;
112    private IconPathTableModel iconPathsModel;
113
114    protected boolean sourcesInitiallyLoaded;
115
116    /**
117     * Constructs a new {@code SourceEditor}.
118     * @param sourceType the type of source managed by this editor
119     * @param availableSourcesUrl the URL to the list of available sources
120     * @param sourceProviders the list of additional source providers, from plugins
121     * @param handleIcons {@code true} if icons may be managed, {@code false} otherwise
122     */
123    public SourceEditor(SourceType sourceType, String availableSourcesUrl, List<SourceProvider> sourceProviders, boolean handleIcons) {
124
125        this.sourceType = sourceType;
126        this.canEnable = sourceType.equals(SourceType.MAP_PAINT_STYLE) || sourceType.equals(SourceType.TAGCHECKER_RULE);
127
128        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
129        this.availableSourcesModel = new AvailableSourcesListModel(selectionModel);
130        this.lstAvailableSources = new JList<>(availableSourcesModel);
131        this.lstAvailableSources.setSelectionModel(selectionModel);
132        final SourceEntryListCellRenderer listCellRenderer = new SourceEntryListCellRenderer();
133        this.lstAvailableSources.setCellRenderer(listCellRenderer);
134        GuiHelper.extendTooltipDelay(lstAvailableSources);
135        this.availableSourcesUrl = availableSourcesUrl;
136        this.sourceProviders = sourceProviders;
137
138        selectionModel = new DefaultListSelectionModel();
139        activeSourcesModel = new ActiveSourcesModel(selectionModel);
140        tblActiveSources = new JTable(activeSourcesModel) {
141            // some kind of hack to prevent the table from scrolling slightly to the right when clicking on the text
142            @Override
143            public void scrollRectToVisible(Rectangle aRect) {
144                super.scrollRectToVisible(new Rectangle(0, aRect.y, aRect.width, aRect.height));
145            }
146        };
147        tblActiveSources.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
148        tblActiveSources.setSelectionModel(selectionModel);
149        tblActiveSources.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
150        tblActiveSources.setShowGrid(false);
151        tblActiveSources.setIntercellSpacing(new Dimension(0, 0));
152        tblActiveSources.setTableHeader(null);
153        tblActiveSources.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
154        SourceEntryTableCellRenderer sourceEntryRenderer = new SourceEntryTableCellRenderer();
155        if (canEnable) {
156            tblActiveSources.getColumnModel().getColumn(0).setMaxWidth(1);
157            tblActiveSources.getColumnModel().getColumn(0).setResizable(false);
158            tblActiveSources.getColumnModel().getColumn(1).setCellRenderer(sourceEntryRenderer);
159        } else {
160            tblActiveSources.getColumnModel().getColumn(0).setCellRenderer(sourceEntryRenderer);
161        }
162
163        activeSourcesModel.addTableModelListener(e -> {
164            listCellRenderer.updateSources(activeSourcesModel.getSources());
165            lstAvailableSources.repaint();
166        });
167        tblActiveSources.addPropertyChangeListener(evt -> {
168            listCellRenderer.updateSources(activeSourcesModel.getSources());
169            lstAvailableSources.repaint();
170        });
171        // Force Swing to show horizontal scrollbars for the JTable
172        // Yes, this is a little ugly, but should work
173        activeSourcesModel.addTableModelListener(e -> TableHelper.adjustColumnWidth(tblActiveSources, canEnable ? 1 : 0, 800));
174        activeSourcesModel.setActiveSources(getInitialSourcesList());
175
176        final EditActiveSourceAction editActiveSourceAction = new EditActiveSourceAction();
177        tblActiveSources.getSelectionModel().addListSelectionListener(editActiveSourceAction);
178        tblActiveSources.addMouseListener(new MouseAdapter() {
179            @Override
180            public void mouseClicked(MouseEvent e) {
181                if (e.getClickCount() == 2) {
182                    int row = tblActiveSources.rowAtPoint(e.getPoint());
183                    int col = tblActiveSources.columnAtPoint(e.getPoint());
184                    if (row < 0 || row >= tblActiveSources.getRowCount())
185                        return;
186                    if (canEnable && col != 1)
187                        return;
188                    editActiveSourceAction.actionPerformed(null);
189                }
190            }
191        });
192
193        RemoveActiveSourcesAction removeActiveSourcesAction = new RemoveActiveSourcesAction();
194        tblActiveSources.getSelectionModel().addListSelectionListener(removeActiveSourcesAction);
195        tblActiveSources.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete");
196        tblActiveSources.getActionMap().put("delete", removeActiveSourcesAction);
197
198        MoveUpDownAction moveUp = null;
199        MoveUpDownAction moveDown = null;
200        if (sourceType.equals(SourceType.MAP_PAINT_STYLE)) {
201            moveUp = new MoveUpDownAction(false);
202            moveDown = new MoveUpDownAction(true);
203            tblActiveSources.getSelectionModel().addListSelectionListener(moveUp);
204            tblActiveSources.getSelectionModel().addListSelectionListener(moveDown);
205            activeSourcesModel.addTableModelListener(moveUp);
206            activeSourcesModel.addTableModelListener(moveDown);
207        }
208
209        ActivateSourcesAction activateSourcesAction = new ActivateSourcesAction();
210        lstAvailableSources.addListSelectionListener(activateSourcesAction);
211        JButton activate = new JButton(activateSourcesAction);
212
213        setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
214        setLayout(new GridBagLayout());
215
216        GridBagConstraints gbc = new GridBagConstraints();
217        gbc.gridx = 0;
218        gbc.gridy = 0;
219        gbc.weightx = 0.5;
220        gbc.gridwidth = 2;
221        gbc.anchor = GBC.WEST;
222        gbc.insets = new Insets(5, 11, 0, 0);
223
224        add(new JLabel(getStr(I18nString.AVAILABLE_SOURCES)), gbc);
225
226        gbc.gridx = 2;
227        gbc.insets = new Insets(5, 0, 0, 6);
228
229        add(new JLabel(getStr(I18nString.ACTIVE_SOURCES)), gbc);
230
231        gbc.gridwidth = 1;
232        gbc.gridx = 0;
233        gbc.gridy++;
234        gbc.weighty = 0.8;
235        gbc.fill = GBC.BOTH;
236        gbc.anchor = GBC.CENTER;
237        gbc.insets = new Insets(0, 11, 0, 0);
238
239        JScrollPane sp1 = new JScrollPane(lstAvailableSources);
240        add(sp1, gbc);
241
242        gbc.gridx = 1;
243        gbc.weightx = 0.0;
244        gbc.fill = GBC.VERTICAL;
245        gbc.insets = new Insets(0, 0, 0, 0);
246
247        JToolBar middleTB = new JToolBar();
248        middleTB.setFloatable(false);
249        middleTB.setBorderPainted(false);
250        middleTB.setOpaque(false);
251        middleTB.add(Box.createHorizontalGlue());
252        middleTB.add(activate);
253        middleTB.add(Box.createHorizontalGlue());
254        add(middleTB, gbc);
255
256        gbc.gridx++;
257        gbc.weightx = 0.5;
258        gbc.fill = GBC.BOTH;
259
260        JScrollPane sp = new JScrollPane(tblActiveSources);
261        add(sp, gbc);
262        sp.setColumnHeaderView(null);
263
264        gbc.gridx++;
265        gbc.weightx = 0.0;
266        gbc.fill = GBC.VERTICAL;
267        gbc.insets = new Insets(0, 0, 0, 6);
268
269        JToolBar sideButtonTB = new JToolBar(JToolBar.VERTICAL);
270        sideButtonTB.setFloatable(false);
271        sideButtonTB.setBorderPainted(false);
272        sideButtonTB.setOpaque(false);
273        sideButtonTB.add(new NewActiveSourceAction());
274        sideButtonTB.add(editActiveSourceAction);
275        sideButtonTB.add(removeActiveSourcesAction);
276        sideButtonTB.addSeparator(new Dimension(12, 30));
277        if (sourceType.equals(SourceType.MAP_PAINT_STYLE)) {
278            sideButtonTB.add(moveUp);
279            sideButtonTB.add(moveDown);
280        }
281        add(sideButtonTB, gbc);
282
283        gbc.gridx = 0;
284        gbc.gridy++;
285        gbc.weighty = 0.0;
286        gbc.weightx = 0.5;
287        gbc.fill = GBC.HORIZONTAL;
288        gbc.anchor = GBC.WEST;
289        gbc.insets = new Insets(0, 11, 0, 0);
290
291        JToolBar bottomLeftTB = new JToolBar();
292        bottomLeftTB.setFloatable(false);
293        bottomLeftTB.setBorderPainted(false);
294        bottomLeftTB.setOpaque(false);
295        bottomLeftTB.add(new ReloadSourcesAction(availableSourcesUrl, sourceProviders));
296        bottomLeftTB.add(Box.createHorizontalGlue());
297        add(bottomLeftTB, gbc);
298
299        gbc.gridx = 2;
300        gbc.anchor = GBC.CENTER;
301        gbc.insets = new Insets(0, 0, 0, 0);
302
303        JToolBar bottomRightTB = new JToolBar();
304        bottomRightTB.setFloatable(false);
305        bottomRightTB.setBorderPainted(false);
306        bottomRightTB.setOpaque(false);
307        bottomRightTB.add(Box.createHorizontalGlue());
308        bottomRightTB.add(new JButton(new ResetAction()));
309        add(bottomRightTB, gbc);
310
311        // Icon configuration
312        if (handleIcons) {
313            buildIcons(gbc);
314        }
315    }
316
317    private void buildIcons(GridBagConstraints gbc) {
318        DefaultListSelectionModel selectionModel = new DefaultListSelectionModel();
319        iconPathsModel = new IconPathTableModel(selectionModel);
320        tblIconPaths = new JTable(iconPathsModel);
321        tblIconPaths.setSelectionModel(selectionModel);
322        tblIconPaths.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
323        tblIconPaths.setTableHeader(null);
324        tblIconPaths.getColumnModel().getColumn(0).setCellEditor(new FileOrUrlCellEditor(false));
325        tblIconPaths.setRowHeight(20);
326        tblIconPaths.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
327        iconPathsModel.setIconPaths(getInitialIconPathsList());
328
329        EditIconPathAction editIconPathAction = new EditIconPathAction();
330        tblIconPaths.getSelectionModel().addListSelectionListener(editIconPathAction);
331
332        RemoveIconPathAction removeIconPathAction = new RemoveIconPathAction();
333        tblIconPaths.getSelectionModel().addListSelectionListener(removeIconPathAction);
334        tblIconPaths.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete");
335        tblIconPaths.getActionMap().put("delete", removeIconPathAction);
336
337        gbc.gridx = 0;
338        gbc.gridy++;
339        gbc.weightx = 1.0;
340        gbc.gridwidth = GBC.REMAINDER;
341        gbc.insets = new Insets(8, 11, 8, 6);
342
343        add(new JSeparator(), gbc);
344
345        gbc.gridy++;
346        gbc.insets = new Insets(0, 11, 0, 6);
347
348        add(new JLabel(tr("Icon paths:")), gbc);
349
350        gbc.gridy++;
351        gbc.weighty = 0.2;
352        gbc.gridwidth = 3;
353        gbc.fill = GBC.BOTH;
354        gbc.insets = new Insets(0, 11, 0, 0);
355
356        JScrollPane sp = new JScrollPane(tblIconPaths);
357        add(sp, gbc);
358        sp.setColumnHeaderView(null);
359
360        gbc.gridx = 3;
361        gbc.gridwidth = 1;
362        gbc.weightx = 0.0;
363        gbc.fill = GBC.VERTICAL;
364        gbc.insets = new Insets(0, 0, 0, 6);
365
366        JToolBar sideButtonTBIcons = new JToolBar(JToolBar.VERTICAL);
367        sideButtonTBIcons.setFloatable(false);
368        sideButtonTBIcons.setBorderPainted(false);
369        sideButtonTBIcons.setOpaque(false);
370        sideButtonTBIcons.add(new NewIconPathAction());
371        sideButtonTBIcons.add(editIconPathAction);
372        sideButtonTBIcons.add(removeIconPathAction);
373        add(sideButtonTBIcons, gbc);
374    }
375
376    /**
377     * Load the list of source entries that the user has configured.
378     * @return list of source entries that the user has configured
379     */
380    public abstract Collection<? extends SourceEntry> getInitialSourcesList();
381
382    /**
383     * Load the list of configured icon paths.
384     * @return list of configured icon paths
385     */
386    public abstract Collection<String> getInitialIconPathsList();
387
388    /**
389     * Get the default list of entries (used when resetting the list).
390     * @return default list of entries
391     */
392    public abstract Collection<ExtendedSourceEntry> getDefault();
393
394    /**
395     * Save the settings after user clicked "Ok".
396     * @return true if restart is required
397     */
398    public abstract boolean finish();
399
400    protected boolean doFinish(SourcePrefHelper prefHelper, String iconPref) {
401        boolean changed = prefHelper.put(activeSourcesModel.getSources());
402
403        if (tblIconPaths != null) {
404            List<String> iconPaths = iconPathsModel.getIconPaths();
405
406            if (!iconPaths.isEmpty()) {
407                if (Main.pref.putCollection(iconPref, iconPaths)) {
408                    changed = true;
409                }
410            } else if (Main.pref.putCollection(iconPref, null)) {
411                changed = true;
412            }
413        }
414        return changed;
415    }
416
417    /**
418     * Provide the GUI strings. (There are differences for MapPaint, Preset and TagChecker Rule)
419     * @param ident any {@link I18nString} value
420     * @return the translated string for {@code ident}
421     */
422    protected abstract String getStr(I18nString ident);
423
424    /**
425     * Identifiers for strings that need to be provided.
426     */
427    public enum I18nString {
428        /** Available (styles|presets|rules) */
429        AVAILABLE_SOURCES,
430        /** Active (styles|presets|rules) */
431        ACTIVE_SOURCES,
432        /** Add a new (style|preset|rule) by entering filename or URL */
433        NEW_SOURCE_ENTRY_TOOLTIP,
434        /** New (style|preset|rule) entry */
435        NEW_SOURCE_ENTRY,
436        /** Remove the selected (styles|presets|rules) from the list of active (styles|presets|rules) */
437        REMOVE_SOURCE_TOOLTIP,
438        /** Edit the filename or URL for the selected active (style|preset|rule) */
439        EDIT_SOURCE_TOOLTIP,
440        /** Add the selected available (styles|presets|rules) to the list of active (styles|presets|rules) */
441        ACTIVATE_TOOLTIP,
442        /** Reloads the list of available (styles|presets|rules) */
443        RELOAD_ALL_AVAILABLE,
444        /** Loading (style|preset|rule) sources */
445        LOADING_SOURCES_FROM,
446        /** Failed to load the list of (style|preset|rule) sources */
447        FAILED_TO_LOAD_SOURCES_FROM,
448        /** /Preferences/(Styles|Presets|Rules)#FailedToLoad(Style|Preset|Rule)Sources */
449        FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC,
450        /** Illegal format of entry in (style|preset|rule) list */
451        ILLEGAL_FORMAT_OF_ENTRY
452    }
453
454    /**
455     * Determines whether the list of active sources has changed.
456     * @return {@code true} if the list of active sources has changed, {@code false} otherwise
457     */
458    public boolean hasActiveSourcesChanged() {
459        Collection<? extends SourceEntry> prev = getInitialSourcesList();
460        List<SourceEntry> cur = activeSourcesModel.getSources();
461        if (prev.size() != cur.size())
462            return true;
463        Iterator<? extends SourceEntry> p = prev.iterator();
464        Iterator<SourceEntry> c = cur.iterator();
465        while (p.hasNext()) {
466            SourceEntry pe = p.next();
467            SourceEntry ce = c.next();
468            if (!Objects.equals(pe.url, ce.url) || !Objects.equals(pe.name, ce.name) || pe.active != ce.active)
469                return true;
470        }
471        return false;
472    }
473
474    /**
475     * Returns the list of active sources.
476     * @return the list of active sources
477     */
478    public Collection<SourceEntry> getActiveSources() {
479        return activeSourcesModel.getSources();
480    }
481
482    /**
483     * Synchronously loads available sources and returns the parsed list.
484     * @return list of available sources
485     * @throws OsmTransferException in case of OSM transfer error
486     * @throws IOException in case of any I/O error
487     * @throws SAXException in case of any SAX error
488     */
489    public final Collection<ExtendedSourceEntry> loadAndGetAvailableSources() throws SAXException, IOException, OsmTransferException {
490        final SourceLoader loader = new SourceLoader(availableSourcesUrl, sourceProviders);
491        loader.realRun();
492        return loader.sources;
493    }
494
495    /**
496     * Remove sources associated with given indexes from active list.
497     * @param idxs indexes of sources to remove
498     */
499    public void removeSources(Collection<Integer> idxs) {
500        activeSourcesModel.removeIdxs(idxs);
501    }
502
503    protected void reloadAvailableSources(String url, List<SourceProvider> sourceProviders) {
504        Main.worker.submit(new SourceLoader(url, sourceProviders));
505    }
506
507    /**
508     * Performs the initial loading of source providers. Does nothing if already done.
509     */
510    public void initiallyLoadAvailableSources() {
511        if (!sourcesInitiallyLoaded) {
512            reloadAvailableSources(availableSourcesUrl, sourceProviders);
513        }
514        sourcesInitiallyLoaded = true;
515    }
516
517    protected static class AvailableSourcesListModel extends DefaultListModel<ExtendedSourceEntry> {
518        private final transient List<ExtendedSourceEntry> data;
519        private final DefaultListSelectionModel selectionModel;
520
521        public AvailableSourcesListModel(DefaultListSelectionModel selectionModel) {
522            data = new ArrayList<>();
523            this.selectionModel = selectionModel;
524        }
525
526        public void setSources(List<ExtendedSourceEntry> sources) {
527            data.clear();
528            if (sources != null) {
529                data.addAll(sources);
530            }
531            fireContentsChanged(this, 0, data.size());
532        }
533
534        @Override
535        public ExtendedSourceEntry getElementAt(int index) {
536            return data.get(index);
537        }
538
539        @Override
540        public int getSize() {
541            if (data == null) return 0;
542            return data.size();
543        }
544
545        public void deleteSelected() {
546            Iterator<ExtendedSourceEntry> it = data.iterator();
547            int i = 0;
548            while (it.hasNext()) {
549                it.next();
550                if (selectionModel.isSelectedIndex(i)) {
551                    it.remove();
552                }
553                i++;
554            }
555            fireContentsChanged(this, 0, data.size());
556        }
557
558        public List<ExtendedSourceEntry> getSelected() {
559            List<ExtendedSourceEntry> ret = new ArrayList<>();
560            for (int i = 0; i < data.size(); i++) {
561                if (selectionModel.isSelectedIndex(i)) {
562                    ret.add(data.get(i));
563                }
564            }
565            return ret;
566        }
567    }
568
569    protected class ActiveSourcesModel extends AbstractTableModel {
570        private transient List<SourceEntry> data;
571        private final DefaultListSelectionModel selectionModel;
572
573        public ActiveSourcesModel(DefaultListSelectionModel selectionModel) {
574            this.selectionModel = selectionModel;
575            this.data = new ArrayList<>();
576        }
577
578        @Override
579        public int getColumnCount() {
580            return canEnable ? 2 : 1;
581        }
582
583        @Override
584        public int getRowCount() {
585            return data == null ? 0 : data.size();
586        }
587
588        @Override
589        public Object getValueAt(int rowIndex, int columnIndex) {
590            if (canEnable && columnIndex == 0)
591                return data.get(rowIndex).active;
592            else
593                return data.get(rowIndex);
594        }
595
596        @Override
597        public boolean isCellEditable(int rowIndex, int columnIndex) {
598            return canEnable && columnIndex == 0;
599        }
600
601        @Override
602        public Class<?> getColumnClass(int column) {
603            if (canEnable && column == 0)
604                return Boolean.class;
605            else return SourceEntry.class;
606        }
607
608        @Override
609        public void setValueAt(Object aValue, int row, int column) {
610            if (row < 0 || row >= getRowCount() || aValue == null)
611                return;
612            if (canEnable && column == 0) {
613                data.get(row).active = !data.get(row).active;
614            }
615        }
616
617        public void setActiveSources(Collection<? extends SourceEntry> sources) {
618            data.clear();
619            if (sources != null) {
620                for (SourceEntry e : sources) {
621                    data.add(new SourceEntry(e));
622                }
623            }
624            fireTableDataChanged();
625        }
626
627        public void addSource(SourceEntry entry) {
628            if (entry == null) return;
629            data.add(entry);
630            fireTableDataChanged();
631            int idx = data.indexOf(entry);
632            if (idx >= 0) {
633                selectionModel.setSelectionInterval(idx, idx);
634            }
635        }
636
637        public void removeSelected() {
638            Iterator<SourceEntry> it = data.iterator();
639            int i = 0;
640            while (it.hasNext()) {
641                it.next();
642                if (selectionModel.isSelectedIndex(i)) {
643                    it.remove();
644                }
645                i++;
646            }
647            fireTableDataChanged();
648        }
649
650        public void removeIdxs(Collection<Integer> idxs) {
651            List<SourceEntry> newData = new ArrayList<>();
652            for (int i = 0; i < data.size(); ++i) {
653                if (!idxs.contains(i)) {
654                    newData.add(data.get(i));
655                }
656            }
657            data = newData;
658            fireTableDataChanged();
659        }
660
661        public void addExtendedSourceEntries(List<ExtendedSourceEntry> sources) {
662            if (sources == null) return;
663            for (ExtendedSourceEntry info: sources) {
664                data.add(new SourceEntry(info.url, info.name, info.getDisplayName(), true));
665            }
666            fireTableDataChanged();
667            selectionModel.clearSelection();
668            for (ExtendedSourceEntry info: sources) {
669                int pos = data.indexOf(info);
670                if (pos >= 0) {
671                    selectionModel.addSelectionInterval(pos, pos);
672                }
673            }
674        }
675
676        public List<SourceEntry> getSources() {
677            return new ArrayList<>(data);
678        }
679
680        public boolean canMove(int i) {
681            int[] sel = tblActiveSources.getSelectedRows();
682            if (sel.length == 0)
683                return false;
684            if (i < 0)
685                return sel[0] >= -i;
686                else if (i > 0)
687                    return sel[sel.length-1] <= getRowCount()-1 - i;
688                else
689                    return true;
690        }
691
692        public void move(int i) {
693            if (!canMove(i)) return;
694            int[] sel = tblActiveSources.getSelectedRows();
695            for (int row: sel) {
696                SourceEntry t1 = data.get(row);
697                SourceEntry t2 = data.get(row + i);
698                data.set(row, t2);
699                data.set(row + i, t1);
700            }
701            selectionModel.clearSelection();
702            for (int row: sel) {
703                selectionModel.addSelectionInterval(row + i, row + i);
704            }
705        }
706    }
707
708    public static class ExtendedSourceEntry extends SourceEntry implements Comparable<ExtendedSourceEntry> {
709        /** file name used for display */
710        public String simpleFileName;
711        /** version used for display */
712        public String version;
713        /** author name used for display */
714        public String author;
715        /** webpage link used for display */
716        public String link;
717        /** short description used for display */
718        public String description;
719        /** Style type: can only have one value: "xml". Used to filter out old XML styles. For MapCSS styles, the value is not set. */
720        public String styleType;
721        /** minimum JOSM version required to enable this source entry */
722        public Integer minJosmVersion;
723
724        /**
725         * Constructs a new {@code ExtendedSourceEntry}.
726         * @param simpleFileName file name used for display
727         * @param url URL that {@link org.openstreetmap.josm.io.CachedFile} understands
728         */
729        public ExtendedSourceEntry(String simpleFileName, String url) {
730            super(url, null, null, true);
731            this.simpleFileName = simpleFileName;
732        }
733
734        /**
735         * @return string representation for GUI list or menu entry
736         */
737        public String getDisplayName() {
738            return title == null ? simpleFileName : title;
739        }
740
741        private static void appendRow(StringBuilder s, String th, String td) {
742            s.append("<tr><th>").append(th).append("</th><td>").append(td).append("</td</tr>");
743        }
744
745        /**
746         * Returns a tooltip containing available metadata.
747         * @return a tooltip containing available metadata
748         */
749        public String getTooltip() {
750            StringBuilder s = new StringBuilder();
751            appendRow(s, tr("Short Description:"), getDisplayName());
752            appendRow(s, tr("URL:"), url);
753            if (author != null) {
754                appendRow(s, tr("Author:"), author);
755            }
756            if (link != null) {
757                appendRow(s, tr("Webpage:"), link);
758            }
759            if (description != null) {
760                appendRow(s, tr("Description:"), description);
761            }
762            if (version != null) {
763                appendRow(s, tr("Version:"), version);
764            }
765            if (minJosmVersion != null) {
766                appendRow(s, tr("Minimum JOSM Version:"), Integer.toString(minJosmVersion));
767            }
768            return "<html><style>th{text-align:right}td{width:400px}</style>"
769                    + "<table>" + s + "</table></html>";
770        }
771
772        @Override
773        public String toString() {
774            return "<html><b>" + getDisplayName() + "</b>"
775                    + (author == null ? "" : " <span color=\"gray\">" + tr("by {0}", author) + "</color>")
776                    + "</html>";
777        }
778
779        @Override
780        public int compareTo(ExtendedSourceEntry o) {
781            if (url.startsWith("resource") && !o.url.startsWith("resource"))
782                return -1;
783            if (o.url.startsWith("resource"))
784                return 1;
785            else
786                return getDisplayName().compareToIgnoreCase(o.getDisplayName());
787        }
788    }
789
790    private static void prepareFileChooser(String url, AbstractFileChooser fc) {
791        if (url == null || url.trim().isEmpty()) return;
792        URL sourceUrl = null;
793        try {
794            sourceUrl = new URL(url);
795        } catch (MalformedURLException e) {
796            File f = new File(url);
797            if (f.isFile()) {
798                f = f.getParentFile();
799            }
800            if (f != null) {
801                fc.setCurrentDirectory(f);
802            }
803            return;
804        }
805        if (sourceUrl.getProtocol().startsWith("file")) {
806            File f = new File(sourceUrl.getPath());
807            if (f.isFile()) {
808                f = f.getParentFile();
809            }
810            if (f != null) {
811                fc.setCurrentDirectory(f);
812            }
813        }
814    }
815
816    protected class EditSourceEntryDialog extends ExtendedDialog {
817
818        private final JosmTextField tfTitle;
819        private final JosmTextField tfURL;
820        private JCheckBox cbActive;
821
822        public EditSourceEntryDialog(Component parent, String title, SourceEntry e) {
823            super(parent, title, new String[] {tr("Ok"), tr("Cancel")});
824
825            JPanel p = new JPanel(new GridBagLayout());
826
827            tfTitle = new JosmTextField(60);
828            p.add(new JLabel(tr("Name (optional):")), GBC.std().insets(15, 0, 5, 5));
829            p.add(tfTitle, GBC.eol().insets(0, 0, 5, 5));
830
831            tfURL = new JosmTextField(60);
832            p.add(new JLabel(tr("URL / File:")), GBC.std().insets(15, 0, 5, 0));
833            p.add(tfURL, GBC.std().insets(0, 0, 5, 5));
834            JButton fileChooser = new JButton(new LaunchFileChooserAction());
835            fileChooser.setMargin(new Insets(0, 0, 0, 0));
836            p.add(fileChooser, GBC.eol().insets(0, 0, 5, 5));
837
838            if (e != null) {
839                if (e.title != null) {
840                    tfTitle.setText(e.title);
841                }
842                tfURL.setText(e.url);
843            }
844
845            if (canEnable) {
846                cbActive = new JCheckBox(tr("active"), e == null || e.active);
847                p.add(cbActive, GBC.eol().insets(15, 0, 5, 0));
848            }
849            setButtonIcons(new String[] {"ok", "cancel"});
850            setContent(p);
851
852            // Make OK button enabled only when a file/URL has been set
853            tfURL.getDocument().addDocumentListener(new DocumentListener() {
854                @Override
855                public void insertUpdate(DocumentEvent e) {
856                    updateOkButtonState();
857                }
858
859                @Override
860                public void removeUpdate(DocumentEvent e) {
861                    updateOkButtonState();
862                }
863
864                @Override
865                public void changedUpdate(DocumentEvent e) {
866                    updateOkButtonState();
867                }
868            });
869        }
870
871        private void updateOkButtonState() {
872            buttons.get(0).setEnabled(!Utils.strip(tfURL.getText()).isEmpty());
873        }
874
875        @Override
876        public void setupDialog() {
877            super.setupDialog();
878            updateOkButtonState();
879        }
880
881        class LaunchFileChooserAction extends AbstractAction {
882            LaunchFileChooserAction() {
883                new ImageProvider("open").getResource().attachImageIcon(this);
884                putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
885            }
886
887            @Override
888            public void actionPerformed(ActionEvent e) {
889                FileFilter ff;
890                switch (sourceType) {
891                case MAP_PAINT_STYLE:
892                    ff = new ExtensionFileFilter("xml,mapcss,css,zip", "xml", tr("Map paint style file (*.xml, *.mapcss, *.zip)"));
893                    break;
894                case TAGGING_PRESET:
895                    ff = new ExtensionFileFilter("xml,zip", "xml", tr("Preset definition file (*.xml, *.zip)"));
896                    break;
897                case TAGCHECKER_RULE:
898                    ff = new ExtensionFileFilter("validator.mapcss,zip", "validator.mapcss", tr("Tag checker rule (*.validator.mapcss, *.zip)"));
899                    break;
900                default:
901                    Main.error("Unsupported source type: "+sourceType);
902                    return;
903                }
904                FileChooserManager fcm = new FileChooserManager(true)
905                        .createFileChooser(true, null, Arrays.asList(ff, FileFilterAllFiles.getInstance()), ff, JFileChooser.FILES_ONLY);
906                prepareFileChooser(tfURL.getText(), fcm.getFileChooser());
907                AbstractFileChooser fc = fcm.openFileChooser(GuiHelper.getFrameForComponent(SourceEditor.this));
908                if (fc != null) {
909                    tfURL.setText(fc.getSelectedFile().toString());
910                }
911            }
912        }
913
914        @Override
915        public String getTitle() {
916            return tfTitle.getText();
917        }
918
919        public String getURL() {
920            return tfURL.getText();
921        }
922
923        public boolean active() {
924            if (!canEnable)
925                throw new UnsupportedOperationException();
926            return cbActive.isSelected();
927        }
928    }
929
930    class NewActiveSourceAction extends AbstractAction {
931        NewActiveSourceAction() {
932            putValue(NAME, tr("New"));
933            putValue(SHORT_DESCRIPTION, getStr(I18nString.NEW_SOURCE_ENTRY_TOOLTIP));
934            new ImageProvider("dialogs", "add").getResource().attachImageIcon(this);
935        }
936
937        @Override
938        public void actionPerformed(ActionEvent evt) {
939            EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog(
940                    SourceEditor.this,
941                    getStr(I18nString.NEW_SOURCE_ENTRY),
942                    null);
943            editEntryDialog.showDialog();
944            if (editEntryDialog.getValue() == 1) {
945                boolean active = true;
946                if (canEnable) {
947                    active = editEntryDialog.active();
948                }
949                final SourceEntry entry = new SourceEntry(
950                        editEntryDialog.getURL(),
951                        null, editEntryDialog.getTitle(), active);
952                entry.title = getTitleForSourceEntry(entry);
953                activeSourcesModel.addSource(entry);
954                activeSourcesModel.fireTableDataChanged();
955            }
956        }
957    }
958
959    class RemoveActiveSourcesAction extends AbstractAction implements ListSelectionListener {
960
961        RemoveActiveSourcesAction() {
962            putValue(NAME, tr("Remove"));
963            putValue(SHORT_DESCRIPTION, getStr(I18nString.REMOVE_SOURCE_TOOLTIP));
964            new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
965            updateEnabledState();
966        }
967
968        protected final void updateEnabledState() {
969            setEnabled(tblActiveSources.getSelectedRowCount() > 0);
970        }
971
972        @Override
973        public void valueChanged(ListSelectionEvent e) {
974            updateEnabledState();
975        }
976
977        @Override
978        public void actionPerformed(ActionEvent e) {
979            activeSourcesModel.removeSelected();
980        }
981    }
982
983    class EditActiveSourceAction extends AbstractAction implements ListSelectionListener {
984        EditActiveSourceAction() {
985            putValue(NAME, tr("Edit"));
986            putValue(SHORT_DESCRIPTION, getStr(I18nString.EDIT_SOURCE_TOOLTIP));
987            new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this);
988            updateEnabledState();
989        }
990
991        protected final void updateEnabledState() {
992            setEnabled(tblActiveSources.getSelectedRowCount() == 1);
993        }
994
995        @Override
996        public void valueChanged(ListSelectionEvent e) {
997            updateEnabledState();
998        }
999
1000        @Override
1001        public void actionPerformed(ActionEvent evt) {
1002            int pos = tblActiveSources.getSelectedRow();
1003            if (pos < 0 || pos >= tblActiveSources.getRowCount())
1004                return;
1005
1006            SourceEntry e = (SourceEntry) activeSourcesModel.getValueAt(pos, 1);
1007
1008            EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog(
1009                    SourceEditor.this, tr("Edit source entry:"), e);
1010            editEntryDialog.showDialog();
1011            if (editEntryDialog.getValue() == 1) {
1012                if (e.title != null || !"".equals(editEntryDialog.getTitle())) {
1013                    e.title = editEntryDialog.getTitle();
1014                    e.title = getTitleForSourceEntry(e);
1015                }
1016                e.url = editEntryDialog.getURL();
1017                if (canEnable) {
1018                    e.active = editEntryDialog.active();
1019                }
1020                activeSourcesModel.fireTableRowsUpdated(pos, pos);
1021            }
1022        }
1023    }
1024
1025    /**
1026     * The action to move the currently selected entries up or down in the list.
1027     */
1028    class MoveUpDownAction extends AbstractAction implements ListSelectionListener, TableModelListener {
1029        private final int increment;
1030
1031        MoveUpDownAction(boolean isDown) {
1032            increment = isDown ? 1 : -1;
1033            putValue(SMALL_ICON, isDown ? ImageProvider.get("dialogs", "down") : ImageProvider.get("dialogs", "up"));
1034            putValue(SHORT_DESCRIPTION, isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up."));
1035            updateEnabledState();
1036        }
1037
1038        public final void updateEnabledState() {
1039            setEnabled(activeSourcesModel.canMove(increment));
1040        }
1041
1042        @Override
1043        public void actionPerformed(ActionEvent e) {
1044            activeSourcesModel.move(increment);
1045        }
1046
1047        @Override
1048        public void valueChanged(ListSelectionEvent e) {
1049            updateEnabledState();
1050        }
1051
1052        @Override
1053        public void tableChanged(TableModelEvent e) {
1054            updateEnabledState();
1055        }
1056    }
1057
1058    class ActivateSourcesAction extends AbstractAction implements ListSelectionListener {
1059        ActivateSourcesAction() {
1060            putValue(SHORT_DESCRIPTION, getStr(I18nString.ACTIVATE_TOOLTIP));
1061            new ImageProvider("preferences", "activate-right").getResource().attachImageIcon(this);
1062            updateEnabledState();
1063        }
1064
1065        protected final void updateEnabledState() {
1066            setEnabled(lstAvailableSources.getSelectedIndices().length > 0);
1067        }
1068
1069        @Override
1070        public void valueChanged(ListSelectionEvent e) {
1071            updateEnabledState();
1072        }
1073
1074        @Override
1075        public void actionPerformed(ActionEvent e) {
1076            List<ExtendedSourceEntry> sources = availableSourcesModel.getSelected();
1077            int josmVersion = Version.getInstance().getVersion();
1078            if (josmVersion != Version.JOSM_UNKNOWN_VERSION) {
1079                Collection<String> messages = new ArrayList<>();
1080                for (ExtendedSourceEntry entry : sources) {
1081                    if (entry.minJosmVersion != null && entry.minJosmVersion > josmVersion) {
1082                        messages.add(tr("Entry ''{0}'' requires JOSM Version {1}. (Currently running: {2})",
1083                                entry.title,
1084                                Integer.toString(entry.minJosmVersion),
1085                                Integer.toString(josmVersion))
1086                        );
1087                    }
1088                }
1089                if (!messages.isEmpty()) {
1090                    ExtendedDialog dlg = new ExtendedDialog(Main.parent, tr("Warning"), new String[] {tr("Cancel"), tr("Continue anyway")});
1091                    dlg.setButtonIcons(new Icon[] {
1092                        ImageProvider.get("cancel"),
1093                        new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).addOverlay(
1094                                new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()
1095                    });
1096                    dlg.setToolTipTexts(new String[] {
1097                        tr("Cancel and return to the previous dialog"),
1098                        tr("Ignore warning and install style anyway")});
1099                    dlg.setContent("<html>" + tr("Some entries have unmet dependencies:") +
1100                            "<br>" + Utils.join("<br>", messages) + "</html>");
1101                    dlg.setIcon(JOptionPane.WARNING_MESSAGE);
1102                    if (dlg.showDialog().getValue() != 2)
1103                        return;
1104                }
1105            }
1106            activeSourcesModel.addExtendedSourceEntries(sources);
1107        }
1108    }
1109
1110    class ResetAction extends AbstractAction {
1111
1112        ResetAction() {
1113            putValue(NAME, tr("Reset"));
1114            putValue(SHORT_DESCRIPTION, tr("Reset to default"));
1115            new ImageProvider("preferences", "reset").getResource().attachImageIcon(this);
1116        }
1117
1118        @Override
1119        public void actionPerformed(ActionEvent e) {
1120            activeSourcesModel.setActiveSources(getDefault());
1121        }
1122    }
1123
1124    class ReloadSourcesAction extends AbstractAction {
1125        private final String url;
1126        private final transient List<SourceProvider> sourceProviders;
1127
1128        ReloadSourcesAction(String url, List<SourceProvider> sourceProviders) {
1129            putValue(NAME, tr("Reload"));
1130            putValue(SHORT_DESCRIPTION, tr(getStr(I18nString.RELOAD_ALL_AVAILABLE), url));
1131            new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this);
1132            this.url = url;
1133            this.sourceProviders = sourceProviders;
1134            setEnabled(!Main.isOffline(OnlineResource.JOSM_WEBSITE));
1135        }
1136
1137        @Override
1138        public void actionPerformed(ActionEvent e) {
1139            CachedFile.cleanup(url);
1140            reloadAvailableSources(url, sourceProviders);
1141        }
1142    }
1143
1144    protected static class IconPathTableModel extends AbstractTableModel {
1145        private final List<String> data;
1146        private final DefaultListSelectionModel selectionModel;
1147
1148        public IconPathTableModel(DefaultListSelectionModel selectionModel) {
1149            this.selectionModel = selectionModel;
1150            this.data = new ArrayList<>();
1151        }
1152
1153        @Override
1154        public int getColumnCount() {
1155            return 1;
1156        }
1157
1158        @Override
1159        public int getRowCount() {
1160            return data == null ? 0 : data.size();
1161        }
1162
1163        @Override
1164        public Object getValueAt(int rowIndex, int columnIndex) {
1165            return data.get(rowIndex);
1166        }
1167
1168        @Override
1169        public boolean isCellEditable(int rowIndex, int columnIndex) {
1170            return true;
1171        }
1172
1173        @Override
1174        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
1175            updatePath(rowIndex, (String) aValue);
1176        }
1177
1178        public void setIconPaths(Collection<String> paths) {
1179            data.clear();
1180            if (paths != null) {
1181                data.addAll(paths);
1182            }
1183            sort();
1184            fireTableDataChanged();
1185        }
1186
1187        public void addPath(String path) {
1188            if (path == null) return;
1189            data.add(path);
1190            sort();
1191            fireTableDataChanged();
1192            int idx = data.indexOf(path);
1193            if (idx >= 0) {
1194                selectionModel.setSelectionInterval(idx, idx);
1195            }
1196        }
1197
1198        public void updatePath(int pos, String path) {
1199            if (path == null) return;
1200            if (pos < 0 || pos >= getRowCount()) return;
1201            data.set(pos, path);
1202            sort();
1203            fireTableDataChanged();
1204            int idx = data.indexOf(path);
1205            if (idx >= 0) {
1206                selectionModel.setSelectionInterval(idx, idx);
1207            }
1208        }
1209
1210        public void removeSelected() {
1211            Iterator<String> it = data.iterator();
1212            int i = 0;
1213            while (it.hasNext()) {
1214                it.next();
1215                if (selectionModel.isSelectedIndex(i)) {
1216                    it.remove();
1217                }
1218                i++;
1219            }
1220            fireTableDataChanged();
1221            selectionModel.clearSelection();
1222        }
1223
1224        protected void sort() {
1225            data.sort((o1, o2) -> {
1226                    if (o1.isEmpty() && o2.isEmpty())
1227                        return 0;
1228                    if (o1.isEmpty()) return 1;
1229                    if (o2.isEmpty()) return -1;
1230                    return o1.compareTo(o2);
1231                });
1232        }
1233
1234        public List<String> getIconPaths() {
1235            return new ArrayList<>(data);
1236        }
1237    }
1238
1239    class NewIconPathAction extends AbstractAction {
1240        NewIconPathAction() {
1241            putValue(NAME, tr("New"));
1242            putValue(SHORT_DESCRIPTION, tr("Add a new icon path"));
1243            new ImageProvider("dialogs", "add").getResource().attachImageIcon(this);
1244        }
1245
1246        @Override
1247        public void actionPerformed(ActionEvent e) {
1248            iconPathsModel.addPath("");
1249            tblIconPaths.editCellAt(iconPathsModel.getRowCount() -1, 0);
1250        }
1251    }
1252
1253    class RemoveIconPathAction extends AbstractAction implements ListSelectionListener {
1254        RemoveIconPathAction() {
1255            putValue(NAME, tr("Remove"));
1256            putValue(SHORT_DESCRIPTION, tr("Remove the selected icon paths"));
1257            new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
1258            updateEnabledState();
1259        }
1260
1261        protected final void updateEnabledState() {
1262            setEnabled(tblIconPaths.getSelectedRowCount() > 0);
1263        }
1264
1265        @Override
1266        public void valueChanged(ListSelectionEvent e) {
1267            updateEnabledState();
1268        }
1269
1270        @Override
1271        public void actionPerformed(ActionEvent e) {
1272            iconPathsModel.removeSelected();
1273        }
1274    }
1275
1276    class EditIconPathAction extends AbstractAction implements ListSelectionListener {
1277        EditIconPathAction() {
1278            putValue(NAME, tr("Edit"));
1279            putValue(SHORT_DESCRIPTION, tr("Edit the selected icon path"));
1280            new ImageProvider("dialogs", "edit").getResource().attachImageIcon(this);
1281            updateEnabledState();
1282        }
1283
1284        protected final void updateEnabledState() {
1285            setEnabled(tblIconPaths.getSelectedRowCount() == 1);
1286        }
1287
1288        @Override
1289        public void valueChanged(ListSelectionEvent e) {
1290            updateEnabledState();
1291        }
1292
1293        @Override
1294        public void actionPerformed(ActionEvent e) {
1295            int row = tblIconPaths.getSelectedRow();
1296            tblIconPaths.editCellAt(row, 0);
1297        }
1298    }
1299
1300    static class SourceEntryListCellRenderer extends JLabel implements ListCellRenderer<ExtendedSourceEntry> {
1301
1302        private final ImageIcon GREEN_CHECK = ImageProvider.getIfAvailable("misc", "green_check");
1303        private final ImageIcon GRAY_CHECK = ImageProvider.getIfAvailable("misc", "gray_check");
1304        private final Map<String, SourceEntry> entryByUrl = new HashMap<>();
1305
1306        @Override
1307        public Component getListCellRendererComponent(JList<? extends ExtendedSourceEntry> list, ExtendedSourceEntry value,
1308                int index, boolean isSelected, boolean cellHasFocus) {
1309            String s = value.toString();
1310            setText(s);
1311            if (isSelected) {
1312                setBackground(list.getSelectionBackground());
1313                setForeground(list.getSelectionForeground());
1314            } else {
1315                setBackground(list.getBackground());
1316                setForeground(list.getForeground());
1317            }
1318            setEnabled(list.isEnabled());
1319            setFont(list.getFont());
1320            setFont(getFont().deriveFont(Font.PLAIN));
1321            setOpaque(true);
1322            setToolTipText(value.getTooltip());
1323            final SourceEntry sourceEntry = entryByUrl.get(value.url);
1324            setIcon(sourceEntry == null ? null : sourceEntry.active ? GREEN_CHECK : GRAY_CHECK);
1325            return this;
1326        }
1327
1328        public void updateSources(List<SourceEntry> sources) {
1329            synchronized (entryByUrl) {
1330                entryByUrl.clear();
1331                for (SourceEntry i : sources) {
1332                    entryByUrl.put(i.url, i);
1333                }
1334            }
1335        }
1336    }
1337
1338    class SourceLoader extends PleaseWaitRunnable {
1339        private final String url;
1340        private final List<SourceProvider> sourceProviders;
1341        private CachedFile cachedFile;
1342        private boolean canceled;
1343        private final List<ExtendedSourceEntry> sources = new ArrayList<>();
1344
1345        SourceLoader(String url, List<SourceProvider> sourceProviders) {
1346            super(tr(getStr(I18nString.LOADING_SOURCES_FROM), url));
1347            this.url = url;
1348            this.sourceProviders = sourceProviders;
1349        }
1350
1351        @Override
1352        protected void cancel() {
1353            canceled = true;
1354            Utils.close(cachedFile);
1355        }
1356
1357        protected void warn(Exception e) {
1358            String emsg = Utils.escapeReservedCharactersHTML(e.getMessage() != null ? e.getMessage() : e.toString());
1359            final String msg = tr(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM), url, emsg);
1360
1361            GuiHelper.runInEDT(() -> HelpAwareOptionPane.showOptionDialog(
1362                    Main.parent,
1363                    msg,
1364                    tr("Error"),
1365                    JOptionPane.ERROR_MESSAGE,
1366                    ht(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC))
1367                    ));
1368        }
1369
1370        @Override
1371        protected void realRun() throws SAXException, IOException, OsmTransferException {
1372            try {
1373                sources.addAll(getDefault());
1374
1375                for (SourceProvider provider : sourceProviders) {
1376                    for (SourceEntry src : provider.getSources()) {
1377                        if (src instanceof ExtendedSourceEntry) {
1378                            sources.add((ExtendedSourceEntry) src);
1379                        }
1380                    }
1381                }
1382                readFile();
1383                for (Iterator<ExtendedSourceEntry> it = sources.iterator(); it.hasNext();) {
1384                    if ("xml".equals(it.next().styleType)) {
1385                        Main.debug("Removing XML source entry");
1386                        it.remove();
1387                    }
1388                }
1389            } catch (IOException e) {
1390                if (canceled)
1391                    // ignore the exception and return
1392                    return;
1393                OsmTransferException ex = new OsmTransferException(e);
1394                ex.setUrl(url);
1395                warn(ex);
1396            }
1397        }
1398
1399        protected void readFile() throws IOException {
1400            final String lang = LanguageInfo.getLanguageCodeXML();
1401            cachedFile = new CachedFile(url);
1402            try (BufferedReader reader = cachedFile.getContentReader()) {
1403
1404                String line;
1405                ExtendedSourceEntry last = null;
1406
1407                while ((line = reader.readLine()) != null && !canceled) {
1408                    if (line.trim().isEmpty()) {
1409                        continue; // skip empty lines
1410                    }
1411                    if (line.startsWith("\t")) {
1412                        Matcher m = Pattern.compile("^\t([^:]+): *(.+)$").matcher(line);
1413                        if (!m.matches()) {
1414                            Main.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line));
1415                            continue;
1416                        }
1417                        if (last != null) {
1418                            String key = m.group(1);
1419                            String value = m.group(2);
1420                            if ("author".equals(key) && last.author == null) {
1421                                last.author = value;
1422                            } else if ("version".equals(key)) {
1423                                last.version = value;
1424                            } else if ("link".equals(key) && last.link == null) {
1425                                last.link = value;
1426                            } else if ("description".equals(key) && last.description == null) {
1427                                last.description = value;
1428                            } else if ((lang + "shortdescription").equals(key) && last.title == null) {
1429                                last.title = value;
1430                            } else if ("shortdescription".equals(key) && last.title == null) {
1431                                last.title = value;
1432                            } else if ((lang + "title").equals(key) && last.title == null) {
1433                                last.title = value;
1434                            } else if ("title".equals(key) && last.title == null) {
1435                                last.title = value;
1436                            } else if ("name".equals(key) && last.name == null) {
1437                                last.name = value;
1438                            } else if ((lang + "author").equals(key)) {
1439                                last.author = value;
1440                            } else if ((lang + "link").equals(key)) {
1441                                last.link = value;
1442                            } else if ((lang + "description").equals(key)) {
1443                                last.description = value;
1444                            } else if ("min-josm-version".equals(key)) {
1445                                try {
1446                                    last.minJosmVersion = Integer.valueOf(value);
1447                                } catch (NumberFormatException e) {
1448                                    // ignore
1449                                    Main.trace(e);
1450                                }
1451                            } else if ("style-type".equals(key)) {
1452                                last.styleType = value;
1453                            }
1454                        }
1455                    } else {
1456                        last = null;
1457                        Matcher m = Pattern.compile("^(.+);(.+)$").matcher(line);
1458                        if (m.matches()) {
1459                            last = new ExtendedSourceEntry(m.group(1), m.group(2));
1460                            sources.add(last);
1461                        } else {
1462                            Main.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line));
1463                        }
1464                    }
1465                }
1466            }
1467        }
1468
1469        @Override
1470        protected void finish() {
1471            Collections.sort(sources);
1472            availableSourcesModel.setSources(sources);
1473        }
1474    }
1475
1476    static class SourceEntryTableCellRenderer extends DefaultTableCellRenderer {
1477        @Override
1478        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
1479            if (value == null)
1480                return this;
1481            return super.getTableCellRendererComponent(table,
1482                    fromSourceEntry((SourceEntry) value), isSelected, hasFocus, row, column);
1483        }
1484
1485        private static String fromSourceEntry(SourceEntry entry) {
1486            if (entry == null)
1487                return null;
1488            StringBuilder s = new StringBuilder(128).append("<html><b>");
1489            if (entry.title != null) {
1490                s.append(entry.title).append("</b> <span color=\"gray\">");
1491            }
1492            s.append(entry.url);
1493            if (entry.title != null) {
1494                s.append("</span>");
1495            }
1496            s.append("</html>");
1497            return s.toString();
1498        }
1499    }
1500
1501    class FileOrUrlCellEditor extends JPanel implements TableCellEditor {
1502        private final JosmTextField tfFileName = new JosmTextField();
1503        private final CopyOnWriteArrayList<CellEditorListener> listeners;
1504        private String value;
1505        private final boolean isFile;
1506
1507        /**
1508         * build the GUI
1509         */
1510        protected final void build() {
1511            setLayout(new GridBagLayout());
1512            GridBagConstraints gc = new GridBagConstraints();
1513            gc.gridx = 0;
1514            gc.gridy = 0;
1515            gc.fill = GridBagConstraints.BOTH;
1516            gc.weightx = 1.0;
1517            gc.weighty = 1.0;
1518            add(tfFileName, gc);
1519
1520            gc.gridx = 1;
1521            gc.gridy = 0;
1522            gc.fill = GridBagConstraints.BOTH;
1523            gc.weightx = 0.0;
1524            gc.weighty = 1.0;
1525            add(new JButton(new LaunchFileChooserAction()));
1526
1527            tfFileName.addFocusListener(
1528                    new FocusAdapter() {
1529                        @Override
1530                        public void focusGained(FocusEvent e) {
1531                            tfFileName.selectAll();
1532                        }
1533                    }
1534                    );
1535        }
1536
1537        FileOrUrlCellEditor(boolean isFile) {
1538            this.isFile = isFile;
1539            listeners = new CopyOnWriteArrayList<>();
1540            build();
1541        }
1542
1543        @Override
1544        public void addCellEditorListener(CellEditorListener l) {
1545            if (l != null) {
1546                listeners.addIfAbsent(l);
1547            }
1548        }
1549
1550        protected void fireEditingCanceled() {
1551            for (CellEditorListener l: listeners) {
1552                l.editingCanceled(new ChangeEvent(this));
1553            }
1554        }
1555
1556        protected void fireEditingStopped() {
1557            for (CellEditorListener l: listeners) {
1558                l.editingStopped(new ChangeEvent(this));
1559            }
1560        }
1561
1562        @Override
1563        public void cancelCellEditing() {
1564            fireEditingCanceled();
1565        }
1566
1567        @Override
1568        public Object getCellEditorValue() {
1569            return value;
1570        }
1571
1572        @Override
1573        public boolean isCellEditable(EventObject anEvent) {
1574            if (anEvent instanceof MouseEvent)
1575                return ((MouseEvent) anEvent).getClickCount() >= 2;
1576            return true;
1577        }
1578
1579        @Override
1580        public void removeCellEditorListener(CellEditorListener l) {
1581            listeners.remove(l);
1582        }
1583
1584        @Override
1585        public boolean shouldSelectCell(EventObject anEvent) {
1586            return true;
1587        }
1588
1589        @Override
1590        public boolean stopCellEditing() {
1591            value = tfFileName.getText();
1592            fireEditingStopped();
1593            return true;
1594        }
1595
1596        public void setInitialValue(String initialValue) {
1597            this.value = initialValue;
1598            if (initialValue == null) {
1599                this.tfFileName.setText("");
1600            } else {
1601                this.tfFileName.setText(initialValue);
1602            }
1603        }
1604
1605        @Override
1606        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
1607            setInitialValue((String) value);
1608            tfFileName.selectAll();
1609            return this;
1610        }
1611
1612        class LaunchFileChooserAction extends AbstractAction {
1613            LaunchFileChooserAction() {
1614                putValue(NAME, "...");
1615                putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
1616            }
1617
1618            @Override
1619            public void actionPerformed(ActionEvent e) {
1620                FileChooserManager fcm = new FileChooserManager(true).createFileChooser();
1621                if (!isFile) {
1622                    fcm.getFileChooser().setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1623                }
1624                prepareFileChooser(tfFileName.getText(), fcm.getFileChooser());
1625                AbstractFileChooser fc = fcm.openFileChooser(GuiHelper.getFrameForComponent(SourceEditor.this));
1626                if (fc != null) {
1627                    tfFileName.setText(fc.getSelectedFile().toString());
1628                }
1629            }
1630        }
1631    }
1632
1633    public abstract static class SourcePrefHelper {
1634
1635        private final String pref;
1636
1637        /**
1638         * Constructs a new {@code SourcePrefHelper} for the given preference key.
1639         * @param pref The preference key
1640         */
1641        public SourcePrefHelper(String pref) {
1642            this.pref = pref;
1643        }
1644
1645        /**
1646         * Returns the default sources provided by JOSM core.
1647         * @return the default sources provided by JOSM core
1648         */
1649        public abstract Collection<ExtendedSourceEntry> getDefault();
1650
1651        /**
1652         * Serializes the given source entry as a map.
1653         * @param entry source entry to serialize
1654         * @return map (key=value)
1655         */
1656        public abstract Map<String, String> serialize(SourceEntry entry);
1657
1658        /**
1659         * Deserializes the given map as a source entry.
1660         * @param entryStr map (key=value)
1661         * @return source entry
1662         */
1663        public abstract SourceEntry deserialize(Map<String, String> entryStr);
1664
1665        /**
1666         * Returns the list of sources.
1667         * @return The list of sources
1668         */
1669        public List<SourceEntry> get() {
1670
1671            Collection<Map<String, String>> src = Main.pref.getListOfStructs(pref, (Collection<Map<String, String>>) null);
1672            if (src == null)
1673                return new ArrayList<SourceEntry>(getDefault());
1674
1675            List<SourceEntry> entries = new ArrayList<>();
1676            for (Map<String, String> sourcePref : src) {
1677                SourceEntry e = deserialize(new HashMap<>(sourcePref));
1678                if (e != null) {
1679                    entries.add(e);
1680                }
1681            }
1682            return entries;
1683        }
1684
1685        /**
1686         * Saves a list of sources to JOSM preferences.
1687         * @param entries list of sources
1688         * @return {@code true}, if something has changed (i.e. value is different than before)
1689         */
1690        public boolean put(Collection<? extends SourceEntry> entries) {
1691            Collection<Map<String, String>> setting = new ArrayList<>(entries.size());
1692            for (SourceEntry e : entries) {
1693                setting.add(serialize(e));
1694            }
1695            return Main.pref.putListOfStructs(pref, setting);
1696        }
1697
1698        /**
1699         * Returns the set of active source URLs.
1700         * @return The set of active source URLs.
1701         */
1702        public final Set<String> getActiveUrls() {
1703            Set<String> urls = new LinkedHashSet<>(); // retain order
1704            for (SourceEntry e : get()) {
1705                if (e.active) {
1706                    urls.add(e.url);
1707                }
1708            }
1709            return urls;
1710        }
1711    }
1712
1713    /**
1714     * Defers loading of sources to the first time the adequate tab is selected.
1715     * @param tab The preferences tab
1716     * @param component The tab component
1717     * @since 6670
1718     */
1719    public final void deferLoading(final DefaultTabPreferenceSetting tab, final Component component) {
1720        tab.getTabPane().addChangeListener(e -> {
1721            if (tab.getTabPane().getSelectedComponent() == component) {
1722                initiallyLoadAvailableSources();
1723            }
1724        });
1725    }
1726
1727    protected String getTitleForSourceEntry(SourceEntry entry) {
1728        return "".equals(entry.title) ? null : entry.title;
1729    }
1730}