047db50c7ae3c1a65599be088552aacb9a87168b
[debian/openrocket] / src / net / sf / openrocket / util / Prefs.java
1 package net.sf.openrocket.util;
2
3 import java.awt.Color;
4 import java.awt.Dimension;
5 import java.awt.Point;
6 import java.awt.Toolkit;
7 import java.io.File;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Map;
15 import java.util.MissingResourceException;
16 import java.util.Properties;
17 import java.util.Set;
18 import java.util.prefs.BackingStoreException;
19 import java.util.prefs.Preferences;
20
21 import net.sf.openrocket.arch.SystemInfo;
22 import net.sf.openrocket.database.Databases;
23 import net.sf.openrocket.document.Simulation;
24 import net.sf.openrocket.gui.main.ExceptionHandler;
25 import net.sf.openrocket.gui.print.PrintSettings;
26 import net.sf.openrocket.l10n.Translator;
27 import net.sf.openrocket.logging.LogHelper;
28 import net.sf.openrocket.material.Material;
29 import net.sf.openrocket.rocketcomponent.BodyComponent;
30 import net.sf.openrocket.rocketcomponent.FinSet;
31 import net.sf.openrocket.rocketcomponent.InternalComponent;
32 import net.sf.openrocket.rocketcomponent.LaunchLug;
33 import net.sf.openrocket.rocketcomponent.MassObject;
34 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
35 import net.sf.openrocket.rocketcomponent.Rocket;
36 import net.sf.openrocket.rocketcomponent.RocketComponent;
37 import net.sf.openrocket.simulation.FlightDataType;
38 import net.sf.openrocket.simulation.GUISimulationConditions;
39 import net.sf.openrocket.simulation.RK4SimulationStepper;
40 import net.sf.openrocket.startup.Application;
41 import net.sf.openrocket.unit.UnitGroup;
42
43
44 public class Prefs {
45         private static final LogHelper log = Application.getLogger();
46         
47         private static final String SPLIT_CHARACTER = "|";
48         
49
50         /**
51          * Whether to use the debug-node instead of the normal node.
52          */
53         private static final boolean DEBUG;
54         static {
55                 DEBUG = (System.getProperty("openrocket.debug.prefs") != null);
56         }
57         
58         /**
59          * Whether to clear all preferences at application startup.  This has an effect only
60          * if DEBUG is true.
61          */
62         private static final boolean CLEARPREFS = true;
63         
64         /**
65          * The node name to use in the Java preferences storage.
66          */
67         private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket");
68         
69         
70         /*
71          * Load property file only when necessary.
72          */
73         private static class BuildPropertyHolder {
74                 
75                 public static final Properties PROPERTIES;
76                 public static final String BUILD_VERSION;
77                 public static final String BUILD_SOURCE;
78                 public static final boolean DEFAULT_CHECK_UPDATES;
79                 
80                 static {
81                         try {
82                                 InputStream is = ClassLoader.getSystemResourceAsStream("build.properties");
83                                 if (is == null) {
84                                         throw new MissingResourceException(
85                                                         "build.properties not found, distribution built wrong" +
86                                                                         "   classpath:" + System.getProperty("java.class.path"),
87                                                         "build.properties", "build.version");
88                                 }
89                                 
90                                 PROPERTIES = new Properties();
91                                 PROPERTIES.load(is);
92                                 is.close();
93                                 
94                                 String version = PROPERTIES.getProperty("build.version");
95                                 if (version == null) {
96                                         throw new MissingResourceException(
97                                                         "build.version not found in property file",
98                                                         "build.properties", "build.version");
99                                 }
100                                 BUILD_VERSION = version.trim();
101                                 
102                                 BUILD_SOURCE = PROPERTIES.getProperty("build.source");
103                                 if (BUILD_SOURCE == null) {
104                                         throw new MissingResourceException(
105                                                         "build.source not found in property file",
106                                                         "build.properties", "build.source");
107                                 }
108                                 
109                                 String value = PROPERTIES.getProperty("build.checkupdates");
110                                 if (value != null)
111                                         DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value);
112                                 else
113                                         DEFAULT_CHECK_UPDATES = true;
114                                 
115                         } catch (IOException e) {
116                                 throw new MissingResourceException(
117                                                 "Error reading build.properties",
118                                                 "build.properties", "build.version");
119                         }
120                 }
121         }
122         
123         public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
124         
125         public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves";
126         
127         public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
128         
129         // Preferences related to data export
130         public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator";
131         public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment";
132         public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment";
133         public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments";
134         public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter";
135         
136         public static final String PLOT_SHOW_POINTS = "ShowPlotPoints";
137         
138         private static final String CHECK_UPDATES = "CheckUpdates";
139         public static final String LAST_UPDATE = "LastUpdateVersion";
140         
141         public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch";
142         public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar";
143         
144
145         // Node names
146         public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors";
147         
148
149         /**
150          * Node to this application's preferences.
151          * @deprecated  Use the static methods instead.
152          */
153         @Deprecated
154         public static final Preferences NODE;
155         private static final Preferences PREFNODE;
156         
157
158         // Clear the preferences if debug mode and clearprefs is defined
159         static {
160                 Preferences root = Preferences.userRoot();
161                 if (DEBUG && CLEARPREFS) {
162                         try {
163                                 if (root.nodeExists(NODENAME)) {
164                                         root.node(NODENAME).removeNode();
165                                 }
166                         } catch (BackingStoreException e) {
167                                 throw new BugException("Unable to clear preference node", e);
168                         }
169                 }
170                 PREFNODE = root.node(NODENAME);
171                 NODE = PREFNODE;
172         }
173         
174
175
176
177         /////////  Default component attributes
178         
179         private static final HashMap<Class<?>, String> DEFAULT_COLORS =
180                         new HashMap<Class<?>, String>();
181         static {
182                 DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
183                 DEFAULT_COLORS.put(FinSet.class, "0,0,200");
184                 DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
185                 DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
186                 DEFAULT_COLORS.put(MassObject.class, "0,0,0");
187                 DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
188         }
189         
190
191         private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES =
192                         new HashMap<Class<?>, String>();
193         static {
194                 DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
195                 DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
196         }
197         
198         
199         /*
200          * Within a holder class so they will load only when needed.
201          */
202         private static class DefaultMaterialHolder {
203                 private static final Translator trans = Application.getTranslator();
204                 
205                 //// Elastic cord (round 2mm, 1/16 in)
206                 private static final Material DEFAULT_LINE_MATERIAL =
207                                 Databases.findMaterial(Material.Type.LINE, trans.get("Databases.materials.Elasticcordround2mm"),
208                                                 0.0018, false);
209                 //// Ripstop nylon
210                 private static final Material DEFAULT_SURFACE_MATERIAL =
211                                 Databases.findMaterial(Material.Type.SURFACE, trans.get("Databases.materials.Ripstopnylon"), 0.067, false);
212                 //// Cardboard
213                 private static final Material DEFAULT_BULK_MATERIAL =
214                                 Databases.findMaterial(Material.Type.BULK, trans.get("Databases.materials.Cardboard"), 680, false);
215         }
216         
217         //////////////////////
218         
219
220         /**
221          * Return the OpenRocket version number.
222          */
223         public static String getVersion() {
224                 return BuildPropertyHolder.BUILD_VERSION;
225         }
226         
227         
228         /**
229          * Return the OpenRocket build source (e.g. "default" or "Debian")
230          */
231         public static String getBuildSource() {
232                 return BuildPropertyHolder.BUILD_SOURCE;
233         }
234         
235         
236         /**
237          * Return the OpenRocket unique ID.
238          * 
239          * @return      a random ID string that stays constant between OpenRocket executions
240          */
241         public static String getUniqueID() {
242                 String id = PREFNODE.get("id", null);
243                 if (id == null) {
244                         id = UniqueID.uuid();
245                         PREFNODE.put("id", id);
246                 }
247                 return id;
248         }
249         
250         
251
252         /**
253          * Store the current OpenRocket version into the preferences to allow for preferences migration.
254          */
255         private static void storeVersion() {
256                 PREFNODE.put("OpenRocketVersion", getVersion());
257         }
258         
259         
260         /**
261          * Returns a limited-range integer value from the preferences.  If the value 
262          * in the preferences is negative or greater than max, then the default value 
263          * is returned.
264          * 
265          * @param key  The preference to retrieve.
266          * @param max  Maximum allowed value for the choice.
267          * @param def  Default value.
268          * @return   The preference value.
269          */
270         public static int getChoise(String key, int max, int def) {
271                 int v = PREFNODE.getInt(key, def);
272                 if ((v < 0) || (v > max))
273                         return def;
274                 return v;
275         }
276         
277         
278         /**
279          * Helper method that puts an integer choice value into the preferences.
280          * 
281          * @param key     the preference key.
282          * @param value   the value to store.
283          */
284         public static void putChoise(String key, int value) {
285                 PREFNODE.putInt(key, value);
286                 storeVersion();
287         }
288         
289         
290         /**
291          * Return a string preference.
292          * 
293          * @param key   the preference key.
294          * @param def   the default if no preference is stored
295          * @return              the preference value
296          */
297         public static String getString(String key, String def) {
298                 return PREFNODE.get(key, def);
299         }
300         
301         /**
302          * Set a string preference.
303          * 
304          * @param key           the preference key
305          * @param value         the value to set, or <code>null</code> to remove the key
306          */
307         public static void putString(String key, String value) {
308                 if (value == null) {
309                         PREFNODE.remove(key);
310                         return;
311                 }
312                 PREFNODE.put(key, value);
313                 storeVersion();
314         }
315         
316         
317         /**
318          * Retrieve an enum value from the user preferences.
319          * 
320          * @param <T>   the enum type
321          * @param key   the key
322          * @param def   the default value, cannot be null
323          * @return              the value in the preferences, or the default value
324          */
325         public static <T extends Enum<T>> T getEnum(String key, T def) {
326                 if (def == null) {
327                         throw new BugException("Default value cannot be null");
328                 }
329                 
330                 String value = getString(key, null);
331                 if (value == null) {
332                         return def;
333                 }
334                 
335                 try {
336                         return Enum.valueOf(def.getDeclaringClass(), value);
337                 } catch (IllegalArgumentException e) {
338                         return def;
339                 }
340         }
341         
342         /**
343          * Store an enum value to the user preferences.
344          * 
345          * @param key           the key
346          * @param value         the value to store, or null to remove the value
347          */
348         public static void putEnum(String key, Enum<?> value) {
349                 if (value == null) {
350                         putString(key, null);
351                 } else {
352                         putString(key, value.name());
353                 }
354         }
355         
356         
357         /**
358          * Return a boolean preference.
359          * 
360          * @param key   the preference key
361          * @param def   the default if no preference is stored
362          * @return              the preference value
363          */
364         public static boolean getBoolean(String key, boolean def) {
365                 return PREFNODE.getBoolean(key, def);
366         }
367         
368         /**
369          * Set a boolean preference.
370          * 
371          * @param key           the preference key
372          * @param value         the value to set
373          */
374         public static void putBoolean(String key, boolean value) {
375                 PREFNODE.putBoolean(key, value);
376                 storeVersion();
377         }
378         
379         
380         /**
381          * Return a preferences object for the specified node name.
382          * 
383          * @param nodeName      the node name
384          * @return                      the preferences object for that node
385          */
386         public static Preferences getNode(String nodeName) {
387                 return PREFNODE.node(nodeName);
388         }
389         
390         
391         //////////////////
392         
393
394
395
396         public static boolean getCheckUpdates() {
397                 return PREFNODE.getBoolean(CHECK_UPDATES, BuildPropertyHolder.DEFAULT_CHECK_UPDATES);
398         }
399         
400         public static void setCheckUpdates(boolean check) {
401                 PREFNODE.putBoolean(CHECK_UPDATES, check);
402                 storeVersion();
403         }
404         
405         public static File getDefaultDirectory() {
406                 String file = PREFNODE.get("defaultDirectory", null);
407                 if (file == null)
408                         return null;
409                 return new File(file);
410         }
411         
412         public static void setDefaultDirectory(File dir) {
413                 String d;
414                 if (dir == null) {
415                         d = null;
416                 } else {
417                         d = dir.getAbsolutePath();
418                 }
419                 PREFNODE.put("defaultDirectory", d);
420                 storeVersion();
421         }
422         
423         
424         /**
425          * Return a list of files/directories to be loaded as custom thrust curves.
426          * <p>
427          * If this property has not been set, the directory "ThrustCurves" in the user
428          * application directory will be used.  The directory will be created if it does not
429          * exist.
430          * 
431          * @return      a list of files to load as thrust curves.
432          */
433         public static List<File> getUserThrustCurveFiles() {
434                 List<File> list = new ArrayList<File>();
435                 
436                 String files = getString(USER_THRUST_CURVES_KEY, null);
437                 if (files == null) {
438                         // Default to application directory
439                         File tcdir = getDefaultUserThrustCurveFile();
440                         if (!tcdir.isDirectory()) {
441                                 tcdir.mkdirs();
442                         }
443                         list.add(tcdir);
444                 } else {
445                         for (String file : files.split("\\" + SPLIT_CHARACTER)) {
446                                 file = file.trim();
447                                 if (file.length() > 0) {
448                                         list.add(new File(file));
449                                 }
450                         }
451                 }
452                 
453                 return list;
454         }
455         
456         public static File getDefaultUserThrustCurveFile() {
457                 File appdir = SystemInfo.getUserApplicationDirectory();
458                 File tcdir = new File(appdir, "ThrustCurves");
459                 return tcdir;
460         }
461         
462         
463         /**
464          * Set the list of files/directories to be loaded as custom thrust curves.
465          * 
466          * @param files         the files to load, or <code>null</code> to reset to default value.
467          */
468         public static void setUserThrustCurveFiles(List<File> files) {
469                 if (files == null) {
470                         putString(USER_THRUST_CURVES_KEY, null);
471                         return;
472                 }
473                 
474                 String str = "";
475                 
476                 for (File file : files) {
477                         if (str.length() > 0) {
478                                 str += SPLIT_CHARACTER;
479                         }
480                         str += file.getAbsolutePath();
481                 }
482                 putString(USER_THRUST_CURVES_KEY, str);
483         }
484         
485         
486
487
488
489         public static Color getDefaultColor(Class<? extends RocketComponent> c) {
490                 String color = get("componentColors", c, DEFAULT_COLORS);
491                 if (color == null)
492                         return Color.BLACK;
493                 
494                 Color clr = parseColor(color);
495                 if (clr != null) {
496                         return clr;
497                 } else {
498                         return Color.BLACK;
499                 }
500         }
501         
502         public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
503                 if (color == null)
504                         return;
505                 set("componentColors", c, stringifyColor(color));
506         }
507         
508         
509         private static Color parseColor(String color) {
510                 if (color == null) {
511                         return null;
512                 }
513                 
514                 String[] rgb = color.split(",");
515                 if (rgb.length == 3) {
516                         try {
517                                 int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255);
518                                 int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255);
519                                 int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255);
520                                 return new Color(red, green, blue);
521                         } catch (NumberFormatException ignore) {
522                         }
523                 }
524                 return null;
525         }
526         
527         
528         private static String stringifyColor(Color color) {
529                 String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
530                 return string;
531         }
532         
533         
534
535         public static Color getMotorBorderColor() {
536                 // TODO: MEDIUM:  Motor color (settable?)
537                 return new Color(0, 0, 0, 200);
538         }
539         
540         
541         public static Color getMotorFillColor() {
542                 // TODO: MEDIUM:  Motor fill color (settable?)
543                 return new Color(0, 0, 0, 100);
544         }
545         
546         
547         public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
548                 String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
549                 try {
550                         return LineStyle.valueOf(value);
551                 } catch (Exception e) {
552                         return LineStyle.SOLID;
553                 }
554         }
555         
556         public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
557                         LineStyle style) {
558                 if (style == null)
559                         return;
560                 set("componentStyle", c, style.name());
561         }
562         
563         
564         /**
565          * Return the DPI setting of the monitor.  This is either the setting provided
566          * by the system or a user-specified DPI setting.
567          * 
568          * @return    the DPI setting to use.
569          */
570         public static double getDPI() {
571                 int dpi = PREFNODE.getInt("DPI", 0); // Tenths of a dpi
572                 
573                 if (dpi < 10) {
574                         dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10;
575                 }
576                 if (dpi < 10)
577                         dpi = 960;
578                 
579                 return (dpi) / 10.0;
580         }
581         
582         
583         public static double getDefaultMach() {
584                 // TODO: HIGH: implement custom default mach number
585                 return 0.3;
586         }
587         
588         
589
590
591         public static Material getDefaultComponentMaterial(
592                         Class<? extends RocketComponent> componentClass,
593                         Material.Type type) {
594                 
595                 String material = get("componentMaterials", componentClass, null);
596                 if (material != null) {
597                         try {
598                                 Material m = Material.fromStorableString(material, false);
599                                 if (m.getType() == type)
600                                         return m;
601                         } catch (IllegalArgumentException ignore) {
602                         }
603                 }
604                 
605                 switch (type) {
606                 case LINE:
607                         return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL;
608                 case SURFACE:
609                         return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL;
610                 case BULK:
611                         return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL;
612                 }
613                 throw new IllegalArgumentException("Unknown material type: " + type);
614         }
615         
616         public static void setDefaultComponentMaterial(
617                         Class<? extends RocketComponent> componentClass, Material material) {
618                 
619                 set("componentMaterials", componentClass,
620                                 material == null ? null : material.toStorableString());
621         }
622         
623         
624         public static int getMaxThreadCount() {
625                 return Runtime.getRuntime().availableProcessors();
626         }
627         
628         
629         /**
630          * Return whether to use additional safety code checks.
631          */
632         public static boolean useSafetyChecks() {
633                 // Currently default to false unless openrocket.debug.safetycheck is defined
634                 String s = System.getProperty("openrocket.debug.safetycheck");
635                 if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) {
636                         return true;
637                 }
638                 return false;
639         }
640         
641         
642         public static Point getWindowPosition(Class<?> c) {
643                 int x, y;
644                 String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null);
645                 
646                 if (pref == null)
647                         return null;
648                 
649                 if (pref.indexOf(',') < 0)
650                         return null;
651                 
652                 try {
653                         x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
654                         y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
655                 } catch (NumberFormatException e) {
656                         return null;
657                 }
658                 return new Point(x, y);
659         }
660         
661         public static void setWindowPosition(Class<?> c, Point p) {
662                 PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
663                 storeVersion();
664         }
665         
666         
667
668
669         public static Dimension getWindowSize(Class<?> c) {
670                 int x, y;
671                 String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null);
672                 
673                 if (pref == null)
674                         return null;
675                 
676                 if (pref.indexOf(',') < 0)
677                         return null;
678                 
679                 try {
680                         x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
681                         y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
682                 } catch (NumberFormatException e) {
683                         return null;
684                 }
685                 return new Dimension(x, y);
686         }
687         
688         public static void setWindowSize(Class<?> c, Dimension d) {
689                 PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
690                 storeVersion();
691         }
692         
693         
694         ////  Printing
695         
696         public static PrintSettings getPrintSettings() {
697                 PrintSettings settings = new PrintSettings();
698                 Color c;
699                 
700                 c = parseColor(getString("print.template.fillColor", null));
701                 if (c != null) {
702                         settings.setTemplateFillColor(c);
703                 }
704                 
705                 c = parseColor(getString("print.template.borderColor", null));
706                 if (c != null) {
707                         settings.setTemplateBorderColor(c);
708                 }
709                 
710                 settings.setPaperSize(getEnum("print.paper.size", settings.getPaperSize()));
711                 settings.setPaperOrientation(getEnum("print.paper.orientation", settings.getPaperOrientation()));
712                 
713                 return settings;
714         }
715         
716         public static void setPrintSettings(PrintSettings settings) {
717                 putString("print.template.fillColor", stringifyColor(settings.getTemplateFillColor()));
718                 putString("print.template.borderColor", stringifyColor(settings.getTemplateBorderColor()));
719                 putEnum("print.paper.size", settings.getPaperSize());
720                 putEnum("print.paper.orientation", settings.getPaperOrientation());
721         }
722         
723         ////  Background flight data computation
724         
725         public static boolean computeFlightInBackground() {
726                 return PREFNODE.getBoolean("backgroundFlight", true);
727         }
728         
729         public static Simulation getBackgroundSimulation(Rocket rocket) {
730                 Simulation s = new Simulation(rocket);
731                 GUISimulationConditions cond = s.getConditions();
732                 
733                 cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2);
734                 cond.setWindSpeedAverage(1.0);
735                 cond.setWindSpeedDeviation(0.1);
736                 cond.setLaunchRodLength(5);
737                 return s;
738         }
739         
740         
741
742         /////////  Export variables
743         
744         public static boolean isExportSelected(FlightDataType type) {
745                 Preferences prefs = PREFNODE.node("exports");
746                 return prefs.getBoolean(type.getName(), false);
747         }
748         
749         public static void setExportSelected(FlightDataType type, boolean selected) {
750                 Preferences prefs = PREFNODE.node("exports");
751                 prefs.putBoolean(type.getName(), selected);
752         }
753         
754         
755
756         /////////  Default unit storage
757         
758         public static void loadDefaultUnits() {
759                 Preferences prefs = PREFNODE.node("units");
760                 try {
761                         
762                         for (String key : prefs.keys()) {
763                                 UnitGroup group = UnitGroup.UNITS.get(key);
764                                 if (group == null)
765                                         continue;
766                                 
767                                 try {
768                                         group.setDefaultUnit(prefs.get(key, null));
769                                 } catch (IllegalArgumentException ignore) {
770                                 }
771                         }
772                         
773                 } catch (BackingStoreException e) {
774                         ExceptionHandler.handleErrorCondition(e);
775                 }
776         }
777         
778         public static void storeDefaultUnits() {
779                 Preferences prefs = PREFNODE.node("units");
780                 
781                 for (String key : UnitGroup.UNITS.keySet()) {
782                         UnitGroup group = UnitGroup.UNITS.get(key);
783                         if (group == null || group.getUnitCount() < 2)
784                                 continue;
785                         
786                         prefs.put(key, group.getDefaultUnit().getUnit());
787                 }
788         }
789         
790         
791
792         ////  Material storage
793         
794
795         /**
796          * Add a user-defined material to the preferences.  The preferences are
797          * first checked for an existing material matching the provided one using
798          * {@link Material#equals(Object)}.
799          * 
800          * @param m             the material to add.
801          */
802         public static void addUserMaterial(Material m) {
803                 Preferences prefs = PREFNODE.node("userMaterials");
804                 
805
806                 // Check whether material already exists
807                 if (getUserMaterials().contains(m)) {
808                         return;
809                 }
810                 
811                 // Add material using next free key (key is not used when loading)
812                 String mat = m.toStorableString();
813                 for (int i = 0;; i++) {
814                         String key = "material" + i;
815                         if (prefs.get(key, null) == null) {
816                                 prefs.put(key, mat);
817                                 return;
818                         }
819                 }
820         }
821         
822         
823         /**
824          * Remove a user-defined material from the preferences.  The matching is performed
825          * using {@link Material#equals(Object)}.
826          * 
827          * @param m             the material to remove.
828          */
829         public static void removeUserMaterial(Material m) {
830                 Preferences prefs = PREFNODE.node("userMaterials");
831                 
832                 try {
833                         
834                         // Iterate through materials and remove all keys with a matching material
835                         for (String key : prefs.keys()) {
836                                 String value = prefs.get(key, null);
837                                 try {
838                                         
839                                         Material existing = Material.fromStorableString(value, true);
840                                         if (existing.equals(m)) {
841                                                 prefs.remove(key);
842                                         }
843                                         
844                                 } catch (IllegalArgumentException ignore) {
845                                 }
846                                 
847                         }
848                         
849                 } catch (BackingStoreException e) {
850                         throw new IllegalStateException("Cannot read preferences!", e);
851                 }
852         }
853         
854         
855         /**
856          * Return a set of all user-defined materials in the preferences.  The materials
857          * are created marked as user-defined.
858          * 
859          * @return      a set of all user-defined materials.
860          */
861         public static Set<Material> getUserMaterials() {
862                 Preferences prefs = PREFNODE.node("userMaterials");
863                 
864                 HashSet<Material> materials = new HashSet<Material>();
865                 try {
866                         
867                         for (String key : prefs.keys()) {
868                                 String value = prefs.get(key, null);
869                                 try {
870                                         
871                                         Material m = Material.fromStorableString(value, true);
872                                         materials.add(m);
873                                         
874                                 } catch (IllegalArgumentException e) {
875                                         log.warn("Illegal material string " + value);
876                                 }
877                                 
878                         }
879                         
880                 } catch (BackingStoreException e) {
881                         throw new IllegalStateException("Cannot read preferences!", e);
882                 }
883                 
884                 return materials;
885         }
886         
887         
888         ////  Helper methods
889         
890         private static String get(String directory,
891                         Class<? extends RocketComponent> componentClass,
892                         Map<Class<?>, String> defaultMap) {
893                 
894                 // Search preferences
895                 Class<?> c = componentClass;
896                 Preferences prefs = PREFNODE.node(directory);
897                 while (c != null && RocketComponent.class.isAssignableFrom(c)) {
898                         String value = prefs.get(c.getSimpleName(), null);
899                         if (value != null)
900                                 return value;
901                         c = c.getSuperclass();
902                 }
903                 
904                 if (defaultMap == null)
905                         return null;
906                 
907                 // Search defaults
908                 c = componentClass;
909                 while (RocketComponent.class.isAssignableFrom(c)) {
910                         String value = defaultMap.get(c);
911                         if (value != null)
912                                 return value;
913                         c = c.getSuperclass();
914                 }
915                 
916                 return null;
917         }
918         
919         
920         private static void set(String directory, Class<? extends RocketComponent> componentClass,
921                         String value) {
922                 Preferences prefs = PREFNODE.node(directory);
923                 if (value == null)
924                         prefs.remove(componentClass.getSimpleName());
925                 else
926                         prefs.put(componentClass.getSimpleName(), value);
927                 storeVersion();
928         }
929         
930 }