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