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