source: josm/trunk/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java@ 15779

Last change on this file since 15779 was 15779, checked in by simon04, 4 years ago

fix #14197 - Advanced Preferences: support search keywords "modified", "default", "changed"

  • Property svn:eol-style set to native
File size: 19.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.preferences.advanced;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.Dimension;
8import java.awt.event.ActionEvent;
9import java.awt.event.ActionListener;
10import java.io.File;
11import java.io.IOException;
12import java.nio.file.InvalidPathException;
13import java.util.ArrayList;
14import java.util.Collections;
15import java.util.Comparator;
16import java.util.LinkedHashMap;
17import java.util.List;
18import java.util.Locale;
19import java.util.Map;
20import java.util.Map.Entry;
21import java.util.Objects;
22import java.util.regex.Pattern;
23
24import javax.swing.AbstractAction;
25import javax.swing.Box;
26import javax.swing.JButton;
27import javax.swing.JFileChooser;
28import javax.swing.JLabel;
29import javax.swing.JMenu;
30import javax.swing.JOptionPane;
31import javax.swing.JPanel;
32import javax.swing.JPopupMenu;
33import javax.swing.JScrollPane;
34import javax.swing.event.DocumentEvent;
35import javax.swing.event.DocumentListener;
36import javax.swing.event.MenuEvent;
37import javax.swing.event.MenuListener;
38import javax.swing.filechooser.FileFilter;
39
40import org.openstreetmap.josm.actions.DiskAccessAction;
41import org.openstreetmap.josm.data.Preferences;
42import org.openstreetmap.josm.data.PreferencesUtils;
43import org.openstreetmap.josm.data.osm.DataSet;
44import org.openstreetmap.josm.gui.MainApplication;
45import org.openstreetmap.josm.gui.dialogs.LogShowDialog;
46import org.openstreetmap.josm.gui.help.HelpUtil;
47import org.openstreetmap.josm.gui.io.CustomConfigurator;
48import org.openstreetmap.josm.gui.layer.MainLayerManager;
49import org.openstreetmap.josm.gui.layer.OsmDataLayer;
50import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
51import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
52import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
53import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
54import org.openstreetmap.josm.gui.util.GuiHelper;
55import org.openstreetmap.josm.gui.widgets.AbstractFileChooser;
56import org.openstreetmap.josm.gui.widgets.JosmTextField;
57import org.openstreetmap.josm.spi.preferences.Config;
58import org.openstreetmap.josm.spi.preferences.Setting;
59import org.openstreetmap.josm.spi.preferences.StringSetting;
60import org.openstreetmap.josm.tools.GBC;
61import org.openstreetmap.josm.tools.Logging;
62import org.openstreetmap.josm.tools.Territories;
63import org.openstreetmap.josm.tools.Utils;
64
65/**
66 * Advanced preferences, allowing to set preference entries directly.
67 */
68public final class AdvancedPreference extends DefaultTabPreferenceSetting {
69
70 /**
71 * Factory used to create a new {@code AdvancedPreference}.
72 */
73 public static class Factory implements PreferenceSettingFactory {
74 @Override
75 public PreferenceSetting createPreferenceSetting() {
76 return new AdvancedPreference();
77 }
78 }
79
80 private static class UnclearableOsmDataLayer extends OsmDataLayer {
81 UnclearableOsmDataLayer(DataSet data, String name) {
82 super(data, name, null);
83 }
84
85 @Override
86 public void clear() {
87 // Do nothing
88 }
89 }
90
91 private static final class EditBoundariesAction extends AbstractAction {
92 EditBoundariesAction() {
93 super(tr("Edit boundaries"));
94 }
95
96 @Override
97 public void actionPerformed(ActionEvent ae) {
98 DataSet dataSet = Territories.getOriginalDataSet();
99 MainLayerManager layerManager = MainApplication.getLayerManager();
100 if (layerManager.getLayersOfType(OsmDataLayer.class).stream().noneMatch(l -> dataSet.equals(l.getDataSet()))) {
101 layerManager.addLayer(new UnclearableOsmDataLayer(dataSet, tr("Internal JOSM boundaries")));
102 }
103 }
104 }
105
106 private final class ResetPreferencesAction extends AbstractAction {
107 ResetPreferencesAction() {
108 super(tr("Reset preferences"));
109 }
110
111 @Override
112 public void actionPerformed(ActionEvent ae) {
113 if (!GuiHelper.warnUser(tr("Reset preferences"),
114 "<html>"+
115 tr("You are about to clear all preferences to their default values<br />"+
116 "All your settings will be deleted: plugins, imagery, filters, toolbar buttons, keyboard, etc. <br />"+
117 "Are you sure you want to continue?")
118 +"</html>", null, "")) {
119 Preferences.main().resetToDefault();
120 try {
121 Preferences.main().save();
122 } catch (IOException | InvalidPathException e) {
123 Logging.log(Logging.LEVEL_WARN, "Exception while saving preferences:", e);
124 }
125 readPreferences(Preferences.main());
126 applyFilter();
127 }
128 }
129 }
130
131 private List<PrefEntry> allData;
132 private final List<PrefEntry> displayData = new ArrayList<>();
133 private JosmTextField txtFilter;
134 private PreferencesTable table;
135
136 private final Map<String, String> profileTypes = new LinkedHashMap<>();
137
138 private final Comparator<PrefEntry> customComparator = (o1, o2) -> {
139 if (o1.isChanged() && !o2.isChanged())
140 return -1;
141 if (o2.isChanged() && !o1.isChanged())
142 return 1;
143 if (!(o1.isDefault()) && o2.isDefault())
144 return -1;
145 if (!(o2.isDefault()) && o1.isDefault())
146 return 1;
147 return o1.compareTo(o2);
148 };
149
150 private AdvancedPreference() {
151 super(/* ICON(preferences/) */ "advanced", tr("Advanced Preferences"), tr("Setting Preference entries directly. Use with caution!"));
152 }
153
154 @Override
155 public boolean isExpert() {
156 return true;
157 }
158
159 @Override
160 public void addGui(final PreferenceTabbedPane gui) {
161 JPanel p = gui.createPreferenceTab(this);
162
163 txtFilter = new JosmTextField();
164 JLabel lbFilter = new JLabel(tr("Search:"));
165 lbFilter.setLabelFor(txtFilter);
166 p.add(lbFilter);
167 p.add(txtFilter, GBC.eol().fill(GBC.HORIZONTAL));
168 txtFilter.getDocument().addDocumentListener(new DocumentListener() {
169 @Override
170 public void changedUpdate(DocumentEvent e) {
171 action();
172 }
173
174 @Override
175 public void insertUpdate(DocumentEvent e) {
176 action();
177 }
178
179 @Override
180 public void removeUpdate(DocumentEvent e) {
181 action();
182 }
183
184 private void action() {
185 applyFilter();
186 }
187 });
188 readPreferences(Preferences.main());
189
190 applyFilter();
191 table = new PreferencesTable(displayData);
192 JScrollPane scroll = new JScrollPane(table);
193 p.add(scroll, GBC.eol().fill(GBC.BOTH));
194 scroll.setPreferredSize(new Dimension(400, 200));
195
196 JButton add = new JButton(tr("Add"));
197 p.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
198 p.add(add, GBC.std().insets(0, 5, 0, 0));
199 add.addActionListener(e -> {
200 PrefEntry pe = table.addPreference(gui);
201 if (pe != null) {
202 allData.add(pe);
203 Collections.sort(allData);
204 applyFilter();
205 }
206 });
207
208 JButton edit = new JButton(tr("Edit"));
209 p.add(edit, GBC.std().insets(5, 5, 5, 0));
210 edit.addActionListener(e -> {
211 if (table.editPreference(gui))
212 applyFilter();
213 });
214
215 JButton reset = new JButton(tr("Reset"));
216 p.add(reset, GBC.std().insets(0, 5, 0, 0));
217 reset.addActionListener(e -> table.resetPreferences(gui));
218
219 JButton read = new JButton(tr("Read from file"));
220 p.add(read, GBC.std().insets(5, 5, 0, 0));
221 read.addActionListener(e -> readPreferencesFromXML());
222
223 JButton export = new JButton(tr("Export selected items"));
224 p.add(export, GBC.std().insets(5, 5, 0, 0));
225 export.addActionListener(e -> exportSelectedToXML());
226
227 final JButton more = new JButton(tr("More..."));
228 p.add(more, GBC.std().insets(5, 5, 0, 0));
229 more.addActionListener(new ActionListener() {
230 private JPopupMenu menu = buildPopupMenu();
231 @Override
232 public void actionPerformed(ActionEvent ev) {
233 if (more.isShowing()) {
234 menu.show(more, 0, 0);
235 }
236 }
237 });
238 }
239
240 private void readPreferences(Preferences tmpPrefs) {
241 Map<String, Setting<?>> loaded;
242 Map<String, Setting<?>> orig = Preferences.main().getAllSettings();
243 Map<String, Setting<?>> defaults = tmpPrefs.getAllDefaults();
244 orig.remove("osm-server.password");
245 defaults.remove("osm-server.password");
246 if (tmpPrefs != Preferences.main()) {
247 loaded = tmpPrefs.getAllSettings();
248 // plugins preference keys may be changed directly later, after plugins are downloaded
249 // so we do not want to show it in the table as "changed" now
250 Setting<?> pluginSetting = orig.get("plugins");
251 if (pluginSetting != null) {
252 loaded.put("plugins", pluginSetting);
253 }
254 } else {
255 loaded = orig;
256 }
257 allData = prepareData(loaded, orig, defaults);
258 }
259
260 private static File[] askUserForCustomSettingsFiles(boolean saveFileFlag, String title) {
261 FileFilter filter = new FileFilter() {
262 @Override
263 public boolean accept(File f) {
264 return f.isDirectory() || Utils.hasExtension(f, "xml");
265 }
266
267 @Override
268 public String getDescription() {
269 return tr("JOSM custom settings files (*.xml)");
270 }
271 };
272 AbstractFileChooser fc = DiskAccessAction.createAndOpenFileChooser(!saveFileFlag, !saveFileFlag, title, filter,
273 JFileChooser.FILES_ONLY, "customsettings.lastDirectory");
274 if (fc != null) {
275 File[] sel = fc.isMultiSelectionEnabled() ? fc.getSelectedFiles() : (new File[]{fc.getSelectedFile()});
276 if (sel.length == 1 && !sel[0].getName().contains("."))
277 sel[0] = new File(sel[0].getAbsolutePath()+".xml");
278 return sel;
279 }
280 return new File[0];
281 }
282
283 private void exportSelectedToXML() {
284 List<String> keys = new ArrayList<>();
285 boolean hasLists = false;
286
287 for (PrefEntry p: table.getSelectedItems()) {
288 // preferences with default values are not saved
289 if (!(p.getValue() instanceof StringSetting)) {
290 hasLists = true; // => append and replace differs
291 }
292 if (!p.isDefault()) {
293 keys.add(p.getKey());
294 }
295 }
296
297 if (keys.isEmpty()) {
298 JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
299 tr("Please select some preference keys not marked as default"), tr("Warning"), JOptionPane.WARNING_MESSAGE);
300 return;
301 }
302
303 File[] files = askUserForCustomSettingsFiles(true, tr("Export preferences keys to JOSM customization file"));
304 if (files.length == 0) {
305 return;
306 }
307
308 int answer = 0;
309 if (hasLists) {
310 answer = JOptionPane.showOptionDialog(
311 MainApplication.getMainFrame(), tr("What to do with preference lists when this file is to be imported?"), tr("Question"),
312 JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null,
313 new String[]{tr("Append preferences from file to existing values"), tr("Replace existing values")}, 0);
314 }
315 CustomConfigurator.exportPreferencesKeysToFile(files[0].getAbsolutePath(), answer == 0, keys);
316 }
317
318 private void readPreferencesFromXML() {
319 File[] files = askUserForCustomSettingsFiles(false, tr("Open JOSM customization file"));
320 if (files.length == 0)
321 return;
322
323 Preferences tmpPrefs = new Preferences(Preferences.main());
324
325 StringBuilder log = new StringBuilder();
326 log.append("<html>");
327 for (File f : files) {
328 CustomConfigurator.readXML(f, tmpPrefs);
329 log.append(PreferencesUtils.getLog());
330 }
331 log.append("</html>");
332 String msg = log.toString().replace("\n", "<br/>");
333
334 new LogShowDialog(tr("Import log"), tr("<html>Here is file import summary. <br/>"
335 + "You can reject preferences changes by pressing \"Cancel\" in preferences dialog <br/>"
336 + "To activate some changes JOSM restart may be needed.</html>"), msg).showDialog();
337
338 readPreferences(tmpPrefs);
339 // sorting after modification - first modified, then non-default, then default entries
340 allData.sort(customComparator);
341 applyFilter();
342 }
343
344 private List<PrefEntry> prepareData(Map<String, Setting<?>> loaded, Map<String, Setting<?>> orig, Map<String, Setting<?>> defaults) {
345 List<PrefEntry> data = new ArrayList<>();
346 for (Entry<String, Setting<?>> e : loaded.entrySet()) {
347 Setting<?> value = e.getValue();
348 Setting<?> old = orig.get(e.getKey());
349 Setting<?> def = defaults.get(e.getKey());
350 if (def == null) {
351 def = value.getNullInstance();
352 }
353 PrefEntry en = new PrefEntry(e.getKey(), value, def, false);
354 // after changes we have nondefault value. Value is changed if is not equal to old value
355 if (!Objects.equals(old, value)) {
356 en.markAsChanged();
357 }
358 data.add(en);
359 }
360 for (Entry<String, Setting<?>> e : defaults.entrySet()) {
361 if (!loaded.containsKey(e.getKey())) {
362 PrefEntry en = new PrefEntry(e.getKey(), e.getValue(), e.getValue(), true);
363 // after changes we have default value. So, value is changed if old value is not default
364 Setting<?> old = orig.get(e.getKey());
365 if (old != null) {
366 en.markAsChanged();
367 }
368 data.add(en);
369 }
370 }
371 Collections.sort(data);
372 displayData.clear();
373 displayData.addAll(data);
374 return data;
375 }
376
377 private JPopupMenu buildPopupMenu() {
378 JPopupMenu menu = new JPopupMenu();
379 profileTypes.put(marktr("shortcut"), "shortcut\\..*");
380 profileTypes.put(marktr("color"), "color\\..*");
381 profileTypes.put(marktr("toolbar"), "toolbar.*");
382 profileTypes.put(marktr("imagery"), "imagery.*");
383
384 for (Entry<String, String> e: profileTypes.entrySet()) {
385 menu.add(new ExportProfileAction(Preferences.main(), e.getKey(), e.getValue()));
386 }
387
388 menu.addSeparator();
389 menu.add(getProfileMenu());
390 menu.addSeparator();
391 menu.add(new EditBoundariesAction());
392 menu.addSeparator();
393 menu.add(new ResetPreferencesAction());
394 return menu;
395 }
396
397 private JMenu getProfileMenu() {
398 final JMenu p = new JMenu(tr("Load profile"));
399 p.addMenuListener(new MenuListener() {
400 @Override
401 public void menuSelected(MenuEvent me) {
402 p.removeAll();
403 File[] files = new File(".").listFiles();
404 if (files != null) {
405 for (File f: files) {
406 String s = f.getName();
407 int idx = s.indexOf('_');
408 if (idx >= 0) {
409 String t = s.substring(0, idx);
410 if (profileTypes.containsKey(t)) {
411 p.add(new ImportProfileAction(s, f, t));
412 }
413 }
414 }
415 }
416 files = Config.getDirs().getPreferencesDirectory(false).listFiles();
417 if (files != null) {
418 for (File f: files) {
419 String s = f.getName();
420 int idx = s.indexOf('_');
421 if (idx >= 0) {
422 String t = s.substring(0, idx);
423 if (profileTypes.containsKey(t)) {
424 p.add(new ImportProfileAction(s, f, t));
425 }
426 }
427 }
428 }
429 }
430
431 @Override
432 public void menuDeselected(MenuEvent me) {
433 // Not implemented
434 }
435
436 @Override
437 public void menuCanceled(MenuEvent me) {
438 // Not implemented
439 }
440 });
441 return p;
442 }
443
444 private class ImportProfileAction extends AbstractAction {
445 private final File file;
446 private final String type;
447
448 ImportProfileAction(String name, File file, String type) {
449 super(name);
450 this.file = file;
451 this.type = type;
452 }
453
454 @Override
455 public void actionPerformed(ActionEvent ae) {
456 Preferences tmpPrefs = new Preferences(Preferences.main());
457 CustomConfigurator.readXML(file, tmpPrefs);
458 readPreferences(tmpPrefs);
459 String prefRegex = profileTypes.get(type);
460 // clean all the preferences from the chosen group
461 for (PrefEntry p : allData) {
462 if (p.getKey().matches(prefRegex) && !p.isDefault()) {
463 p.reset();
464 }
465 }
466 // allow user to review the changes in table
467 allData.sort(customComparator);
468 applyFilter();
469 }
470 }
471
472 private void applyFilter() {
473 displayData.clear();
474 for (PrefEntry e : allData) {
475 String prefKey = e.getKey();
476 Setting<?> valueSetting = e.getValue();
477 String prefValue = valueSetting.getValue() == null ? "" : valueSetting.getValue().toString();
478
479
480 // Make 'wmsplugin cache' search for e.g. 'cache.wmsplugin'
481 final String prefKeyLower = prefKey.toLowerCase(Locale.ENGLISH);
482 final String prefValueLower = prefValue.toLowerCase(Locale.ENGLISH);
483 final boolean canHas = Pattern.compile("\\s+").splitAsStream(txtFilter.getText())
484 .map(bit -> bit.toLowerCase(Locale.ENGLISH))
485 .anyMatch(bit -> {
486 switch (bit) {
487 // syntax inspired by SearchCompiler
488 case "changed":
489 return e.isChanged();
490 case "modified":
491 case "-default":
492 return !e.isDefault();
493 case "-modified":
494 case "default":
495 return e.isDefault();
496 default:
497 return prefKeyLower.contains(bit) || prefValueLower.contains(bit);
498 }
499 });
500 if (canHas) {
501 displayData.add(e);
502 }
503 }
504 if (table != null)
505 table.fireDataChanged();
506 }
507
508 @Override
509 public boolean ok() {
510 for (PrefEntry e : allData) {
511 if (e.isChanged()) {
512 Preferences.main().putSetting(e.getKey(), e.getValue().getValue() == null ? null : e.getValue());
513 }
514 }
515 return false;
516 }
517
518 @Override
519 public String getHelpContext() {
520 return HelpUtil.ht("/Preferences/Advanced");
521 }
522}
Note: See TracBrowser for help on using the repository browser.