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