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