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