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