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("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) {
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 (!downloaded.isEmpty()) {
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    @Override
279    public boolean ok() {
280        if (! pluginPreferencesActivated)
281            return false;
282        pnlPluginUpdatePolicy.rememberInPreferences();
283        if (model.isActivePluginsChanged()) {
284            LinkedList<String> l = new LinkedList<>(model.getSelectedPluginNames());
285            Collections.sort(l);
286            Main.pref.putCollection("plugins", l);
287            return true;
288        }
289        return false;
290    }
291
292    /**
293     * Reads locally available information about plugins from the local file system.
294     * Scans cached plugin lists from plugin download sites and locally available
295     * plugin jar files.
296     *
297     */
298    public void readLocalPluginInformation() {
299        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
300        Runnable r = new Runnable() {
301            @Override
302            public void run() {
303                if (task.isCanceled()) return;
304                SwingUtilities.invokeLater(new Runnable() {
305                    @Override
306                    public void run() {
307                        model.setAvailablePlugins(task.getAvailablePlugins());
308                        pnlPluginPreferences.refreshView();
309                    }
310                });
311            }
312        };
313        Main.worker.submit(task);
314        Main.worker.submit(r);
315    }
316
317    private static Collection<String> getOnlinePluginSites() {
318        Collection<String> pluginSites = new ArrayList<>(Main.pref.getPluginSites());
319        for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) {
320            try {
321                OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite());
322            } catch (OfflineAccessException ex) {
323                Main.warn(ex.getMessage());
324                it.remove();
325            }
326        }
327        return pluginSites;
328    }
329
330    /**
331     * The action for downloading the list of available plugins
332     */
333    class DownloadAvailablePluginsAction extends AbstractAction {
334
335        public DownloadAvailablePluginsAction() {
336            putValue(NAME,tr("Download list"));
337            putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins"));
338            putValue(SMALL_ICON, ImageProvider.get("download"));
339        }
340
341        @Override
342        public void actionPerformed(ActionEvent e) {
343            Collection<String> pluginSites = getOnlinePluginSites();
344            if (pluginSites.isEmpty()) {
345                return;
346            }
347            final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites);
348            Runnable continuation = new Runnable() {
349                @Override
350                public void run() {
351                    if (task.isCanceled()) return;
352                    SwingUtilities.invokeLater(new Runnable() {
353                        @Override
354                        public void run() {
355                            model.updateAvailablePlugins(task.getAvailablePlugins());
356                            pnlPluginPreferences.refreshView();
357                            Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030
358                        }
359                    });
360                }
361            };
362            Main.worker.submit(task);
363            Main.worker.submit(continuation);
364        }
365
366    }
367
368    /**
369     * The action for updating the list of selected plugins
370     */
371    class UpdateSelectedPluginsAction extends AbstractAction {
372        public UpdateSelectedPluginsAction() {
373            putValue(NAME,tr("Update plugins"));
374            putValue(SHORT_DESCRIPTION, tr("Update the selected plugins"));
375            putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh"));
376        }
377
378        protected void alertNothingToUpdate() {
379            try {
380                SwingUtilities.invokeAndWait(new Runnable() {
381                    @Override
382                    public void run() {
383                        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                    }
391                });
392            } catch (InterruptedException | InvocationTargetException e) {
393                Main.error(e);
394            }
395        }
396
397        @Override
398        public void actionPerformed(ActionEvent e) {
399            final List<PluginInformation> toUpdate = model.getSelectedPlugins();
400            // the async task for downloading plugins
401            final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(
402                    pnlPluginPreferences,
403                    toUpdate,
404                    tr("Update plugins")
405                    );
406            // the async task for downloading plugin information
407            final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(getOnlinePluginSites());
408
409            // to be run asynchronously after the plugin download
410            //
411            final Runnable pluginDownloadContinuation = new Runnable() {
412                @Override
413                public void run() {
414                    if (pluginDownloadTask.isCanceled())
415                        return;
416                    notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask);
417                    model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins());
418                    model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins());
419                    GuiHelper.runInEDT(new Runnable() {
420                        @Override
421                        public void run() {
422                            pnlPluginPreferences.refreshView();                        }
423                    });
424                }
425            };
426
427            // to be run asynchronously after the plugin list download
428            //
429            final Runnable pluginInfoDownloadContinuation = new Runnable() {
430                @Override
431                public void run() {
432                    if (pluginInfoDownloadTask.isCanceled())
433                        return;
434                    model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins());
435                    // select plugins which actually have to be updated
436                    //
437                    Iterator<PluginInformation> it = toUpdate.iterator();
438                    while(it.hasNext()) {
439                        PluginInformation pi = it.next();
440                        if (!pi.isUpdateRequired()) {
441                            it.remove();
442                        }
443                    }
444                    if (toUpdate.isEmpty()) {
445                        alertNothingToUpdate();
446                        return;
447                    }
448                    pluginDownloadTask.setPluginsToDownload(toUpdate);
449                    Main.worker.submit(pluginDownloadTask);
450                    Main.worker.submit(pluginDownloadContinuation);
451                }
452            };
453
454            Main.worker.submit(pluginInfoDownloadTask);
455            Main.worker.submit(pluginInfoDownloadContinuation);
456        }
457    }
458
459
460    /**
461     * The action for configuring the plugin download sites
462     *
463     */
464    class ConfigureSitesAction extends AbstractAction {
465        public ConfigureSitesAction() {
466            putValue(NAME,tr("Configure sites..."));
467            putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from"));
468            putValue(SMALL_ICON, ImageProvider.get("dialogs", "settings"));
469        }
470
471        @Override
472        public void actionPerformed(ActionEvent e) {
473            configureSites();
474        }
475    }
476
477    /**
478     * Applies the current filter condition in the filter text field to the
479     * model
480     */
481    class SearchFieldAdapter implements DocumentListener {
482        public void filter() {
483            String expr = tfFilter.getText().trim();
484            if (expr.isEmpty()) {
485                expr = null;
486            }
487            model.filterDisplayedPlugins(expr);
488            pnlPluginPreferences.refreshView();
489        }
490
491        @Override
492        public void changedUpdate(DocumentEvent arg0) {
493            filter();
494        }
495
496        @Override
497        public void insertUpdate(DocumentEvent arg0) {
498            filter();
499        }
500
501        @Override
502        public void removeUpdate(DocumentEvent arg0) {
503            filter();
504        }
505    }
506
507    private static class PluginConfigurationSitesPanel extends JPanel {
508
509        private DefaultListModel<String> model;
510
511        protected final void build() {
512            setLayout(new GridBagLayout());
513            add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol());
514            model = new DefaultListModel<>();
515            for (String s : Main.pref.getPluginSites()) {
516                model.addElement(s);
517            }
518            final JList<String> list = new JList<>(model);
519            add(new JScrollPane(list), GBC.std().fill());
520            JPanel buttons = new JPanel(new GridBagLayout());
521            buttons.add(new JButton(new AbstractAction(tr("Add")){
522                @Override
523                public void actionPerformed(ActionEvent e) {
524                    String s = JOptionPane.showInputDialog(
525                            JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
526                            tr("Add JOSM Plugin description URL."),
527                            tr("Enter URL"),
528                            JOptionPane.QUESTION_MESSAGE
529                            );
530                    if (s != null) {
531                        model.addElement(s);
532                    }
533                }
534            }), GBC.eol().fill(GBC.HORIZONTAL));
535            buttons.add(new JButton(new AbstractAction(tr("Edit")){
536                @Override
537                public void actionPerformed(ActionEvent e) {
538                    if (list.getSelectedValue() == null) {
539                        JOptionPane.showMessageDialog(
540                                JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
541                                tr("Please select an entry."),
542                                tr("Warning"),
543                                JOptionPane.WARNING_MESSAGE
544                                );
545                        return;
546                    }
547                    String s = (String)JOptionPane.showInputDialog(
548                            Main.parent,
549                            tr("Edit JOSM Plugin description URL."),
550                            tr("JOSM Plugin description URL"),
551                            JOptionPane.QUESTION_MESSAGE,
552                            null,
553                            null,
554                            list.getSelectedValue()
555                            );
556                    if (s != null) {
557                        model.setElementAt(s, list.getSelectedIndex());
558                    }
559                }
560            }), GBC.eol().fill(GBC.HORIZONTAL));
561            buttons.add(new JButton(new AbstractAction(tr("Delete")){
562                @Override
563                public void actionPerformed(ActionEvent event) {
564                    if (list.getSelectedValue() == null) {
565                        JOptionPane.showMessageDialog(
566                                JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this),
567                                tr("Please select an entry."),
568                                tr("Warning"),
569                                JOptionPane.WARNING_MESSAGE
570                                );
571                        return;
572                    }
573                    model.removeElement(list.getSelectedValue());
574                }
575            }), GBC.eol().fill(GBC.HORIZONTAL));
576            add(buttons, GBC.eol());
577        }
578
579        public PluginConfigurationSitesPanel() {
580            build();
581        }
582
583        public List<String> getUpdateSites() {
584            if (model.getSize() == 0) return Collections.emptyList();
585            List<String> ret = new ArrayList<>(model.getSize());
586            for (int i=0; i< model.getSize();i++){
587                ret.add(model.get(i));
588            }
589            return ret;
590        }
591    }
592}