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.GridLayout; 010import java.awt.event.ActionEvent; 011import java.awt.event.ActionListener; 012import java.io.File; 013import java.io.IOException; 014import java.nio.file.InvalidPathException; 015import java.util.ArrayList; 016import java.util.Collections; 017import java.util.Comparator; 018import java.util.LinkedHashMap; 019import java.util.List; 020import java.util.Locale; 021import java.util.Map; 022import java.util.Map.Entry; 023import java.util.Objects; 024import java.util.regex.Pattern; 025 026import javax.swing.AbstractAction; 027import javax.swing.JButton; 028import javax.swing.JFileChooser; 029import javax.swing.JMenu; 030import javax.swing.JOptionPane; 031import javax.swing.JPanel; 032import javax.swing.JPopupMenu; 033import javax.swing.JScrollPane; 034import javax.swing.event.MenuEvent; 035import javax.swing.event.MenuListener; 036import javax.swing.filechooser.FileFilter; 037 038import org.openstreetmap.josm.actions.DiskAccessAction; 039import org.openstreetmap.josm.data.Preferences; 040import org.openstreetmap.josm.data.PreferencesUtils; 041import org.openstreetmap.josm.data.osm.DataSet; 042import org.openstreetmap.josm.gui.MainApplication; 043import org.openstreetmap.josm.gui.dialogs.LogShowDialog; 044import org.openstreetmap.josm.gui.help.HelpUtil; 045import org.openstreetmap.josm.gui.io.CustomConfigurator; 046import org.openstreetmap.josm.gui.layer.MainLayerManager; 047import org.openstreetmap.josm.gui.layer.OsmDataLayer; 048import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting; 049import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 050import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 051import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; 052import org.openstreetmap.josm.gui.util.DocumentAdapter; 053import org.openstreetmap.josm.gui.util.GuiHelper; 054import org.openstreetmap.josm.gui.widgets.AbstractFileChooser; 055import org.openstreetmap.josm.gui.widgets.FilterField; 056import org.openstreetmap.josm.gui.widgets.JosmTextField; 057import org.openstreetmap.josm.spi.preferences.Config; 058import org.openstreetmap.josm.spi.preferences.Setting; 059import org.openstreetmap.josm.spi.preferences.StringSetting; 060import org.openstreetmap.josm.tools.GBC; 061import org.openstreetmap.josm.tools.ImageProvider; 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 /** 093 * Requires {@link Logging#isDebugEnabled()}, otherwise dataset is unloaded 094 * @see Territories#initializeInternalData() 095 */ 096 private static final class EditBoundariesAction extends AbstractAction { 097 EditBoundariesAction() { 098 super(tr("Edit boundaries"), ImageProvider.get("dialogs/edit", ImageProvider.ImageSizes.MENU)); 099 } 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 orig.remove("osm-server.password"); 239 defaults.remove("osm-server.password"); 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}