001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.advanced;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Dimension;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.io.File;
011import java.io.IOException;
012import java.util.ArrayList;
013import java.util.Collections;
014import java.util.Comparator;
015import java.util.LinkedHashMap;
016import java.util.List;
017import java.util.Map;
018import java.util.Map.Entry;
019import java.util.Objects;
020
021import javax.swing.AbstractAction;
022import javax.swing.Box;
023import javax.swing.JButton;
024import javax.swing.JFileChooser;
025import javax.swing.JLabel;
026import javax.swing.JMenu;
027import javax.swing.JOptionPane;
028import javax.swing.JPanel;
029import javax.swing.JPopupMenu;
030import javax.swing.JScrollPane;
031import javax.swing.event.DocumentEvent;
032import javax.swing.event.DocumentListener;
033import javax.swing.event.MenuEvent;
034import javax.swing.event.MenuListener;
035import javax.swing.filechooser.FileFilter;
036
037import org.openstreetmap.josm.Main;
038import org.openstreetmap.josm.actions.DiskAccessAction;
039import org.openstreetmap.josm.data.CustomConfigurator;
040import org.openstreetmap.josm.data.Preferences;
041import org.openstreetmap.josm.data.Preferences.Setting;
042import org.openstreetmap.josm.gui.actionsupport.LogShowDialog;
043import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
044import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
045import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
046import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
047import org.openstreetmap.josm.gui.util.GuiHelper;
048import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
049import org.openstreetmap.josm.gui.widgets.JosmTextField;
050import org.openstreetmap.josm.tools.GBC;
051
052/**
053 * Advanced preferences, allowing to set preference entries directly.
054 */
055public final class AdvancedPreference extends DefaultTabPreferenceSetting {
056
057    /**
058     * Factory used to create a new {@code AdvancedPreference}.
059     */
060    public static class Factory implements PreferenceSettingFactory {
061        @Override
062        public PreferenceSetting createPreferenceSetting() {
063            return new AdvancedPreference();
064        }
065    }
066
067    private AdvancedPreference() {
068        super("advanced", tr("Advanced Preferences"), tr("Setting Preference entries directly. Use with caution!"));
069    }
070
071    @Override
072    public boolean isExpert() {
073        return true;
074    }
075
076    protected List<PrefEntry> allData;
077    protected List<PrefEntry> displayData = new ArrayList<>();
078    protected JosmTextField txtFilter;
079    protected PreferencesTable table;
080
081    @Override
082    public void addGui(final PreferenceTabbedPane gui) {
083        JPanel p = gui.createPreferenceTab(this);
084
085        txtFilter = new JosmTextField();
086        JLabel lbFilter = new JLabel(tr("Search: "));
087        lbFilter.setLabelFor(txtFilter);
088        p.add(lbFilter);
089        p.add(txtFilter, GBC.eol().fill(GBC.HORIZONTAL));
090        txtFilter.getDocument().addDocumentListener(new DocumentListener(){
091            @Override public void changedUpdate(DocumentEvent e) {
092                action();
093            }
094            @Override public void insertUpdate(DocumentEvent e) {
095                action();
096            }
097            @Override public void removeUpdate(DocumentEvent e) {
098                action();
099            }
100            private void action() {
101                applyFilter();
102            }
103        });
104        readPreferences(Main.pref);
105
106        applyFilter();
107        table = new PreferencesTable(displayData);
108        JScrollPane scroll = new JScrollPane(table);
109        p.add(scroll, GBC.eol().fill(GBC.BOTH));
110        scroll.setPreferredSize(new Dimension(400,200));
111
112        JButton add = new JButton(tr("Add"));
113        p.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
114        p.add(add, GBC.std().insets(0,5,0,0));
115        add.addActionListener(new ActionListener(){
116            @Override public void actionPerformed(ActionEvent e) {
117                PrefEntry pe = table.addPreference(gui);
118                if (pe!=null) {
119                    allData.add(pe);
120                    Collections.sort(allData);
121                    applyFilter();
122                }
123            }
124        });
125
126        JButton edit = new JButton(tr("Edit"));
127        p.add(edit, GBC.std().insets(5,5,5,0));
128        edit.addActionListener(new ActionListener(){
129            @Override public void actionPerformed(ActionEvent e) {
130                boolean ok = table.editPreference(gui);
131                if (ok) applyFilter();
132            }
133        });
134
135        JButton reset = new JButton(tr("Reset"));
136        p.add(reset, GBC.std().insets(0,5,0,0));
137        reset.addActionListener(new ActionListener(){
138            @Override public void actionPerformed(ActionEvent e) {
139                table.resetPreferences(gui);
140            }
141        });
142
143        JButton read = new JButton(tr("Read from file"));
144        p.add(read, GBC.std().insets(5,5,0,0));
145        read.addActionListener(new ActionListener(){
146            @Override public void actionPerformed(ActionEvent e) {
147                readPreferencesFromXML();
148            }
149        });
150
151        JButton export = new JButton(tr("Export selected items"));
152        p.add(export, GBC.std().insets(5,5,0,0));
153        export.addActionListener(new ActionListener(){
154            @Override public void actionPerformed(ActionEvent e) {
155                exportSelectedToXML();
156            }
157        });
158
159        final JButton more = new JButton(tr("More..."));
160        p.add(more, GBC.std().insets(5,5,0,0));
161        more.addActionListener(new ActionListener() {
162            JPopupMenu menu = buildPopupMenu();
163            @Override public void actionPerformed(ActionEvent ev) {
164                menu.show(more, 0, 0);
165            }
166        });
167    }
168
169    private void readPreferences(Preferences tmpPrefs) {
170        Map<String, Setting<?>> loaded;
171        Map<String, Setting<?>> orig = Main.pref.getAllSettings();
172        Map<String, Setting<?>> defaults = tmpPrefs.getAllDefaults();
173        orig.remove("osm-server.password");
174        defaults.remove("osm-server.password");
175        if (tmpPrefs != Main.pref) {
176            loaded = tmpPrefs.getAllSettings();
177            // plugins preference keys may be changed directly later, after plugins are downloaded
178            // so we do not want to show it in the table as "changed" now
179            Setting<?> pluginSetting = orig.get("plugins");
180            if (pluginSetting!=null) {
181                loaded.put("plugins", pluginSetting);
182            }
183        } else {
184            loaded = orig;
185        }
186        allData = prepareData(loaded, orig, defaults);
187    }
188
189    private File[] askUserForCustomSettingsFiles(boolean saveFileFlag, String title) {
190        FileFilter filter = new FileFilter() {
191            @Override
192            public boolean accept(File f) {
193                return f.isDirectory() || f.getName().toLowerCase().endsWith(".xml");
194            }
195            @Override
196            public String getDescription() {
197                return tr("JOSM custom settings files (*.xml)");
198            }
199        };
200        AbstractFileChooser fc = DiskAccessAction.createAndOpenFileChooser(!saveFileFlag, !saveFileFlag, title, filter,
201                JFileChooser.FILES_ONLY, "customsettings.lastDirectory");
202        if (fc != null) {
203            File[] sel = fc.isMultiSelectionEnabled() ? fc.getSelectedFiles() : (new File[]{fc.getSelectedFile()});
204            if (sel.length==1 && !sel[0].getName().contains(".")) sel[0]=new File(sel[0].getAbsolutePath()+".xml");
205            return sel;
206        }
207        return new File[0];
208    }
209
210    private void exportSelectedToXML() {
211        List<String> keys = new ArrayList<>();
212        boolean hasLists = false;
213
214        for (PrefEntry p: table.getSelectedItems()) {
215            // preferences with default values are not saved
216            if (!(p.getValue() instanceof Preferences.StringSetting)) {
217                hasLists = true; // => append and replace differs
218            }
219            if (!p.isDefault()) {
220                keys.add(p.getKey());
221            }
222        }
223
224        if (keys.isEmpty()) {
225            JOptionPane.showMessageDialog(Main.parent,
226                    tr("Please select some preference keys not marked as default"), tr("Warning"), JOptionPane.WARNING_MESSAGE);
227            return;
228        }
229
230        File[] files = askUserForCustomSettingsFiles(true, tr("Export preferences keys to JOSM customization file"));
231        if (files.length == 0) {
232            return;
233        }
234
235        int answer = 0;
236        if (hasLists) {
237            answer = JOptionPane.showOptionDialog(
238                    Main.parent, tr("What to do with preference lists when this file is to be imported?"), tr("Question"),
239                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
240                    new String[]{tr("Append preferences from file to existing values"), tr("Replace existing values")}, 0);
241        }
242        CustomConfigurator.exportPreferencesKeysToFile(files[0].getAbsolutePath(), answer == 0, keys);
243    }
244
245    private void readPreferencesFromXML() {
246        File[] files = askUserForCustomSettingsFiles(false, tr("Open JOSM customization file"));
247        if (files.length == 0) return;
248
249        Preferences tmpPrefs = CustomConfigurator.clonePreferences(Main.pref);
250
251        StringBuilder log = new StringBuilder();
252        log.append("<html>");
253        for (File f : files) {
254            CustomConfigurator.readXML(f, tmpPrefs);
255            log.append(CustomConfigurator.getLog());
256        }
257        log.append("</html>");
258        String msg = log.toString().replace("\n", "<br/>");
259
260        new LogShowDialog(tr("Import log"), tr("<html>Here is file import summary. <br/>"
261                + "You can reject preferences changes by pressing \"Cancel\" in preferences dialog <br/>"
262                + "To activate some changes JOSM restart may be needed.</html>"), msg).showDialog();
263
264        readPreferences(tmpPrefs);
265        // sorting after modification - first modified, then non-default, then default entries
266        Collections.sort(allData, customComparator);
267        applyFilter();
268    }
269
270    private Comparator<PrefEntry> customComparator = new Comparator<PrefEntry>() {
271        @Override
272        public int compare(PrefEntry o1, PrefEntry o2) {
273            if (o1.isChanged() && !o2.isChanged()) return -1;
274            if (o2.isChanged() && !o1.isChanged()) return 1;
275            if (!(o1.isDefault()) && o2.isDefault()) return -1;
276            if (!(o2.isDefault()) && o1.isDefault()) return 1;
277            return o1.compareTo(o2);
278        }
279    };
280
281    private List<PrefEntry> prepareData(Map<String, Setting<?>> loaded, Map<String, Setting<?>> orig, Map<String, Setting<?>> defaults) {
282        List<PrefEntry> data = new ArrayList<>();
283        for (Entry<String, Setting<?>> e : loaded.entrySet()) {
284            Setting<?> value = e.getValue();
285            Setting<?> old = orig.get(e.getKey());
286            Setting<?> def = defaults.get(e.getKey());
287            if (def == null) {
288                def = value.getNullInstance();
289            }
290            PrefEntry en = new PrefEntry(e.getKey(), value, def, false);
291            // after changes we have nondefault value. Value is changed if is not equal to old value
292            if (!Objects.equals(old, value)) {
293                en.markAsChanged();
294            }
295            data.add(en);
296        }
297        for (Entry<String, Setting<?>> e : defaults.entrySet()) {
298            if (!loaded.containsKey(e.getKey())) {
299                PrefEntry en = new PrefEntry(e.getKey(), e.getValue(), e.getValue(), true);
300                // after changes we have default value. So, value is changed if old value is not default
301                Setting<?> old = orig.get(e.getKey());
302                if (old != null) {
303                    en.markAsChanged();
304                }
305                data.add(en);
306            }
307        }
308        Collections.sort(data);
309        displayData.clear();
310        displayData.addAll(data);
311        return data;
312    }
313
314    Map<String,String> profileTypes = new LinkedHashMap<>();
315
316    private JPopupMenu buildPopupMenu() {
317        JPopupMenu menu = new JPopupMenu();
318        profileTypes.put(marktr("shortcut"), "shortcut\\..*");
319        profileTypes.put(marktr("color"), "color\\..*");
320        profileTypes.put(marktr("toolbar"), "toolbar.*");
321        profileTypes.put(marktr("imagery"), "imagery.*");
322
323        for (Entry<String,String> e: profileTypes.entrySet()) {
324            menu.add(new ExportProfileAction(Main.pref, e.getKey(), e.getValue()));
325        }
326
327        menu.addSeparator();
328        menu.add(getProfileMenu());
329        menu.addSeparator();
330        menu.add(new AbstractAction(tr("Reset preferences")) {
331            @Override
332            public void actionPerformed(ActionEvent ae) {
333                if (!GuiHelper.warnUser(tr("Reset preferences"),
334                        "<html>"+
335                        tr("You are about to clear all preferences to their default values<br />"+
336                        "All your settings will be deleted: plugins, imagery, filters, toolbar buttons, keyboard, etc. <br />"+
337                        "Are you sure you want to continue?")
338                        +"</html>", null, "")) {
339                    Main.pref.resetToDefault();
340                    try {
341                        Main.pref.save();
342                    } catch (IOException e) {
343                        Main.warn("IOException while saving preferences: "+e.getMessage());
344                    }
345                    readPreferences(Main.pref);
346                    applyFilter();
347                }
348            }
349        });
350        return menu;
351    }
352
353    private JMenu getProfileMenu() {
354        final JMenu p =new JMenu(tr("Load profile"));
355        p.addMenuListener(new MenuListener() {
356            @Override
357            public void menuSelected(MenuEvent me) {
358                p.removeAll();
359                for (File f: new File(".").listFiles()) {
360                   String s = f.getName();
361                   int idx = s.indexOf('_');
362                   if (idx>=0) {
363                        String t=s.substring(0,idx);
364                        if (profileTypes.containsKey(t)) {
365                            p.add(new ImportProfileAction(s, f, t));
366                        }
367                   }
368                }
369                for (File f: Main.pref.getPreferencesDirFile().listFiles()) {
370                   String s = f.getName();
371                   int idx = s.indexOf('_');
372                   if (idx>=0) {
373                        String t=s.substring(0,idx);
374                        if (profileTypes.containsKey(t)) {
375                            p.add(new ImportProfileAction(s, f, t));
376                        }
377                   }
378                }
379            }
380            @Override public void menuDeselected(MenuEvent me) { }
381            @Override public void menuCanceled(MenuEvent me) { }
382        });
383        return p;
384    }
385
386    private class ImportProfileAction extends AbstractAction {
387        private final File file;
388        private final String type;
389
390        public ImportProfileAction(String name, File file, String type) {
391            super(name);
392            this.file = file;
393            this.type = type;
394        }
395
396        @Override
397        public void actionPerformed(ActionEvent ae) {
398            Preferences tmpPrefs = CustomConfigurator.clonePreferences(Main.pref);
399            CustomConfigurator.readXML(file, tmpPrefs);
400            readPreferences(tmpPrefs);
401            String prefRegex = profileTypes.get(type);
402            // clean all the preferences from the chosen group
403            for (PrefEntry p : allData) {
404               if (p.getKey().matches(prefRegex) && !p.isDefault()) {
405                    p.reset();
406               }
407            }
408            // allow user to review the changes in table
409            Collections.sort(allData, customComparator);
410            applyFilter();
411        }
412    }
413
414    private void applyFilter() {
415        displayData.clear();
416        for (PrefEntry e : allData) {
417            String prefKey = e.getKey();
418            Setting<?> valueSetting = e.getValue();
419            String prefValue = valueSetting.getValue() == null ? "" : valueSetting.getValue().toString();
420
421            String[] input = txtFilter.getText().split("\\s+");
422            boolean canHas = true;
423
424            // Make 'wmsplugin cache' search for e.g. 'cache.wmsplugin'
425            final String prefKeyLower = prefKey.toLowerCase();
426            final String prefValueLower = prefValue.toLowerCase();
427            for (String bit : input) {
428                bit = bit.toLowerCase();
429                if (!prefKeyLower.contains(bit) && !prefValueLower.contains(bit)) {
430                    canHas = false;
431                    break;
432                }
433            }
434            if (canHas) {
435                displayData.add(e);
436            }
437        }
438        if (table!=null) table.fireDataChanged();
439    }
440
441    @Override
442    public boolean ok() {
443        for (PrefEntry e : allData) {
444            if (e.isChanged()) {
445                Main.pref.putSetting(e.getKey(), e.getValue().getValue() == null ? null : e.getValue());
446            }
447        }
448        return false;
449    }
450}