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