001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.plugin;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.awt.BorderLayout;
008import java.awt.Component;
009import java.awt.GridBagConstraints;
010import java.awt.GridBagLayout;
011import java.awt.GridLayout;
012import java.awt.Insets;
013import java.awt.event.ActionEvent;
014import java.awt.event.ComponentAdapter;
015import java.awt.event.ComponentEvent;
016import java.lang.reflect.InvocationTargetException;
017import java.util.ArrayList;
018import java.util.Arrays;
019import java.util.Collection;
020import java.util.Collections;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Set;
024import java.util.regex.Pattern;
025
026import javax.swing.AbstractAction;
027import javax.swing.BorderFactory;
028import javax.swing.ButtonGroup;
029import javax.swing.DefaultListModel;
030import javax.swing.JButton;
031import javax.swing.JCheckBox;
032import javax.swing.JLabel;
033import javax.swing.JList;
034import javax.swing.JOptionPane;
035import javax.swing.JPanel;
036import javax.swing.JRadioButton;
037import javax.swing.JScrollPane;
038import javax.swing.JTabbedPane;
039import javax.swing.JTextArea;
040import javax.swing.SwingUtilities;
041import javax.swing.UIManager;
042import javax.swing.event.DocumentEvent;
043import javax.swing.event.DocumentListener;
044
045import org.openstreetmap.josm.Main;
046import org.openstreetmap.josm.actions.ExpertToggleAction;
047import org.openstreetmap.josm.data.Version;
048import org.openstreetmap.josm.gui.HelpAwareOptionPane;
049import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
050import org.openstreetmap.josm.gui.MainApplication;
051import org.openstreetmap.josm.gui.help.HelpUtil;
052import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
053import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
054import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
055import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
056import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel;
057import org.openstreetmap.josm.gui.util.GuiHelper;
058import org.openstreetmap.josm.gui.widgets.JosmTextField;
059import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
060import org.openstreetmap.josm.plugins.PluginDownloadTask;
061import org.openstreetmap.josm.plugins.PluginInformation;
062import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
063import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
064import org.openstreetmap.josm.spi.preferences.Config;
065import org.openstreetmap.josm.tools.GBC;
066import org.openstreetmap.josm.tools.ImageProvider;
067import org.openstreetmap.josm.tools.Logging;
068import org.openstreetmap.josm.tools.Utils;
069
070/**
071 * Preference settings for plugins.
072 * @since 168
073 */
074public final class PluginPreference extends DefaultTabPreferenceSetting {
075
076    /**
077     * Factory used to create a new {@code PluginPreference}.
078     */
079    public static class Factory implements PreferenceSettingFactory {
080        @Override
081        public PreferenceSetting createPreferenceSetting() {
082            return new PluginPreference();
083        }
084    }
085
086    private JosmTextField tfFilter;
087    private PluginListPanel pnlPluginPreferences;
088    private PluginPreferencesModel model;
089    private JScrollPane spPluginPreferences;
090    private PluginUpdatePolicyPanel pnlPluginUpdatePolicy;
091
092    /**
093     * is set to true if this preference pane has been selected by the user
094     */
095    private boolean pluginPreferencesActivated;
096
097    private PluginPreference() {
098        super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false, new JTabbedPane());
099    }
100
101    /**
102     * Returns the download summary string to be shown.
103     * @param task The plugin download task that has completed
104     * @return the download summary string to be shown. Contains summary of success/failed plugins.
105     */
106    public static String buildDownloadSummary(PluginDownloadTask task) {
107        Collection<PluginInformation> downloaded = task.getDownloadedPlugins();
108        Collection<PluginInformation> failed = task.getFailedPlugins();
109        Exception exception = task.getLastException();
110        StringBuilder sb = new StringBuilder();
111        if (!downloaded.isEmpty()) {
112            sb.append(trn(
113                    "The following plugin has been downloaded <strong>successfully</strong>:",
114                    "The following {0} plugins have been downloaded <strong>successfully</strong>:",
115                    downloaded.size(),
116                    downloaded.size()
117                    ));
118            sb.append("<ul>");
119            for (PluginInformation pi: downloaded) {
120                sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")</li>");
121            }
122            sb.append("</ul>");
123        }
124        if (!failed.isEmpty()) {
125            sb.append(trn(
126                    "Downloading the following plugin has <strong>failed</strong>:",
127                    "Downloading the following {0} plugins has <strong>failed</strong>:",
128                    failed.size(),
129                    failed.size()
130                    ));
131            sb.append("<ul>");
132            for (PluginInformation pi: failed) {
133                sb.append("<li>").append(pi.name).append("</li>");
134            }
135            sb.append("</ul>");
136        }
137        if (exception != null) {
138            // Same i18n string in ExceptionUtil.explainBadRequest()
139            sb.append(tr("<br>Error message(untranslated): {0}", exception.getMessage()));
140        }
141        return sb.toString();
142    }
143
144    /**
145     * Notifies user about result of a finished plugin download task.
146     * @param parent The parent component
147     * @param task The finished plugin download task
148     * @param restartRequired true if a restart is required
149     * @since 6797
150     */
151    public static void notifyDownloadResults(final Component parent, PluginDownloadTask task, boolean restartRequired) {
152        final Collection<PluginInformation> failed = task.getFailedPlugins();
153        final StringBuilder sb = new StringBuilder();
154        sb.append("<html>")
155          .append(buildDownloadSummary(task));
156        if (restartRequired) {
157            sb.append(tr("Please restart JOSM to activate the downloaded plugins."));
158        }
159        sb.append("</html>");
160        GuiHelper.runInEDTAndWait(() -> HelpAwareOptionPane.showOptionDialog(
161                parent,
162                sb.toString(),
163                tr("Update plugins"),
164                !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE,
165                        HelpUtil.ht("/Preferences/Plugins")
166                ));
167    }
168
169    private JPanel buildSearchFieldPanel() {
170        JPanel pnl = new JPanel(new GridBagLayout());
171        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
172        GridBagConstraints gc = new GridBagConstraints();
173
174        gc.anchor = GridBagConstraints.NORTHWEST;
175        gc.fill = GridBagConstraints.HORIZONTAL;
176        gc.weightx = 0.0;
177        gc.insets = new Insets(0, 0, 0, 3);
178        pnl.add(GBC.glue(0, 0));
179
180        gc.weightx = 1.0;
181        ButtonGroup bg = new ButtonGroup();
182        JPanel radios = new JPanel();
183        addRadioButton(bg, radios, new JRadioButton(tr("All"), true), gc, PluginInstallation.ALL);
184        addRadioButton(bg, radios, new JRadioButton(tr("Installed")), gc, PluginInstallation.INSTALLED);
185        addRadioButton(bg, radios, new JRadioButton(tr("Available")), gc, PluginInstallation.AVAILABLE);
186        pnl.add(radios, gc);
187
188        gc.gridx = 0;
189        gc.weightx = 0.0;
190        pnl.add(new JLabel(tr("Search:")), gc);
191
192        gc.gridx = 1;
193        gc.weightx = 1.0;
194        tfFilter = new JosmTextField();
195        pnl.add(tfFilter, gc);
196        tfFilter.setToolTipText(tr("Enter a search expression"));
197        SelectAllOnFocusGainedDecorator.decorate(tfFilter);
198        tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter());
199        return pnl;
200    }
201
202    private void addRadioButton(ButtonGroup bg, JPanel pnl, JRadioButton rb, GridBagConstraints gc, PluginInstallation value) {
203        bg.add(rb);
204        pnl.add(rb, gc);
205        rb.addActionListener(e -> {
206            model.filterDisplayedPlugins(value);
207            pnlPluginPreferences.refreshView();
208        });
209    }
210
211    private static Component addButton(JPanel pnl, JButton button, String buttonName) {
212        button.setName(buttonName);
213        return pnl.add(button);
214    }
215
216    private JPanel buildActionPanel() {
217        JPanel pnl = new JPanel(new GridLayout(1, 4));
218
219        // assign some component names to these as we go to aid testing
220        addButton(pnl, new JButton(new DownloadAvailablePluginsAction()), "downloadListButton");
221        addButton(pnl, new JButton(new UpdateSelectedPluginsAction()), "updatePluginsButton");
222        ExpertToggleAction.addVisibilitySwitcher(addButton(pnl, new JButton(new SelectByListAction()), "loadFromListButton"));
223        ExpertToggleAction.addVisibilitySwitcher(addButton(pnl, new JButton(new ConfigureSitesAction()), "configureSitesButton"));
224        return pnl;
225    }
226
227    private JPanel buildPluginListPanel() {
228        JPanel pnl = new JPanel(new BorderLayout());
229        pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH);
230        model = new PluginPreferencesModel();
231        pnlPluginPreferences = new PluginListPanel(model);
232        spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences);
233        spPluginPreferences.getVerticalScrollBar().addComponentListener(
234                new ComponentAdapter() {
235                    @Override
236                    public void componentShown(ComponentEvent e) {
237                        spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border"));
238                    }
239
240                    @Override
241                    public void componentHidden(ComponentEvent e) {
242                        spPluginPreferences.setBorder(null);
243                    }
244                }
245                );
246
247        pnl.add(spPluginPreferences, BorderLayout.CENTER);
248        pnl.add(buildActionPanel(), BorderLayout.SOUTH);
249        return pnl;
250    }
251
252    private JTabbedPane buildContentPane() {
253        JTabbedPane pane = getTabPane();
254        pnlPluginUpdatePolicy = new PluginUpdatePolicyPanel();
255        pane.addTab(tr("Plugins"), buildPluginListPanel());
256        pane.addTab(tr("Plugin update policy"), pnlPluginUpdatePolicy);
257        return pane;
258    }
259
260    @Override
261    public void addGui(final PreferenceTabbedPane gui) {
262        GridBagConstraints gc = new GridBagConstraints();
263        gc.weightx = 1.0;
264        gc.weighty = 1.0;
265        gc.anchor = GridBagConstraints.NORTHWEST;
266        gc.fill = GridBagConstraints.BOTH;
267        PreferencePanel plugins = gui.createPreferenceTab(this);
268        plugins.add(buildContentPane(), gc);
269        readLocalPluginInformation();
270        pluginPreferencesActivated = true;
271    }
272
273    private void configureSites() {
274        ButtonSpec[] options = new ButtonSpec[] {
275                new ButtonSpec(
276                        tr("OK"),
277                        new ImageProvider("ok"),
278                        tr("Accept the new plugin sites and close the dialog"),
279                        null /* no special help topic */
280                        ),
281                        new ButtonSpec(
282                                tr("Cancel"),
283                                new ImageProvider("cancel"),
284                                tr("Close the dialog"),
285                                null /* no special help topic */
286                                )
287        };
288        PluginConfigurationSitesPanel pnl = new PluginConfigurationSitesPanel();
289
290        int answer = HelpAwareOptionPane.showOptionDialog(
291                pnlPluginPreferences,
292                pnl,
293                tr("Configure Plugin Sites"),
294                JOptionPane.QUESTION_MESSAGE,
295                null,
296                options,
297                options[0],
298                null /* no help topic */
299                );
300        if (answer != 0 /* OK */)
301            return;
302        Main.pref.setPluginSites(pnl.getUpdateSites());
303    }
304
305    /**
306     * Replies the set of plugins waiting for update or download
307     *
308     * @return the set of plugins waiting for update or download
309     */
310    public Set<PluginInformation> getPluginsScheduledForUpdateOrDownload() {
311        return model != null ? model.getPluginsScheduledForUpdateOrDownload() : null;
312    }
313
314    /**
315     * Replies the list of plugins which have been added by the user to the set of activated plugins
316     *
317     * @return the list of newly activated plugins
318     */
319    public List<PluginInformation> getNewlyActivatedPlugins() {
320        return model != null ? model.getNewlyActivatedPlugins() : null;
321    }
322
323    @Override
324    public boolean ok() {
325        if (!pluginPreferencesActivated)
326            return false;
327        pnlPluginUpdatePolicy.rememberInPreferences();
328        if (model.isActivePluginsChanged()) {
329            List<String> l = new LinkedList<>(model.getSelectedPluginNames());
330            Collections.sort(l);
331            Config.getPref().putList("plugins", l);
332            if (!model.getNewlyDeactivatedPlugins().isEmpty())
333                return true;
334            for (PluginInformation pi : model.getNewlyActivatedPlugins()) {
335                if (!pi.canloadatruntime)
336                    return true;
337            }
338        }
339        return false;
340    }
341
342    /**
343     * Reads locally available information about plugins from the local file system.
344     * Scans cached plugin lists from plugin download sites and locally available
345     * plugin jar files.
346     *
347     */
348    public void readLocalPluginInformation() {
349        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
350        Runnable r = () -> {
351            if (!task.isCanceled()) {
352                SwingUtilities.invokeLater(() -> {
353                    model.setAvailablePlugins(task.getAvailablePlugins());
354                    pnlPluginPreferences.refreshView();
355                });
356            }
357        };
358        MainApplication.worker.submit(task);
359        MainApplication.worker.submit(r);
360    }
361
362    /**
363     * The action for downloading the list of available plugins
364     */
365    class DownloadAvailablePluginsAction extends AbstractAction {
366
367        /**
368         * Constructs a new {@code DownloadAvailablePluginsAction}.
369         */
370        DownloadAvailablePluginsAction() {
371            putValue(NAME, tr("Download list"));
372            putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins"));
373            new ImageProvider("download").getResource().attachImageIcon(this);
374        }
375
376        @Override
377        public void actionPerformed(ActionEvent e) {
378            Collection<String> pluginSites = Main.pref.getOnlinePluginSites();
379            if (pluginSites.isEmpty()) {
380                return;
381            }
382            final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites);
383            Runnable continuation = () -> {
384                if (!task.isCanceled()) {
385                    SwingUtilities.invokeLater(() -> {
386                        model.updateAvailablePlugins(task.getAvailablePlugins());
387                        pnlPluginPreferences.refreshView();
388                        Config.getPref().putInt("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030
389                    });
390                }
391            };
392            MainApplication.worker.submit(task);
393            MainApplication.worker.submit(continuation);
394        }
395    }
396
397    /**
398     * The action for updating the list of selected plugins
399     */
400    class UpdateSelectedPluginsAction extends AbstractAction {
401        UpdateSelectedPluginsAction() {
402            putValue(NAME, tr("Update plugins"));
403            putValue(SHORT_DESCRIPTION, tr("Update the selected plugins"));
404            new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this);
405        }
406
407        protected void alertNothingToUpdate() {
408            try {
409                SwingUtilities.invokeAndWait(() -> HelpAwareOptionPane.showOptionDialog(
410                        pnlPluginPreferences,
411                        tr("All installed plugins are up to date. JOSM does not have to download newer versions."),
412                        tr("Plugins up to date"),
413                        JOptionPane.INFORMATION_MESSAGE,
414                        null // FIXME: provide help context
415                        ));
416            } catch (InterruptedException | InvocationTargetException e) {
417                Logging.error(e);
418            }
419        }
420
421        @Override
422        public void actionPerformed(ActionEvent e) {
423            final List<PluginInformation> toUpdate = model.getSelectedPlugins();
424            // the async task for downloading plugins
425            final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(
426                    pnlPluginPreferences,
427                    toUpdate,
428                    tr("Update plugins")
429                    );
430            // the async task for downloading plugin information
431            final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(
432                    Main.pref.getOnlinePluginSites());
433
434            // to be run asynchronously after the plugin download
435            //
436            final Runnable pluginDownloadContinuation = () -> {
437                if (pluginDownloadTask.isCanceled())
438                    return;
439                boolean restartRequired = false;
440                for (PluginInformation pi : pluginDownloadTask.getDownloadedPlugins()) {
441                    if (!model.getNewlyActivatedPlugins().contains(pi) || !pi.canloadatruntime) {
442                        restartRequired = true;
443                        break;
444                    }
445                }
446                notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask, restartRequired);
447                model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins());
448                model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins());
449                GuiHelper.runInEDT(pnlPluginPreferences::refreshView);
450            };
451
452            // to be run asynchronously after the plugin list download
453            //
454            final Runnable pluginInfoDownloadContinuation = () -> {
455                if (pluginInfoDownloadTask.isCanceled())
456                    return;
457                model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins());
458                // select plugins which actually have to be updated
459                //
460                toUpdate.removeIf(pi -> !pi.isUpdateRequired());
461                if (toUpdate.isEmpty()) {
462                    alertNothingToUpdate();
463                    return;
464                }
465                pluginDownloadTask.setPluginsToDownload(toUpdate);
466                MainApplication.worker.submit(pluginDownloadTask);
467                MainApplication.worker.submit(pluginDownloadContinuation);
468            };
469
470            MainApplication.worker.submit(pluginInfoDownloadTask);
471            MainApplication.worker.submit(pluginInfoDownloadContinuation);
472        }
473    }
474
475    /**
476     * The action for configuring the plugin download sites
477     *
478     */
479    class ConfigureSitesAction extends AbstractAction {
480        ConfigureSitesAction() {
481            putValue(NAME, tr("Configure sites..."));
482            putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from"));
483            new ImageProvider("dialogs", "settings").getResource().attachImageIcon(this);
484        }
485
486        @Override
487        public void actionPerformed(ActionEvent e) {
488            configureSites();
489        }
490    }
491
492    /**
493     * The action for selecting the plugins given by a text file compatible to JOSM bug report.
494     * @author Michael Zangl
495     */
496    class SelectByListAction extends AbstractAction {
497        SelectByListAction() {
498            putValue(NAME, tr("Load from list..."));
499            putValue(SHORT_DESCRIPTION, tr("Load plugins from a list of plugins"));
500        }
501
502        @Override
503        public void actionPerformed(ActionEvent e) {
504            JTextArea textField = new JTextArea(10, 0);
505            JCheckBox deleteNotInList = new JCheckBox(tr("Disable all other plugins"));
506
507            JLabel helpLabel = new JLabel("<html>" + Utils.join("<br/>", Arrays.asList(
508                    tr("Enter a list of plugins you want to download."),
509                    tr("You should add one plugin id per line, version information is ignored."),
510                    tr("You can copy+paste the list of a status report here."))) + "</html>");
511
512            if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()),
513                    new Object[] {helpLabel, new JScrollPane(textField), deleteNotInList},
514                    tr("Load plugins from list"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) {
515                activatePlugins(textField, deleteNotInList.isSelected());
516            }
517        }
518
519        private void activatePlugins(JTextArea textField, boolean deleteNotInList) {
520            String[] lines = textField.getText().split("\n");
521            List<String> toActivate = new ArrayList<>();
522            List<String> notFound = new ArrayList<>();
523            // This pattern matches the default list format JOSM uses for bug reports.
524            // It removes a list item mark at the beginning of the line: +, -, *
525            // It removes the version number after the plugin, like: 123, (123), (v5.7alpha3), (1b3), (v1-SNAPSHOT-1)...
526            Pattern regex = Pattern.compile("^[-+\\*\\s]*|\\s[\\d\\s]*(\\([^\\(\\)\\[\\]]*\\))?[\\d\\s]*$");
527            for (String line : lines) {
528                String name = regex.matcher(line).replaceAll("");
529                if (name.isEmpty()) {
530                    continue;
531                }
532                PluginInformation plugin = model.getPluginInformation(name);
533                if (plugin == null) {
534                    notFound.add(name);
535                } else {
536                    toActivate.add(name);
537                }
538            }
539
540            if (notFound.isEmpty() || confirmIgnoreNotFound(notFound)) {
541                activatePlugins(toActivate, deleteNotInList);
542            }
543        }
544
545        private void activatePlugins(List<String> toActivate, boolean deleteNotInList) {
546            if (deleteNotInList) {
547                for (String name : model.getSelectedPluginNames()) {
548                    if (!toActivate.contains(name)) {
549                        model.setPluginSelected(name, false);
550                    }
551                }
552            }
553            for (String name : toActivate) {
554                model.setPluginSelected(name, true);
555            }
556            pnlPluginPreferences.refreshView();
557        }
558
559        private boolean confirmIgnoreNotFound(List<String> notFound) {
560            String list = "<ul><li>" + Utils.join("</li><li>", notFound) + "</li></ul>";
561            String message = "<html>" + tr("The following plugins were not found. Continue anyway?") + list + "</html>";
562            return JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()),
563                    message) == JOptionPane.OK_OPTION;
564        }
565    }
566
567    /**
568     * Applies the current filter condition in the filter text field to the model.
569     */
570    class SearchFieldAdapter implements DocumentListener {
571        private void filter() {
572            String expr = tfFilter.getText().trim();
573            if (expr.isEmpty()) {
574                expr = null;
575            }
576            model.filterDisplayedPlugins(expr);
577            pnlPluginPreferences.refreshView();
578        }
579
580        @Override
581        public void changedUpdate(DocumentEvent evt) {
582            filter();
583        }
584
585        @Override
586        public void insertUpdate(DocumentEvent evt) {
587            filter();
588        }
589
590        @Override
591        public void removeUpdate(DocumentEvent evt) {
592            filter();
593        }
594    }
595
596    private static class PluginConfigurationSitesPanel extends JPanel {
597
598        private final DefaultListModel<String> model = new DefaultListModel<>();
599
600        PluginConfigurationSitesPanel() {
601            super(new GridBagLayout());
602            add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol());
603            for (String s : Main.pref.getPluginSites()) {
604                model.addElement(s);
605            }
606            final JList<String> list = new JList<>(model);
607            add(new JScrollPane(list), GBC.std().fill());
608            JPanel buttons = new JPanel(new GridBagLayout());
609            buttons.add(new JButton(new AbstractAction(tr("Add")) {
610                @Override
611                public void actionPerformed(ActionEvent e) {
612                    String s = JOptionPane.showInputDialog(
613                            GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
614                            tr("Add JOSM Plugin description URL."),
615                            tr("Enter URL"),
616                            JOptionPane.QUESTION_MESSAGE
617                            );
618                    if (s != null && !s.isEmpty()) {
619                        model.addElement(s);
620                    }
621                }
622            }), GBC.eol().fill(GBC.HORIZONTAL));
623            buttons.add(new JButton(new AbstractAction(tr("Edit")) {
624                @Override
625                public void actionPerformed(ActionEvent e) {
626                    if (list.getSelectedValue() == null) {
627                        JOptionPane.showMessageDialog(
628                                GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
629                                tr("Please select an entry."),
630                                tr("Warning"),
631                                JOptionPane.WARNING_MESSAGE
632                                );
633                        return;
634                    }
635                    String s = (String) JOptionPane.showInputDialog(
636                            Main.parent,
637                            tr("Edit JOSM Plugin description URL."),
638                            tr("JOSM Plugin description URL"),
639                            JOptionPane.QUESTION_MESSAGE,
640                            null,
641                            null,
642                            list.getSelectedValue()
643                            );
644                    if (s != null && !s.isEmpty()) {
645                        model.setElementAt(s, list.getSelectedIndex());
646                    }
647                }
648            }), GBC.eol().fill(GBC.HORIZONTAL));
649            buttons.add(new JButton(new AbstractAction(tr("Delete")) {
650                @Override
651                public void actionPerformed(ActionEvent event) {
652                    if (list.getSelectedValue() == null) {
653                        JOptionPane.showMessageDialog(
654                                GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
655                                tr("Please select an entry."),
656                                tr("Warning"),
657                                JOptionPane.WARNING_MESSAGE
658                                );
659                        return;
660                    }
661                    model.removeElement(list.getSelectedValue());
662                }
663            }), GBC.eol().fill(GBC.HORIZONTAL));
664            add(buttons, GBC.eol());
665        }
666
667        protected List<String> getUpdateSites() {
668            if (model.getSize() == 0)
669                return Collections.emptyList();
670            List<String> ret = new ArrayList<>(model.getSize());
671            for (int i = 0; i < model.getSize(); i++) {
672                ret.add(model.get(i));
673            }
674            return ret;
675        }
676    }
677
678    @Override
679    public String getHelpContext() {
680        return HelpUtil.ht("/Preferences/Plugins");
681    }
682}