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