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.PluginInformation;
059import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
060import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
061import org.openstreetmap.josm.spi.preferences.Config;
062import org.openstreetmap.josm.tools.GBC;
063import org.openstreetmap.josm.tools.ImageProvider;
064import org.openstreetmap.josm.tools.Logging;
065import org.openstreetmap.josm.tools.Utils;
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(tr("All"), true), gc, PluginInstallation.ALL);
180        addRadioButton(bg, radios, new JRadioButton(tr("Installed")), gc, PluginInstallation.INSTALLED);
181        addRadioButton(bg, radios, new JRadioButton(tr("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 = new ButtonSpec[] {
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            if (!model.getNewlyDeactivatedPlugins().isEmpty())
328                return true;
329            for (PluginInformation pi : model.getNewlyActivatedPlugins()) {
330                if (!pi.canloadatruntime)
331                    return true;
332            }
333        }
334        return false;
335    }
336
337    /**
338     * Reads locally available information about plugins from the local file system.
339     * Scans cached plugin lists from plugin download sites and locally available
340     * plugin jar files.
341     *
342     */
343    public void readLocalPluginInformation() {
344        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
345        Runnable r = () -> {
346            if (!task.isCanceled()) {
347                SwingUtilities.invokeLater(() -> {
348                    model.setAvailablePlugins(task.getAvailablePlugins());
349                    pnlPluginPreferences.refreshView();
350                });
351            }
352        };
353        MainApplication.worker.submit(task);
354        MainApplication.worker.submit(r);
355    }
356
357    /**
358     * The action for downloading the list of available plugins
359     */
360    class DownloadAvailablePluginsAction extends AbstractAction {
361
362        /**
363         * Constructs a new {@code DownloadAvailablePluginsAction}.
364         */
365        DownloadAvailablePluginsAction() {
366            putValue(NAME, tr("Download list"));
367            putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins"));
368            new ImageProvider("download").getResource().attachImageIcon(this);
369        }
370
371        @Override
372        public void actionPerformed(ActionEvent e) {
373            Collection<String> pluginSites = Preferences.main().getOnlinePluginSites();
374            if (pluginSites.isEmpty()) {
375                return;
376            }
377            final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites);
378            Runnable continuation = () -> {
379                if (!task.isCanceled()) {
380                    SwingUtilities.invokeLater(() -> {
381                        model.updateAvailablePlugins(task.getAvailablePlugins());
382                        pnlPluginPreferences.refreshView();
383                        Config.getPref().putInt("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030
384                    });
385                }
386            };
387            MainApplication.worker.submit(task);
388            MainApplication.worker.submit(continuation);
389        }
390    }
391
392    /**
393     * The action for updating the list of selected plugins
394     */
395    class UpdateSelectedPluginsAction extends AbstractAction {
396        UpdateSelectedPluginsAction() {
397            putValue(NAME, tr("Update plugins"));
398            putValue(SHORT_DESCRIPTION, tr("Update the selected plugins"));
399            new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this);
400        }
401
402        protected void alertNothingToUpdate() {
403            try {
404                SwingUtilities.invokeAndWait(() -> HelpAwareOptionPane.showOptionDialog(
405                        pnlPluginPreferences,
406                        tr("All installed plugins are up to date. JOSM does not have to download newer versions."),
407                        tr("Plugins up to date"),
408                        JOptionPane.INFORMATION_MESSAGE,
409                        null // FIXME: provide help context
410                        ));
411            } catch (InterruptedException | InvocationTargetException e) {
412                Logging.error(e);
413            }
414        }
415
416        @Override
417        public void actionPerformed(ActionEvent e) {
418            final List<PluginInformation> toUpdate = model.getSelectedPlugins();
419            // the async task for downloading plugins
420            final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(
421                    pnlPluginPreferences,
422                    toUpdate,
423                    tr("Update plugins")
424                    );
425            // the async task for downloading plugin information
426            final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(
427                    Preferences.main().getOnlinePluginSites());
428
429            // to be run asynchronously after the plugin download
430            //
431            final Runnable pluginDownloadContinuation = () -> {
432                if (pluginDownloadTask.isCanceled())
433                    return;
434                boolean restartRequired = false;
435                for (PluginInformation pi : pluginDownloadTask.getDownloadedPlugins()) {
436                    if (!model.getNewlyActivatedPlugins().contains(pi) || !pi.canloadatruntime) {
437                        restartRequired = true;
438                        break;
439                    }
440                }
441                notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask, restartRequired);
442                model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins());
443                model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins());
444                GuiHelper.runInEDT(pnlPluginPreferences::refreshView);
445            };
446
447            // to be run asynchronously after the plugin list download
448            //
449            final Runnable pluginInfoDownloadContinuation = () -> {
450                if (pluginInfoDownloadTask.isCanceled())
451                    return;
452                model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins());
453                // select plugins which actually have to be updated
454                //
455                toUpdate.removeIf(pi -> !pi.isUpdateRequired());
456                if (toUpdate.isEmpty()) {
457                    alertNothingToUpdate();
458                    return;
459                }
460                pluginDownloadTask.setPluginsToDownload(toUpdate);
461                MainApplication.worker.submit(pluginDownloadTask);
462                MainApplication.worker.submit(pluginDownloadContinuation);
463            };
464
465            MainApplication.worker.submit(pluginInfoDownloadTask);
466            MainApplication.worker.submit(pluginInfoDownloadContinuation);
467        }
468    }
469
470    /**
471     * The action for configuring the plugin download sites
472     *
473     */
474    class ConfigureSitesAction extends AbstractAction {
475        ConfigureSitesAction() {
476            putValue(NAME, tr("Configure sites..."));
477            putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from"));
478            new ImageProvider("dialogs", "settings").getResource().attachImageIcon(this);
479        }
480
481        @Override
482        public void actionPerformed(ActionEvent e) {
483            configureSites();
484        }
485    }
486
487    /**
488     * The action for selecting the plugins given by a text file compatible to JOSM bug report.
489     * @author Michael Zangl
490     */
491    class SelectByListAction extends AbstractAction {
492        SelectByListAction() {
493            putValue(NAME, tr("Load from list..."));
494            putValue(SHORT_DESCRIPTION, tr("Load plugins from a list of plugins"));
495        }
496
497        @Override
498        public void actionPerformed(ActionEvent e) {
499            JTextArea textField = new JTextArea(10, 0);
500            JCheckBox deleteNotInList = new JCheckBox(tr("Disable all other plugins"));
501
502            JLabel helpLabel = new JLabel("<html>" + Utils.join("<br/>", Arrays.asList(
503                    tr("Enter a list of plugins you want to download."),
504                    tr("You should add one plugin id per line, version information is ignored."),
505                    tr("You can copy+paste the list of a status report here."))) + "</html>");
506
507            if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()),
508                    new Object[] {helpLabel, new JScrollPane(textField), deleteNotInList},
509                    tr("Load plugins from list"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) {
510                activatePlugins(textField, deleteNotInList.isSelected());
511            }
512        }
513
514        private void activatePlugins(JTextArea textField, boolean deleteNotInList) {
515            String[] lines = textField.getText().split("\n");
516            List<String> toActivate = new ArrayList<>();
517            List<String> notFound = new ArrayList<>();
518            // This pattern matches the default list format JOSM uses for bug reports.
519            // It removes a list item mark at the beginning of the line: +, -, *
520            // It removes the version number after the plugin, like: 123, (123), (v5.7alpha3), (1b3), (v1-SNAPSHOT-1)...
521            Pattern regex = Pattern.compile("^[-+\\*\\s]*|\\s[\\d\\s]*(\\([^\\(\\)\\[\\]]*\\))?[\\d\\s]*$");
522            for (String line : lines) {
523                String name = regex.matcher(line).replaceAll("");
524                if (name.isEmpty()) {
525                    continue;
526                }
527                PluginInformation plugin = model.getPluginInformation(name);
528                if (plugin == null) {
529                    notFound.add(name);
530                } else {
531                    toActivate.add(name);
532                }
533            }
534
535            if (notFound.isEmpty() || confirmIgnoreNotFound(notFound)) {
536                activatePlugins(toActivate, deleteNotInList);
537            }
538        }
539
540        private void activatePlugins(List<String> toActivate, boolean deleteNotInList) {
541            if (deleteNotInList) {
542                for (String name : model.getSelectedPluginNames()) {
543                    if (!toActivate.contains(name)) {
544                        model.setPluginSelected(name, false);
545                    }
546                }
547            }
548            for (String name : toActivate) {
549                model.setPluginSelected(name, true);
550            }
551            pnlPluginPreferences.refreshView();
552        }
553
554        private boolean confirmIgnoreNotFound(List<String> notFound) {
555            String list = "<ul><li>" + Utils.join("</li><li>", notFound) + "</li></ul>";
556            String message = "<html>" + tr("The following plugins were not found. Continue anyway?") + list + "</html>";
557            return JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()),
558                    message) == JOptionPane.OK_OPTION;
559        }
560    }
561
562    private static class PluginConfigurationSitesPanel extends JPanel {
563
564        private final DefaultListModel<String> model = new DefaultListModel<>();
565
566        PluginConfigurationSitesPanel() {
567            super(new GridBagLayout());
568            add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol());
569            for (String s : Preferences.main().getPluginSites()) {
570                model.addElement(s);
571            }
572            final JList<String> list = new JList<>(model);
573            add(new JScrollPane(list), GBC.std().fill());
574            JPanel buttons = new JPanel(new GridBagLayout());
575            buttons.add(new JButton(new AbstractAction(tr("Add")) {
576                @Override
577                public void actionPerformed(ActionEvent e) {
578                    String s = JOptionPane.showInputDialog(
579                            GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
580                            tr("Add JOSM Plugin description URL."),
581                            tr("Enter URL"),
582                            JOptionPane.QUESTION_MESSAGE
583                            );
584                    if (s != null && !s.isEmpty()) {
585                        model.addElement(s);
586                    }
587                }
588            }), GBC.eol().fill(GBC.HORIZONTAL));
589            buttons.add(new JButton(new AbstractAction(tr("Edit")) {
590                @Override
591                public void actionPerformed(ActionEvent e) {
592                    if (list.getSelectedValue() == null) {
593                        JOptionPane.showMessageDialog(
594                                GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
595                                tr("Please select an entry."),
596                                tr("Warning"),
597                                JOptionPane.WARNING_MESSAGE
598                                );
599                        return;
600                    }
601                    String s = (String) JOptionPane.showInputDialog(
602                            MainApplication.getMainFrame(),
603                            tr("Edit JOSM Plugin description URL."),
604                            tr("JOSM Plugin description URL"),
605                            JOptionPane.QUESTION_MESSAGE,
606                            null,
607                            null,
608                            list.getSelectedValue()
609                            );
610                    if (s != null && !s.isEmpty()) {
611                        model.setElementAt(s, list.getSelectedIndex());
612                    }
613                }
614            }), GBC.eol().fill(GBC.HORIZONTAL));
615            buttons.add(new JButton(new AbstractAction(tr("Delete")) {
616                @Override
617                public void actionPerformed(ActionEvent event) {
618                    if (list.getSelectedValue() == null) {
619                        JOptionPane.showMessageDialog(
620                                GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this),
621                                tr("Please select an entry."),
622                                tr("Warning"),
623                                JOptionPane.WARNING_MESSAGE
624                                );
625                        return;
626                    }
627                    model.removeElement(list.getSelectedValue());
628                }
629            }), GBC.eol().fill(GBC.HORIZONTAL));
630            add(buttons, GBC.eol());
631        }
632
633        protected List<String> getUpdateSites() {
634            if (model.getSize() == 0)
635                return Collections.emptyList();
636            List<String> ret = new ArrayList<>(model.getSize());
637            for (int i = 0; i < model.getSize(); i++) {
638                ret.add(model.get(i));
639            }
640            return ret;
641        }
642    }
643
644    @Override
645    public String getHelpContext() {
646        return HelpUtil.ht("/Preferences/Plugins");
647    }
648}