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.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.LinkedList; 023import java.util.List; 024import java.util.Set; 025import java.util.regex.Pattern; 026 027import javax.swing.AbstractAction; 028import javax.swing.BorderFactory; 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.JScrollPane; 037import javax.swing.JTabbedPane; 038import javax.swing.JTextArea; 039import javax.swing.SwingUtilities; 040import javax.swing.UIManager; 041import javax.swing.event.DocumentEvent; 042import javax.swing.event.DocumentListener; 043 044import org.openstreetmap.josm.Main; 045import org.openstreetmap.josm.actions.ExpertToggleAction; 046import org.openstreetmap.josm.data.Version; 047import org.openstreetmap.josm.gui.HelpAwareOptionPane; 048import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 049import org.openstreetmap.josm.gui.MainApplication; 050import org.openstreetmap.josm.gui.help.HelpUtil; 051import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting; 052import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 053import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 054import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 055import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel; 056import org.openstreetmap.josm.gui.util.GuiHelper; 057import org.openstreetmap.josm.gui.widgets.JosmTextField; 058import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator; 059import org.openstreetmap.josm.plugins.PluginDownloadTask; 060import org.openstreetmap.josm.plugins.PluginInformation; 061import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask; 062import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask; 063import org.openstreetmap.josm.spi.preferences.Config; 064import org.openstreetmap.josm.tools.GBC; 065import org.openstreetmap.josm.tools.ImageProvider; 066import org.openstreetmap.josm.tools.Logging; 067import org.openstreetmap.josm.tools.Utils; 068 069/** 070 * Preference settings for plugins. 071 * @since 168 072 */ 073public final class PluginPreference extends DefaultTabPreferenceSetting { 074 075 /** 076 * Factory used to create a new {@code PluginPreference}. 077 */ 078 public static class Factory implements PreferenceSettingFactory { 079 @Override 080 public PreferenceSetting createPreferenceSetting() { 081 return new PluginPreference(); 082 } 083 } 084 085 private JosmTextField tfFilter; 086 private PluginListPanel pnlPluginPreferences; 087 private PluginPreferencesModel model; 088 private JScrollPane spPluginPreferences; 089 private PluginUpdatePolicyPanel pnlPluginUpdatePolicy; 090 091 /** 092 * is set to true if this preference pane has been selected by the user 093 */ 094 private boolean pluginPreferencesActivated; 095 096 private PluginPreference() { 097 super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false, new JTabbedPane()); 098 } 099 100 /** 101 * Returns the download summary string to be shown. 102 * @param task The plugin download task that has completed 103 * @return the download summary string to be shown. Contains summary of success/failed plugins. 104 */ 105 public static String buildDownloadSummary(PluginDownloadTask task) { 106 Collection<PluginInformation> downloaded = task.getDownloadedPlugins(); 107 Collection<PluginInformation> failed = task.getFailedPlugins(); 108 Exception exception = task.getLastException(); 109 StringBuilder sb = new StringBuilder(); 110 if (!downloaded.isEmpty()) { 111 sb.append(trn( 112 "The following plugin has been downloaded <strong>successfully</strong>:", 113 "The following {0} plugins have been downloaded <strong>successfully</strong>:", 114 downloaded.size(), 115 downloaded.size() 116 )); 117 sb.append("<ul>"); 118 for (PluginInformation pi: downloaded) { 119 sb.append("<li>").append(pi.name).append(" (").append(pi.version).append(")</li>"); 120 } 121 sb.append("</ul>"); 122 } 123 if (!failed.isEmpty()) { 124 sb.append(trn( 125 "Downloading the following plugin has <strong>failed</strong>:", 126 "Downloading the following {0} plugins has <strong>failed</strong>:", 127 failed.size(), 128 failed.size() 129 )); 130 sb.append("<ul>"); 131 for (PluginInformation pi: failed) { 132 sb.append("<li>").append(pi.name).append("</li>"); 133 } 134 sb.append("</ul>"); 135 } 136 if (exception != null) { 137 // Same i18n string in ExceptionUtil.explainBadRequest() 138 sb.append(tr("<br>Error message(untranslated): {0}", exception.getMessage())); 139 } 140 return sb.toString(); 141 } 142 143 /** 144 * Notifies user about result of a finished plugin download task. 145 * @param parent The parent component 146 * @param task The finished plugin download task 147 * @param restartRequired true if a restart is required 148 * @since 6797 149 */ 150 public static void notifyDownloadResults(final Component parent, PluginDownloadTask task, boolean restartRequired) { 151 final Collection<PluginInformation> failed = task.getFailedPlugins(); 152 final StringBuilder sb = new StringBuilder(); 153 sb.append("<html>") 154 .append(buildDownloadSummary(task)); 155 if (restartRequired) { 156 sb.append(tr("Please restart JOSM to activate the downloaded plugins.")); 157 } 158 sb.append("</html>"); 159 if (!GraphicsEnvironment.isHeadless()) { 160 GuiHelper.runInEDTAndWait(() -> HelpAwareOptionPane.showOptionDialog( 161 parent, 162 sb.toString(), 163 tr("Update plugins"), 164 !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE, 165 HelpUtil.ht("/Preferences/Plugins") 166 )); 167 } 168 } 169 170 private JPanel buildSearchFieldPanel() { 171 JPanel pnl = new JPanel(new GridBagLayout()); 172 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 173 GridBagConstraints gc = new GridBagConstraints(); 174 175 gc.anchor = GridBagConstraints.NORTHWEST; 176 gc.fill = GridBagConstraints.HORIZONTAL; 177 gc.weightx = 0.0; 178 gc.insets = new Insets(0, 0, 0, 3); 179 pnl.add(new JLabel(tr("Search:")), gc); 180 181 gc.gridx = 1; 182 gc.weightx = 1.0; 183 tfFilter = new JosmTextField(); 184 pnl.add(tfFilter, gc); 185 tfFilter.setToolTipText(tr("Enter a search expression")); 186 SelectAllOnFocusGainedDecorator.decorate(tfFilter); 187 tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter()); 188 return pnl; 189 } 190 191 private JPanel buildActionPanel() { 192 JPanel pnl = new JPanel(new GridLayout(1, 4)); 193 194 pnl.add(new JButton(new DownloadAvailablePluginsAction())); 195 pnl.add(new JButton(new UpdateSelectedPluginsAction())); 196 ExpertToggleAction.addVisibilitySwitcher(pnl.add(new JButton(new SelectByListAction()))); 197 ExpertToggleAction.addVisibilitySwitcher(pnl.add(new JButton(new ConfigureSitesAction()))); 198 return pnl; 199 } 200 201 private JPanel buildPluginListPanel() { 202 JPanel pnl = new JPanel(new BorderLayout()); 203 pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH); 204 model = new PluginPreferencesModel(); 205 pnlPluginPreferences = new PluginListPanel(model); 206 spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences); 207 spPluginPreferences.getVerticalScrollBar().addComponentListener( 208 new ComponentAdapter() { 209 @Override 210 public void componentShown(ComponentEvent e) { 211 spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border")); 212 } 213 214 @Override 215 public void componentHidden(ComponentEvent e) { 216 spPluginPreferences.setBorder(null); 217 } 218 } 219 ); 220 221 pnl.add(spPluginPreferences, BorderLayout.CENTER); 222 pnl.add(buildActionPanel(), BorderLayout.SOUTH); 223 return pnl; 224 } 225 226 private JTabbedPane buildContentPane() { 227 JTabbedPane pane = getTabPane(); 228 pnlPluginUpdatePolicy = new PluginUpdatePolicyPanel(); 229 pane.addTab(tr("Plugins"), buildPluginListPanel()); 230 pane.addTab(tr("Plugin update policy"), pnlPluginUpdatePolicy); 231 return pane; 232 } 233 234 @Override 235 public void addGui(final PreferenceTabbedPane gui) { 236 GridBagConstraints gc = new GridBagConstraints(); 237 gc.weightx = 1.0; 238 gc.weighty = 1.0; 239 gc.anchor = GridBagConstraints.NORTHWEST; 240 gc.fill = GridBagConstraints.BOTH; 241 PreferencePanel plugins = gui.createPreferenceTab(this); 242 plugins.add(buildContentPane(), gc); 243 readLocalPluginInformation(); 244 pluginPreferencesActivated = true; 245 } 246 247 private void configureSites() { 248 ButtonSpec[] options = new ButtonSpec[] { 249 new ButtonSpec( 250 tr("OK"), 251 ImageProvider.get("ok"), 252 tr("Accept the new plugin sites and close the dialog"), 253 null /* no special help topic */ 254 ), 255 new ButtonSpec( 256 tr("Cancel"), 257 ImageProvider.get("cancel"), 258 tr("Close the dialog"), 259 null /* no special help topic */ 260 ) 261 }; 262 PluginConfigurationSitesPanel pnl = new PluginConfigurationSitesPanel(); 263 264 int answer = HelpAwareOptionPane.showOptionDialog( 265 pnlPluginPreferences, 266 pnl, 267 tr("Configure Plugin Sites"), 268 JOptionPane.QUESTION_MESSAGE, 269 null, 270 options, 271 options[0], 272 null /* no help topic */ 273 ); 274 if (answer != 0 /* OK */) 275 return; 276 Main.pref.setPluginSites(pnl.getUpdateSites()); 277 } 278 279 /** 280 * Replies the set of plugins waiting for update or download 281 * 282 * @return the set of plugins waiting for update or download 283 */ 284 public Set<PluginInformation> getPluginsScheduledForUpdateOrDownload() { 285 return model != null ? model.getPluginsScheduledForUpdateOrDownload() : null; 286 } 287 288 /** 289 * Replies the list of plugins which have been added by the user to the set of activated plugins 290 * 291 * @return the list of newly activated plugins 292 */ 293 public List<PluginInformation> getNewlyActivatedPlugins() { 294 return model != null ? model.getNewlyActivatedPlugins() : null; 295 } 296 297 @Override 298 public boolean ok() { 299 if (!pluginPreferencesActivated) 300 return false; 301 pnlPluginUpdatePolicy.rememberInPreferences(); 302 if (model.isActivePluginsChanged()) { 303 List<String> l = new LinkedList<>(model.getSelectedPluginNames()); 304 Collections.sort(l); 305 Config.getPref().putList("plugins", l); 306 if (!model.getNewlyDeactivatedPlugins().isEmpty()) 307 return true; 308 for (PluginInformation pi : model.getNewlyActivatedPlugins()) { 309 if (!pi.canloadatruntime) 310 return true; 311 } 312 } 313 return false; 314 } 315 316 /** 317 * Reads locally available information about plugins from the local file system. 318 * Scans cached plugin lists from plugin download sites and locally available 319 * plugin jar files. 320 * 321 */ 322 public void readLocalPluginInformation() { 323 final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(); 324 Runnable r = () -> { 325 if (!task.isCanceled()) { 326 SwingUtilities.invokeLater(() -> { 327 model.setAvailablePlugins(task.getAvailablePlugins()); 328 pnlPluginPreferences.refreshView(); 329 }); 330 } 331 }; 332 MainApplication.worker.submit(task); 333 MainApplication.worker.submit(r); 334 } 335 336 /** 337 * The action for downloading the list of available plugins 338 */ 339 class DownloadAvailablePluginsAction extends AbstractAction { 340 341 /** 342 * Constructs a new {@code DownloadAvailablePluginsAction}. 343 */ 344 DownloadAvailablePluginsAction() { 345 putValue(NAME, tr("Download list")); 346 putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins")); 347 new ImageProvider("download").getResource().attachImageIcon(this); 348 } 349 350 @Override 351 public void actionPerformed(ActionEvent e) { 352 Collection<String> pluginSites = Main.pref.getOnlinePluginSites(); 353 if (pluginSites.isEmpty()) { 354 return; 355 } 356 final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites); 357 Runnable continuation = () -> { 358 if (!task.isCanceled()) { 359 SwingUtilities.invokeLater(() -> { 360 model.updateAvailablePlugins(task.getAvailablePlugins()); 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 Main.pref.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 = 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(pnlPluginPreferences::refreshView); 424 }; 425 426 // to be run asynchronously after the plugin list download 427 // 428 final Runnable pluginInfoDownloadContinuation = () -> { 429 if (pluginInfoDownloadTask.isCanceled()) 430 return; 431 model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins()); 432 // select plugins which actually have to be updated 433 // 434 toUpdate.removeIf(pi -> !pi.isUpdateRequired()); 435 if (toUpdate.isEmpty()) { 436 alertNothingToUpdate(); 437 return; 438 } 439 pluginDownloadTask.setPluginsToDownload(toUpdate); 440 MainApplication.worker.submit(pluginDownloadTask); 441 MainApplication.worker.submit(pluginDownloadContinuation); 442 }; 443 444 MainApplication.worker.submit(pluginInfoDownloadTask); 445 MainApplication.worker.submit(pluginInfoDownloadContinuation); 446 } 447 } 448 449 /** 450 * The action for configuring the plugin download sites 451 * 452 */ 453 class ConfigureSitesAction extends AbstractAction { 454 ConfigureSitesAction() { 455 putValue(NAME, tr("Configure sites...")); 456 putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from")); 457 new ImageProvider("dialogs", "settings").getResource().attachImageIcon(this); 458 } 459 460 @Override 461 public void actionPerformed(ActionEvent e) { 462 configureSites(); 463 } 464 } 465 466 /** 467 * The action for selecting the plugins given by a text file compatible to JOSM bug report. 468 * @author Michael Zangl 469 */ 470 class SelectByListAction extends AbstractAction { 471 SelectByListAction() { 472 putValue(NAME, tr("Load from list...")); 473 putValue(SHORT_DESCRIPTION, tr("Load plugins from a list of plugins")); 474 } 475 476 @Override 477 public void actionPerformed(ActionEvent e) { 478 JTextArea textField = new JTextArea(10, 0); 479 JCheckBox deleteNotInList = new JCheckBox(tr("Disable all other plugins")); 480 481 JLabel helpLabel = new JLabel("<html>" + Utils.join("<br/>", Arrays.asList( 482 tr("Enter a list of plugins you want to download."), 483 tr("You should add one plugin id per line, version information is ignored."), 484 tr("You can copy+paste the list of a status report here."))) + "</html>"); 485 486 if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()), 487 new Object[] {helpLabel, new JScrollPane(textField), deleteNotInList}, 488 tr("Load plugins from list"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) { 489 activatePlugins(textField, deleteNotInList.isSelected()); 490 } 491 } 492 493 private void activatePlugins(JTextArea textField, boolean deleteNotInList) { 494 String[] lines = textField.getText().split("\n"); 495 List<String> toActivate = new ArrayList<>(); 496 List<String> notFound = new ArrayList<>(); 497 // This pattern matches the default list format JOSM uses for bug reports. 498 // It removes a list item mark at the beginning of the line: +, -, * 499 // It removes the version number after the plugin, like: 123, (123), (v5.7alpha3), (1b3), (v1-SNAPSHOT-1)... 500 Pattern regex = Pattern.compile("^[-+\\*\\s]*|\\s[\\d\\s]*(\\([^\\(\\)\\[\\]]*\\))?[\\d\\s]*$"); 501 for (String line : lines) { 502 String name = regex.matcher(line).replaceAll(""); 503 if (name.isEmpty()) { 504 continue; 505 } 506 PluginInformation plugin = model.getPluginInformation(name); 507 if (plugin == null) { 508 notFound.add(name); 509 } else { 510 toActivate.add(name); 511 } 512 } 513 514 if (notFound.isEmpty() || confirmIgnoreNotFound(notFound)) { 515 activatePlugins(toActivate, deleteNotInList); 516 } 517 } 518 519 private void activatePlugins(List<String> toActivate, boolean deleteNotInList) { 520 if (deleteNotInList) { 521 for (String name : model.getSelectedPluginNames()) { 522 if (!toActivate.contains(name)) { 523 model.setPluginSelected(name, false); 524 } 525 } 526 } 527 for (String name : toActivate) { 528 model.setPluginSelected(name, true); 529 } 530 pnlPluginPreferences.refreshView(); 531 } 532 533 private boolean confirmIgnoreNotFound(List<String> notFound) { 534 String list = "<ul><li>" + Utils.join("</li><li>", notFound) + "</li></ul>"; 535 String message = "<html>" + tr("The following plugins were not found. Continue anyway?") + list + "</html>"; 536 return JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()), 537 message) == JOptionPane.OK_OPTION; 538 } 539 } 540 541 /** 542 * Applies the current filter condition in the filter text field to the model. 543 */ 544 class SearchFieldAdapter implements DocumentListener { 545 private void filter() { 546 String expr = tfFilter.getText().trim(); 547 if (expr.isEmpty()) { 548 expr = null; 549 } 550 model.filterDisplayedPlugins(expr); 551 pnlPluginPreferences.refreshView(); 552 } 553 554 @Override 555 public void changedUpdate(DocumentEvent evt) { 556 filter(); 557 } 558 559 @Override 560 public void insertUpdate(DocumentEvent evt) { 561 filter(); 562 } 563 564 @Override 565 public void removeUpdate(DocumentEvent evt) { 566 filter(); 567 } 568 } 569 570 private static class PluginConfigurationSitesPanel extends JPanel { 571 572 private final DefaultListModel<String> model = new DefaultListModel<>(); 573 574 PluginConfigurationSitesPanel() { 575 super(new GridBagLayout()); 576 add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol()); 577 for (String s : Main.pref.getPluginSites()) { 578 model.addElement(s); 579 } 580 final JList<String> list = new JList<>(model); 581 add(new JScrollPane(list), GBC.std().fill()); 582 JPanel buttons = new JPanel(new GridBagLayout()); 583 buttons.add(new JButton(new AbstractAction(tr("Add")) { 584 @Override 585 public void actionPerformed(ActionEvent e) { 586 String s = JOptionPane.showInputDialog( 587 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 588 tr("Add JOSM Plugin description URL."), 589 tr("Enter URL"), 590 JOptionPane.QUESTION_MESSAGE 591 ); 592 if (s != null && !s.isEmpty()) { 593 model.addElement(s); 594 } 595 } 596 }), GBC.eol().fill(GBC.HORIZONTAL)); 597 buttons.add(new JButton(new AbstractAction(tr("Edit")) { 598 @Override 599 public void actionPerformed(ActionEvent e) { 600 if (list.getSelectedValue() == null) { 601 JOptionPane.showMessageDialog( 602 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 603 tr("Please select an entry."), 604 tr("Warning"), 605 JOptionPane.WARNING_MESSAGE 606 ); 607 return; 608 } 609 String s = (String) JOptionPane.showInputDialog( 610 Main.parent, 611 tr("Edit JOSM Plugin description URL."), 612 tr("JOSM Plugin description URL"), 613 JOptionPane.QUESTION_MESSAGE, 614 null, 615 null, 616 list.getSelectedValue() 617 ); 618 if (s != null && !s.isEmpty()) { 619 model.setElementAt(s, list.getSelectedIndex()); 620 } 621 } 622 }), GBC.eol().fill(GBC.HORIZONTAL)); 623 buttons.add(new JButton(new AbstractAction(tr("Delete")) { 624 @Override 625 public void actionPerformed(ActionEvent event) { 626 if (list.getSelectedValue() == null) { 627 JOptionPane.showMessageDialog( 628 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 629 tr("Please select an entry."), 630 tr("Warning"), 631 JOptionPane.WARNING_MESSAGE 632 ); 633 return; 634 } 635 model.removeElement(list.getSelectedValue()); 636 } 637 }), GBC.eol().fill(GBC.HORIZONTAL)); 638 add(buttons, GBC.eol()); 639 } 640 641 protected List<String> getUpdateSites() { 642 if (model.getSize() == 0) 643 return Collections.emptyList(); 644 List<String> ret = new ArrayList<>(model.getSize()); 645 for (int i = 0; i < model.getSize(); i++) { 646 ret.add(model.get(i)); 647 } 648 return ret; 649 } 650 } 651 652 @Override 653 public String getHelpContext() { 654 return HelpUtil.ht("/Preferences/Plugins"); 655 } 656}