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