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