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