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.Collection;
019import java.util.Collections;
020import java.util.Iterator;
021import java.util.LinkedList;
022import java.util.List;
023import java.util.Set;
024
025import javax.swing.AbstractAction;
026import javax.swing.BorderFactory;
027import javax.swing.DefaultListModel;
028import javax.swing.JButton;
029import javax.swing.JLabel;
030import javax.swing.JList;
031import javax.swing.JOptionPane;
032import javax.swing.JPanel;
033import javax.swing.JScrollPane;
034import javax.swing.JTabbedPane;
035import javax.swing.SwingUtilities;
036import javax.swing.UIManager;
037import javax.swing.event.DocumentEvent;
038import javax.swing.event.DocumentListener;
039
040import org.openstreetmap.josm.Main;
041import org.openstreetmap.josm.data.Version;
042import org.openstreetmap.josm.gui.HelpAwareOptionPane;
043import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
044import org.openstreetmap.josm.gui.help.HelpUtil;
045import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
046import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
047import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
048import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
049import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel;
050import org.openstreetmap.josm.gui.util.GuiHelper;
051import org.openstreetmap.josm.gui.widgets.JosmTextField;
052import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
053import org.openstreetmap.josm.plugins.PluginDownloadTask;
054import org.openstreetmap.josm.plugins.PluginInformation;
055import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
056import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
057import org.openstreetmap.josm.tools.GBC;
058import org.openstreetmap.josm.tools.ImageProvider;
059
060/**
061 * Preference settings for plugins.
062 * @since 168
063 */
064public final class PluginPreference extends DefaultTabPreferenceSetting {
065
066    /**
067     * Factory used to create a new {@code PluginPreference}.
068     */
069    public static class Factory implements PreferenceSettingFactory {
070        @Override
071        public PreferenceSetting createPreferenceSetting() {
072            return new PluginPreference();
073        }
074    }
075
076    private PluginPreference() {
077        super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false, new JTabbedPane());
078    }
079
080    /**
081     * Returns the download summary string to be shown.
082     * @param task The plugin download task that has completed
083     * @return the download summary string to be shown. Contains summary of success/failed plugins.
084     */
085    public static String buildDownloadSummary(PluginDownloadTask task) {
086        Collection<PluginInformation> downloaded = task.getDownloadedPlugins();
087        Collection<PluginInformation> failed = task.getFailedPlugins();
088        StringBuilder sb = new StringBuilder();
089        if (!downloaded.isEmpty()) {
090            sb.append(trn(
091                    "The following plugin has been downloaded <strong>successfully</strong>:",
092                    "The following {0} plugins have been downloaded <strong>successfully</strong>:",
093                    downloaded.size(),
094                    downloaded.size()
095                    ));
096            sb.append("<ul>");
097            for (PluginInformation pi: downloaded) {
098                sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")</li>");
099            }
100            sb.append("</ul>");
101        }
102        if (!failed.isEmpty()) {
103            sb.append(trn(
104                    "Downloading the following plugin has <strong>failed</strong>:",
105                    "Downloading the following {0} plugins has <strong>failed</strong>:",
106                    failed.size(),
107                    failed.size()
108                    ));
109            sb.append("<ul>");
110            for (PluginInformation pi: failed) {
111                sb.append("<li>").append(pi.name).append("</li>");
112            }
113            sb.append("</ul>");
114        }
115        return sb.toString();
116    }
117
118    /**
119     * Notifies user about result of a finished plugin download task.
120     * @param parent The parent component
121     * @param task The finished plugin download task
122     * @param restartRequired true if a restart is required
123     * @since 6797
124     */
125    public static void notifyDownloadResults(final Component parent, PluginDownloadTask task, boolean restartRequired) {
126        final Collection<PluginInformation> failed = task.getFailedPlugins();
127        final StringBuilder sb = new StringBuilder();
128        sb.append("<html>")
129          .append(buildDownloadSummary(task));
130        if (restartRequired) {
131            sb.append(tr("Please restart JOSM to activate the downloaded plugins."));
132        }
133        sb.append("</html>");
134        GuiHelper.runInEDTAndWait(new Runnable() {
135            @Override
136            public void run() {
137                HelpAwareOptionPane.showOptionDialog(
138                        parent,
139                        sb.toString(),
140                        tr("Update plugins"),
141                        !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE,
142                                HelpUtil.ht("/Preferences/Plugins")
143                        );
144            }
145        });
146    }
147
148    private JosmTextField tfFilter;
149    private PluginListPanel pnlPluginPreferences;
150    private PluginPreferencesModel model;
151    private JScrollPane spPluginPreferences;
152    private PluginUpdatePolicyPanel pnlPluginUpdatePolicy;
153
154    /**
155     * is set to true if this preference pane has been selected
156     * by the user
157     */
158    private boolean pluginPreferencesActivated;
159
160    protected JPanel buildSearchFieldPanel() {
161        JPanel pnl  = new JPanel(new GridBagLayout());
162        pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
163        GridBagConstraints gc = new GridBagConstraints();
164
165        gc.anchor = GridBagConstraints.NORTHWEST;
166        gc.fill = GridBagConstraints.HORIZONTAL;
167        gc.weightx = 0.0;
168        gc.insets = new Insets(0, 0, 0, 3);
169        pnl.add(new JLabel(tr("Search:")), gc);
170
171        gc.gridx = 1;
172        gc.weightx = 1.0;
173        tfFilter = new JosmTextField();
174        pnl.add(tfFilter, gc);
175        tfFilter.setToolTipText(tr("Enter a search expression"));
176        SelectAllOnFocusGainedDecorator.decorate(tfFilter);
177        tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter());
178        return pnl;
179    }
180
181    protected JPanel buildActionPanel() {
182        JPanel pnl = new JPanel(new GridLayout(1, 3));
183
184        pnl.add(new JButton(new DownloadAvailablePluginsAction()));
185        pnl.add(new JButton(new UpdateSelectedPluginsAction()));
186        pnl.add(new JButton(new ConfigureSitesAction()));
187        return pnl;
188    }
189
190    protected JPanel buildPluginListPanel() {
191        JPanel pnl = new JPanel(new BorderLayout());
192        pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH);
193        model  = new PluginPreferencesModel();
194        pnlPluginPreferences = new PluginListPanel(model);
195        spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences);
196        spPluginPreferences.getVerticalScrollBar().addComponentListener(
197                new ComponentAdapter() {
198                    @Override
199                    public void componentShown(ComponentEvent e) {
200                        spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border"));
201                    }
202
203                    @Override
204                    public void componentHidden(ComponentEvent e) {
205                        spPluginPreferences.setBorder(null);
206                    }
207                }
208                );
209
210        pnl.add(spPluginPreferences, BorderLayout.CENTER);
211        pnl.add(buildActionPanel(), BorderLayout.SOUTH);
212        return pnl;
213    }
214
215    protected JTabbedPane buildContentPane() {
216        JTabbedPane pane = getTabPane();
217        pnlPluginUpdatePolicy = new PluginUpdatePolicyPanel();
218        pane.addTab(tr("Plugins"), buildPluginListPanel());
219        pane.addTab(tr("Plugin update policy"), pnlPluginUpdatePolicy);
220        return pane;
221    }
222
223    @Override
224    public void addGui(final PreferenceTabbedPane gui) {
225        GridBagConstraints gc = new GridBagConstraints();
226        gc.weightx = 1.0;
227        gc.weighty = 1.0;
228        gc.anchor = GridBagConstraints.NORTHWEST;
229        gc.fill = GridBagConstraints.BOTH;
230        PreferencePanel plugins = gui.createPreferenceTab(this);
231        plugins.add(buildContentPane(), gc);
232        readLocalPluginInformation();
233        pluginPreferencesActivated = true;
234    }
235
236    private void configureSites() {
237        ButtonSpec[] options = new ButtonSpec[] {
238                new ButtonSpec(
239                        tr("OK"),
240                        ImageProvider.get("ok"),
241                        tr("Accept the new plugin sites and close the dialog"),
242                        null /* no special help topic */
243                        ),
244                        new ButtonSpec(
245                                tr("Cancel"),
246                                ImageProvider.get("cancel"),
247                                tr("Close the dialog"),
248                                null /* no special help topic */
249                                )
250        };
251        PluginConfigurationSitesPanel pnl = new PluginConfigurationSitesPanel();
252
253        int answer = HelpAwareOptionPane.showOptionDialog(
254                pnlPluginPreferences,
255                pnl,
256                tr("Configure Plugin Sites"),
257                JOptionPane.QUESTION_MESSAGE,
258                null,
259                options,
260                options[0],
261                null /* no help topic */
262                );
263        if (answer != 0 /* OK */)
264            return;
265        List<String> sites = pnl.getUpdateSites();
266        Main.pref.setPluginSites(sites);
267    }
268
269    /**
270     * Replies the list of plugins waiting for update or download
271     *
272     * @return the list of plugins waiting for update or download
273     */
274    public Set<PluginInformation> getPluginsScheduledForUpdateOrDownload() {
275        return model != null ? model.getPluginsScheduledForUpdateOrDownload() : null;
276    }
277
278    public List<PluginInformation> getNewlyActivatedPlugins() {
279        return model != null ? model.getNewlyActivatedPlugins() : null;
280    }
281
282    @Override
283    public boolean ok() {
284        if (!pluginPreferencesActivated)
285            return false;
286        pnlPluginUpdatePolicy.rememberInPreferences();
287        if (model.isActivePluginsChanged()) {
288            List<String> l = new LinkedList<>(model.getSelectedPluginNames());
289            Collections.sort(l);
290            Main.pref.putCollection("plugins", l);
291            if (!model.getNewlyDeactivatedPlugins().isEmpty()) return true;
292            for (PluginInformation pi : model.getNewlyActivatedPlugins()) {
293                if (!pi.canloadatruntime) return true;
294            }
295        }
296        return false;
297    }
298
299    /**
300     * Reads locally available information about plugins from the local file system.
301     * Scans cached plugin lists from plugin download sites and locally available
302     * plugin jar files.
303     *
304     */
305    public void readLocalPluginInformation() {
306        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
307        Runnable r = new Runnable() {
308            @Override
309            public void run() {
310                if (task.isCanceled()) return;
311                SwingUtilities.invokeLater(new Runnable() {
312                    @Override
313                    public void run() {
314                        model.setAvailablePlugins(task.getAvailablePlugins());
315                        pnlPluginPreferences.refreshView();
316                    }
317                });
318            }
319        };
320        Main.worker.submit(task);
321        Main.worker.submit(r);
322    }
323
324    /**
325     * The action for downloading the list of available plugins
326     */
327    class DownloadAvailablePluginsAction extends AbstractAction {
328
329        /**
330         * Constructs a new {@code DownloadAvailablePluginsAction}.
331         */
332        DownloadAvailablePluginsAction() {
333            putValue(NAME, tr("Download list"));
334            putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins"));
335            putValue(SMALL_ICON, ImageProvider.get("download"));
336        }
337
338        @Override
339        public void actionPerformed(ActionEvent e) {
340            Collection<String> pluginSites = Main.pref.getOnlinePluginSites();
341            if (pluginSites.isEmpty()) {
342                return;
343            }
344            final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites);
345            Runnable continuation = new Runnable() {
346                @Override
347                public void run() {
348                    if (task.isCanceled()) return;
349                    SwingUtilities.invokeLater(new Runnable() {
350                        @Override
351                        public void run() {
352                            model.updateAvailablePlugins(task.getAvailablePlugins());
353                            pnlPluginPreferences.refreshView();
354                            Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030
355                        }
356                    });
357                }
358            };
359            Main.worker.submit(task);
360            Main.worker.submit(continuation);
361        }
362    }
363
364    /**
365     * The action for updating the list of selected plugins
366     */
367    class UpdateSelectedPluginsAction extends AbstractAction {
368        UpdateSelectedPluginsAction() {
369            putValue(NAME, tr("Update plugins"));
370            putValue(SHORT_DESCRIPTION, tr("Update the selected plugins"));
371            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
372        }
373
374        protected void alertNothingToUpdate() {
375            try {
376                SwingUtilities.invokeAndWait(new Runnable() {
377                    @Override
378                    public void run() {
379                        HelpAwareOptionPane.showOptionDialog(
380                                pnlPluginPreferences,
381                                tr("All installed plugins are up to date. JOSM does not have to download newer versions."),
382                                tr("Plugins up to date"),
383                                JOptionPane.INFORMATION_MESSAGE,
384                                null // FIXME: provide help context
385                                );
386                    }
387                });
388            } catch (InterruptedException | InvocationTargetException e) {
389                Main.error(e);
390            }
391        }
392
393        @Override
394        public void actionPerformed(ActionEvent e) {
395            final List<PluginInformation> toUpdate = model.getSelectedPlugins();
396            // the async task for downloading plugins
397            final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(
398                    pnlPluginPreferences,
399                    toUpdate,
400                    tr("Update plugins")
401                    );
402            // the async task for downloading plugin information
403            final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(
404                    Main.pref.getOnlinePluginSites());
405
406            // to be run asynchronously after the plugin download
407            //
408            final Runnable pluginDownloadContinuation = new Runnable() {
409                @Override
410                public void run() {
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(new Runnable() {
424                        @Override
425                        public void run() {
426                            pnlPluginPreferences.refreshView();
427                        }
428                    });
429                }
430            };
431
432            // to be run asynchronously after the plugin list download
433            //
434            final Runnable pluginInfoDownloadContinuation = new Runnable() {
435                @Override
436                public void run() {
437                    if (pluginInfoDownloadTask.isCanceled())
438                        return;
439                    model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins());
440                    // select plugins which actually have to be updated
441                    //
442                    Iterator<PluginInformation> it = toUpdate.iterator();
443                    while (it.hasNext()) {
444                        PluginInformation pi = it.next();
445                        if (!pi.isUpdateRequired()) {
446                            it.remove();
447                        }
448                    }
449                    if (toUpdate.isEmpty()) {
450                        alertNothingToUpdate();
451                        return;
452                    }
453                    pluginDownloadTask.setPluginsToDownload(toUpdate);
454                    Main.worker.submit(pluginDownloadTask);
455                    Main.worker.submit(pluginDownloadContinuation);
456                }
457            };
458
459            Main.worker.submit(pluginInfoDownloadTask);
460            Main.worker.submit(pluginInfoDownloadContinuation);
461        }
462    }
463
464
465    /**
466     * The action for configuring the plugin download sites
467     *
468     */
469    class ConfigureSitesAction extends AbstractAction {
470        ConfigureSitesAction() {
471            putValue(NAME, tr("Configure sites..."));
472            putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from"));
473            putValue(SMALL_ICON, ImageProvider.get("dialogs", "settings"));
474        }
475
476        @Override
477        public void actionPerformed(ActionEvent e) {
478            configureSites();
479        }
480    }
481
482    /**
483     * Applies the current filter condition in the filter text field to the
484     * model
485     */
486    class SearchFieldAdapter implements DocumentListener {
487        public void filter() {
488            String expr = tfFilter.getText().trim();
489            if (expr.isEmpty()) {
490                expr = null;
491            }
492            model.filterDisplayedPlugins(expr);
493            pnlPluginPreferences.refreshView();
494        }
495
496        @Override
497        public void changedUpdate(DocumentEvent arg0) {
498            filter();
499        }
500
501        @Override
502        public void insertUpdate(DocumentEvent arg0) {
503            filter();
504        }
505
506        @Override
507        public void removeUpdate(DocumentEvent arg0) {
508            filter();
509        }
510    }
511
512    private static class PluginConfigurationSitesPanel extends JPanel {
513
514        private DefaultListModel<String> model;
515
516        protected final void build() {
517            setLayout(new GridBagLayout());
518            add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol());
519            model = new DefaultListModel<>();
520            for (String s : Main.pref.getPluginSites()) {
521                model.addElement(s);
522            }
523            final JList<String> list = new JList<>(model);
524            add(new JScrollPane(list), GBC.std().fill());
525            JPanel buttons = new JPanel(new GridBagLayout());
526            buttons.add(new JButton(new AbstractAction(tr("Add")) {
527                @Override
528                public void actionPerformed(ActionEvent e) {
529                    String s = JOptionPane.showInputDialog(
530                            JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
531                            tr("Add JOSM Plugin description URL."),
532                            tr("Enter URL"),
533                            JOptionPane.QUESTION_MESSAGE
534                            );
535                    if (s != null) {
536                        model.addElement(s);
537                    }
538                }
539            }), GBC.eol().fill(GBC.HORIZONTAL));
540            buttons.add(new JButton(new AbstractAction(tr("Edit")) {
541                @Override
542                public void actionPerformed(ActionEvent e) {
543                    if (list.getSelectedValue() == null) {
544                        JOptionPane.showMessageDialog(
545                                JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
546                                tr("Please select an entry."),
547                                tr("Warning"),
548                                JOptionPane.WARNING_MESSAGE
549                                );
550                        return;
551                    }
552                    String s = (String) JOptionPane.showInputDialog(
553                            Main.parent,
554                            tr("Edit JOSM Plugin description URL."),
555                            tr("JOSM Plugin description URL"),
556                            JOptionPane.QUESTION_MESSAGE,
557                            null,
558                            null,
559                            list.getSelectedValue()
560                            );
561                    if (s != null) {
562                        model.setElementAt(s, list.getSelectedIndex());
563                    }
564                }
565            }), GBC.eol().fill(GBC.HORIZONTAL));
566            buttons.add(new JButton(new AbstractAction(tr("Delete")) {
567                @Override
568                public void actionPerformed(ActionEvent event) {
569                    if (list.getSelectedValue() == null) {
570                        JOptionPane.showMessageDialog(
571                                JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
572                                tr("Please select an entry."),
573                                tr("Warning"),
574                                JOptionPane.WARNING_MESSAGE
575                                );
576                        return;
577                    }
578                    model.removeElement(list.getSelectedValue());
579                }
580            }), GBC.eol().fill(GBC.HORIZONTAL));
581            add(buttons, GBC.eol());
582        }
583
584        PluginConfigurationSitesPanel() {
585            build();
586        }
587
588        public List<String> getUpdateSites() {
589            if (model.getSize() == 0) return Collections.emptyList();
590            List<String> ret = new ArrayList<>(model.getSize());
591            for (int i = 0; i < model.getSize(); i++) {
592                ret.add(model.get(i));
593            }
594            return ret;
595        }
596    }
597}