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