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