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