001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.preferences.plugin; 003 004import static java.awt.GridBagConstraints.HORIZONTAL; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trc; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.awt.BorderLayout; 010import java.awt.Component; 011import java.awt.GridBagLayout; 012import java.awt.GridLayout; 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.LinkedList; 021import java.util.List; 022import java.util.Set; 023import java.util.regex.Pattern; 024import java.util.stream.Collectors; 025import java.util.stream.IntStream; 026 027import javax.swing.AbstractAction; 028import javax.swing.ButtonGroup; 029import javax.swing.DefaultListModel; 030import javax.swing.JButton; 031import javax.swing.JCheckBox; 032import javax.swing.JLabel; 033import javax.swing.JList; 034import javax.swing.JOptionPane; 035import javax.swing.JPanel; 036import javax.swing.JRadioButton; 037import javax.swing.JScrollPane; 038import javax.swing.JTabbedPane; 039import javax.swing.JTextArea; 040import javax.swing.SwingUtilities; 041import javax.swing.UIManager; 042 043import org.openstreetmap.josm.actions.ExpertToggleAction; 044import org.openstreetmap.josm.data.Preferences; 045import org.openstreetmap.josm.data.Version; 046import org.openstreetmap.josm.gui.HelpAwareOptionPane; 047import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 048import org.openstreetmap.josm.gui.MainApplication; 049import org.openstreetmap.josm.gui.help.HelpUtil; 050import org.openstreetmap.josm.gui.preferences.ExtensibleTabPreferenceSetting; 051import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 052import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 053import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 054import org.openstreetmap.josm.gui.util.GuiHelper; 055import org.openstreetmap.josm.gui.widgets.FilterField; 056import org.openstreetmap.josm.plugins.PluginDownloadTask; 057import org.openstreetmap.josm.plugins.PluginHandler; 058import org.openstreetmap.josm.plugins.PluginInformation; 059import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask; 060import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask; 061import org.openstreetmap.josm.spi.preferences.Config; 062import org.openstreetmap.josm.tools.GBC; 063import org.openstreetmap.josm.tools.ImageProvider; 064import org.openstreetmap.josm.tools.Logging; 065import org.openstreetmap.josm.tools.Utils; 066 067/** 068 * Preference settings for plugins. 069 * @since 168 070 */ 071public final class PluginPreference extends ExtensibleTabPreferenceSetting { 072 073 /** 074 * Factory used to create a new {@code PluginPreference}. 075 */ 076 public static class Factory implements PreferenceSettingFactory { 077 @Override 078 public PreferenceSetting createPreferenceSetting() { 079 return new PluginPreference(); 080 } 081 } 082 083 private PluginListPanel pnlPluginPreferences; 084 private PluginPreferencesModel model; 085 private JScrollPane spPluginPreferences; 086 private PluginUpdatePolicyPanel pnlPluginUpdatePolicy; 087 088 /** 089 * is set to true if this preference pane has been selected by the user 090 */ 091 private boolean pluginPreferencesActivated; 092 093 private PluginPreference() { 094 super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false); 095 } 096 097 /** 098 * Returns the download summary string to be shown. 099 * @param task The plugin download task that has completed 100 * @return the download summary string to be shown. Contains summary of success/failed plugins. 101 */ 102 public static String buildDownloadSummary(PluginDownloadTask task) { 103 Collection<PluginInformation> downloaded = task.getDownloadedPlugins(); 104 Collection<PluginInformation> failed = task.getFailedPlugins(); 105 Exception exception = task.getLastException(); 106 StringBuilder sb = new StringBuilder(); 107 if (!downloaded.isEmpty()) { 108 sb.append(trn( 109 "The following plugin has been downloaded <strong>successfully</strong>:", 110 "The following {0} plugins have been downloaded <strong>successfully</strong>:", 111 downloaded.size(), 112 downloaded.size() 113 )); 114 sb.append("<ul>"); 115 for (PluginInformation pi: downloaded) { 116 sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")</li>"); 117 } 118 sb.append("</ul>"); 119 } 120 if (!failed.isEmpty()) { 121 sb.append(trn( 122 "Downloading the following plugin has <strong>failed</strong>:", 123 "Downloading the following {0} plugins has <strong>failed</strong>:", 124 failed.size(), 125 failed.size() 126 )); 127 sb.append("<ul>"); 128 for (PluginInformation pi: failed) { 129 sb.append("<li>").append(pi.name).append("</li>"); 130 } 131 sb.append("</ul>"); 132 } 133 if (exception != null) { 134 // Same i18n string in ExceptionUtil.explainBadRequest() 135 sb.append(tr("<br>Error message(untranslated): {0}", exception.getMessage())); 136 } 137 return sb.toString(); 138 } 139 140 /** 141 * Notifies user about result of a finished plugin download task. 142 * @param parent The parent component 143 * @param task The finished plugin download task 144 * @param restartRequired true if a restart is required 145 * @since 6797 146 */ 147 public static void notifyDownloadResults(final Component parent, PluginDownloadTask task, boolean restartRequired) { 148 final Collection<PluginInformation> failed = task.getFailedPlugins(); 149 final StringBuilder sb = new StringBuilder(); 150 sb.append("<html>") 151 .append(buildDownloadSummary(task)); 152 if (restartRequired) { 153 sb.append(tr("Please restart JOSM to activate the downloaded plugins.")); 154 } 155 sb.append("</html>"); 156 GuiHelper.runInEDTAndWait(() -> HelpAwareOptionPane.showOptionDialog( 157 parent, 158 sb.toString(), 159 tr("Update plugins"), 160 !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE, 161 HelpUtil.ht("/Preferences/Plugins") 162 )); 163 } 164 165 private JPanel buildSearchFieldPanel() { 166 JPanel pnl = new JPanel(new GridBagLayout()); 167 pnl.add(GBC.glue(0, 0)); 168 169 ButtonGroup bg = new ButtonGroup(); 170 JPanel radios = new JPanel(); 171 addRadioButton(bg, radios, new JRadioButton(trc("plugins", "All"), true), PluginInstallation.ALL); 172 addRadioButton(bg, radios, new JRadioButton(trc("plugins", "Installed")), PluginInstallation.INSTALLED); 173 addRadioButton(bg, radios, new JRadioButton(trc("plugins", "Available")), PluginInstallation.AVAILABLE); 174 pnl.add(radios, GBC.eol().fill(HORIZONTAL)); 175 176 pnl.add(new FilterField().filter(expr -> { 177 model.filterDisplayedPlugins(expr); 178 pnlPluginPreferences.refreshView(); 179 }), GBC.eol().insets(0, 0, 0, 5).fill(HORIZONTAL)); 180 return pnl; 181 } 182 183 private void addRadioButton(ButtonGroup bg, JPanel pnl, JRadioButton rb, PluginInstallation value) { 184 bg.add(rb); 185 pnl.add(rb, GBC.std()); 186 rb.addActionListener(e -> { 187 model.filterDisplayedPlugins(value); 188 pnlPluginPreferences.refreshView(); 189 }); 190 } 191 192 private static Component addButton(JPanel pnl, JButton button, String buttonName) { 193 button.setName(buttonName); 194 return pnl.add(button); 195 } 196 197 private JPanel buildActionPanel() { 198 JPanel pnl = new JPanel(new GridLayout(1, 4)); 199 200 // assign some component names to these as we go to aid testing 201 addButton(pnl, new JButton(new DownloadAvailablePluginsAction()), "downloadListButton"); 202 addButton(pnl, new JButton(new UpdateSelectedPluginsAction()), "updatePluginsButton"); 203 ExpertToggleAction.addVisibilitySwitcher(addButton(pnl, new JButton(new SelectByListAction()), "loadFromListButton")); 204 ExpertToggleAction.addVisibilitySwitcher(addButton(pnl, new JButton(new ConfigureSitesAction()), "configureSitesButton")); 205 return pnl; 206 } 207 208 private JPanel buildPluginListPanel() { 209 JPanel pnl = new JPanel(new BorderLayout()); 210 pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH); 211 model = new PluginPreferencesModel(); 212 pnlPluginPreferences = new PluginListPanel(model); 213 spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences); 214 spPluginPreferences.getVerticalScrollBar().addComponentListener( 215 new ComponentAdapter() { 216 @Override 217 public void componentShown(ComponentEvent e) { 218 spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border")); 219 } 220 221 @Override 222 public void componentHidden(ComponentEvent e) { 223 spPluginPreferences.setBorder(null); 224 } 225 } 226 ); 227 228 pnl.add(spPluginPreferences, BorderLayout.CENTER); 229 pnl.add(buildActionPanel(), BorderLayout.SOUTH); 230 return pnl; 231 } 232 233 @Override 234 public void addGui(final PreferenceTabbedPane gui) { 235 JTabbedPane pane = getTabPane(); 236 pnlPluginUpdatePolicy = new PluginUpdatePolicyPanel(); 237 pane.addTab(tr("Plugins"), buildPluginListPanel()); 238 pane.addTab(tr("Plugin update policy"), pnlPluginUpdatePolicy); 239 super.addGui(gui); 240 readLocalPluginInformation(); 241 pluginPreferencesActivated = true; 242 } 243 244 private void configureSites() { 245 ButtonSpec[] options = { 246 new ButtonSpec( 247 tr("OK"), 248 new ImageProvider("ok"), 249 tr("Accept the new plugin sites and close the dialog"), 250 null /* no special help topic */ 251 ), 252 new ButtonSpec( 253 tr("Cancel"), 254 new ImageProvider("cancel"), 255 tr("Close the dialog"), 256 null /* no special help topic */ 257 ) 258 }; 259 PluginConfigurationSitesPanel pnl = new PluginConfigurationSitesPanel(); 260 261 int answer = HelpAwareOptionPane.showOptionDialog( 262 pnlPluginPreferences, 263 pnl, 264 tr("Configure Plugin Sites"), 265 JOptionPane.QUESTION_MESSAGE, 266 null, 267 options, 268 options[0], 269 null /* no help topic */ 270 ); 271 if (answer != 0 /* OK */) 272 return; 273 Preferences.main().setPluginSites(pnl.getUpdateSites()); 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 Config.getPref().putList("plugins", l); 303 List<PluginInformation> deactivatedPlugins = model.getNewlyDeactivatedPlugins(); 304 if (!deactivatedPlugins.isEmpty()) { 305 boolean requiresRestart = PluginHandler.removePlugins(deactivatedPlugins); 306 if (requiresRestart) 307 return requiresRestart; 308 } 309 return model.getNewlyActivatedPlugins().stream().anyMatch(pi -> !pi.canloadatruntime); 310 } 311 return false; 312 } 313 314 /** 315 * Reads locally available information about plugins from the local file system. 316 * Scans cached plugin lists from plugin download sites and locally available 317 * plugin jar files. 318 * 319 */ 320 public void readLocalPluginInformation() { 321 final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(); 322 Runnable r = () -> { 323 if (!task.isCanceled()) { 324 SwingUtilities.invokeLater(() -> { 325 model.setAvailablePlugins(task.getAvailablePlugins()); 326 pnlPluginPreferences.resetDisplayedComponents(); 327 pnlPluginPreferences.refreshView(); 328 }); 329 } 330 }; 331 MainApplication.worker.submit(task); 332 MainApplication.worker.submit(r); 333 } 334 335 /** 336 * The action for downloading the list of available plugins 337 */ 338 class DownloadAvailablePluginsAction extends AbstractAction { 339 340 /** 341 * Constructs a new {@code DownloadAvailablePluginsAction}. 342 */ 343 DownloadAvailablePluginsAction() { 344 putValue(NAME, tr("Download list")); 345 putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins")); 346 new ImageProvider("download").getResource().attachImageIcon(this); 347 } 348 349 @Override 350 public void actionPerformed(ActionEvent e) { 351 Collection<String> pluginSites = Preferences.main().getOnlinePluginSites(); 352 if (pluginSites.isEmpty()) { 353 return; 354 } 355 final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites); 356 Runnable continuation = () -> { 357 if (!task.isCanceled()) { 358 SwingUtilities.invokeLater(() -> { 359 model.updateAvailablePlugins(task.getAvailablePlugins()); 360 pnlPluginPreferences.resetDisplayedComponents(); 361 pnlPluginPreferences.refreshView(); 362 Config.getPref().putInt("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030 363 }); 364 } 365 }; 366 MainApplication.worker.submit(task); 367 MainApplication.worker.submit(continuation); 368 } 369 } 370 371 /** 372 * The action for updating the list of selected plugins 373 */ 374 class UpdateSelectedPluginsAction extends AbstractAction { 375 UpdateSelectedPluginsAction() { 376 putValue(NAME, tr("Update plugins")); 377 putValue(SHORT_DESCRIPTION, tr("Update the selected plugins")); 378 new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this); 379 } 380 381 protected void alertNothingToUpdate() { 382 try { 383 SwingUtilities.invokeAndWait(() -> 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 } catch (InterruptedException | InvocationTargetException e) { 391 Logging.error(e); 392 } 393 } 394 395 @Override 396 public void actionPerformed(ActionEvent e) { 397 final List<PluginInformation> toUpdate = model.getSelectedPlugins(); 398 // the async task for downloading plugins 399 final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask( 400 pnlPluginPreferences, 401 toUpdate, 402 tr("Update plugins") 403 ); 404 // the async task for downloading plugin information 405 final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask( 406 Preferences.main().getOnlinePluginSites()); 407 408 // to be run asynchronously after the plugin download 409 // 410 final Runnable pluginDownloadContinuation = () -> { 411 if (pluginDownloadTask.isCanceled()) 412 return; 413 boolean restartRequired = pluginDownloadTask.getDownloadedPlugins().stream() 414 .anyMatch(pi -> !(model.getNewlyActivatedPlugins().contains(pi) && pi.canloadatruntime)); 415 notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask, restartRequired); 416 model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins()); 417 model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins()); 418 GuiHelper.runInEDT(pnlPluginPreferences::refreshView); 419 }; 420 421 // to be run asynchronously after the plugin list download 422 // 423 final Runnable pluginInfoDownloadContinuation = () -> { 424 if (pluginInfoDownloadTask.isCanceled()) 425 return; 426 model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins()); 427 // select plugins which actually have to be updated 428 // 429 toUpdate.removeIf(pi -> !pi.isUpdateRequired()); 430 if (toUpdate.isEmpty()) { 431 alertNothingToUpdate(); 432 return; 433 } 434 pluginDownloadTask.setPluginsToDownload(toUpdate); 435 MainApplication.worker.submit(pluginDownloadTask); 436 MainApplication.worker.submit(pluginDownloadContinuation); 437 }; 438 439 MainApplication.worker.submit(pluginInfoDownloadTask); 440 MainApplication.worker.submit(pluginInfoDownloadContinuation); 441 } 442 } 443 444 /** 445 * The action for configuring the plugin download sites 446 * 447 */ 448 class ConfigureSitesAction extends AbstractAction { 449 ConfigureSitesAction() { 450 putValue(NAME, tr("Configure sites...")); 451 putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from")); 452 new ImageProvider("preference").getResource().attachImageIcon(this); 453 } 454 455 @Override 456 public void actionPerformed(ActionEvent e) { 457 configureSites(); 458 } 459 } 460 461 /** 462 * The action for selecting the plugins given by a text file compatible to JOSM bug report. 463 * @author Michael Zangl 464 */ 465 class SelectByListAction extends AbstractAction { 466 SelectByListAction() { 467 putValue(NAME, tr("Load from list...")); 468 putValue(SHORT_DESCRIPTION, tr("Load plugins from a list of plugins")); 469 new ImageProvider("misc/statusreport").getResource().attachImageIcon(this); 470 } 471 472 @Override 473 public void actionPerformed(ActionEvent e) { 474 JTextArea textField = new JTextArea(10, 0); 475 JCheckBox deleteNotInList = new JCheckBox(tr("Disable all other plugins")); 476 477 JLabel helpLabel = new JLabel("<html>" + String.join("<br/>", 478 tr("Enter a list of plugins you want to download."), 479 tr("You should add one plugin id per line, version information is ignored."), 480 tr("You can copy+paste the list of a status report here.")) + "</html>"); 481 482 if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()), 483 new Object[] {helpLabel, new JScrollPane(textField), deleteNotInList}, 484 tr("Load plugins from list"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) { 485 activatePlugins(textField, deleteNotInList.isSelected()); 486 } 487 } 488 489 private void activatePlugins(JTextArea textField, boolean deleteNotInList) { 490 String[] lines = textField.getText().split("\n", -1); 491 List<String> toActivate = new ArrayList<>(); 492 List<String> notFound = new ArrayList<>(); 493 // This pattern matches the default list format JOSM uses for bug reports. 494 // It removes a list item mark at the beginning of the line: +, -, * 495 // It removes the version number after the plugin, like: 123, (123), (v5.7alpha3), (1b3), (v1-SNAPSHOT-1)... 496 Pattern regex = Pattern.compile("^[-+\\*\\s]*|\\s[\\d\\s]*(\\([^\\(\\)\\[\\]]*\\))?[\\d\\s]*$"); 497 for (String line : lines) { 498 String name = regex.matcher(line).replaceAll(""); 499 if (name.isEmpty()) { 500 continue; 501 } 502 PluginInformation plugin = model.getPluginInformation(name); 503 if (plugin == null) { 504 notFound.add(name); 505 } else { 506 toActivate.add(name); 507 } 508 } 509 510 if (notFound.isEmpty() || confirmIgnoreNotFound(notFound)) { 511 activatePlugins(toActivate, deleteNotInList); 512 } 513 } 514 515 private void activatePlugins(List<String> toActivate, boolean deleteNotInList) { 516 if (deleteNotInList) { 517 for (String name : model.getSelectedPluginNames()) { 518 if (!toActivate.contains(name)) { 519 model.setPluginSelected(name, false); 520 } 521 } 522 } 523 for (String name : toActivate) { 524 model.setPluginSelected(name, true); 525 } 526 pnlPluginPreferences.refreshView(); 527 } 528 529 private boolean confirmIgnoreNotFound(List<String> notFound) { 530 String list = "<ul><li>" + String.join("</li><li>", notFound) + "</li></ul>"; 531 String message = "<html>" + tr("The following plugins were not found. Continue anyway?") + list + "</html>"; 532 return JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()), 533 message) == JOptionPane.OK_OPTION; 534 } 535 } 536 537 private static class PluginConfigurationSitesPanel extends JPanel { 538 539 private final DefaultListModel<String> model = new DefaultListModel<>(); 540 541 PluginConfigurationSitesPanel() { 542 super(new GridBagLayout()); 543 add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol()); 544 for (String s : Preferences.main().getPluginSites()) { 545 model.addElement(s); 546 } 547 final JList<String> list = new JList<>(model); 548 add(new JScrollPane(list), GBC.std().fill()); 549 JPanel buttons = new JPanel(new GridBagLayout()); 550 buttons.add(new JButton(new AbstractAction(tr("Add")) { 551 @Override 552 public void actionPerformed(ActionEvent e) { 553 String s = JOptionPane.showInputDialog( 554 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 555 tr("Add JOSM Plugin description URL."), 556 tr("Enter URL"), 557 JOptionPane.QUESTION_MESSAGE 558 ); 559 if (!Utils.isEmpty(s)) { 560 model.addElement(s); 561 } 562 } 563 }), GBC.eol().fill(HORIZONTAL)); 564 buttons.add(new JButton(new AbstractAction(tr("Edit")) { 565 @Override 566 public void actionPerformed(ActionEvent e) { 567 if (list.getSelectedValue() == null) { 568 JOptionPane.showMessageDialog( 569 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 570 tr("Please select an entry."), 571 tr("Warning"), 572 JOptionPane.WARNING_MESSAGE 573 ); 574 return; 575 } 576 String s = (String) JOptionPane.showInputDialog( 577 MainApplication.getMainFrame(), 578 tr("Edit JOSM Plugin description URL."), 579 tr("JOSM Plugin description URL"), 580 JOptionPane.QUESTION_MESSAGE, 581 null, 582 null, 583 list.getSelectedValue() 584 ); 585 if (!Utils.isEmpty(s)) { 586 model.setElementAt(s, list.getSelectedIndex()); 587 } 588 } 589 }), GBC.eol().fill(HORIZONTAL)); 590 buttons.add(new JButton(new AbstractAction(tr("Delete")) { 591 @Override 592 public void actionPerformed(ActionEvent event) { 593 if (list.getSelectedValue() == null) { 594 JOptionPane.showMessageDialog( 595 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 596 tr("Please select an entry."), 597 tr("Warning"), 598 JOptionPane.WARNING_MESSAGE 599 ); 600 return; 601 } 602 model.removeElement(list.getSelectedValue()); 603 } 604 }), GBC.eol().fill(HORIZONTAL)); 605 add(buttons, GBC.eol()); 606 } 607 608 protected List<String> getUpdateSites() { 609 if (model.getSize() == 0) 610 return Collections.emptyList(); 611 return IntStream.range(0, model.getSize()) 612 .mapToObj(model::get) 613 .collect(Collectors.toList()); 614 } 615 } 616 617 @Override 618 public String getHelpContext() { 619 return HelpUtil.ht("/Preferences/Plugins"); 620 } 621}