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}