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