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