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.Collections;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015
016import javax.swing.BorderFactory;
017import javax.swing.JCheckBox;
018import javax.swing.JLabel;
019import javax.swing.JOptionPane;
020import javax.swing.JPanel;
021
022import org.openstreetmap.josm.Main;
023import org.openstreetmap.josm.gui.ExtendedDialog;
024import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
025import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
026import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
027import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.ValidationListener;
028import org.openstreetmap.josm.gui.preferences.SourceEditor;
029import org.openstreetmap.josm.gui.preferences.SourceEditor.ExtendedSourceEntry;
030import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetReader;
031import org.openstreetmap.josm.gui.preferences.SourceEntry;
032import org.openstreetmap.josm.gui.preferences.SourceProvider;
033import org.openstreetmap.josm.gui.preferences.SourceType;
034import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
035import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
036import org.openstreetmap.josm.tools.GBC;
037import org.xml.sax.SAXException;
038import org.xml.sax.SAXParseException;
039
040/**
041 * Preference settings for tagging presets.
042 */
043public final class TaggingPresetPreference implements SubPreferenceSetting {
044
045    private final class TaggingPresetValidationListener implements ValidationListener {
046        @Override
047        public boolean validatePreferences() {
048            if (sources.hasActiveSourcesChanged()) {
049                List<Integer> sourcesToRemove = new ArrayList<>();
050                int i = -1;
051                SOURCES:
052                    for (SourceEntry source: sources.getActiveSources()) {
053                        i++;
054                        boolean canLoad = false;
055                        try {
056                            TaggingPresetReader.readAll(source.url, false);
057                            canLoad = true;
058                        } catch (IOException e) {
059                            Main.warn(tr("Could not read tagging preset source: {0}", source));
060                            ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Error"),
061                                    new String[] {tr("Yes"), tr("No"), tr("Cancel")});
062                            ed.setContent(tr("Could not read tagging preset source: {0}\nDo you want to keep it?", source));
063                            switch (ed.showDialog().getValue()) {
064                            case 1:
065                                continue SOURCES;
066                            case 2:
067                                sourcesToRemove.add(i);
068                                continue SOURCES;
069                            default:
070                                return false;
071                            }
072                        } catch (SAXException e) {
073                            // We will handle this in step with validation
074                            if (Main.isTraceEnabled()) {
075                                Main.trace(e.getMessage());
076                            }
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                            Main.error(msg);
087                            JOptionPane.showMessageDialog(Main.parent, 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(), 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(), e.getMessage());
098                            }
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,  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, e.getMessage());
108                            }
109                        }
110
111                        if (errorMessage != null) {
112                            Main.error(errorMessage);
113                            int result = JOptionPane.showConfirmDialog(Main.parent, new JLabel(errorMessage), tr("Error"),
114                                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE);
115
116                            switch (result) {
117                            case JOptionPane.YES_OPTION:
118                                continue SOURCES;
119                            case JOptionPane.NO_OPTION:
120                                sourcesToRemove.add(i);
121                                continue SOURCES;
122                            default:
123                                return false;
124                            }
125                        }
126                    }
127                sources.removeSources(sourcesToRemove);
128                return true;
129            }  else {
130                return true;
131            }
132        }
133    }
134
135    /**
136     * Factory used to create a new {@code TaggingPresetPreference}.
137     */
138    public static class Factory implements PreferenceSettingFactory {
139        @Override
140        public PreferenceSetting createPreferenceSetting() {
141            return new TaggingPresetPreference();
142        }
143    }
144
145    private TaggingPresetPreference() {
146        super();
147    }
148
149    private static final List<SourceProvider> presetSourceProviders = new ArrayList<>();
150
151    private SourceEditor sources;
152    private JCheckBox sortMenu;
153
154    /**
155     * Registers a new additional preset source provider.
156     * @param provider The preset source provider
157     * @return {@code true}, if the provider has been added, {@code false} otherwise
158     */
159    public static boolean registerSourceProvider(SourceProvider provider) {
160        if (provider != null)
161            return presetSourceProviders.add(provider);
162        return false;
163    }
164
165    private final ValidationListener validationListener = new TaggingPresetValidationListener();
166
167    @Override
168    public void addGui(PreferenceTabbedPane gui) {
169        sortMenu = new JCheckBox(tr("Sort presets menu"),
170                Main.pref.getBoolean("taggingpreset.sortmenu", false));
171
172        final JPanel panel = new JPanel(new GridBagLayout());
173        panel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
174        panel.add(sortMenu, GBC.eol().insets(5, 5, 5, 0));
175        sources = new TaggingPresetSourceEditor();
176        panel.add(sources, GBC.eol().fill(GBC.BOTH));
177        final MapPreference mapPref = gui.getMapPreference();
178        mapPref.addSubTab(this, tr("Tagging Presets"), panel);
179        sources.deferLoading(mapPref, panel);
180        gui.addValidationListener(validationListener);
181    }
182
183    static class TaggingPresetSourceEditor extends SourceEditor {
184
185        private static final String iconpref = "taggingpreset.icon.sources";
186
187        TaggingPresetSourceEditor() {
188            super(SourceType.TAGGING_PRESET, Main.getJOSMWebsite()+"/presets", presetSourceProviders, true);
189        }
190
191        @Override
192        public Collection<? extends SourceEntry> getInitialSourcesList() {
193            return PresetPrefHelper.INSTANCE.get();
194        }
195
196        @Override
197        public boolean finish() {
198            List<SourceEntry> activeStyles = activeSourcesModel.getSources();
199
200            boolean changed = PresetPrefHelper.INSTANCE.put(activeStyles);
201
202            if (tblIconPaths != null) {
203                List<String> iconPaths = iconPathsModel.getIconPaths();
204
205                if (!iconPaths.isEmpty()) {
206                    if (Main.pref.putCollection(iconpref, iconPaths)) {
207                        changed = true;
208                    }
209                } else if (Main.pref.putCollection(iconpref, null)) {
210                    changed = true;
211                }
212            }
213            return changed;
214        }
215
216        @Override
217        public Collection<ExtendedSourceEntry> getDefault() {
218            return PresetPrefHelper.INSTANCE.getDefault();
219        }
220
221        @Override
222        public Collection<String> getInitialIconPathsList() {
223            return Main.pref.getCollection(iconpref, null);
224        }
225
226        @Override
227        public String getStr(I18nString ident) {
228            switch (ident) {
229            case AVAILABLE_SOURCES:
230                return tr("Available presets:");
231            case ACTIVE_SOURCES:
232                return tr("Active presets:");
233            case NEW_SOURCE_ENTRY_TOOLTIP:
234                return tr("Add a new preset by entering filename or URL");
235            case NEW_SOURCE_ENTRY:
236                return tr("New preset entry:");
237            case REMOVE_SOURCE_TOOLTIP:
238                return tr("Remove the selected presets from the list of active presets");
239            case EDIT_SOURCE_TOOLTIP:
240                return tr("Edit the filename or URL for the selected active preset");
241            case ACTIVATE_TOOLTIP:
242                return tr("Add the selected available presets to the list of active presets");
243            case RELOAD_ALL_AVAILABLE:
244                return marktr("Reloads the list of available presets from ''{0}''");
245            case LOADING_SOURCES_FROM:
246                return marktr("Loading preset sources from ''{0}''");
247            case FAILED_TO_LOAD_SOURCES_FROM:
248                return marktr("<html>Failed to load the list of preset sources from<br>"
249                        + "''{0}''.<br>"
250                        + "<br>"
251                        + "Details (untranslated):<br>{1}</html>");
252            case FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC:
253                return "/Preferences/Presets#FailedToLoadPresetSources";
254            case ILLEGAL_FORMAT_OF_ENTRY:
255                return marktr("Warning: illegal format of entry in preset list ''{0}''. Got ''{1}''");
256            default: throw new AssertionError();
257            }
258        }
259    }
260
261    @Override
262    public boolean ok() {
263        boolean restart = Main.pref.put("taggingpreset.sortmenu", sortMenu.getSelectedObjects() != null);
264        restart |= sources.finish();
265
266        return restart;
267    }
268
269    /**
270     * Helper class for tagging presets preferences.
271     */
272    public static class PresetPrefHelper extends SourceEditor.SourcePrefHelper {
273
274        /**
275         * The unique instance.
276         */
277        public static final PresetPrefHelper INSTANCE = new PresetPrefHelper();
278
279        /**
280         * Constructs a new {@code PresetPrefHelper}.
281         */
282        public PresetPrefHelper() {
283            super("taggingpreset.entries");
284        }
285
286        @Override
287        public Collection<ExtendedSourceEntry> getDefault() {
288            ExtendedSourceEntry i = new ExtendedSourceEntry("defaultpresets.xml", "resource://data/defaultpresets.xml");
289            i.title = tr("Internal Preset");
290            i.description = tr("The default preset for JOSM");
291            return Collections.singletonList(i);
292        }
293
294        @Override
295        public Map<String, String> serialize(SourceEntry entry) {
296            Map<String, String> res = new HashMap<>();
297            res.put("url", entry.url);
298            res.put("title", entry.title == null ? "" : entry.title);
299            return res;
300        }
301
302        @Override
303        public SourceEntry deserialize(Map<String, String> s) {
304            return new SourceEntry(s.get("url"), null, s.get("title"), true);
305        }
306    }
307
308    @Override
309    public boolean isExpert() {
310        return false;
311    }
312
313    @Override
314    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
315        return gui.getMapPreference();
316    }
317}