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.help.HelpUtil; 050import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting; 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.preferences.PreferenceTabbedPane.PreferencePanel; 055import org.openstreetmap.josm.gui.util.GuiHelper; 056import org.openstreetmap.josm.gui.widgets.JosmTextField; 057import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator; 058import org.openstreetmap.josm.plugins.PluginDownloadTask; 059import org.openstreetmap.josm.plugins.PluginInformation; 060import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask; 061import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask; 062import org.openstreetmap.josm.tools.GBC; 063import org.openstreetmap.josm.tools.ImageProvider; 064import org.openstreetmap.josm.tools.Utils; 065 066/** 067 * Preference settings for plugins. 068 * @since 168 069 */ 070public final class PluginPreference extends DefaultTabPreferenceSetting { 071 072 /** 073 * Factory used to create a new {@code PluginPreference}. 074 */ 075 public static class Factory implements PreferenceSettingFactory { 076 @Override 077 public PreferenceSetting createPreferenceSetting() { 078 return new PluginPreference(); 079 } 080 } 081 082 private JosmTextField tfFilter; 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, new JTabbedPane()); 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 if (!GraphicsEnvironment.isHeadless()) { 157 GuiHelper.runInEDTAndWait(() -> HelpAwareOptionPane.showOptionDialog( 158 parent, 159 sb.toString(), 160 tr("Update plugins"), 161 !failed.isEmpty() ? JOptionPane.WARNING_MESSAGE : JOptionPane.INFORMATION_MESSAGE, 162 HelpUtil.ht("/Preferences/Plugins") 163 )); 164 } 165 } 166 167 private JPanel buildSearchFieldPanel() { 168 JPanel pnl = new JPanel(new GridBagLayout()); 169 pnl.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 170 GridBagConstraints gc = new GridBagConstraints(); 171 172 gc.anchor = GridBagConstraints.NORTHWEST; 173 gc.fill = GridBagConstraints.HORIZONTAL; 174 gc.weightx = 0.0; 175 gc.insets = new Insets(0, 0, 0, 3); 176 pnl.add(new JLabel(tr("Search:")), gc); 177 178 gc.gridx = 1; 179 gc.weightx = 1.0; 180 tfFilter = new JosmTextField(); 181 pnl.add(tfFilter, gc); 182 tfFilter.setToolTipText(tr("Enter a search expression")); 183 SelectAllOnFocusGainedDecorator.decorate(tfFilter); 184 tfFilter.getDocument().addDocumentListener(new SearchFieldAdapter()); 185 return pnl; 186 } 187 188 private JPanel buildActionPanel() { 189 JPanel pnl = new JPanel(new GridLayout(1, 4)); 190 191 pnl.add(new JButton(new DownloadAvailablePluginsAction())); 192 pnl.add(new JButton(new UpdateSelectedPluginsAction())); 193 ExpertToggleAction.addVisibilitySwitcher(pnl.add(new JButton(new SelectByListAction()))); 194 ExpertToggleAction.addVisibilitySwitcher(pnl.add(new JButton(new ConfigureSitesAction()))); 195 return pnl; 196 } 197 198 private JPanel buildPluginListPanel() { 199 JPanel pnl = new JPanel(new BorderLayout()); 200 pnl.add(buildSearchFieldPanel(), BorderLayout.NORTH); 201 model = new PluginPreferencesModel(); 202 pnlPluginPreferences = new PluginListPanel(model); 203 spPluginPreferences = GuiHelper.embedInVerticalScrollPane(pnlPluginPreferences); 204 spPluginPreferences.getVerticalScrollBar().addComponentListener( 205 new ComponentAdapter() { 206 @Override 207 public void componentShown(ComponentEvent e) { 208 spPluginPreferences.setBorder(UIManager.getBorder("ScrollPane.border")); 209 } 210 211 @Override 212 public void componentHidden(ComponentEvent e) { 213 spPluginPreferences.setBorder(null); 214 } 215 } 216 ); 217 218 pnl.add(spPluginPreferences, BorderLayout.CENTER); 219 pnl.add(buildActionPanel(), BorderLayout.SOUTH); 220 return pnl; 221 } 222 223 private JTabbedPane buildContentPane() { 224 JTabbedPane pane = getTabPane(); 225 pnlPluginUpdatePolicy = new PluginUpdatePolicyPanel(); 226 pane.addTab(tr("Plugins"), buildPluginListPanel()); 227 pane.addTab(tr("Plugin update policy"), pnlPluginUpdatePolicy); 228 return pane; 229 } 230 231 @Override 232 public void addGui(final PreferenceTabbedPane gui) { 233 GridBagConstraints gc = new GridBagConstraints(); 234 gc.weightx = 1.0; 235 gc.weighty = 1.0; 236 gc.anchor = GridBagConstraints.NORTHWEST; 237 gc.fill = GridBagConstraints.BOTH; 238 PreferencePanel plugins = gui.createPreferenceTab(this); 239 plugins.add(buildContentPane(), gc); 240 readLocalPluginInformation(); 241 pluginPreferencesActivated = true; 242 } 243 244 private void configureSites() { 245 ButtonSpec[] options = new ButtonSpec[] { 246 new ButtonSpec( 247 tr("OK"), 248 ImageProvider.get("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 ImageProvider.get("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 Main.pref.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 Main.pref.putCollection("plugins", l); 303 if (!model.getNewlyDeactivatedPlugins().isEmpty()) 304 return true; 305 for (PluginInformation pi : model.getNewlyActivatedPlugins()) { 306 if (!pi.canloadatruntime) 307 return true; 308 } 309 } 310 return false; 311 } 312 313 /** 314 * Reads locally available information about plugins from the local file system. 315 * Scans cached plugin lists from plugin download sites and locally available 316 * plugin jar files. 317 * 318 */ 319 public void readLocalPluginInformation() { 320 final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(); 321 Runnable r = () -> { 322 if (!task.isCanceled()) { 323 SwingUtilities.invokeLater(() -> { 324 model.setAvailablePlugins(task.getAvailablePlugins()); 325 pnlPluginPreferences.refreshView(); 326 }); 327 } 328 }; 329 Main.worker.submit(task); 330 Main.worker.submit(r); 331 } 332 333 /** 334 * The action for downloading the list of available plugins 335 */ 336 class DownloadAvailablePluginsAction extends AbstractAction { 337 338 /** 339 * Constructs a new {@code DownloadAvailablePluginsAction}. 340 */ 341 DownloadAvailablePluginsAction() { 342 putValue(NAME, tr("Download list")); 343 putValue(SHORT_DESCRIPTION, tr("Download the list of available plugins")); 344 new ImageProvider("download").getResource().attachImageIcon(this); 345 } 346 347 @Override 348 public void actionPerformed(ActionEvent e) { 349 Collection<String> pluginSites = Main.pref.getOnlinePluginSites(); 350 if (pluginSites.isEmpty()) { 351 return; 352 } 353 final ReadRemotePluginInformationTask task = new ReadRemotePluginInformationTask(pluginSites); 354 Runnable continuation = () -> { 355 if (!task.isCanceled()) { 356 SwingUtilities.invokeLater(() -> { 357 model.updateAvailablePlugins(task.getAvailablePlugins()); 358 pnlPluginPreferences.refreshView(); 359 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion()); // fix #7030 360 }); 361 } 362 }; 363 Main.worker.submit(task); 364 Main.worker.submit(continuation); 365 } 366 } 367 368 /** 369 * The action for updating the list of selected plugins 370 */ 371 class UpdateSelectedPluginsAction extends AbstractAction { 372 UpdateSelectedPluginsAction() { 373 putValue(NAME, tr("Update plugins")); 374 putValue(SHORT_DESCRIPTION, tr("Update the selected plugins")); 375 new ImageProvider("dialogs", "refresh").getResource().attachImageIcon(this); 376 } 377 378 protected void alertNothingToUpdate() { 379 try { 380 SwingUtilities.invokeAndWait(() -> HelpAwareOptionPane.showOptionDialog( 381 pnlPluginPreferences, 382 tr("All installed plugins are up to date. JOSM does not have to download newer versions."), 383 tr("Plugins up to date"), 384 JOptionPane.INFORMATION_MESSAGE, 385 null // FIXME: provide help context 386 )); 387 } catch (InterruptedException | InvocationTargetException e) { 388 Main.error(e); 389 } 390 } 391 392 @Override 393 public void actionPerformed(ActionEvent e) { 394 final List<PluginInformation> toUpdate = model.getSelectedPlugins(); 395 // the async task for downloading plugins 396 final PluginDownloadTask pluginDownloadTask = new PluginDownloadTask( 397 pnlPluginPreferences, 398 toUpdate, 399 tr("Update plugins") 400 ); 401 // the async task for downloading plugin information 402 final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask( 403 Main.pref.getOnlinePluginSites()); 404 405 // to be run asynchronously after the plugin download 406 // 407 final Runnable pluginDownloadContinuation = () -> { 408 if (pluginDownloadTask.isCanceled()) 409 return; 410 boolean restartRequired = false; 411 for (PluginInformation pi : pluginDownloadTask.getDownloadedPlugins()) { 412 if (!model.getNewlyActivatedPlugins().contains(pi) || !pi.canloadatruntime) { 413 restartRequired = true; 414 break; 415 } 416 } 417 notifyDownloadResults(pnlPluginPreferences, pluginDownloadTask, restartRequired); 418 model.refreshLocalPluginVersion(pluginDownloadTask.getDownloadedPlugins()); 419 model.clearPendingPlugins(pluginDownloadTask.getDownloadedPlugins()); 420 GuiHelper.runInEDT(pnlPluginPreferences::refreshView); 421 }; 422 423 // to be run asynchronously after the plugin list download 424 // 425 final Runnable pluginInfoDownloadContinuation = () -> { 426 if (pluginInfoDownloadTask.isCanceled()) 427 return; 428 model.updateAvailablePlugins(pluginInfoDownloadTask.getAvailablePlugins()); 429 // select plugins which actually have to be updated 430 // 431 toUpdate.removeIf(pi -> !pi.isUpdateRequired()); 432 if (toUpdate.isEmpty()) { 433 alertNothingToUpdate(); 434 return; 435 } 436 pluginDownloadTask.setPluginsToDownload(toUpdate); 437 Main.worker.submit(pluginDownloadTask); 438 Main.worker.submit(pluginDownloadContinuation); 439 }; 440 441 Main.worker.submit(pluginInfoDownloadTask); 442 Main.worker.submit(pluginInfoDownloadContinuation); 443 } 444 } 445 446 /** 447 * The action for configuring the plugin download sites 448 * 449 */ 450 class ConfigureSitesAction extends AbstractAction { 451 ConfigureSitesAction() { 452 putValue(NAME, tr("Configure sites...")); 453 putValue(SHORT_DESCRIPTION, tr("Configure the list of sites where plugins are downloaded from")); 454 new ImageProvider("dialogs", "settings").getResource().attachImageIcon(this); 455 } 456 457 @Override 458 public void actionPerformed(ActionEvent e) { 459 configureSites(); 460 } 461 } 462 463 /** 464 * The action for selecting the plugins given by a text file compatible to JOSM bug report. 465 * @author Michael Zangl 466 */ 467 class SelectByListAction extends AbstractAction { 468 SelectByListAction() { 469 putValue(NAME, tr("Load from list...")); 470 putValue(SHORT_DESCRIPTION, tr("Load plugins from a list of plugins")); 471 } 472 473 @Override 474 public void actionPerformed(ActionEvent e) { 475 JTextArea textField = new JTextArea(10, 0); 476 JCheckBox deleteNotInList = new JCheckBox(tr("Disable all other plugins")); 477 478 JLabel helpLabel = new JLabel("<html>" + Utils.join("<br/>", Arrays.asList( 479 tr("Enter a list of plugins you want to download."), 480 tr("You should add one plugin id per line, version information is ignored."), 481 tr("You can copy+paste the list of a status report here."))) + "</html>"); 482 483 if (JOptionPane.OK_OPTION == JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()), 484 new Object[] {helpLabel, new JScrollPane(textField), deleteNotInList}, 485 tr("Load plugins from list"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE)) { 486 activatePlugins(textField, deleteNotInList.isSelected()); 487 } 488 } 489 490 private void activatePlugins(JTextArea textField, boolean deleteNotInList) { 491 String[] lines = textField.getText().split("\n"); 492 List<String> toActivate = new ArrayList<>(); 493 List<String> notFound = new ArrayList<>(); 494 Pattern regex = Pattern.compile("^[-+\\s]*|\\s[\\(\\)\\d\\s]*"); 495 for (String line : lines) { 496 String name = regex.matcher(line).replaceAll(""); 497 if (name.isEmpty()) { 498 continue; 499 } 500 PluginInformation plugin = model.getPluginInformation(name); 501 if (plugin == null) { 502 notFound.add(name); 503 } else { 504 toActivate.add(name); 505 } 506 } 507 508 if (notFound.isEmpty() || confirmIgnoreNotFound(notFound)) { 509 activatePlugins(toActivate, deleteNotInList); 510 } 511 } 512 513 private void activatePlugins(List<String> toActivate, boolean deleteNotInList) { 514 if (deleteNotInList) { 515 for (String name : model.getSelectedPluginNames()) { 516 if (!toActivate.contains(name)) { 517 model.setPluginSelected(name, false); 518 } 519 } 520 } 521 for (String name : toActivate) { 522 model.setPluginSelected(name, true); 523 } 524 pnlPluginPreferences.refreshView(); 525 } 526 527 private boolean confirmIgnoreNotFound(List<String> notFound) { 528 String list = "<ul><li>" + Utils.join("</li><li>", notFound) + "</li></ul>"; 529 String message = "<html>" + tr("The following plugins were not found. Continue anyway?") + list + "</html>"; 530 return JOptionPane.showConfirmDialog(GuiHelper.getFrameForComponent(getTabPane()), 531 message) == JOptionPane.OK_OPTION; 532 } 533 } 534 535 /** 536 * Applies the current filter condition in the filter text field to the model. 537 */ 538 class SearchFieldAdapter implements DocumentListener { 539 private void filter() { 540 String expr = tfFilter.getText().trim(); 541 if (expr.isEmpty()) { 542 expr = null; 543 } 544 model.filterDisplayedPlugins(expr); 545 pnlPluginPreferences.refreshView(); 546 } 547 548 @Override 549 public void changedUpdate(DocumentEvent evt) { 550 filter(); 551 } 552 553 @Override 554 public void insertUpdate(DocumentEvent evt) { 555 filter(); 556 } 557 558 @Override 559 public void removeUpdate(DocumentEvent evt) { 560 filter(); 561 } 562 } 563 564 private static class PluginConfigurationSitesPanel extends JPanel { 565 566 private final DefaultListModel<String> model = new DefaultListModel<>(); 567 568 PluginConfigurationSitesPanel() { 569 super(new GridBagLayout()); 570 add(new JLabel(tr("Add JOSM Plugin description URL.")), GBC.eol()); 571 for (String s : Main.pref.getPluginSites()) { 572 model.addElement(s); 573 } 574 final JList<String> list = new JList<>(model); 575 add(new JScrollPane(list), GBC.std().fill()); 576 JPanel buttons = new JPanel(new GridBagLayout()); 577 buttons.add(new JButton(new AbstractAction(tr("Add")) { 578 @Override 579 public void actionPerformed(ActionEvent e) { 580 String s = JOptionPane.showInputDialog( 581 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 582 tr("Add JOSM Plugin description URL."), 583 tr("Enter URL"), 584 JOptionPane.QUESTION_MESSAGE 585 ); 586 if (s != null && !s.isEmpty()) { 587 model.addElement(s); 588 } 589 } 590 }), GBC.eol().fill(GBC.HORIZONTAL)); 591 buttons.add(new JButton(new AbstractAction(tr("Edit")) { 592 @Override 593 public void actionPerformed(ActionEvent e) { 594 if (list.getSelectedValue() == null) { 595 JOptionPane.showMessageDialog( 596 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 597 tr("Please select an entry."), 598 tr("Warning"), 599 JOptionPane.WARNING_MESSAGE 600 ); 601 return; 602 } 603 String s = (String) JOptionPane.showInputDialog( 604 Main.parent, 605 tr("Edit JOSM Plugin description URL."), 606 tr("JOSM Plugin description URL"), 607 JOptionPane.QUESTION_MESSAGE, 608 null, 609 null, 610 list.getSelectedValue() 611 ); 612 if (s != null && !s.isEmpty()) { 613 model.setElementAt(s, list.getSelectedIndex()); 614 } 615 } 616 }), GBC.eol().fill(GBC.HORIZONTAL)); 617 buttons.add(new JButton(new AbstractAction(tr("Delete")) { 618 @Override 619 public void actionPerformed(ActionEvent event) { 620 if (list.getSelectedValue() == null) { 621 JOptionPane.showMessageDialog( 622 GuiHelper.getFrameForComponent(PluginConfigurationSitesPanel.this), 623 tr("Please select an entry."), 624 tr("Warning"), 625 JOptionPane.WARNING_MESSAGE 626 ); 627 return; 628 } 629 model.removeElement(list.getSelectedValue()); 630 } 631 }), GBC.eol().fill(GBC.HORIZONTAL)); 632 add(buttons, GBC.eol()); 633 } 634 635 protected List<String> getUpdateSites() { 636 if (model.getSize() == 0) 637 return Collections.emptyList(); 638 List<String> ret = new ArrayList<>(model.getSize()); 639 for (int i = 0; i < model.getSize(); i++) { 640 ret.add(model.get(i)); 641 } 642 return ret; 643 } 644 } 645}