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;
023
024import javax.swing.AbstractAction;
025import javax.swing.BorderFactory;
026import javax.swing.DefaultListModel;
027import javax.swing.JButton;
028import javax.swing.JLabel;
029import javax.swing.JList;
030import javax.swing.JOptionPane;
031import javax.swing.JPanel;
032import javax.swing.JScrollPane;
033import javax.swing.JTabbedPane;
034import javax.swing.SwingUtilities;
035import javax.swing.UIManager;
036import javax.swing.event.DocumentEvent;
037import javax.swing.event.DocumentListener;
038
039import org.openstreetmap.josm.Main;
040import org.openstreetmap.josm.data.Version;
041import org.openstreetmap.josm.gui.HelpAwareOptionPane;
042import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
043import org.openstreetmap.josm.gui.help.HelpUtil;
044import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
045import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
046import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
047import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
048import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel;
049import org.openstreetmap.josm.gui.util.GuiHelper;
050import org.openstreetmap.josm.gui.widgets.JosmTextField;
051import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
052import org.openstreetmap.josm.io.OfflineAccessException;
053import org.openstreetmap.josm.io.OnlineResource;
054import org.openstreetmap.josm.plugins.PluginDownloadTask;
055import org.openstreetmap.josm.plugins.PluginInformation;
056import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
057import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
058import org.openstreetmap.josm.tools.GBC;
059import org.openstreetmap.josm.tools.ImageProvider;
060
061/**
062 * Preference settings for plugins.
063 * @since 168
064 */
065public final class PluginPreference extends DefaultTabPreferenceSetting {
066
067    /**
068     * Factory used to create a new {@code PluginPreference}.
069     */
070    public static class Factory implements PreferenceSettingFactory {
071        @Override
072        public PreferenceSetting createPreferenceSetting() {
073            return new PluginPreference();
074        }
075    }
076
077    private PluginPreference() {
078        super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false, new JTabbedPane());
079    }
080
081    /**
082     * Returns the download summary string to be shown.
083     * @param task The plugin download task that has completed
084     * @return the download summary string to be shown. Contains summary of success/failed plugins.
085     */
086    public static String buildDownloadSummary(PluginDownloadTask task) {
087        Collection<PluginInformation> downloaded = task.getDownloadedPlugins();
088        Collection<PluginInformation> failed = task.getFailedPlugins();
089        StringBuilder sb = new StringBuilder();
090        if (! downloaded.isEmpty()) {
091            sb.append(trn(
092                    "The following plugin has been downloaded <strong>successfully</strong>:",
093                    "The following {0} plugins have been downloaded <strong>successfully</strong>:",
094                    downloaded.size(),
095                    downloaded.size()
096                    ));
097            sb.append("<ul>");
098            for(PluginInformation pi: downloaded) {
099                sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")").append("</li>");
100            }
101            sb.append("</ul>");
102        }
103        if (! failed.isEmpty()) {
104            sb.append(trn(
105                    "Downloading the following plugin has <strong>failed</strong>:",
106                    "Downloading the following {0} plugins has <strong>failed</strong>:",
107                    failed.size(),
108                    failed.size()
109                    ));
110            sb.append("<ul>");
111            for(PluginInformation pi: failed) {
112                sb.append("<li>").append(pi.name).append("</li>");
113            }
114            sb.append("</ul>");
115        }
116        return sb.toString();
117    }
118
119    /**
120     * Notifies user about result of a finished plugin download task.
121     * @param parent The parent component
122     * @param task The finished plugin download task
123     * @since 6797
124     */
125    public static void notifyDownloadResults(final Component parent, PluginDownloadTask task, boolean restartRequired) {
126        final Collection<PluginInformation> downloaded = task.getDownloadedPlugins();
127        final Collection<PluginInformation> failed = task.getFailedPlugins();
128        final StringBuilder sb = new StringBuilder();
129        sb.append("<html>");
130        sb.append(buildDownloadSummary(task));
131        if (restartRequired) {
132            sb.append(tr("Please restart JOSM to activate the downloaded plugins."));
133        }
134        sb.append("</html>");
135        GuiHelper.runInEDTAndWait(new Runnable() {
136            @Override
137            public void run() {
138                HelpAwareOptionPane.showOptionDialog(
139                        parent,
140                        sb.toString(),
141                        tr("Update plugins"),
142                        !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE,
143                                HelpUtil.ht("/Preferences/Plugins")
144                        );
145            }
146        });
147    }
148
149    private JosmTextField tfFilter;
150    private PluginListPanel pnlPluginPreferences;
151    private PluginPreferencesModel model;
152    private JScrollPane spPluginPreferences;
153    private PluginUpdatePolicyPanel pnlPluginUpdatePolicy;
154
155    /**
156     * is set to true if this preference pane has been selected
157     * by the user
158     */
159    private boolean pluginPreferencesActivated = false;
160
161    protected JPanel buildSearchFieldPanel() {
162        JPanel pnl  = new JPanel(new GridBagLayout());
163        pnl.setBorder(BorderFactory.createEmptyBorder(5,5,5,5));
164        GridBagConstraints gc = new GridBagConstraints();
165
166        gc.anchor = GridBagConstraints.NORTHWEST;
167        gc.fill = GridBagConstraints.HORIZONTAL;
168        gc.weightx = 0.0;
169        gc.insets = new Insets(0,0,0,3);
170        pnl.add(new JLabel(tr("Search:")), gc);
171
172        gc.gridx = 1;
173        gc.weightx = 1.0;
174        tfFilter = new JosmTextField();
175        pnl.add(tfFilter, gc);
176        tfFilter.setToolTipText(tr("Enter a search expression"));
177        SelectAllOnFocusGainedDecorator.decorate(tfFilter);
178        tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter());
179        return pnl;
180    }
181
182    protected JPanel buildActionPanel() {
183        JPanel pnl = new JPanel(new GridLayout(1,3));
184
185        pnl.add(new JButton(new DownloadAvailablePluginsAction()));
186        pnl.add(new JButton(new UpdateSelectedPluginsAction()));
187        pnl.add(new JButton(new ConfigureSitesAction()));
188        return pnl;
189    }
190
191    protected JPanel buildPluginListPanel() {
192        JPanel pnl = new JPanel(new BorderLayout());
193        pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH);
194        model  = new PluginPreferencesModel();
195        pnlPluginPreferences = new PluginListPanel(model);
196        spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences);
197        spPluginPreferences.getVerticalScrollBar().addComponentListener(
198                new ComponentAdapter(){
199                    @Override
200                    public void componentShown(ComponentEvent e) {
201                        spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border"));
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 List<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            LinkedList<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    private static Collection<String> getOnlinePluginSites() {
325        Collection<String> pluginSites = new ArrayList<>(Main.pref.getPluginSites());
326        for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
327            try {
328                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite());
329            } catch (OfflineAccessException ex) {
330                Main.warn(ex.getMessage());
331                it.remove();
332            }
333        }
334        return pluginSites;
335    }
336
337    /**
338     * The action for downloading the list of available plugins
339     */
340    class DownloadAvailablePluginsAction extends AbstractAction {
341
342        public DownloadAvailablePluginsAction() {
343            putValue(NAME,tr("Download list"));
344            putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins"));
345            putValue(SMALL_ICON, ImageProvider.get("download"));
346        }
347
348        @Override
349        public void actionPerformed(ActionEvent e) {
350            Collection<String> pluginSites = getOnlinePluginSites();
351            if (pluginSites.isEmpty()) {
352                return;
353            }
354            final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites);
355            Runnable continuation = new Runnable() {
356                @Override
357                public void run() {
358                    if (task.isCanceled()) return;
359                    SwingUtilities.invokeLater(new Runnable() {
360                        @Override
361                        public void run() {
362                            model.updateAvailablePlugins(task.getAvailablePlugins());
363                            pnlPluginPreferences.refreshView();
364                            Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030
365                        }
366                    });
367                }
368            };
369            Main.worker.submit(task);
370            Main.worker.submit(continuation);
371        }
372
373    }
374
375    /**
376     * The action for updating the list of selected plugins
377     */
378    class UpdateSelectedPluginsAction extends AbstractAction {
379        public UpdateSelectedPluginsAction() {
380            putValue(NAME,tr("Update plugins"));
381            putValue(SHORT_DESCRIPTION, tr("Update the selected plugins"));
382            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
383        }
384
385        protected void alertNothingToUpdate() {
386            try {
387                SwingUtilities.invokeAndWait(new Runnable() {
388                    @Override
389                    public void run() {
390                        HelpAwareOptionPane.showOptionDialog(
391                                pnlPluginPreferences,
392                                tr("All installed plugins are up to date. JOSM does not have to download newer versions."),
393                                tr("Plugins up to date"),
394                                JOptionPane.INFORMATION_MESSAGE,
395                                null // FIXME: provide help context
396                                );
397                    }
398                });
399            } catch (InterruptedException | InvocationTargetException e) {
400                Main.error(e);
401            }
402        }
403
404        @Override
405        public void actionPerformed(ActionEvent e) {
406            final List<PluginInformation> toUpdate = model.getSelectedPlugins();
407            // the async task for downloading plugins
408            final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(
409                    pnlPluginPreferences,
410                    toUpdate,
411                    tr("Update plugins")
412                    );
413            // the async task for downloading plugin information
414            final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(getOnlinePluginSites());
415
416            // to be run asynchronously after the plugin download
417            //
418            final Runnable pluginDownloadContinuation = new Runnable() {
419                @Override
420                public void run() {
421                    if (pluginDownloadTask.isCanceled())
422                        return;
423                    boolean restartRequired = false;
424                    for (PluginInformation pi : pluginDownloadTask.getDownloadedPlugins()) {
425                        if (!model.getNewlyActivatedPlugins().contains(pi) || !pi.canloadatruntime) {
426                            restartRequired = true;
427                            break;
428                        }
429                    }
430                    notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask, restartRequired);
431                    model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins());
432                    model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins());
433                    GuiHelper.runInEDT(new Runnable() {
434                        @Override
435                        public void run() {
436                            pnlPluginPreferences.refreshView();                        }
437                    });
438                }
439            };
440
441            // to be run asynchronously after the plugin list download
442            //
443            final Runnable pluginInfoDownloadContinuation = new Runnable() {
444                @Override
445                public void run() {
446                    if (pluginInfoDownloadTask.isCanceled())
447                        return;
448                    model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins());
449                    // select plugins which actually have to be updated
450                    //
451                    Iterator<PluginInformation> it = toUpdate.iterator();
452                    while (it.hasNext()) {
453                        PluginInformation pi = it.next();
454                        if (!pi.isUpdateRequired()) {
455                            it.remove();
456                        }
457                    }
458                    if (toUpdate.isEmpty()) {
459                        alertNothingToUpdate();
460                        return;
461                    }
462                    pluginDownloadTask.setPluginsToDownload(toUpdate);
463                    Main.worker.submit(pluginDownloadTask);
464                    Main.worker.submit(pluginDownloadContinuation);
465                }
466            };
467
468            Main.worker.submit(pluginInfoDownloadTask);
469            Main.worker.submit(pluginInfoDownloadContinuation);
470        }
471    }
472
473
474    /**
475     * The action for configuring the plugin download sites
476     *
477     */
478    class ConfigureSitesAction extends AbstractAction {
479        public ConfigureSitesAction() {
480            putValue(NAME,tr("Configure sites..."));
481            putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from"));
482            putValue(SMALL_ICON, ImageProvider.get("dialogs", "settings"));
483        }
484
485        @Override
486        public void actionPerformed(ActionEvent e) {
487            configureSites();
488        }
489    }
490
491    /**
492     * Applies the current filter condition in the filter text field to the
493     * model
494     */
495    class SearchFieldAdapter implements DocumentListener {
496        public void filter() {
497            String expr = tfFilter.getText().trim();
498            if (expr.isEmpty()) {
499                expr = null;
500            }
501            model.filterDisplayedPlugins(expr);
502            pnlPluginPreferences.refreshView();
503        }
504
505        @Override
506        public void changedUpdate(DocumentEvent arg0) {
507            filter();
508        }
509
510        @Override
511        public void insertUpdate(DocumentEvent arg0) {
512            filter();
513        }
514
515        @Override
516        public void removeUpdate(DocumentEvent arg0) {
517            filter();
518        }
519    }
520
521    private static class PluginConfigurationSitesPanel extends JPanel {
522
523        private DefaultListModel<String> model;
524
525        protected final void build() {
526            setLayout(new GridBagLayout());
527            add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol());
528            model = new DefaultListModel<>();
529            for (String s : Main.pref.getPluginSites()) {
530                model.addElement(s);
531            }
532            final JList<String> list = new JList<>(model);
533            add(new JScrollPane(list), GBC.std().fill());
534            JPanel buttons = new JPanel(new GridBagLayout());
535            buttons.add(new JButton(new AbstractAction(tr("Add")){
536                @Override
537                public void actionPerformed(ActionEvent e) {
538                    String s = JOptionPane.showInputDialog(
539                            JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
540                            tr("Add JOSM Plugin description URL."),
541                            tr("Enter URL"),
542                            JOptionPane.QUESTION_MESSAGE
543                            );
544                    if (s != null) {
545                        model.addElement(s);
546                    }
547                }
548            }), GBC.eol().fill(GBC.HORIZONTAL));
549            buttons.add(new JButton(new AbstractAction(tr("Edit")){
550                @Override
551                public void actionPerformed(ActionEvent e) {
552                    if (list.getSelectedValue() == null) {
553                        JOptionPane.showMessageDialog(
554                                JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
555                                tr("Please select an entry."),
556                                tr("Warning"),
557                                JOptionPane.WARNING_MESSAGE
558                                );
559                        return;
560                    }
561                    String s = (String)JOptionPane.showInputDialog(
562                            Main.parent,
563                            tr("Edit JOSM Plugin description URL."),
564                            tr("JOSM Plugin description URL"),
565                            JOptionPane.QUESTION_MESSAGE,
566                            null,
567                            null,
568                            list.getSelectedValue()
569                            );
570                    if (s != null) {
571                        model.setElementAt(s, list.getSelectedIndex());
572                    }
573                }
574            }), GBC.eol().fill(GBC.HORIZONTAL));
575            buttons.add(new JButton(new AbstractAction(tr("Delete")){
576                @Override
577                public void actionPerformed(ActionEvent event) {
578                    if (list.getSelectedValue() == null) {
579                        JOptionPane.showMessageDialog(
580                                JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
581                                tr("Please select an entry."),
582                                tr("Warning"),
583                                JOptionPane.WARNING_MESSAGE
584                                );
585                        return;
586                    }
587                    model.removeElement(list.getSelectedValue());
588                }
589            }), GBC.eol().fill(GBC.HORIZONTAL));
590            add(buttons, GBC.eol());
591        }
592
593        public PluginConfigurationSitesPanel() {
594            build();
595        }
596
597        public List<String> getUpdateSites() {
598            if (model.getSize() == 0) return Collections.emptyList();
599            List<String> ret = new ArrayList<>(model.getSize());
600            for (int i=0; i< model.getSize();i++){
601                ret.add(model.get(i));
602            }
603            return ret;
604        }
605    }
606
607
608}