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