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