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;
042
043import org.openstreetmap.josm.actions.ExpertToggleAction;
044import org.openstreetmap.josm.data.Preferences;
045import org.openstreetmap.josm.data.Version;
046import org.openstreetmap.josm.gui.HelpAwareOptionPane;
047import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
048import org.openstreetmap.josm.gui.MainApplication;
049import org.openstreetmap.josm.gui.help.HelpUtil;
050import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
051import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
052import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
053import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
054import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel;
055import org.openstreetmap.josm.gui.util.GuiHelper;
056import org.openstreetmap.josm.gui.widgets.FilterField;
057import org.openstreetmap.josm.plugins.PluginDownloadTask;
058import org.openstreetmap.josm.plugins.PluginHandler;
059import org.openstreetmap.josm.plugins.PluginInformation;
060import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
061import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
062import org.openstreetmap.josm.spi.preferences.Config;
063import org.openstreetmap.josm.tools.GBC;
064import org.openstreetmap.josm.tools.ImageProvider;
065import org.openstreetmap.josm.tools.Logging;
066import org.openstreetmap.josm.tools.Utils;
067
068/**
069 * Preference settings for plugins.
070 * @since 168
071 */
072public final class PluginPreference extends DefaultTabPreferenceSetting {
073
074    /**
075     * Factory used to create a new {@code PluginPreference}.
076     */
077    public static class Factory implements PreferenceSettingFactory {
078        @Override
079        public PreferenceSetting createPreferenceSetting() {
080            return new PluginPreference();
081        }
082    }
083
084    private PluginListPanel pnlPluginPreferences;
085    private PluginPreferencesModel model;
086    private JScrollPane spPluginPreferences;
087    private PluginUpdatePolicyPanel pnlPluginUpdatePolicy;
088
089    /**
090     * is set to true if this preference pane has been selected by the user
091     */
092    private boolean pluginPreferencesActivated;
093
094    private PluginPreference() {
095        super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false, new JTabbedPane());
096    }
097
098    /**
099     * Returns the download summary string to be shown.
100     * @param task The plugin download task that has completed
101     * @return the download summary string to be shown. Contains summary of success/failed plugins.
102     */
103    public static String buildDownloadSummary(PluginDownloadTask task) {
104        Collection<PluginInformation> downloaded = task.getDownloadedPlugins();
105        Collection<PluginInformation> failed = task.getFailedPlugins();
106        Exception exception = task.getLastException();
107        StringBuilder sb = new StringBuilder();
108        if (!downloaded.isEmpty()) {
109            sb.append(trn(
110                    "The following plugin has been downloaded <strong>successfully</strong>:",
111                    "The following {0} plugins have been downloaded <strong>successfully</strong>:",
112                    downloaded.size(),
113                    downloaded.size()
114                    ));
115            sb.append("<ul>");
116            for (PluginInformation pi: downloaded) {
117                sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")</li>");
118            }
119            sb.append("</ul>");
120        }
121        if (!failed.isEmpty()) {
122            sb.append(trn(
123                    "Downloading the following plugin has <strong>failed</strong>:",
124                    "Downloading the following {0} plugins has <strong>failed</strong>:",
125                    failed.size(),
126                    failed.size()
127                    ));
128            sb.append("<ul>");
129            for (PluginInformation pi: failed) {
130                sb.append("<li>").append(pi.name).append("</li>");
131            }
132            sb.append("</ul>");
133        }
134        if (exception != null) {
135            // Same i18n string in ExceptionUtil.explainBadRequest()
136            sb.append(tr("<br>Error message(untranslated): {0}", exception.getMessage()));
137        }
138        return sb.toString();
139    }
140
141    /**
142     * Notifies user about result of a finished plugin download task.
143     * @param parent The parent component
144     * @param task The finished plugin download task
145     * @param restartRequired true if a restart is required
146     * @since 6797
147     */
148    public static void notifyDownloadResults(final Component parent, PluginDownloadTask task, boolean restartRequired) {
149        final Collection<PluginInformation> failed = task.getFailedPlugins();
150        final StringBuilder sb = new StringBuilder();
151        sb.append("<html>")
152          .append(buildDownloadSummary(task));
153        if (restartRequired) {
154            sb.append(tr("Please restart JOSM to activate the downloaded plugins."));
155        }
156        sb.append("</html>");
157        GuiHelper.runInEDTAndWait(() -> HelpAwareOptionPane.showOptionDialog(
158                parent,
159                sb.toString(),
160                tr("Update plugins"),
161                !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE,
162                        HelpUtil.ht("/Preferences/Plugins")
163                ));
164    }
165
166    private JPanel buildSearchFieldPanel() {
167        JPanel pnl = new JPanel(new GridBagLayout());
168        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
169        GridBagConstraints gc = new GridBagConstraints();
170
171        gc.anchor = GridBagConstraints.NORTHWEST;
172        gc.fill = GridBagConstraints.HORIZONTAL;
173        gc.weightx = 0.0;
174        gc.insets = new Insets(0, 0, 0, 3);
175        pnl.add(GBC.glue(0, 0));
176
177        gc.weightx = 1.0;
178        ButtonGroup bg = new ButtonGroup();
179        JPanel radios = new JPanel();
180        addRadioButton(bg, radios, new JRadioButton(tr("All"), true), gc, PluginInstallation.ALL);
181        addRadioButton(bg, radios, new JRadioButton(tr("Installed")), gc, PluginInstallation.INSTALLED);
182        addRadioButton(bg, radios, new JRadioButton(tr("Available")), gc, PluginInstallation.AVAILABLE);
183        pnl.add(radios, gc);
184
185        gc.gridx = 0;
186        gc.weightx = 0.0;
187        pnl.add(new JLabel(tr("Search:")), gc);
188
189        gc.gridx = 1;
190        gc.weightx = 1.0;
191        pnl.add(new FilterField().filter(expr -> {
192            model.filterDisplayedPlugins(expr);
193            pnlPluginPreferences.refreshView();
194        }), gc);
195        return pnl;
196    }
197
198    private void addRadioButton(ButtonGroup bg, JPanel pnl, JRadioButton rb, GridBagConstraints gc, PluginInstallation value) {
199        bg.add(rb);
200        pnl.add(rb, gc);
201        rb.addActionListener(e -> {
202            model.filterDisplayedPlugins(value);
203            pnlPluginPreferences.refreshView();
204        });
205    }
206
207    private static Component addButton(JPanel pnl, JButton button, String buttonName) {
208        button.setName(buttonName);
209        return pnl.add(button);
210    }
211
212    private JPanel buildActionPanel() {
213        JPanel pnl = new JPanel(new GridLayout(1, 4));
214
215        // assign some component names to these as we go to aid testing
216        addButton(pnl, new JButton(new DownloadAvailablePluginsAction()), "downloadListButton");
217        addButton(pnl, new JButton(new UpdateSelectedPluginsAction()), "updatePluginsButton");
218        ExpertToggleAction.addVisibilitySwitcher(addButton(pnl, new JButton(new SelectByListAction()), "loadFromListButton"));
219        ExpertToggleAction.addVisibilitySwitcher(addButton(pnl, new JButton(new ConfigureSitesAction()), "configureSitesButton"));
220        return pnl;
221    }
222
223    private JPanel buildPluginListPanel() {
224        JPanel pnl = new JPanel(new BorderLayout());
225        pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH);
226        model = new PluginPreferencesModel();
227        pnlPluginPreferences = new PluginListPanel(model);
228        spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences);
229        spPluginPreferences.getVerticalScrollBar().addComponentListener(
230                new ComponentAdapter() {
231                    @Override
232                    public void componentShown(ComponentEvent e) {
233                        spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border"));
234                    }
235
236                    @Override
237                    public void componentHidden(ComponentEvent e) {
238                        spPluginPreferences.setBorder(null);
239                    }
240                }
241                );
242
243        pnl.add(spPluginPreferences, BorderLayout.CENTER);
244        pnl.add(buildActionPanel(), BorderLayout.SOUTH);
245        return pnl;
246    }
247
248    private JTabbedPane buildContentPane() {
249        JTabbedPane pane = getTabPane();
250        pnlPluginUpdatePolicy = new PluginUpdatePolicyPanel();
251        pane.addTab(tr("Plugins"), buildPluginListPanel());
252        pane.addTab(tr("Plugin update policy"), pnlPluginUpdatePolicy);
253        return pane;
254    }
255
256    @Override
257    public void addGui(final PreferenceTabbedPane gui) {
258        GridBagConstraints gc = new GridBagConstraints();
259        gc.weightx = 1.0;
260        gc.weighty = 1.0;
261        gc.anchor = GridBagConstraints.NORTHWEST;
262        gc.fill = GridBagConstraints.BOTH;
263        PreferencePanel plugins = gui.createPreferenceTab(this);
264        plugins.add(buildContentPane(), gc);
265        readLocalPluginInformation();
266        pluginPreferencesActivated = true;
267    }
268
269    private void configureSites() {
270        ButtonSpec[] options = new ButtonSpec[] {
271                new ButtonSpec(
272                        tr("OK"),
273                        new ImageProvider("ok"),
274                        tr("Accept the new plugin sites and close the dialog"),
275                        null /* no special help topic */
276                        ),
277                        new ButtonSpec(
278                                tr("Cancel"),
279                                new ImageProvider("cancel"),
280                                tr("Close the dialog"),
281                                null /* no special help topic */
282                                )
283        };
284        PluginConfigurationSitesPanel pnl = new PluginConfigurationSitesPanel();
285
286        int answer = HelpAwareOptionPane.showOptionDialog(
287                pnlPluginPreferences,
288                pnl,
289                tr("Configure Plugin Sites"),
290                JOptionPane.QUESTION_MESSAGE,
291                null,
292                options,
293                options[0],
294                null /* no help topic */
295                );
296        if (answer != 0 /* OK */)
297            return;
298        Preferences.main().setPluginSites(pnl.getUpdateSites());
299    }
300
301    /**
302     * Replies the set of plugins waiting for update or download
303     *
304     * @return the set of plugins waiting for update or download
305     */
306    public Set<PluginInformation> getPluginsScheduledForUpdateOrDownload() {
307        return model != null ? model.getPluginsScheduledForUpdateOrDownload() : null;
308    }
309
310    /**
311     * Replies the list of plugins which have been added by the user to the set of activated plugins
312     *
313     * @return the list of newly activated plugins
314     */
315    public List<PluginInformation> getNewlyActivatedPlugins() {
316        return model != null ? model.getNewlyActivatedPlugins() : null;
317    }
318
319    @Override
320    public boolean ok() {
321        if (!pluginPreferencesActivated)
322            return false;
323        pnlPluginUpdatePolicy.rememberInPreferences();
324        if (model.isActivePluginsChanged()) {
325            List<String> l = new LinkedList<>(model.getSelectedPluginNames());
326            Collections.sort(l);
327            Config.getPref().putList("plugins", l);
328            List<PluginInformation> deactivatedPlugins = model.getNewlyDeactivatedPlugins();
329            if (!deactivatedPlugins.isEmpty()) {
330                boolean requiresRestart = PluginHandler.removePlugins(deactivatedPlugins);
331                if (requiresRestart)
332                    return requiresRestart;
333            }
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 = Preferences.main().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                    Preferences.main().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    private static class PluginConfigurationSitesPanel extends JPanel {
568
569        private final DefaultListModel<String> model = new DefaultListModel<>();
570
571        PluginConfigurationSitesPanel() {
572            super(new GridBagLayout());
573            add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol());
574            for (String s : Preferences.main().getPluginSites()) {
575                model.addElement(s);
576            }
577            final JList<String> list = new JList<>(model);
578            add(new JScrollPane(list), GBC.std().fill());
579            JPanel buttons = new JPanel(new GridBagLayout());
580            buttons.add(new JButton(new AbstractAction(tr("Add")) {
581                @Override
582                public void actionPerformed(ActionEvent e) {
583                    String s = JOptionPane.showInputDialog(
584                            GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
585                            tr("Add JOSM Plugin description URL."),
586                            tr("Enter URL"),
587                            JOptionPane.QUESTION_MESSAGE
588                            );
589                    if (s != null && !s.isEmpty()) {
590                        model.addElement(s);
591                    }
592                }
593            }), GBC.eol().fill(GBC.HORIZONTAL));
594            buttons.add(new JButton(new AbstractAction(tr("Edit")) {
595                @Override
596                public void actionPerformed(ActionEvent e) {
597                    if (list.getSelectedValue() == null) {
598                        JOptionPane.showMessageDialog(
599                                GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
600                                tr("Please select an entry."),
601                                tr("Warning"),
602                                JOptionPane.WARNING_MESSAGE
603                                );
604                        return;
605                    }
606                    String s = (String) JOptionPane.showInputDialog(
607                            MainApplication.getMainFrame(),
608                            tr("Edit JOSM Plugin description URL."),
609                            tr("JOSM Plugin description URL"),
610                            JOptionPane.QUESTION_MESSAGE,
611                            null,
612                            null,
613                            list.getSelectedValue()
614                            );
615                    if (s != null && !s.isEmpty()) {
616                        model.setElementAt(s, list.getSelectedIndex());
617                    }
618                }
619            }), GBC.eol().fill(GBC.HORIZONTAL));
620            buttons.add(new JButton(new AbstractAction(tr("Delete")) {
621                @Override
622                public void actionPerformed(ActionEvent event) {
623                    if (list.getSelectedValue() == null) {
624                        JOptionPane.showMessageDialog(
625                                GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
626                                tr("Please select an entry."),
627                                tr("Warning"),
628                                JOptionPane.WARNING_MESSAGE
629                                );
630                        return;
631                    }
632                    model.removeElement(list.getSelectedValue());
633                }
634            }), GBC.eol().fill(GBC.HORIZONTAL));
635            add(buttons, GBC.eol());
636        }
637
638        protected List<String> getUpdateSites() {
639            if (model.getSize() == 0)
640                return Collections.emptyList();
641            List<String> ret = new ArrayList<>(model.getSize());
642            for (int i = 0; i < model.getSize(); i++) {
643                ret.add(model.get(i));
644            }
645            return ret;
646        }
647    }
648
649    @Override
650    public String getHelpContext() {
651        return HelpUtil.ht("/Preferences/Plugins");
652    }
653}