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