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