001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.map;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.GridBagLayout;
008import java.io.IOException;
009import java.util.ArrayList;
010import java.util.Collection;
011import java.util.List;
012
013import javax.swing.BorderFactory;
014import javax.swing.JCheckBox;
015import javax.swing.JLabel;
016import javax.swing.JOptionPane;
017import javax.swing.JPanel;
018
019import org.openstreetmap.josm.data.preferences.sources.ExtendedSourceEntry;
020import org.openstreetmap.josm.data.preferences.sources.PresetPrefHelper;
021import org.openstreetmap.josm.data.preferences.sources.SourceEntry;
022import org.openstreetmap.josm.data.preferences.sources.SourceProvider;
023import org.openstreetmap.josm.data.preferences.sources.SourceType;
024import org.openstreetmap.josm.gui.ExtendedDialog;
025import org.openstreetmap.josm.gui.MainApplication;
026import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
027import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
028import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
029import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.ValidationListener;
030import org.openstreetmap.josm.gui.preferences.SourceEditor;
031import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
032import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
033import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
034import org.openstreetmap.josm.spi.preferences.Config;
035import org.openstreetmap.josm.tools.GBC;
036import org.openstreetmap.josm.tools.Logging;
037import org.openstreetmap.josm.tools.Utils;
038import org.xml.sax.SAXException;
039import org.xml.sax.SAXParseException;
040
041/**
042 * Preference settings for tagging presets.
043 */
044public final class TaggingPresetPreference implements SubPreferenceSetting {
045
046    private final class TaggingPresetValidationListener implements ValidationListener {
047        @Override
048        public boolean validatePreferences() {
049            if (sources.hasActiveSourcesChanged()) {
050                List<Integer> sourcesToRemove = new ArrayList<>();
051                int i = -1;
052                SOURCES:
053                    for (SourceEntry source: sources.getActiveSources()) {
054                        i++;
055                        boolean canLoad = false;
056                        try {
057                            TaggingPresetReader.readAll(source.url, false);
058                            canLoad = true;
059                        } catch (IOException e) {
060                            Logging.log(Logging.LEVEL_WARN, tr("Could not read tagging preset source: {0}", source), e);
061                            ExtendedDialog ed = new ExtendedDialog(MainApplication.getMainFrame(), tr("Error"),
062                                    tr("Yes"), tr("No"), tr("Cancel"));
063                            ed.setContent(tr("Could not read tagging preset source: {0}\nDo you want to keep it?", source));
064                            switch (ed.showDialog().getValue()) {
065                            case 1:
066                                continue SOURCES;
067                            case 2:
068                                sourcesToRemove.add(i);
069                                continue SOURCES;
070                            default:
071                                return false;
072                            }
073                        } catch (SAXException e) {
074                            // We will handle this in step with validation
075                            Logging.trace(e);
076                        }
077
078                        String errorMessage = null;
079
080                        try {
081                            TaggingPresetReader.readAll(source.url, true);
082                        } catch (IOException e) {
083                            // Should not happen, but at least show message
084                            String msg = tr("Could not read tagging preset source: {0}", source);
085                            Logging.log(Logging.LEVEL_ERROR, msg, e);
086                            JOptionPane.showMessageDialog(MainApplication.getMainFrame(), msg);
087                            return false;
088                        } catch (SAXParseException e) {
089                            if (canLoad) {
090                                errorMessage = tr("<html>Tagging preset source {0} can be loaded but it contains errors. " +
091                                        "Do you really want to use it?<br><br><table width=600>Error is: [{1}:{2}] {3}</table></html>",
092                                        source, e.getLineNumber(), e.getColumnNumber(), Utils.escapeReservedCharactersHTML(e.getMessage()));
093                            } else {
094                                errorMessage = tr("<html>Unable to parse tagging preset source: {0}. " +
095                                        "Do you really want to use it?<br><br><table width=400>Error is: [{1}:{2}] {3}</table></html>",
096                                        source, e.getLineNumber(), e.getColumnNumber(), Utils.escapeReservedCharactersHTML(e.getMessage()));
097                            }
098                            Logging.log(Logging.LEVEL_WARN, errorMessage, e);
099                        } catch (SAXException e) {
100                            if (canLoad) {
101                                errorMessage = tr("<html>Tagging preset source {0} can be loaded but it contains errors. " +
102                                        "Do you really want to use it?<br><br><table width=600>Error is: {1}</table></html>",
103                                        source, Utils.escapeReservedCharactersHTML(e.getMessage()));
104                            } else {
105                                errorMessage = tr("<html>Unable to parse tagging preset source: {0}. " +
106                                        "Do you really want to use it?<br><br><table width=600>Error is: {1}</table></html>",
107                                        source, Utils.escapeReservedCharactersHTML(e.getMessage()));
108                            }
109                            Logging.log(Logging.LEVEL_ERROR, errorMessage, e);
110                        }
111
112                        if (errorMessage != null) {
113                            Logging.error(errorMessage);
114                            int result = JOptionPane.showConfirmDialog(MainApplication.getMainFrame(), new JLabel(errorMessage), tr("Error"),
115                                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE);
116
117                            switch (result) {
118                            case JOptionPane.YES_OPTION:
119                                continue SOURCES;
120                            case JOptionPane.NO_OPTION:
121                                sourcesToRemove.add(i);
122                                continue SOURCES;
123                            default:
124                                return false;
125                            }
126                        }
127                    }
128                sources.removeSources(sourcesToRemove);
129                return true;
130            } else {
131                return true;
132            }
133        }
134    }
135
136    /**
137     * Factory used to create a new {@code TaggingPresetPreference}.
138     */
139    public static class Factory implements PreferenceSettingFactory {
140        @Override
141        public PreferenceSetting createPreferenceSetting() {
142            return new TaggingPresetPreference();
143        }
144    }
145
146    private TaggingPresetPreference() {
147        super();
148    }
149
150    private static final List<SourceProvider> presetSourceProviders = new ArrayList<>();
151
152    private SourceEditor sources;
153    private JCheckBox sortMenu;
154
155    /**
156     * Registers a new additional preset source provider.
157     * @param provider The preset source provider
158     * @return {@code true}, if the provider has been added, {@code false} otherwise
159     */
160    public static boolean registerSourceProvider(SourceProvider provider) {
161        if (provider != null)
162            return presetSourceProviders.add(provider);
163        return false;
164    }
165
166    private final ValidationListener validationListener = new TaggingPresetValidationListener();
167
168    @Override
169    public void addGui(PreferenceTabbedPane gui) {
170        sortMenu = new JCheckBox(tr("Sort presets menu alphabetically"),
171                Config.getPref().getBoolean("taggingpreset.sortmenu", false));
172
173        final JPanel panel = new JPanel(new GridBagLayout());
174        panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
175        panel.add(sortMenu, GBC.eol().insets(5, 5, 5, 0));
176        sources = new TaggingPresetSourceEditor();
177        panel.add(sources, GBC.eol().fill(GBC.BOTH));
178        final MapPreference mapPref = gui.getMapPreference();
179        mapPref.addSubTab(this, tr("Tagging Presets"), panel);
180        sources.deferLoading(mapPref, panel);
181        gui.addValidationListener(validationListener);
182    }
183
184    public static class TaggingPresetSourceEditor extends SourceEditor {
185
186        private static final String ICONPREF = "taggingpreset.icon.sources";
187
188        public TaggingPresetSourceEditor() {
189            super(SourceType.TAGGING_PRESET, Config.getUrls().getJOSMWebsite()+"/presets", presetSourceProviders, true);
190        }
191
192        @Override
193        public Collection<? extends SourceEntry> getInitialSourcesList() {
194            return PresetPrefHelper.INSTANCE.get();
195        }
196
197        @Override
198        public boolean finish() {
199            return doFinish(PresetPrefHelper.INSTANCE, ICONPREF);
200        }
201
202        @Override
203        public Collection<ExtendedSourceEntry> getDefault() {
204            return PresetPrefHelper.INSTANCE.getDefault();
205        }
206
207        @Override
208        public Collection<String> getInitialIconPathsList() {
209            return Config.getPref().getList(ICONPREF, null);
210        }
211
212        @Override
213        public String getStr(I18nString ident) {
214            switch (ident) {
215            case AVAILABLE_SOURCES:
216                return tr("Available presets:");
217            case ACTIVE_SOURCES:
218                return tr("Active presets:");
219            case NEW_SOURCE_ENTRY_TOOLTIP:
220                return tr("Add a new preset by entering filename or URL");
221            case NEW_SOURCE_ENTRY:
222                return tr("New preset entry:");
223            case REMOVE_SOURCE_TOOLTIP:
224                return tr("Remove the selected presets from the list of active presets");
225            case EDIT_SOURCE_TOOLTIP:
226                return tr("Edit the filename or URL for the selected active preset");
227            case ACTIVATE_TOOLTIP:
228                return tr("Add the selected available presets to the list of active presets");
229            case RELOAD_ALL_AVAILABLE:
230                return marktr("Reloads the list of available presets from ''{0}''");
231            case LOADING_SOURCES_FROM:
232                return marktr("Loading preset sources from ''{0}''");
233            case FAILED_TO_LOAD_SOURCES_FROM:
234                return marktr("<html>Failed to load the list of preset sources from<br>"
235                        + "''{0}''.<br>"
236                        + "<br>"
237                        + "Details (untranslated):<br>{1}</html>");
238            case FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC:
239                return "/Preferences/Presets#FailedToLoadPresetSources";
240            case ILLEGAL_FORMAT_OF_ENTRY:
241                return marktr("Warning: illegal format of entry in preset list ''{0}''. Got ''{1}''");
242            default: throw new AssertionError();
243            }
244        }
245    }
246
247    @Override
248    public boolean ok() {
249        boolean restart = Config.getPref().putBoolean("taggingpreset.sortmenu", sortMenu.getSelectedObjects() != null);
250        restart |= sources.finish();
251
252        return restart;
253    }
254
255    @Override
256    public boolean isExpert() {
257        return false;
258    }
259
260    @Override
261    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
262        return gui.getMapPreference();
263    }
264}