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

Last change on this file was 18650, checked in by taylor.smock, 3 years ago

Fix #20768: Add OAuth 2.0 support

This also fixes #21607: authentication buttons are unavailable when credentials
are set.

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