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 }