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