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