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