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}