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