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