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(/* ICON(preferences/) */ "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, boolean restartRequired) { 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 (restartRequired) { 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 public List<PluginInformation> getNewlyActivatedPlugins() { 279 return model != null ? model.getNewlyActivatedPlugins() : null; 280 } 281 282 @Override 283 public boolean ok() { 284 if (! pluginPreferencesActivated) 285 return false; 286 pnlPluginUpdatePolicy.rememberInPreferences(); 287 if (model.isActivePluginsChanged()) { 288 LinkedList<String> l = new LinkedList<>(model.getSelectedPluginNames()); 289 Collections.sort(l); 290 Main.pref.putCollection("plugins", l); 291 if (!model.getNewlyDeactivatedPlugins().isEmpty()) return true; 292 for (PluginInformation pi : model.getNewlyActivatedPlugins()) { 293 if (!pi.canloadatruntime) return true; 294 } 295 } 296 return false; 297 } 298 299 /** 300 * Reads locally available information about plugins from the local file system. 301 * Scans cached plugin lists from plugin download sites and locally available 302 * plugin jar files. 303 * 304 */ 305 public void readLocalPluginInformation() { 306 final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(); 307 Runnable r = new Runnable() { 308 @Override 309 public void run() { 310 if (task.isCanceled()) return; 311 SwingUtilities.invokeLater(new Runnable() { 312 @Override 313 public void run() { 314 model.setAvailablePlugins(task.getAvailablePlugins()); 315 pnlPluginPreferences.refreshView(); 316 } 317 }); 318 } 319 }; 320 Main.worker.submit(task); 321 Main.worker.submit(r); 322 } 323 324 private static Collection<String> getOnlinePluginSites() { 325 Collection<String> pluginSites = new ArrayList<>(Main.pref.getPluginSites()); 326 for (Iterator<String> it = pluginSites.iterator(); it.hasNext();) { 327 try { 328 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(it.next(), Main.getJOSMWebsite()); 329 } catch (OfflineAccessException ex) { 330 Main.warn(ex.getMessage()); 331 it.remove(); 332 } 333 } 334 return pluginSites; 335 } 336 337 /** 338 * The action for downloading the list of available plugins 339 */ 340 class DownloadAvailablePluginsAction extends AbstractAction { 341 342 public DownloadAvailablePluginsAction() { 343 putValue(NAME,tr("Download list")); 344 putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins")); 345 putValue(SMALL_ICON, ImageProvider.get("download")); 346 } 347 348 @Override 349 public void actionPerformed(ActionEvent e) { 350 Collection<String> pluginSites = getOnlinePluginSites(); 351 if (pluginSites.isEmpty()) { 352 return; 353 } 354 final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites); 355 Runnable continuation = new Runnable() { 356 @Override 357 public void run() { 358 if (task.isCanceled()) return; 359 SwingUtilities.invokeLater(new Runnable() { 360 @Override 361 public void run() { 362 model.updateAvailablePlugins(task.getAvailablePlugins()); 363 pnlPluginPreferences.refreshView(); 364 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030 365 } 366 }); 367 } 368 }; 369 Main.worker.submit(task); 370 Main.worker.submit(continuation); 371 } 372 373 } 374 375 /** 376 * The action for updating the list of selected plugins 377 */ 378 class UpdateSelectedPluginsAction extends AbstractAction { 379 public UpdateSelectedPluginsAction() { 380 putValue(NAME,tr("Update plugins")); 381 putValue(SHORT_DESCRIPTION, tr("Update the selected plugins")); 382 putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh")); 383 } 384 385 protected void alertNothingToUpdate() { 386 try { 387 SwingUtilities.invokeAndWait(new Runnable() { 388 @Override 389 public void run() { 390 HelpAwareOptionPane.showOptionDialog( 391 pnlPluginPreferences, 392 tr("All installed plugins are up to date. JOSM does not have to download newer versions."), 393 tr("Plugins up to date"), 394 JOptionPane.INFORMATION_MESSAGE, 395 null // FIXME: provide help context 396 ); 397 } 398 }); 399 } catch (InterruptedException | InvocationTargetException e) { 400 Main.error(e); 401 } 402 } 403 404 @Override 405 public void actionPerformed(ActionEvent e) { 406 final List<PluginInformation> toUpdate = model.getSelectedPlugins(); 407 // the async task for downloading plugins 408 final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask( 409 pnlPluginPreferences, 410 toUpdate, 411 tr("Update plugins") 412 ); 413 // the async task for downloading plugin information 414 final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(getOnlinePluginSites()); 415 416 // to be run asynchronously after the plugin download 417 // 418 final Runnable pluginDownloadContinuation = new Runnable() { 419 @Override 420 public void run() { 421 if (pluginDownloadTask.isCanceled()) 422 return; 423 boolean restartRequired = false; 424 for (PluginInformation pi : pluginDownloadTask.getDownloadedPlugins()) { 425 if (!model.getNewlyActivatedPlugins().contains(pi) || !pi.canloadatruntime) { 426 restartRequired = true; 427 break; 428 } 429 } 430 notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask, restartRequired); 431 model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins()); 432 model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins()); 433 GuiHelper.runInEDT(new Runnable() { 434 @Override 435 public void run() { 436 pnlPluginPreferences.refreshView(); } 437 }); 438 } 439 }; 440 441 // to be run asynchronously after the plugin list download 442 // 443 final Runnable pluginInfoDownloadContinuation = new Runnable() { 444 @Override 445 public void run() { 446 if (pluginInfoDownloadTask.isCanceled()) 447 return; 448 model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins()); 449 // select plugins which actually have to be updated 450 // 451 Iterator<PluginInformation> it = toUpdate.iterator(); 452 while (it.hasNext()) { 453 PluginInformation pi = it.next(); 454 if (!pi.isUpdateRequired()) { 455 it.remove(); 456 } 457 } 458 if (toUpdate.isEmpty()) { 459 alertNothingToUpdate(); 460 return; 461 } 462 pluginDownloadTask.setPluginsToDownload(toUpdate); 463 Main.worker.submit(pluginDownloadTask); 464 Main.worker.submit(pluginDownloadContinuation); 465 } 466 }; 467 468 Main.worker.submit(pluginInfoDownloadTask); 469 Main.worker.submit(pluginInfoDownloadContinuation); 470 } 471 } 472 473 474 /** 475 * The action for configuring the plugin download sites 476 * 477 */ 478 class ConfigureSitesAction extends AbstractAction { 479 public ConfigureSitesAction() { 480 putValue(NAME,tr("Configure sites...")); 481 putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from")); 482 putValue(SMALL_ICON, ImageProvider.get("dialogs", "settings")); 483 } 484 485 @Override 486 public void actionPerformed(ActionEvent e) { 487 configureSites(); 488 } 489 } 490 491 /** 492 * Applies the current filter condition in the filter text field to the 493 * model 494 */ 495 class SearchFieldAdapter implements DocumentListener { 496 public void filter() { 497 String expr = tfFilter.getText().trim(); 498 if (expr.isEmpty()) { 499 expr = null; 500 } 501 model.filterDisplayedPlugins(expr); 502 pnlPluginPreferences.refreshView(); 503 } 504 505 @Override 506 public void changedUpdate(DocumentEvent arg0) { 507 filter(); 508 } 509 510 @Override 511 public void insertUpdate(DocumentEvent arg0) { 512 filter(); 513 } 514 515 @Override 516 public void removeUpdate(DocumentEvent arg0) { 517 filter(); 518 } 519 } 520 521 private static class PluginConfigurationSitesPanel extends JPanel { 522 523 private DefaultListModel<String> model; 524 525 protected final void build() { 526 setLayout(new GridBagLayout()); 527 add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol()); 528 model = new DefaultListModel<>(); 529 for (String s : Main.pref.getPluginSites()) { 530 model.addElement(s); 531 } 532 final JList<String> list = new JList<>(model); 533 add(new JScrollPane(list), GBC.std().fill()); 534 JPanel buttons = new JPanel(new GridBagLayout()); 535 buttons.add(new JButton(new AbstractAction(tr("Add")){ 536 @Override 537 public void actionPerformed(ActionEvent e) { 538 String s = JOptionPane.showInputDialog( 539 JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this), 540 tr("Add JOSM Plugin description URL."), 541 tr("Enter URL"), 542 JOptionPane.QUESTION_MESSAGE 543 ); 544 if (s != null) { 545 model.addElement(s); 546 } 547 } 548 }), GBC.eol().fill(GBC.HORIZONTAL)); 549 buttons.add(new JButton(new AbstractAction(tr("Edit")){ 550 @Override 551 public void actionPerformed(ActionEvent e) { 552 if (list.getSelectedValue() == null) { 553 JOptionPane.showMessageDialog( 554 JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this), 555 tr("Please select an entry."), 556 tr("Warning"), 557 JOptionPane.WARNING_MESSAGE 558 ); 559 return; 560 } 561 String s = (String)JOptionPane.showInputDialog( 562 Main.parent, 563 tr("Edit JOSM Plugin description URL."), 564 tr("JOSM Plugin description URL"), 565 JOptionPane.QUESTION_MESSAGE, 566 null, 567 null, 568 list.getSelectedValue() 569 ); 570 if (s != null) { 571 model.setElementAt(s, list.getSelectedIndex()); 572 } 573 } 574 }), GBC.eol().fill(GBC.HORIZONTAL)); 575 buttons.add(new JButton(new AbstractAction(tr("Delete")){ 576 @Override 577 public void actionPerformed(ActionEvent event) { 578 if (list.getSelectedValue() == null) { 579 JOptionPane.showMessageDialog( 580 JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this), 581 tr("Please select an entry."), 582 tr("Warning"), 583 JOptionPane.WARNING_MESSAGE 584 ); 585 return; 586 } 587 model.removeElement(list.getSelectedValue()); 588 } 589 }), GBC.eol().fill(GBC.HORIZONTAL)); 590 add(buttons, GBC.eol()); 591 } 592 593 public PluginConfigurationSitesPanel() { 594 build(); 595 } 596 597 public List<String> getUpdateSites() { 598 if (model.getSize() == 0) return Collections.emptyList(); 599 List<String> ret = new ArrayList<>(model.getSize()); 600 for (int i=0; i< model.getSize();i++){ 601 ret.add(model.get(i)); 602 } 603 return ret; 604 } 605 } 606 607 608}