source: josm/trunk/src/org/openstreetmap/josm/gui/preferences/plugin/PluginPreference.java@ 11339

Last change on this file since 11339 was 11339, checked in by simon04, 7 years ago

Loop can be replaced with Collection.removeIf()

  • Property svn:eol-style set to native
File size: 25.7 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.preferences.plugin;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.awt.BorderLayout;
8import java.awt.Component;
9import java.awt.GraphicsEnvironment;
10import java.awt.GridBagConstraints;
11import java.awt.GridBagLayout;
12import java.awt.GridLayout;
13import java.awt.Insets;
14import java.awt.event.ActionEvent;
15import java.awt.event.ComponentAdapter;
16import java.awt.event.ComponentEvent;
17import java.lang.reflect.InvocationTargetException;
18import java.util.ArrayList;
19import java.util.Arrays;
20import java.util.Collection;
21import java.util.Collections;
22import java.util.LinkedList;
23import java.util.List;
24import java.util.Set;
25import java.util.regex.Pattern;
26
27import javax.swing.AbstractAction;
28import javax.swing.BorderFactory;
29import javax.swing.DefaultListModel;
30import javax.swing.JButton;
31import javax.swing.JCheckBox;
32import javax.swing.JLabel;
33import javax.swing.JList;
34import javax.swing.JOptionPane;
35import javax.swing.JPanel;
36import javax.swing.JScrollPane;
37import javax.swing.JTabbedPane;
38import javax.swing.JTextArea;
39import javax.swing.SwingUtilities;
40import javax.swing.UIManager;
41import javax.swing.event.DocumentEvent;
42import javax.swing.event.DocumentListener;
43
44import org.openstreetmap.josm.Main;
45import org.openstreetmap.josm.actions.ExpertToggleAction;
46import org.openstreetmap.josm.data.Version;
47import org.openstreetmap.josm.gui.HelpAwareOptionPane;
48import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
49import org.openstreetmap.josm.gui.help.HelpUtil;
50import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
51import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
52import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
53import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
54import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.PreferencePanel;
55import org.openstreetmap.josm.gui.util.GuiHelper;
56import org.openstreetmap.josm.gui.widgets.JosmTextField;
57import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
58import org.openstreetmap.josm.plugins.PluginDownloadTask;
59import org.openstreetmap.josm.plugins.PluginInformation;
60import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
61import org.openstreetmap.josm.plugins.ReadRemotePluginInformationTask;
62import org.openstreetmap.josm.tools.GBC;
63import org.openstreetmap.josm.tools.ImageProvider;
64import org.openstreetmap.josm.tools.Utils;
65
66/**
67 * Preference settings for plugins.
68 * @since 168
69 */
70public final class PluginPreference extends DefaultTabPreferenceSetting {
71
72 /**
73 * Factory used to create a new {@code PluginPreference}.
74 */
75 public static class Factory implements PreferenceSettingFactory {
76 @Override
77 public PreferenceSetting createPreferenceSetting() {
78 return new PluginPreference();
79 }
80 }
81
82 private JosmTextField tfFilter;
83 private PluginListPanel pnlPluginPreferences;
84 private PluginPreferencesModel model;
85 private JScrollPane spPluginPreferences;
86 private PluginUpdatePolicyPanel pnlPluginUpdatePolicy;
87
88 /**
89 * is set to true if this preference pane has been selected by the user
90 */
91 private boolean pluginPreferencesActivated;
92
93 private PluginPreference() {
94 super(/* ICON(preferences/) */ "plugin", tr("Plugins"), tr("Configure available plugins."), false, new JTabbedPane());
95 }
96
97 /**
98 * Returns the download summary string to be shown.
99 * @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}
Note: See TracBrowser for help on using the repository browser.