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.JMenu;
020import javax.swing.JMenuItem;
021import javax.swing.JOptionPane;
022import javax.swing.JPanel;
023import javax.swing.JSeparator;
024import javax.swing.event.ChangeEvent;
025import javax.swing.event.ChangeListener;
026
027import org.openstreetmap.josm.Main;
028import org.openstreetmap.josm.gui.ExtendedDialog;
029import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
030import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
031import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
032import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane.ValidationListener;
033import org.openstreetmap.josm.gui.preferences.SourceEditor;
034import org.openstreetmap.josm.gui.preferences.SourceEditor.ExtendedSourceEntry;
035import org.openstreetmap.josm.gui.preferences.SourceEntry;
036import org.openstreetmap.josm.gui.preferences.SourceProvider;
037import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
038import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
039import org.openstreetmap.josm.gui.tagging.TaggingPreset;
040import org.openstreetmap.josm.gui.tagging.TaggingPresetMenu;
041import org.openstreetmap.josm.gui.tagging.TaggingPresetReader;
042import org.openstreetmap.josm.gui.tagging.TaggingPresetSeparator;
043import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionManager;
044import org.openstreetmap.josm.tools.GBC;
045import org.xml.sax.SAXException;
046import org.xml.sax.SAXParseException;
047
048public final class TaggingPresetPreference implements SubPreferenceSetting {
049
050    public static class Factory implements PreferenceSettingFactory {
051        @Override
052        public PreferenceSetting createPreferenceSetting() {
053            return new TaggingPresetPreference();
054        }
055    }
056
057    private TaggingPresetPreference() {
058        super();
059    }
060
061    private static final List<SourceProvider> presetSourceProviders = new ArrayList<SourceProvider>();
062    public static Collection<TaggingPreset> taggingPresets;
063    private SourceEditor sources;
064    private JCheckBox sortMenu;
065
066    public static final boolean registerSourceProvider(SourceProvider provider) {
067        if (provider != null)
068            return presetSourceProviders.add(provider);
069        return false;
070    }
071
072    private ValidationListener validationListener = new ValidationListener() {
073        @Override
074        public boolean validatePreferences() {
075            if (sources.hasActiveSourcesChanged()) {
076                List<Integer> sourcesToRemove = new ArrayList<Integer>();
077                int i = -1;
078                SOURCES:
079                    for (SourceEntry source: sources.getActiveSources()) {
080                        i++;
081                        boolean canLoad = false;
082                        try {
083                            TaggingPresetReader.readAll(source.url, false);
084                            canLoad = true;
085                        } catch (IOException e) {
086                            Main.warn(tr("Could not read tagging preset source: {0}", source));
087                            ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Error"),
088                                    new String[] {tr("Yes"), tr("No"), tr("Cancel")});
089                            ed.setContent(tr("Could not read tagging preset source: {0}\nDo you want to keep it?", source));
090                            switch (ed.showDialog().getValue()) {
091                            case 1:
092                                continue SOURCES;
093                            case 2:
094                                sourcesToRemove.add(i);
095                                continue SOURCES;
096                            default:
097                                return false;
098                            }
099                        } catch (SAXException e) {
100                            // We will handle this in step with validation
101                        }
102
103                        String errorMessage = null;
104
105                        try {
106                            TaggingPresetReader.readAll(source.url, true);
107                        } catch (IOException e) {
108                            // Should not happen, but at least show message
109                            String msg = tr("Could not read tagging preset source {0}", source);
110                            Main.error(msg);
111                            JOptionPane.showMessageDialog(Main.parent, msg);
112                            return false;
113                        } catch (SAXParseException e) {
114                            if (canLoad) {
115                                errorMessage = tr("<html>Tagging preset source {0} can be loaded but it contains errors. " +
116                                        "Do you really want to use it?<br><br><table width=600>Error is: [{1}:{2}] {3}</table></html>",
117                                        source, e.getLineNumber(), e.getColumnNumber(), e.getMessage());
118                            } else {
119                                errorMessage = tr("<html>Unable to parse tagging preset source: {0}. " +
120                                        "Do you really want to use it?<br><br><table width=400>Error is: [{1}:{2}] {3}</table></html>",
121                                        source, e.getLineNumber(), e.getColumnNumber(), e.getMessage());
122                            }
123                        } catch (SAXException e) {
124                            if (canLoad) {
125                                errorMessage = tr("<html>Tagging preset source {0} can be loaded but it contains errors. " +
126                                        "Do you really want to use it?<br><br><table width=600>Error is: {1}</table></html>",
127                                        source,  e.getMessage());
128                            } else {
129                                errorMessage = tr("<html>Unable to parse tagging preset source: {0}. " +
130                                        "Do you really want to use it?<br><br><table width=600>Error is: {1}</table></html>",
131                                        source, e.getMessage());
132                            }
133
134                        }
135
136                        if (errorMessage != null) {
137                            Main.error(errorMessage);
138                            int result = JOptionPane.showConfirmDialog(Main.parent, new JLabel(errorMessage), tr("Error"),
139                                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.ERROR_MESSAGE);
140
141                            switch (result) {
142                            case JOptionPane.YES_OPTION:
143                                continue SOURCES;
144                            case JOptionPane.NO_OPTION:
145                                sourcesToRemove.add(i);
146                                continue SOURCES;
147                            default:
148                                return false;
149                            }
150                        }
151                    }
152                sources.removeSources(sourcesToRemove);
153                return true;
154            }  else
155                return true;
156        }
157    };
158
159    @Override
160    public void addGui(final PreferenceTabbedPane gui) {
161        sortMenu = new JCheckBox(tr("Sort presets menu"),
162                Main.pref.getBoolean("taggingpreset.sortmenu", false));
163
164        final JPanel panel = new JPanel(new GridBagLayout());
165        panel.setBorder(BorderFactory.createEmptyBorder( 0, 0, 0, 0 ));
166        panel.add(sortMenu, GBC.eol().insets(5,5,5,0));
167        sources = new TaggingPresetSourceEditor();
168        panel.add(sources, GBC.eol().fill(GBC.BOTH));
169        gui.getMapPreference().addSubTab(this, tr("Tagging Presets"), panel);
170
171        // this defers loading of tagging preset sources to the first time the tab
172        // with the tagging presets is selected by the user
173        //
174        gui.getMapPreference().getTabPane().addChangeListener(
175                new ChangeListener() {
176                    @Override
177                    public void stateChanged(ChangeEvent e) {
178                        if (gui.getMapPreference().getTabPane().getSelectedComponent() == panel) {
179                            sources.initiallyLoadAvailableSources();
180                        }
181                    }
182                }
183                );
184        gui.addValidationListener(validationListener);
185    }
186
187    static class TaggingPresetSourceEditor extends SourceEditor {
188
189        private static final String iconpref = "taggingpreset.icon.sources";
190
191        public TaggingPresetSourceEditor() {
192            super(false, Main.JOSM_WEBSITE+"/presets", presetSourceProviders);
193        }
194
195        @Override
196        public Collection<? extends SourceEntry> getInitialSourcesList() {
197            return PresetPrefHelper.INSTANCE.get();
198        }
199
200        @Override
201        public boolean finish() {
202            List<SourceEntry> activeStyles = activeSourcesModel.getSources();
203
204            boolean changed = PresetPrefHelper.INSTANCE.put(activeStyles);
205
206            if (tblIconPaths != null) {
207                List<String> iconPaths = iconPathsModel.getIconPaths();
208
209                if (!iconPaths.isEmpty()) {
210                    if (Main.pref.putCollection(iconpref, iconPaths)) {
211                        changed = true;
212                    }
213                } else if (Main.pref.putCollection(iconpref, null)) {
214                    changed = true;
215                }
216            }
217            return changed;
218        }
219
220        @Override
221        public Collection<ExtendedSourceEntry> getDefault() {
222            return PresetPrefHelper.INSTANCE.getDefault();
223        }
224
225        @Override
226        public Collection<String> getInitialIconPathsList() {
227            return Main.pref.getCollection(iconpref, null);
228        }
229
230        @Override
231        public String getStr(I18nString ident) {
232            switch (ident) {
233            case AVAILABLE_SOURCES:
234                return tr("Available presets:");
235            case ACTIVE_SOURCES:
236                return tr("Active presets:");
237            case NEW_SOURCE_ENTRY_TOOLTIP:
238                return tr("Add a new preset by entering filename or URL");
239            case NEW_SOURCE_ENTRY:
240                return tr("New preset entry:");
241            case REMOVE_SOURCE_TOOLTIP:
242                return tr("Remove the selected presets from the list of active presets");
243            case EDIT_SOURCE_TOOLTIP:
244                return tr("Edit the filename or URL for the selected active preset");
245            case ACTIVATE_TOOLTIP:
246                return tr("Add the selected available presets to the list of active presets");
247            case RELOAD_ALL_AVAILABLE:
248                return marktr("Reloads the list of available presets from ''{0}''");
249            case LOADING_SOURCES_FROM:
250                return marktr("Loading preset sources from ''{0}''");
251            case FAILED_TO_LOAD_SOURCES_FROM:
252                return marktr("<html>Failed to load the list of preset sources from<br>"
253                        + "''{0}''.<br>"
254                        + "<br>"
255                        + "Details (untranslated):<br>{1}</html>");
256            case FAILED_TO_LOAD_SOURCES_FROM_HELP_TOPIC:
257                return "/Preferences/Presets#FailedToLoadPresetSources";
258            case ILLEGAL_FORMAT_OF_ENTRY:
259                return marktr("Warning: illegal format of entry in preset list ''{0}''. Got ''{1}''");
260            default: throw new AssertionError();
261            }
262        }
263    }
264
265    @Override
266    public boolean ok() {
267        boolean restart = Main.pref.put("taggingpreset.sortmenu", sortMenu.getSelectedObjects() != null);
268        restart |= sources.finish();
269
270        return restart;
271    }
272
273    /**
274     * Initialize the tagging presets (load and may display error)
275     */
276    public static void initialize() {
277        taggingPresets = TaggingPresetReader.readFromPreferences(false);
278        for (TaggingPreset tp: taggingPresets) {
279            if (!(tp instanceof TaggingPresetSeparator)) {
280                Main.toolbar.register(tp);
281            }
282        }
283        if (taggingPresets.isEmpty()) {
284            Main.main.menu.presetsMenu.setVisible(false);
285        } else {
286            AutoCompletionManager.cachePresets(taggingPresets);
287            HashMap<TaggingPresetMenu,JMenu> submenus = new HashMap<TaggingPresetMenu,JMenu>();
288            for (final TaggingPreset p : taggingPresets) {
289                JMenu m = p.group != null ? submenus.get(p.group) : Main.main.menu.presetsMenu;
290                if (p instanceof TaggingPresetSeparator) {
291                    m.add(new JSeparator());
292                } else if (p instanceof TaggingPresetMenu) {
293                    JMenu submenu = new JMenu(p);
294                    submenu.setText(p.getLocaleName());
295                    ((TaggingPresetMenu)p).menu = submenu;
296                    submenus.put((TaggingPresetMenu)p, submenu);
297                    m.add(submenu);
298                } else {
299                    JMenuItem mi = new JMenuItem(p);
300                    mi.setText(p.getLocaleName());
301                    m.add(mi);
302                }
303            }
304        }
305        if (Main.pref.getBoolean("taggingpreset.sortmenu")) {
306            TaggingPresetMenu.sortMenu(Main.main.menu.presetsMenu);
307        }
308    }
309
310    public static class PresetPrefHelper extends SourceEditor.SourcePrefHelper {
311
312        /**
313         * The unique instance.
314         */
315        public final static PresetPrefHelper INSTANCE = new PresetPrefHelper();
316
317        /**
318         * Constructs a new {@code PresetPrefHelper}.
319         */
320        public PresetPrefHelper() {
321            super("taggingpreset.entries");
322        }
323
324        @Override
325        public Collection<ExtendedSourceEntry> getDefault() {
326            ExtendedSourceEntry i = new ExtendedSourceEntry("defaultpresets.xml", "resource://data/defaultpresets.xml");
327            i.title = tr("Internal Preset");
328            i.description = tr("The default preset for JOSM");
329            return Collections.singletonList(i);
330        }
331
332        @Override
333        public Map<String, String> serialize(SourceEntry entry) {
334            Map<String, String> res = new HashMap<String, String>();
335            res.put("url", entry.url);
336            res.put("title", entry.title == null ? "" : entry.title);
337            return res;
338        }
339
340        @Override
341        public SourceEntry deserialize(Map<String, String> s) {
342            return new SourceEntry(s.get("url"), null, s.get("title"), true);
343        }
344    }
345
346    @Override
347    public boolean isExpert() {
348        return false;
349    }
350
351    @Override
352    public TabPreferenceSetting getTabPreferenceSetting(final PreferenceTabbedPane gui) {
353        return gui.getMapPreference();
354    }
355}