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