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; 023import java.util.Set; 024 025import javax.swing.AbstractAction; 026import javax.swing.BorderFactory; 027import javax.swing.DefaultListModel; 028import javax.swing.JButton; 029import javax.swing.JLabel; 030import javax.swing.JList; 031import javax.swing.JOptionPane; 032import javax.swing.JPanel; 033import javax.swing.JScrollPane; 034import javax.swing.JTabbedPane; 035import javax.swing.SwingUtilities; 036import javax.swing.UIManager; 037import javax.swing.event.DocumentEvent; 038import javax.swing.event.DocumentListener; 039 040import org.openstreetmap.josm.Main; 041import org.openstreetmap.josm.data.Version; 042import org.openstreetmap.josm.gui.HelpAwareOptionPane; 043import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 044import org.openstreetmap.josm.gui.help.HelpUtil; 045import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting; 046import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 047import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 048import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 049import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel; 050import org.openstreetmap.josm.gui.util.GuiHelper; 051import org.openstreetmap.josm.gui.widgets.JosmTextField; 052import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator; 053import org.openstreetmap.josm.plugins.PluginDownloadTask; 054import org.openstreetmap.josm.plugins.PluginInformation; 055import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask; 056import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask; 057import org.openstreetmap.josm.tools.GBC; 058import org.openstreetmap.josm.tools.ImageProvider; 059 060/** 061 * Preference settings for plugins. 062 * @since 168 063 */ 064public final class PluginPreference extends DefaultTabPreferenceSetting { 065 066 /** 067 * Factory used to create a new {@code PluginPreference}. 068 */ 069 public static class Factory implements PreferenceSettingFactory { 070 @Override 071 public PreferenceSetting createPreferenceSetting() { 072 return new PluginPreference(); 073 } 074 } 075 076 private PluginPreference() { 077 super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false, new JTabbedPane()); 078 } 079 080 /** 081 * Returns the download summary string to be shown. 082 * @param task The plugin download task that has completed 083 * @return the download summary string to be shown. Contains summary of success/failed plugins. 084 */ 085 public static String buildDownloadSummary(PluginDownloadTask task) { 086 Collection<PluginInformation> downloaded = task.getDownloadedPlugins(); 087 Collection<PluginInformation> failed = task.getFailedPlugins(); 088 StringBuilder sb = new StringBuilder(); 089 if (!downloaded.isEmpty()) { 090 sb.append(trn( 091 "The following plugin has been downloaded <strong>successfully</strong>:", 092 "The following {0} plugins have been downloaded <strong>successfully</strong>:", 093 downloaded.size(), 094 downloaded.size() 095 )); 096 sb.append("<ul>"); 097 for (PluginInformation pi: downloaded) { 098 sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")</li>"); 099 } 100 sb.append("</ul>"); 101 } 102 if (!failed.isEmpty()) { 103 sb.append(trn( 104 "Downloading the following plugin has <strong>failed</strong>:", 105 "Downloading the following {0} plugins has <strong>failed</strong>:", 106 failed.size(), 107 failed.size() 108 )); 109 sb.append("<ul>"); 110 for (PluginInformation pi: failed) { 111 sb.append("<li>").append(pi.name).append("</li>"); 112 } 113 sb.append("</ul>"); 114 } 115 return sb.toString(); 116 } 117 118 /** 119 * Notifies user about result of a finished plugin download task. 120 * @param parent The parent component 121 * @param task The finished plugin download task 122 * @param restartRequired true if a restart is required 123 * @since 6797 124 */ 125 public static void notifyDownloadResults(final Component parent, PluginDownloadTask task, boolean restartRequired) { 126 final Collection<PluginInformation> failed = task.getFailedPlugins(); 127 final StringBuilder sb = new StringBuilder(); 128 sb.append("<html>") 129 .append(buildDownloadSummary(task)); 130 if (restartRequired) { 131 sb.append(tr("Please restart JOSM to activate the downloaded plugins.")); 132 } 133 sb.append("</html>"); 134 GuiHelper.runInEDTAndWait(new Runnable() { 135 @Override 136 public void run() { 137 HelpAwareOptionPane.showOptionDialog( 138 parent, 139 sb.toString(), 140 tr("Update plugins"), 141 !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE, 142 HelpUtil.ht("/Preferences/Plugins") 143 ); 144 } 145 }); 146 } 147 148 private JosmTextField tfFilter; 149 private PluginListPanel pnlPluginPreferences; 150 private PluginPreferencesModel model; 151 private JScrollPane spPluginPreferences; 152 private PluginUpdatePolicyPanel pnlPluginUpdatePolicy; 153 154 /** 155 * is set to true if this preference pane has been selected 156 * by the user 157 */ 158 private boolean pluginPreferencesActivated; 159 160 protected JPanel buildSearchFieldPanel() { 161 JPanel pnl = new JPanel(new GridBagLayout()); 162 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 163 GridBagConstraints gc = new GridBagConstraints(); 164 165 gc.anchor = GridBagConstraints.NORTHWEST; 166 gc.fill = GridBagConstraints.HORIZONTAL; 167 gc.weightx = 0.0; 168 gc.insets = new Insets(0, 0, 0, 3); 169 pnl.add(new JLabel(tr("Search:")), gc); 170 171 gc.gridx = 1; 172 gc.weightx = 1.0; 173 tfFilter = new JosmTextField(); 174 pnl.add(tfFilter, gc); 175 tfFilter.setToolTipText(tr("Enter a search expression")); 176 SelectAllOnFocusGainedDecorator.decorate(tfFilter); 177 tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter()); 178 return pnl; 179 } 180 181 protected JPanel buildActionPanel() { 182 JPanel pnl = new JPanel(new GridLayout(1, 3)); 183 184 pnl.add(new JButton(new DownloadAvailablePluginsAction())); 185 pnl.add(new JButton(new UpdateSelectedPluginsAction())); 186 pnl.add(new JButton(new ConfigureSitesAction())); 187 return pnl; 188 } 189 190 protected JPanel buildPluginListPanel() { 191 JPanel pnl = new JPanel(new BorderLayout()); 192 pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH); 193 model = new PluginPreferencesModel(); 194 pnlPluginPreferences = new PluginListPanel(model); 195 spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences); 196 spPluginPreferences.getVerticalScrollBar().addComponentListener( 197 new ComponentAdapter() { 198 @Override 199 public void componentShown(ComponentEvent e) { 200 spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border")); 201 } 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 Set<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 List<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 /** 325 * The action for downloading the list of available plugins 326 */ 327 class DownloadAvailablePluginsAction extends AbstractAction { 328 329 /** 330 * Constructs a new {@code DownloadAvailablePluginsAction}. 331 */ 332 DownloadAvailablePluginsAction() { 333 putValue(NAME, tr("Download list")); 334 putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins")); 335 putValue(SMALL_ICON, ImageProvider.get("download")); 336 } 337 338 @Override 339 public void actionPerformed(ActionEvent e) { 340 Collection<String> pluginSites = Main.pref.getOnlinePluginSites(); 341 if (pluginSites.isEmpty()) { 342 return; 343 } 344 final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites); 345 Runnable continuation = new Runnable() { 346 @Override 347 public void run() { 348 if (task.isCanceled()) return; 349 SwingUtilities.invokeLater(new Runnable() { 350 @Override 351 public void run() { 352 model.updateAvailablePlugins(task.getAvailablePlugins()); 353 pnlPluginPreferences.refreshView(); 354 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030 355 } 356 }); 357 } 358 }; 359 Main.worker.submit(task); 360 Main.worker.submit(continuation); 361 } 362 } 363 364 /** 365 * The action for updating the list of selected plugins 366 */ 367 class UpdateSelectedPluginsAction extends AbstractAction { 368 UpdateSelectedPluginsAction() { 369 putValue(NAME, tr("Update plugins")); 370 putValue(SHORT_DESCRIPTION, tr("Update the selected plugins")); 371 putValue(SMALL_ICON, ImageProvider.get("dialogs", "refresh")); 372 } 373 374 protected void alertNothingToUpdate() { 375 try { 376 SwingUtilities.invokeAndWait(new Runnable() { 377 @Override 378 public void run() { 379 HelpAwareOptionPane.showOptionDialog( 380 pnlPluginPreferences, 381 tr("All installed plugins are up to date. JOSM does not have to download newer versions."), 382 tr("Plugins up to date"), 383 JOptionPane.INFORMATION_MESSAGE, 384 null // FIXME: provide help context 385 ); 386 } 387 }); 388 } catch (InterruptedException | InvocationTargetException e) { 389 Main.error(e); 390 } 391 } 392 393 @Override 394 public void actionPerformed(ActionEvent e) { 395 final List<PluginInformation> toUpdate = model.getSelectedPlugins(); 396 // the async task for downloading plugins 397 final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask( 398 pnlPluginPreferences, 399 toUpdate, 400 tr("Update plugins") 401 ); 402 // the async task for downloading plugin information 403 final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask( 404 Main.pref.getOnlinePluginSites()); 405 406 // to be run asynchronously after the plugin download 407 // 408 final Runnable pluginDownloadContinuation = new Runnable() { 409 @Override 410 public void run() { 411 if (pluginDownloadTask.isCanceled()) 412 return; 413 boolean restartRequired = false; 414 for (PluginInformation pi : pluginDownloadTask.getDownloadedPlugins()) { 415 if (!model.getNewlyActivatedPlugins().contains(pi) || !pi.canloadatruntime) { 416 restartRequired = true; 417 break; 418 } 419 } 420 notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask, restartRequired); 421 model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins()); 422 model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins()); 423 GuiHelper.runInEDT(new Runnable() { 424 @Override 425 public void run() { 426 pnlPluginPreferences.refreshView(); 427 } 428 }); 429 } 430 }; 431 432 // to be run asynchronously after the plugin list download 433 // 434 final Runnable pluginInfoDownloadContinuation = new Runnable() { 435 @Override 436 public void run() { 437 if (pluginInfoDownloadTask.isCanceled()) 438 return; 439 model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins()); 440 // select plugins which actually have to be updated 441 // 442 Iterator<PluginInformation> it = toUpdate.iterator(); 443 while (it.hasNext()) { 444 PluginInformation pi = it.next(); 445 if (!pi.isUpdateRequired()) { 446 it.remove(); 447 } 448 } 449 if (toUpdate.isEmpty()) { 450 alertNothingToUpdate(); 451 return; 452 } 453 pluginDownloadTask.setPluginsToDownload(toUpdate); 454 Main.worker.submit(pluginDownloadTask); 455 Main.worker.submit(pluginDownloadContinuation); 456 } 457 }; 458 459 Main.worker.submit(pluginInfoDownloadTask); 460 Main.worker.submit(pluginInfoDownloadContinuation); 461 } 462 } 463 464 465 /** 466 * The action for configuring the plugin download sites 467 * 468 */ 469 class ConfigureSitesAction extends AbstractAction { 470 ConfigureSitesAction() { 471 putValue(NAME, tr("Configure sites...")); 472 putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from")); 473 putValue(SMALL_ICON, ImageProvider.get("dialogs", "settings")); 474 } 475 476 @Override 477 public void actionPerformed(ActionEvent e) { 478 configureSites(); 479 } 480 } 481 482 /** 483 * Applies the current filter condition in the filter text field to the 484 * model 485 */ 486 class SearchFieldAdapter implements DocumentListener { 487 public void filter() { 488 String expr = tfFilter.getText().trim(); 489 if (expr.isEmpty()) { 490 expr = null; 491 } 492 model.filterDisplayedPlugins(expr); 493 pnlPluginPreferences.refreshView(); 494 } 495 496 @Override 497 public void changedUpdate(DocumentEvent arg0) { 498 filter(); 499 } 500 501 @Override 502 public void insertUpdate(DocumentEvent arg0) { 503 filter(); 504 } 505 506 @Override 507 public void removeUpdate(DocumentEvent arg0) { 508 filter(); 509 } 510 } 511 512 private static class PluginConfigurationSitesPanel extends JPanel { 513 514 private DefaultListModel<String> model; 515 516 protected final void build() { 517 setLayout(new GridBagLayout()); 518 add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol()); 519 model = new DefaultListModel<>(); 520 for (String s : Main.pref.getPluginSites()) { 521 model.addElement(s); 522 } 523 final JList<String> list = new JList<>(model); 524 add(new JScrollPane(list), GBC.std().fill()); 525 JPanel buttons = new JPanel(new GridBagLayout()); 526 buttons.add(new JButton(new AbstractAction(tr("Add")) { 527 @Override 528 public void actionPerformed(ActionEvent e) { 529 String s = JOptionPane.showInputDialog( 530 JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this), 531 tr("Add JOSM Plugin description URL."), 532 tr("Enter URL"), 533 JOptionPane.QUESTION_MESSAGE 534 ); 535 if (s != null) { 536 model.addElement(s); 537 } 538 } 539 }), GBC.eol().fill(GBC.HORIZONTAL)); 540 buttons.add(new JButton(new AbstractAction(tr("Edit")) { 541 @Override 542 public void actionPerformed(ActionEvent e) { 543 if (list.getSelectedValue() == null) { 544 JOptionPane.showMessageDialog( 545 JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this), 546 tr("Please select an entry."), 547 tr("Warning"), 548 JOptionPane.WARNING_MESSAGE 549 ); 550 return; 551 } 552 String s = (String) JOptionPane.showInputDialog( 553 Main.parent, 554 tr("Edit JOSM Plugin description URL."), 555 tr("JOSM Plugin description URL"), 556 JOptionPane.QUESTION_MESSAGE, 557 null, 558 null, 559 list.getSelectedValue() 560 ); 561 if (s != null) { 562 model.setElementAt(s, list.getSelectedIndex()); 563 } 564 } 565 }), GBC.eol().fill(GBC.HORIZONTAL)); 566 buttons.add(new JButton(new AbstractAction(tr("Delete")) { 567 @Override 568 public void actionPerformed(ActionEvent event) { 569 if (list.getSelectedValue() == null) { 570 JOptionPane.showMessageDialog( 571 JOptionPane.getFrameForComponent(PluginConfigurationSitesPanel.this), 572 tr("Please select an entry."), 573 tr("Warning"), 574 JOptionPane.WARNING_MESSAGE 575 ); 576 return; 577 } 578 model.removeElement(list.getSelectedValue()); 579 } 580 }), GBC.eol().fill(GBC.HORIZONTAL)); 581 add(buttons, GBC.eol()); 582 } 583 584 PluginConfigurationSitesPanel() { 585 build(); 586 } 587 588 public List<String> getUpdateSites() { 589 if (model.getSize() == 0) return Collections.emptyList(); 590 List<String> ret = new ArrayList<>(model.getSize()); 591 for (int i = 0; i < model.getSize(); i++) { 592 ret.add(model.get(i)); 593 } 594 return ret; 595 } 596 } 597}