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