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 }