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, Preset and TagChecker Rule)
419     * @param ident any {@link I18nString} value
420     * @return the translated string for {@code ident}
421     */
422    protected abstract String getStr(I18nString ident);
423
424    /**
425     * Identifiers for strings that need to be provided.
426     */
427    public enum I18nString {
428        /** Available (styles|presets|rules) */
429        AVAILABLE_SOURCES,
430        /** Active (styles|presets|rules) */
431        ACTIVE_SOURCES,
432        /** Add a new (style|preset|rule) by entering filename or URL */
433        NEW_SOURCE_ENTRY_TOOLTIP,
434        /** New (style|preset|rule) entry */
435        NEW_SOURCE_ENTRY,
436        /** Remove the selected (styles|presets|rules) from the list of active (styles|presets|rules) */
437        REMOVE_SOURCE_TOOLTIP,
438        /** Edit the filename or URL for the selected active (style|preset|rule) */
439        EDIT_SOURCE_TOOLTIP,
440        /** Add the selected available (styles|presets|rules) to the list of active (styles|presets|rules) */
441        ACTIVATE_TOOLTIP,
442        /** Reloads the list of available (styles|presets|rules) */
443        RELOAD_ALL_AVAILABLE,
444        /** Loading (style|preset|rule) sources */
445        LOADING_SOURCES_FROM,
446        /** Failed to load the list of (style|preset|rule) sources */
447        FAILED_TO_LOAD_SOURCES_FROM,
448        /** /Preferences/(Styles|Presets|Rules)#FailedToLoad(Style|Preset|Rule)Sources */
449        FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC,
450        /** Illegal format of entry in (style|preset|rule) list */
451        ILLEGAL_FORMAT_OF_ENTRY
452    }
453
454    public boolean hasActiveSourcesChanged() {
455        Collection<? extends SourceEntry> prev = getInitialSourcesList();
456        List<SourceEntry> cur = activeSourcesModel.getSources();
457        if (prev.size() != cur.size())
458            return true;
459        Iterator<? extends SourceEntry> p = prev.iterator();
460        Iterator<SourceEntry> c = cur.iterator();
461        while (p.hasNext()) {
462            SourceEntry pe = p.next();
463            SourceEntry ce = c.next();
464            if (!Objects.equals(pe.url, ce.url) || !Objects.equals(pe.name, ce.name) || pe.active != ce.active)
465                return true;
466        }
467        return false;
468    }
469
470    public Collection<SourceEntry> getActiveSources() {
471        return activeSourcesModel.getSources();
472    }
473
474    /**
475     * Synchronously loads available sources and returns the parsed list.
476     * @return list of available sources
477     */
478    public final Collection<ExtendedSourceEntry> loadAndGetAvailableSources() {
479        try {
480            final SourceLoader loader = new SourceLoader(availableSourcesUrl, sourceProviders);
481            loader.realRun();
482            return loader.sources;
483        } catch (Exception ex) {
484            throw new RuntimeException(ex);
485        }
486    }
487
488    public void removeSources(Collection<Integer> idxs) {
489        activeSourcesModel.removeIdxs(idxs);
490    }
491
492    protected void reloadAvailableSources(String url, List<SourceProvider> sourceProviders) {
493        Main.worker.submit(new SourceLoader(url, sourceProviders));
494    }
495
496    /**
497     * Performs the initial loading of source providers. Does nothing if already done.
498     */
499    public void initiallyLoadAvailableSources() {
500        if (!sourcesInitiallyLoaded) {
501            reloadAvailableSources(availableSourcesUrl, sourceProviders);
502        }
503        sourcesInitiallyLoaded = true;
504    }
505
506    protected static class AvailableSourcesListModel extends DefaultListModel<ExtendedSourceEntry> {
507        private final transient List<ExtendedSourceEntry> data;
508        private final DefaultListSelectionModel selectionModel;
509
510        public AvailableSourcesListModel(DefaultListSelectionModel selectionModel) {
511            data = new ArrayList<>();
512            this.selectionModel = selectionModel;
513        }
514
515        public void setSources(List<ExtendedSourceEntry> sources) {
516            data.clear();
517            if (sources != null) {
518                data.addAll(sources);
519            }
520            fireContentsChanged(this, 0, data.size());
521        }
522
523        @Override
524        public ExtendedSourceEntry getElementAt(int index) {
525            return data.get(index);
526        }
527
528        @Override
529        public int getSize() {
530            if (data == null) return 0;
531            return data.size();
532        }
533
534        public void deleteSelected() {
535            Iterator<ExtendedSourceEntry> it = data.iterator();
536            int i = 0;
537            while (it.hasNext()) {
538                it.next();
539                if (selectionModel.isSelectedIndex(i)) {
540                    it.remove();
541                }
542                i++;
543            }
544            fireContentsChanged(this, 0, data.size());
545        }
546
547        public List<ExtendedSourceEntry> getSelected() {
548            List<ExtendedSourceEntry> ret = new ArrayList<>();
549            for (int i = 0; i < data.size(); i++) {
550                if (selectionModel.isSelectedIndex(i)) {
551                    ret.add(data.get(i));
552                }
553            }
554            return ret;
555        }
556    }
557
558    protected class ActiveSourcesModel extends AbstractTableModel {
559        private transient List<SourceEntry> data;
560        private final DefaultListSelectionModel selectionModel;
561
562        public ActiveSourcesModel(DefaultListSelectionModel selectionModel) {
563            this.selectionModel = selectionModel;
564            this.data = new ArrayList<>();
565        }
566
567        @Override
568        public int getColumnCount() {
569            return canEnable ? 2 : 1;
570        }
571
572        @Override
573        public int getRowCount() {
574            return data == null ? 0 : data.size();
575        }
576
577        @Override
578        public Object getValueAt(int rowIndex, int columnIndex) {
579            if (canEnable && columnIndex == 0)
580                return data.get(rowIndex).active;
581            else
582                return data.get(rowIndex);
583        }
584
585        @Override
586        public boolean isCellEditable(int rowIndex, int columnIndex) {
587            return canEnable && columnIndex == 0;
588        }
589
590        @Override
591        public Class<?> getColumnClass(int column) {
592            if (canEnable && column == 0)
593                return Boolean.class;
594            else return SourceEntry.class;
595        }
596
597        @Override
598        public void setValueAt(Object aValue, int row, int column) {
599            if (row < 0 || row >= getRowCount() || aValue == null)
600                return;
601            if (canEnable && column == 0) {
602                data.get(row).active = !data.get(row).active;
603            }
604        }
605
606        public void setActiveSources(Collection<? extends SourceEntry> sources) {
607            data.clear();
608            if (sources != null) {
609                for (SourceEntry e : sources) {
610                    data.add(new SourceEntry(e));
611                }
612            }
613            fireTableDataChanged();
614        }
615
616        public void addSource(SourceEntry entry) {
617            if (entry == null) return;
618            data.add(entry);
619            fireTableDataChanged();
620            int idx = data.indexOf(entry);
621            if (idx >= 0) {
622                selectionModel.setSelectionInterval(idx, idx);
623            }
624        }
625
626        public void removeSelected() {
627            Iterator<SourceEntry> it = data.iterator();
628            int i = 0;
629            while (it.hasNext()) {
630                it.next();
631                if (selectionModel.isSelectedIndex(i)) {
632                    it.remove();
633                }
634                i++;
635            }
636            fireTableDataChanged();
637        }
638
639        public void removeIdxs(Collection<Integer> idxs) {
640            List<SourceEntry> newData = new ArrayList<>();
641            for (int i = 0; i < data.size(); ++i) {
642                if (!idxs.contains(i)) {
643                    newData.add(data.get(i));
644                }
645            }
646            data = newData;
647            fireTableDataChanged();
648        }
649
650        public void addExtendedSourceEntries(List<ExtendedSourceEntry> sources) {
651            if (sources == null) return;
652            for (ExtendedSourceEntry info: sources) {
653                data.add(new SourceEntry(info.url, info.name, info.getDisplayName(), true));
654            }
655            fireTableDataChanged();
656            selectionModel.clearSelection();
657            for (ExtendedSourceEntry info: sources) {
658                int pos = data.indexOf(info);
659                if (pos >= 0) {
660                    selectionModel.addSelectionInterval(pos, pos);
661                }
662            }
663        }
664
665        public List<SourceEntry> getSources() {
666            return new ArrayList<>(data);
667        }
668
669        public boolean canMove(int i) {
670            int[] sel = tblActiveSources.getSelectedRows();
671            if (sel.length == 0)
672                return false;
673            if (i < 0)
674                return sel[0] >= -i;
675                else if (i > 0)
676                    return sel[sel.length-1] <= getRowCount()-1 - i;
677                else
678                    return true;
679        }
680
681        public void move(int i) {
682            if (!canMove(i)) return;
683            int[] sel = tblActiveSources.getSelectedRows();
684            for (int row: sel) {
685                SourceEntry t1 = data.get(row);
686                SourceEntry t2 = data.get(row + i);
687                data.set(row, t2);
688                data.set(row + i, t1);
689            }
690            selectionModel.clearSelection();
691            for (int row: sel) {
692                selectionModel.addSelectionInterval(row + i, row + i);
693            }
694        }
695    }
696
697    public static class ExtendedSourceEntry extends SourceEntry implements Comparable<ExtendedSourceEntry> {
698        public String simpleFileName;
699        public String version;
700        public String author;
701        public String link;
702        public String description;
703        public Integer minJosmVersion;
704
705        public ExtendedSourceEntry(String simpleFileName, String url) {
706            super(url, null, null, true);
707            this.simpleFileName = simpleFileName;
708        }
709
710        /**
711         * @return string representation for GUI list or menu entry
712         */
713        public String getDisplayName() {
714            return title == null ? simpleFileName : title;
715        }
716
717        private static void appendRow(StringBuilder s, String th, String td) {
718            s.append("<tr><th>").append(th).append("</th><td>").append(td).append("</td</tr>");
719        }
720
721        public String getTooltip() {
722            StringBuilder s = new StringBuilder();
723            appendRow(s, tr("Short Description:"), getDisplayName());
724            appendRow(s, tr("URL:"), url);
725            if (author != null) {
726                appendRow(s, tr("Author:"), author);
727            }
728            if (link != null) {
729                appendRow(s, tr("Webpage:"), link);
730            }
731            if (description != null) {
732                appendRow(s, tr("Description:"), description);
733            }
734            if (version != null) {
735                appendRow(s, tr("Version:"), version);
736            }
737            if (minJosmVersion != null) {
738                appendRow(s, tr("Minimum JOSM Version:"), Integer.toString(minJosmVersion));
739            }
740            return "<html><style>th{text-align:right}td{width:400px}</style>"
741                    + "<table>" + s + "</table></html>";
742        }
743
744        @Override
745        public String toString() {
746            return "<html><b>" + getDisplayName() + "</b>"
747                    + (author == null ? "" : " <span color=\"gray\">" + tr("by {0}", author) + "</color>")
748                    + "</html>";
749        }
750
751        @Override
752        public int compareTo(ExtendedSourceEntry o) {
753            if (url.startsWith("resource") && !o.url.startsWith("resource"))
754                return -1;
755            if (o.url.startsWith("resource"))
756                return 1;
757            else
758                return getDisplayName().compareToIgnoreCase(o.getDisplayName());
759        }
760    }
761
762    private static void prepareFileChooser(String url, AbstractFileChooser fc) {
763        if (url == null || url.trim().isEmpty()) return;
764        URL sourceUrl = null;
765        try {
766            sourceUrl = new URL(url);
767        } catch (MalformedURLException e) {
768            File f = new File(url);
769            if (f.isFile()) {
770                f = f.getParentFile();
771            }
772            if (f != null) {
773                fc.setCurrentDirectory(f);
774            }
775            return;
776        }
777        if (sourceUrl.getProtocol().startsWith("file")) {
778            File f = new File(sourceUrl.getPath());
779            if (f.isFile()) {
780                f = f.getParentFile();
781            }
782            if (f != null) {
783                fc.setCurrentDirectory(f);
784            }
785        }
786    }
787
788    protected class EditSourceEntryDialog extends ExtendedDialog {
789
790        private final JosmTextField tfTitle;
791        private final JosmTextField tfURL;
792        private JCheckBox cbActive;
793
794        public EditSourceEntryDialog(Component parent, String title, SourceEntry e) {
795            super(parent, title, new String[] {tr("Ok"), tr("Cancel")});
796
797            JPanel p = new JPanel(new GridBagLayout());
798
799            tfTitle = new JosmTextField(60);
800            p.add(new JLabel(tr("Name (optional):")), GBC.std().insets(15, 0, 5, 5));
801            p.add(tfTitle, GBC.eol().insets(0, 0, 5, 5));
802
803            tfURL = new JosmTextField(60);
804            p.add(new JLabel(tr("URL / File:")), GBC.std().insets(15, 0, 5, 0));
805            p.add(tfURL, GBC.std().insets(0, 0, 5, 5));
806            JButton fileChooser = new JButton(new LaunchFileChooserAction());
807            fileChooser.setMargin(new Insets(0, 0, 0, 0));
808            p.add(fileChooser, GBC.eol().insets(0, 0, 5, 5));
809
810            if (e != null) {
811                if (e.title != null) {
812                    tfTitle.setText(e.title);
813                }
814                tfURL.setText(e.url);
815            }
816
817            if (canEnable) {
818                cbActive = new JCheckBox(tr("active"), e == null || e.active);
819                p.add(cbActive, GBC.eol().insets(15, 0, 5, 0));
820            }
821            setButtonIcons(new String[] {"ok", "cancel"});
822            setContent(p);
823
824            // Make OK button enabled only when a file/URL has been set
825            tfURL.getDocument().addDocumentListener(new DocumentListener() {
826                @Override
827                public void insertUpdate(DocumentEvent e) {
828                    updateOkButtonState();
829                }
830
831                @Override
832                public void removeUpdate(DocumentEvent e) {
833                    updateOkButtonState();
834                }
835
836                @Override
837                public void changedUpdate(DocumentEvent e) {
838                    updateOkButtonState();
839                }
840            });
841        }
842
843        private void updateOkButtonState() {
844            buttons.get(0).setEnabled(!Utils.strip(tfURL.getText()).isEmpty());
845        }
846
847        @Override
848        public void setupDialog() {
849            super.setupDialog();
850            updateOkButtonState();
851        }
852
853        class LaunchFileChooserAction extends AbstractAction {
854            LaunchFileChooserAction() {
855                putValue(SMALL_ICON, ImageProvider.get("open"));
856                putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
857            }
858
859            @Override
860            public void actionPerformed(ActionEvent e) {
861                FileFilter ff;
862                switch (sourceType) {
863                case MAP_PAINT_STYLE:
864                    ff = new ExtensionFileFilter("xml,mapcss,css,zip", "xml", tr("Map paint style file (*.xml, *.mapcss, *.zip)"));
865                    break;
866                case TAGGING_PRESET:
867                    ff = new ExtensionFileFilter("xml,zip", "xml", tr("Preset definition file (*.xml, *.zip)"));
868                    break;
869                case TAGCHECKER_RULE:
870                    ff = new ExtensionFileFilter("validator.mapcss,zip", "validator.mapcss", tr("Tag checker rule (*.validator.mapcss, *.zip)"));
871                    break;
872                default:
873                    Main.error("Unsupported source type: "+sourceType);
874                    return;
875                }
876                FileChooserManager fcm = new FileChooserManager(true)
877                        .createFileChooser(true, null, Arrays.asList(ff, FileFilterAllFiles.getInstance()), ff, JFileChooser.FILES_ONLY);
878                prepareFileChooser(tfURL.getText(), fcm.getFileChooser());
879                AbstractFileChooser fc = fcm.openFileChooser(JOptionPane.getFrameForComponent(SourceEditor.this));
880                if (fc != null) {
881                    tfURL.setText(fc.getSelectedFile().toString());
882                }
883            }
884        }
885
886        @Override
887        public String getTitle() {
888            return tfTitle.getText();
889        }
890
891        public String getURL() {
892            return tfURL.getText();
893        }
894
895        public boolean active() {
896            if (!canEnable)
897                throw new UnsupportedOperationException();
898            return cbActive.isSelected();
899        }
900    }
901
902    class NewActiveSourceAction extends AbstractAction {
903        NewActiveSourceAction() {
904            putValue(NAME, tr("New"));
905            putValue(SHORT_DESCRIPTION, getStr(I18nString.NEW_SOURCE_ENTRY_TOOLTIP));
906            putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
907        }
908
909        @Override
910        public void actionPerformed(ActionEvent evt) {
911            EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog(
912                    SourceEditor.this,
913                    getStr(I18nString.NEW_SOURCE_ENTRY),
914                    null);
915            editEntryDialog.showDialog();
916            if (editEntryDialog.getValue() == 1) {
917                boolean active = true;
918                if (canEnable) {
919                    active = editEntryDialog.active();
920                }
921                final SourceEntry entry = new SourceEntry(
922                        editEntryDialog.getURL(),
923                        null, editEntryDialog.getTitle(), active);
924                entry.title = getTitleForSourceEntry(entry);
925                activeSourcesModel.addSource(entry);
926                activeSourcesModel.fireTableDataChanged();
927            }
928        }
929    }
930
931    class RemoveActiveSourcesAction extends AbstractAction implements ListSelectionListener {
932
933        RemoveActiveSourcesAction() {
934            putValue(NAME, tr("Remove"));
935            putValue(SHORT_DESCRIPTION, getStr(I18nString.REMOVE_SOURCE_TOOLTIP));
936            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
937            updateEnabledState();
938        }
939
940        protected final void updateEnabledState() {
941            setEnabled(tblActiveSources.getSelectedRowCount() > 0);
942        }
943
944        @Override
945        public void valueChanged(ListSelectionEvent e) {
946            updateEnabledState();
947        }
948
949        @Override
950        public void actionPerformed(ActionEvent e) {
951            activeSourcesModel.removeSelected();
952        }
953    }
954
955    class EditActiveSourceAction extends AbstractAction implements ListSelectionListener {
956        EditActiveSourceAction() {
957            putValue(NAME, tr("Edit"));
958            putValue(SHORT_DESCRIPTION, getStr(I18nString.EDIT_SOURCE_TOOLTIP));
959            putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
960            updateEnabledState();
961        }
962
963        protected final void updateEnabledState() {
964            setEnabled(tblActiveSources.getSelectedRowCount() == 1);
965        }
966
967        @Override
968        public void valueChanged(ListSelectionEvent e) {
969            updateEnabledState();
970        }
971
972        @Override
973        public void actionPerformed(ActionEvent evt) {
974            int pos = tblActiveSources.getSelectedRow();
975            if (pos < 0 || pos >= tblActiveSources.getRowCount())
976                return;
977
978            SourceEntry e = (SourceEntry) activeSourcesModel.getValueAt(pos, 1);
979
980            EditSourceEntryDialog editEntryDialog = new EditSourceEntryDialog(
981                    SourceEditor.this, tr("Edit source entry:"), e);
982            editEntryDialog.showDialog();
983            if (editEntryDialog.getValue() == 1) {
984                if (e.title != null || !"".equals(editEntryDialog.getTitle())) {
985                    e.title = editEntryDialog.getTitle();
986                    e.title = getTitleForSourceEntry(e);
987                }
988                e.url = editEntryDialog.getURL();
989                if (canEnable) {
990                    e.active = editEntryDialog.active();
991                }
992                activeSourcesModel.fireTableRowsUpdated(pos, pos);
993            }
994        }
995    }
996
997    /**
998     * The action to move the currently selected entries up or down in the list.
999     */
1000    class MoveUpDownAction extends AbstractAction implements ListSelectionListener, TableModelListener {
1001        private final int increment;
1002
1003        MoveUpDownAction(boolean isDown) {
1004            increment = isDown ? 1 : -1;
1005            putValue(SMALL_ICON, isDown ? ImageProvider.get("dialogs", "down") : ImageProvider.get("dialogs", "up"));
1006            putValue(SHORT_DESCRIPTION, isDown ? tr("Move the selected entry one row down.") : tr("Move the selected entry one row up."));
1007            updateEnabledState();
1008        }
1009
1010        public final void updateEnabledState() {
1011            setEnabled(activeSourcesModel.canMove(increment));
1012        }
1013
1014        @Override
1015        public void actionPerformed(ActionEvent e) {
1016            activeSourcesModel.move(increment);
1017        }
1018
1019        @Override
1020        public void valueChanged(ListSelectionEvent e) {
1021            updateEnabledState();
1022        }
1023
1024        @Override
1025        public void tableChanged(TableModelEvent e) {
1026            updateEnabledState();
1027        }
1028    }
1029
1030    class ActivateSourcesAction extends AbstractAction implements ListSelectionListener {
1031        ActivateSourcesAction() {
1032            putValue(SHORT_DESCRIPTION, getStr(I18nString.ACTIVATE_TOOLTIP));
1033            putValue(SMALL_ICON, ImageProvider.get("preferences", "activate-right"));
1034            updateEnabledState();
1035        }
1036
1037        protected final void updateEnabledState() {
1038            setEnabled(lstAvailableSources.getSelectedIndices().length > 0);
1039        }
1040
1041        @Override
1042        public void valueChanged(ListSelectionEvent e) {
1043            updateEnabledState();
1044        }
1045
1046        @Override
1047        public void actionPerformed(ActionEvent e) {
1048            List<ExtendedSourceEntry> sources = availableSourcesModel.getSelected();
1049            int josmVersion = Version.getInstance().getVersion();
1050            if (josmVersion != Version.JOSM_UNKNOWN_VERSION) {
1051                Collection<String> messages = new ArrayList<>();
1052                for (ExtendedSourceEntry entry : sources) {
1053                    if (entry.minJosmVersion != null && entry.minJosmVersion > josmVersion) {
1054                        messages.add(tr("Entry ''{0}'' requires JOSM Version {1}. (Currently running: {2})",
1055                                entry.title,
1056                                Integer.toString(entry.minJosmVersion),
1057                                Integer.toString(josmVersion))
1058                        );
1059                    }
1060                }
1061                if (!messages.isEmpty()) {
1062                    ExtendedDialog dlg = new ExtendedDialog(Main.parent, tr("Warning"), new String[] {tr("Cancel"), tr("Continue anyway")});
1063                    dlg.setButtonIcons(new Icon[] {
1064                        ImageProvider.get("cancel"),
1065                        new ImageProvider("ok").setMaxSize(ImageSizes.LARGEICON).addOverlay(
1066                                new ImageOverlay(new ImageProvider("warning-small"), 0.5, 0.5, 1.0, 1.0)).get()
1067                    });
1068                    dlg.setToolTipTexts(new String[] {
1069                        tr("Cancel and return to the previous dialog"),
1070                        tr("Ignore warning and install style anyway")});
1071                    dlg.setContent("<html>" + tr("Some entries have unmet dependencies:") +
1072                            "<br>" + Utils.join("<br>", messages) + "</html>");
1073                    dlg.setIcon(JOptionPane.WARNING_MESSAGE);
1074                    if (dlg.showDialog().getValue() != 2)
1075                        return;
1076                }
1077            }
1078            activeSourcesModel.addExtendedSourceEntries(sources);
1079        }
1080    }
1081
1082    class ResetAction extends AbstractAction {
1083
1084        ResetAction() {
1085            putValue(NAME, tr("Reset"));
1086            putValue(SHORT_DESCRIPTION, tr("Reset to default"));
1087            putValue(SMALL_ICON, ImageProvider.get("preferences", "reset"));
1088        }
1089
1090        @Override
1091        public void actionPerformed(ActionEvent e) {
1092            activeSourcesModel.setActiveSources(getDefault());
1093        }
1094    }
1095
1096    class ReloadSourcesAction extends AbstractAction {
1097        private final String url;
1098        private final transient List<SourceProvider> sourceProviders;
1099
1100        ReloadSourcesAction(String url, List<SourceProvider> sourceProviders) {
1101            putValue(NAME, tr("Reload"));
1102            putValue(SHORT_DESCRIPTION, tr(getStr(I18nString.RELOAD_ALL_AVAILABLE), url));
1103            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
1104            this.url = url;
1105            this.sourceProviders = sourceProviders;
1106            setEnabled(!Main.isOffline(OnlineResource.JOSM_WEBSITE));
1107        }
1108
1109        @Override
1110        public void actionPerformed(ActionEvent e) {
1111            CachedFile.cleanup(url);
1112            reloadAvailableSources(url, sourceProviders);
1113        }
1114    }
1115
1116    protected static class IconPathTableModel extends AbstractTableModel {
1117        private final List<String> data;
1118        private final DefaultListSelectionModel selectionModel;
1119
1120        public IconPathTableModel(DefaultListSelectionModel selectionModel) {
1121            this.selectionModel = selectionModel;
1122            this.data = new ArrayList<>();
1123        }
1124
1125        @Override
1126        public int getColumnCount() {
1127            return 1;
1128        }
1129
1130        @Override
1131        public int getRowCount() {
1132            return data == null ? 0 : data.size();
1133        }
1134
1135        @Override
1136        public Object getValueAt(int rowIndex, int columnIndex) {
1137            return data.get(rowIndex);
1138        }
1139
1140        @Override
1141        public boolean isCellEditable(int rowIndex, int columnIndex) {
1142            return true;
1143        }
1144
1145        @Override
1146        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
1147            updatePath(rowIndex, (String) aValue);
1148        }
1149
1150        public void setIconPaths(Collection<String> paths) {
1151            data.clear();
1152            if (paths != null) {
1153                data.addAll(paths);
1154            }
1155            sort();
1156            fireTableDataChanged();
1157        }
1158
1159        public void addPath(String path) {
1160            if (path == null) return;
1161            data.add(path);
1162            sort();
1163            fireTableDataChanged();
1164            int idx = data.indexOf(path);
1165            if (idx >= 0) {
1166                selectionModel.setSelectionInterval(idx, idx);
1167            }
1168        }
1169
1170        public void updatePath(int pos, String path) {
1171            if (path == null) return;
1172            if (pos < 0 || pos >= getRowCount()) return;
1173            data.set(pos, path);
1174            sort();
1175            fireTableDataChanged();
1176            int idx = data.indexOf(path);
1177            if (idx >= 0) {
1178                selectionModel.setSelectionInterval(idx, idx);
1179            }
1180        }
1181
1182        public void removeSelected() {
1183            Iterator<String> it = data.iterator();
1184            int i = 0;
1185            while (it.hasNext()) {
1186                it.next();
1187                if (selectionModel.isSelectedIndex(i)) {
1188                    it.remove();
1189                }
1190                i++;
1191            }
1192            fireTableDataChanged();
1193            selectionModel.clearSelection();
1194        }
1195
1196        protected void sort() {
1197            Collections.sort(
1198                    data,
1199                    new Comparator<String>() {
1200                        @Override
1201                        public int compare(String o1, String o2) {
1202                            if (o1.isEmpty() && o2.isEmpty())
1203                                return 0;
1204                            if (o1.isEmpty()) return 1;
1205                            if (o2.isEmpty()) return -1;
1206                            return o1.compareTo(o2);
1207                        }
1208                    }
1209                    );
1210        }
1211
1212        public List<String> getIconPaths() {
1213            return new ArrayList<>(data);
1214        }
1215    }
1216
1217    class NewIconPathAction extends AbstractAction {
1218        NewIconPathAction() {
1219            putValue(NAME, tr("New"));
1220            putValue(SHORT_DESCRIPTION, tr("Add a new icon path"));
1221            putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
1222        }
1223
1224        @Override
1225        public void actionPerformed(ActionEvent e) {
1226            iconPathsModel.addPath("");
1227            tblIconPaths.editCellAt(iconPathsModel.getRowCount() -1, 0);
1228        }
1229    }
1230
1231    class RemoveIconPathAction extends AbstractAction implements ListSelectionListener {
1232        RemoveIconPathAction() {
1233            putValue(NAME, tr("Remove"));
1234            putValue(SHORT_DESCRIPTION, tr("Remove the selected icon paths"));
1235            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
1236            updateEnabledState();
1237        }
1238
1239        protected final void updateEnabledState() {
1240            setEnabled(tblIconPaths.getSelectedRowCount() > 0);
1241        }
1242
1243        @Override
1244        public void valueChanged(ListSelectionEvent e) {
1245            updateEnabledState();
1246        }
1247
1248        @Override
1249        public void actionPerformed(ActionEvent e) {
1250            iconPathsModel.removeSelected();
1251        }
1252    }
1253
1254    class EditIconPathAction extends AbstractAction implements ListSelectionListener {
1255        EditIconPathAction() {
1256            putValue(NAME, tr("Edit"));
1257            putValue(SHORT_DESCRIPTION, tr("Edit the selected icon path"));
1258            putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
1259            updateEnabledState();
1260        }
1261
1262        protected final void updateEnabledState() {
1263            setEnabled(tblIconPaths.getSelectedRowCount() == 1);
1264        }
1265
1266        @Override
1267        public void valueChanged(ListSelectionEvent e) {
1268            updateEnabledState();
1269        }
1270
1271        @Override
1272        public void actionPerformed(ActionEvent e) {
1273            int row = tblIconPaths.getSelectedRow();
1274            tblIconPaths.editCellAt(row, 0);
1275        }
1276    }
1277
1278    static class SourceEntryListCellRenderer extends JLabel implements ListCellRenderer<ExtendedSourceEntry> {
1279
1280        private final ImageIcon GREEN_CHECK = ImageProvider.getIfAvailable("misc", "green_check");
1281        private final ImageIcon GRAY_CHECK = ImageProvider.getIfAvailable("misc", "gray_check");
1282        private final Map<String, SourceEntry> entryByUrl = new HashMap<>();
1283
1284        @Override
1285        public Component getListCellRendererComponent(JList<? extends ExtendedSourceEntry> list, ExtendedSourceEntry value,
1286                int index, boolean isSelected, boolean cellHasFocus) {
1287            String s = value.toString();
1288            setText(s);
1289            if (isSelected) {
1290                setBackground(list.getSelectionBackground());
1291                setForeground(list.getSelectionForeground());
1292            } else {
1293                setBackground(list.getBackground());
1294                setForeground(list.getForeground());
1295            }
1296            setEnabled(list.isEnabled());
1297            setFont(list.getFont());
1298            setFont(getFont().deriveFont(Font.PLAIN));
1299            setOpaque(true);
1300            setToolTipText(value.getTooltip());
1301            final SourceEntry sourceEntry = entryByUrl.get(value.url);
1302            setIcon(sourceEntry == null ? null : sourceEntry.active ? GREEN_CHECK : GRAY_CHECK);
1303            return this;
1304        }
1305
1306        public void updateSources(List<SourceEntry> sources) {
1307            synchronized (entryByUrl) {
1308                entryByUrl.clear();
1309                for (SourceEntry i : sources) {
1310                    entryByUrl.put(i.url, i);
1311                }
1312            }
1313        }
1314    }
1315
1316    class SourceLoader extends PleaseWaitRunnable {
1317        private final String url;
1318        private final List<SourceProvider> sourceProviders;
1319        private BufferedReader reader;
1320        private boolean canceled;
1321        private final List<ExtendedSourceEntry> sources = new ArrayList<>();
1322
1323        SourceLoader(String url, List<SourceProvider> sourceProviders) {
1324            super(tr(getStr(I18nString.LOADING_SOURCES_FROM), url));
1325            this.url = url;
1326            this.sourceProviders = sourceProviders;
1327        }
1328
1329        @Override
1330        protected void cancel() {
1331            canceled = true;
1332            Utils.close(reader);
1333        }
1334
1335        protected void warn(Exception e) {
1336            String emsg = e.getMessage() != null ? e.getMessage() : e.toString();
1337            emsg = emsg.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1338            final String msg = tr(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM), url, emsg);
1339
1340            GuiHelper.runInEDT(new Runnable() {
1341                @Override
1342                public void run() {
1343                    HelpAwareOptionPane.showOptionDialog(
1344                            Main.parent,
1345                            msg,
1346                            tr("Error"),
1347                            JOptionPane.ERROR_MESSAGE,
1348                            ht(getStr(I18nString.FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC))
1349                            );
1350                }
1351            });
1352        }
1353
1354        @Override
1355        protected void realRun() throws SAXException, IOException, OsmTransferException {
1356            String lang = LanguageInfo.getLanguageCodeXML();
1357            try {
1358                sources.addAll(getDefault());
1359
1360                for (SourceProvider provider : sourceProviders) {
1361                    for (SourceEntry src : provider.getSources()) {
1362                        if (src instanceof ExtendedSourceEntry) {
1363                            sources.add((ExtendedSourceEntry) src);
1364                        }
1365                    }
1366                }
1367
1368                InputStream stream = new CachedFile(url).getInputStream();
1369                reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
1370
1371                String line;
1372                ExtendedSourceEntry last = null;
1373
1374                while ((line = reader.readLine()) != null && !canceled) {
1375                    if (line.trim().isEmpty()) {
1376                        continue; // skip empty lines
1377                    }
1378                    if (line.startsWith("\t")) {
1379                        Matcher m = Pattern.compile("^\t([^:]+): *(.+)$").matcher(line);
1380                        if (!m.matches()) {
1381                            Main.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line));
1382                            continue;
1383                        }
1384                        if (last != null) {
1385                            String key = m.group(1);
1386                            String value = m.group(2);
1387                            if ("author".equals(key) && last.author == null) {
1388                                last.author = value;
1389                            } else if ("version".equals(key)) {
1390                                last.version = value;
1391                            } else if ("link".equals(key) && last.link == null) {
1392                                last.link = value;
1393                            } else if ("description".equals(key) && last.description == null) {
1394                                last.description = value;
1395                            } else if ((lang + "shortdescription").equals(key) && last.title == null) {
1396                                last.title = value;
1397                            } else if ("shortdescription".equals(key) && last.title == null) {
1398                                last.title = value;
1399                            } else if ((lang + "title").equals(key) && last.title == null) {
1400                                last.title = value;
1401                            } else if ("title".equals(key) && last.title == null) {
1402                                last.title = value;
1403                            } else if ("name".equals(key) && last.name == null) {
1404                                last.name = value;
1405                            } else if ((lang + "author").equals(key)) {
1406                                last.author = value;
1407                            } else if ((lang + "link").equals(key)) {
1408                                last.link = value;
1409                            } else if ((lang + "description").equals(key)) {
1410                                last.description = value;
1411                            } else if ("min-josm-version".equals(key)) {
1412                                try {
1413                                    last.minJosmVersion = Integer.valueOf(value);
1414                                } catch (NumberFormatException e) {
1415                                    // ignore
1416                                    if (Main.isTraceEnabled()) {
1417                                        Main.trace(e.getMessage());
1418                                    }
1419                                }
1420                            }
1421                        }
1422                    } else {
1423                        last = null;
1424                        Matcher m = Pattern.compile("^(.+);(.+)$").matcher(line);
1425                        if (m.matches()) {
1426                            sources.add(last = new ExtendedSourceEntry(m.group(1), m.group(2)));
1427                        } else {
1428                            Main.error(tr(getStr(I18nString.ILLEGAL_FORMAT_OF_ENTRY), url, line));
1429                        }
1430                    }
1431                }
1432            } catch (IOException e) {
1433                if (canceled)
1434                    // ignore the exception and return
1435                    return;
1436                OsmTransferException ex = new OsmTransferException(e);
1437                ex.setUrl(url);
1438                warn(ex);
1439                return;
1440            }
1441        }
1442
1443        @Override
1444        protected void finish() {
1445            Collections.sort(sources);
1446            availableSourcesModel.setSources(sources);
1447        }
1448    }
1449
1450    static class SourceEntryTableCellRenderer extends DefaultTableCellRenderer {
1451        @Override
1452        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
1453            if (value == null)
1454                return this;
1455            return super.getTableCellRendererComponent(table,
1456                    fromSourceEntry((SourceEntry) value), isSelected, hasFocus, row, column);
1457        }
1458
1459        private static String fromSourceEntry(SourceEntry entry) {
1460            if (entry == null)
1461                return null;
1462            StringBuilder s = new StringBuilder("<html><b>");
1463            if (entry.title != null) {
1464                s.append(entry.title).append("</b> <span color=\"gray\">");
1465            }
1466            s.append(entry.url);
1467            if (entry.title != null) {
1468                s.append("</span>");
1469            }
1470            s.append("</html>");
1471            return s.toString();
1472        }
1473    }
1474
1475    class FileOrUrlCellEditor extends JPanel implements TableCellEditor {
1476        private JosmTextField tfFileName;
1477        private final CopyOnWriteArrayList<CellEditorListener> listeners;
1478        private String value;
1479        private final boolean isFile;
1480
1481        /**
1482         * build the GUI
1483         */
1484        protected final void build() {
1485            setLayout(new GridBagLayout());
1486            GridBagConstraints gc = new GridBagConstraints();
1487            gc.gridx = 0;
1488            gc.gridy = 0;
1489            gc.fill = GridBagConstraints.BOTH;
1490            gc.weightx = 1.0;
1491            gc.weighty = 1.0;
1492            add(tfFileName = new JosmTextField(), gc);
1493
1494            gc.gridx = 1;
1495            gc.gridy = 0;
1496            gc.fill = GridBagConstraints.BOTH;
1497            gc.weightx = 0.0;
1498            gc.weighty = 1.0;
1499            add(new JButton(new LaunchFileChooserAction()));
1500
1501            tfFileName.addFocusListener(
1502                    new FocusAdapter() {
1503                        @Override
1504                        public void focusGained(FocusEvent e) {
1505                            tfFileName.selectAll();
1506                        }
1507                    }
1508                    );
1509        }
1510
1511        FileOrUrlCellEditor(boolean isFile) {
1512            this.isFile = isFile;
1513            listeners = new CopyOnWriteArrayList<>();
1514            build();
1515        }
1516
1517        @Override
1518        public void addCellEditorListener(CellEditorListener l) {
1519            if (l != null) {
1520                listeners.addIfAbsent(l);
1521            }
1522        }
1523
1524        protected void fireEditingCanceled() {
1525            for (CellEditorListener l: listeners) {
1526                l.editingCanceled(new ChangeEvent(this));
1527            }
1528        }
1529
1530        protected void fireEditingStopped() {
1531            for (CellEditorListener l: listeners) {
1532                l.editingStopped(new ChangeEvent(this));
1533            }
1534        }
1535
1536        @Override
1537        public void cancelCellEditing() {
1538            fireEditingCanceled();
1539        }
1540
1541        @Override
1542        public Object getCellEditorValue() {
1543            return value;
1544        }
1545
1546        @Override
1547        public boolean isCellEditable(EventObject anEvent) {
1548            if (anEvent instanceof MouseEvent)
1549                return ((MouseEvent) anEvent).getClickCount() >= 2;
1550                return true;
1551        }
1552
1553        @Override
1554        public void removeCellEditorListener(CellEditorListener l) {
1555            listeners.remove(l);
1556        }
1557
1558        @Override
1559        public boolean shouldSelectCell(EventObject anEvent) {
1560            return true;
1561        }
1562
1563        @Override
1564        public boolean stopCellEditing() {
1565            value = tfFileName.getText();
1566            fireEditingStopped();
1567            return true;
1568        }
1569
1570        public void setInitialValue(String initialValue) {
1571            this.value = initialValue;
1572            if (initialValue == null) {
1573                this.tfFileName.setText("");
1574            } else {
1575                this.tfFileName.setText(initialValue);
1576            }
1577        }
1578
1579        @Override
1580        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
1581            setInitialValue((String) value);
1582            tfFileName.selectAll();
1583            return this;
1584        }
1585
1586        class LaunchFileChooserAction extends AbstractAction {
1587            LaunchFileChooserAction() {
1588                putValue(NAME, "...");
1589                putValue(SHORT_DESCRIPTION, tr("Launch a file chooser to select a file"));
1590            }
1591
1592            @Override
1593            public void actionPerformed(ActionEvent e) {
1594                FileChooserManager fcm = new FileChooserManager(true).createFileChooser();
1595                if (!isFile) {
1596                    fcm.getFileChooser().setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
1597                }
1598                prepareFileChooser(tfFileName.getText(), fcm.getFileChooser());
1599                AbstractFileChooser fc = fcm.openFileChooser(JOptionPane.getFrameForComponent(SourceEditor.this));
1600                if (fc != null) {
1601                    tfFileName.setText(fc.getSelectedFile().toString());
1602                }
1603            }
1604        }
1605    }
1606
1607    public abstract static class SourcePrefHelper {
1608
1609        private final String pref;
1610
1611        /**
1612         * Constructs a new {@code SourcePrefHelper} for the given preference key.
1613         * @param pref The preference key
1614         */
1615        public SourcePrefHelper(String pref) {
1616            this.pref = pref;
1617        }
1618
1619        /**
1620         * Returns the default sources provided by JOSM core.
1621         * @return the default sources provided by JOSM core
1622         */
1623        public abstract Collection<ExtendedSourceEntry> getDefault();
1624
1625        public abstract Map<String, String> serialize(SourceEntry entry);
1626
1627        public abstract SourceEntry deserialize(Map<String, String> entryStr);
1628
1629        /**
1630         * Returns the list of sources.
1631         * @return The list of sources
1632         */
1633        public List<SourceEntry> get() {
1634
1635            Collection<Map<String, String>> src = Main.pref.getListOfStructs(pref, (Collection<Map<String, String>>) null);
1636            if (src == null)
1637                return new ArrayList<SourceEntry>(getDefault());
1638
1639            List<SourceEntry> entries = new ArrayList<>();
1640            for (Map<String, String> sourcePref : src) {
1641                SourceEntry e = deserialize(new HashMap<>(sourcePref));
1642                if (e != null) {
1643                    entries.add(e);
1644                }
1645            }
1646            return entries;
1647        }
1648
1649        public boolean put(Collection<? extends SourceEntry> entries) {
1650            Collection<Map<String, String>> setting = new ArrayList<>(entries.size());
1651            for (SourceEntry e : entries) {
1652                setting.add(serialize(e));
1653            }
1654            return Main.pref.putListOfStructs(pref, setting);
1655        }
1656
1657        /**
1658         * Returns the set of active source URLs.
1659         * @return The set of active source URLs.
1660         */
1661        public final Set<String> getActiveUrls() {
1662            Set<String> urls = new LinkedHashSet<>(); // retain order
1663            for (SourceEntry e : get()) {
1664                if (e.active) {
1665                    urls.add(e.url);
1666                }
1667            }
1668            return urls;
1669        }
1670    }
1671
1672    /**
1673     * Defers loading of sources to the first time the adequate tab is selected.
1674     * @param tab The preferences tab
1675     * @param component The tab component
1676     * @since 6670
1677     */
1678    public final void deferLoading(final DefaultTabPreferenceSetting tab, final Component component) {
1679        tab.getTabPane().addChangeListener(
1680                new ChangeListener() {
1681                    @Override
1682                    public void stateChanged(ChangeEvent e) {
1683                        if (tab.getTabPane().getSelectedComponent() == component) {
1684                            SourceEditor.this.initiallyLoadAvailableSources();
1685                        }
1686                    }
1687                }
1688                );
1689    }
1690
1691    protected String getTitleForSourceEntry(SourceEntry entry) {
1692        return "".equals(entry.title) ? null : entry.title;
1693    }
1694}