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