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