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