1 /*
2  * UserPreferences.java 15 mai 2006
3  *
4  * Sweet Home 3D, Copyright (c) 2006 Emmanuel PUYBARET / eTeks <info@eteks.com>
5  *
6  * This program is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 package com.eteks.sweethome3d.model;
21 
22 import java.beans.PropertyChangeListener;
23 import java.beans.PropertyChangeSupport;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.math.BigDecimal;
27 import java.security.AccessControlException;
28 import java.util.ArrayList;
29 import java.util.Arrays;
30 import java.util.Collections;
31 import java.util.Enumeration;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.LinkedHashMap;
35 import java.util.List;
36 import java.util.Locale;
37 import java.util.Map;
38 import java.util.MissingResourceException;
39 import java.util.Properties;
40 import java.util.PropertyPermission;
41 import java.util.PropertyResourceBundle;
42 import java.util.ResourceBundle;
43 
44 /**
45  * User preferences.
46  * @author Emmanuel Puybaret
47  */
48 public abstract class UserPreferences {
49   /**
50    * The properties of user preferences that may change. <code>PropertyChangeListener</code>s added
51    * to user preferences will be notified under a property name equal to the string value of one these properties.
52    */
53   public enum Property {LANGUAGE, SUPPORTED_LANGUAGES, UNIT, CURRENCY, VALUE_ADDED_TAX_ENABLED, DEFAULT_VALUE_ADDED_TAX_PERCENTAGE,
54                         MAGNETISM_ENABLED, RULERS_VISIBLE, GRID_VISIBLE, DEFAULT_FONT_NAME,
55                         FURNITURE_VIEWED_FROM_TOP, FURNITURE_MODEL_ICON_SIZE, ROOM_FLOOR_COLORED_OR_TEXTURED, WALL_PATTERN, NEW_WALL_PATTERN,
56                         NEW_WALL_THICKNESS, NEW_WALL_HEIGHT, NEW_WALL_SIDEBOARD_THICKNESS, NEW_WALL_SIDEBOARD_HEIGHT, NEW_ROOM_FLOOR_COLOR, NEW_FLOOR_THICKNESS,
57                         RECENT_HOMES, IGNORED_ACTION_TIP, FURNITURE_CATALOG_VIEWED_IN_TREE, NAVIGATION_PANEL_VISIBLE,
58                         AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED, OBSERVER_CAMERA_SELECTED_AT_CHANGE, CHECK_UPDATES_ENABLED,
59                         UPDATES_MINIMUM_DATE, AUTO_SAVE_DELAY_FOR_RECOVERY, AUTO_COMPLETION_STRINGS, RECENT_COLORS, RECENT_TEXTURES, HOME_EXAMPLES}
60 
61   public static final String FURNITURE_LIBRARY_TYPE = "Furniture library";
62   public static final String TEXTURES_LIBRARY_TYPE  = "Textures library";
63   public static final String LANGUAGE_LIBRARY_TYPE  = "Language library";
64 
65   private static final String [] DEFAULT_SUPPORTED_LANGUAGES;
66   private static final List<ClassLoader> DEFAULT_CLASS_LOADER =
67       Arrays.asList(new ClassLoader [] {UserPreferences.class.getClassLoader()});
68 
69   private static final TextStyle DEFAULT_TEXT_STYLE = new TextStyle(18f);
70   private static final TextStyle DEFAULT_ROOM_TEXT_STYLE = new TextStyle(24f);
71 
72   static {
73     Properties supportedLanguagesProperties = new Properties();
74     String [] defaultSupportedLanguages;
75     try {
76       // As of version 4.1 where Trusted-Library manifest attribute was added to applet jars,
77       // UserPreferences.properties was renamed as SupportedLanguages.properties
78       // because strangely UserPreferences.properties resource couldn't be found in applet environment
79       InputStream in = UserPreferences.class.getResourceAsStream("SupportedLanguages.properties");
80       supportedLanguagesProperties.load(in);
in.close()81       in.close();
82       // Get property value of supportedLanguages
83       defaultSupportedLanguages = supportedLanguagesProperties.getProperty("supportedLanguages", "en").split("\\s");
84     } catch (IOException ex) {
85       defaultSupportedLanguages = new String [] {"en"};
86     }
87     DEFAULT_SUPPORTED_LANGUAGES = defaultSupportedLanguages;
88   }
89 
90   private final PropertyChangeSupport          propertyChangeSupport;
91   private final Map<Class<?>, ResourceBundle>  classResourceBundles;
92   private final Map<String, ResourceBundle>    resourceBundles;
93 
94   private FurnitureCatalog furnitureCatalog;
95   private TexturesCatalog  texturesCatalog;
96   private PatternsCatalog  patternsCatalog;
97   private final String     defaultCountry;
98   private String []        supportedLanguages;
99   private String           language;
100   private String           currency;
101   private boolean          valueAddedTaxEnabled;
102   private BigDecimal       defaultValueAddedTaxPercentage;
103   private LengthUnit       unit;
104   private boolean          furnitureCatalogViewedInTree = true;
105   private boolean          aerialViewCenteredOnSelectionEnabled;
106   private boolean          observerCameraSelectedAtChange = true;
107   private boolean          navigationPanelVisible = true;
108   private boolean          magnetismEnabled    = true;
109   private boolean          rulersVisible       = true;
110   private boolean          gridVisible         = true;
111   private String           defaultFontName;
112   private boolean          drawingModeEnabled;
113   private boolean          furnitureViewedFromTop;
114   private int              furnitureModelIconSize = 128;
115   private boolean          roomFloorColoredOrTextured;
116   private TextureImage     wallPattern;
117   private TextureImage     newWallPattern;
118   private float            newWallThickness;
119   private float            newWallHeight;
120   private float            newWallBaseboardThickness;
121   private float            newWallBaseboardHeight;
122   private Integer          newRoomFloorColor;
123   private float            newFloorThickness;
124   private List<String>     recentHomes;
125   private boolean          checkUpdatesEnabled;
126   private Long             updatesMinimumDate;
127   private int              autoSaveDelayForRecovery;
128   private Map<String, List<String>>  autoCompletionStrings;
129   private List<Integer>        recentColors;
130   private List<TextureImage>   recentTextures;
131   private List<HomeDescriptor> homeExamples;
132 
133   /**
134    * Creates user preferences.<br>
135    * Caution: during creation, the default locale will be updated if it doesn't belong to the supported ones.
136    */
UserPreferences()137   public UserPreferences() {
138     this.propertyChangeSupport = new PropertyChangeSupport(this);
139     this.classResourceBundles = new HashMap<Class<?>, ResourceBundle>();
140     this.resourceBundles = new HashMap<String, ResourceBundle>();
141     this.autoCompletionStrings = new LinkedHashMap<String, List<String>>();
142     this.recentHomes = Collections.emptyList();
143     this.recentColors = Collections.emptyList();
144     this.recentTextures = Collections.emptyList();
145     this.homeExamples = Collections.emptyList();
146 
147     try {
148       this.drawingModeEnabled = Boolean.getBoolean("com.eteks.sweethome3d.j3d.drawingModeEnabled");
149     } catch (SecurityException ex) {
150     }
151 
152     this.supportedLanguages = DEFAULT_SUPPORTED_LANGUAGES;
153     this.defaultCountry = Locale.getDefault().getCountry();
154     String defaultLanguage = Locale.getDefault().getLanguage();
155     // Find closest language among supported languages in Sweet Home 3D
156     // For example, use simplified Chinese even for Chinese users (zh_?) not from China (zh_CN)
157     // unless their exact locale is supported as in Taiwan (zh_TW)
158     for (String supportedLanguage : this.supportedLanguages) {
159       if (supportedLanguage.equals(defaultLanguage + "_" + this.defaultCountry)) {
160         this.language = supportedLanguage;
161         break; // Found the exact supported language
162       } else if (this.language == null
163                  && supportedLanguage.startsWith(defaultLanguage)) {
164         this.language = supportedLanguage; // Found a supported language
165       }
166     }
167     // If no language was found, let's use English by default
168     if (this.language == null) {
169       this.language = Locale.ENGLISH.getLanguage();
170     }
171     updateDefaultLocale();
172   }
173 
174   /**
175    * Updates default locale from preferences language.
176    */
updateDefaultLocale()177   private void updateDefaultLocale() {
178     try {
179       int underscoreIndex = this.language.indexOf("_");
180       if (underscoreIndex != -1) {
181         Locale.setDefault(new Locale(this.language.substring(0, underscoreIndex),
182             this.language.substring(underscoreIndex + 1)));
183       } else {
184         Locale.setDefault(new Locale(this.language, this.defaultCountry));
185       }
186     } catch (AccessControlException ex) {
187       // Let's keep default language even if it's not supported
188       this.language = Locale.getDefault().getLanguage();
189     }
190   }
191 
192   /**
193    * Writes user preferences.
194    * @throws RecorderException if user preferences couldn'y be saved.
195    */
write()196   public abstract void write() throws RecorderException;
197 
198   /**
199    * Adds the property change <code>listener</code> in parameter to these preferences.
200    * <br>Caution: a user preferences instance generally exists during all the application ;
201    * therefore you should take care of not bounding permanently listeners to this
202    * object (for example, do not create anonymous listeners on user preferences
203    * in classes depending on an edited home).
204    * @since 6.4
205    */
addPropertyChangeListener(PropertyChangeListener listener)206   public void addPropertyChangeListener(PropertyChangeListener listener) {
207     this.propertyChangeSupport.addPropertyChangeListener(listener);
208   }
209 
210   /**
211    * Removes the property change <code>listener</code> in parameter from these preferences.
212    * @since 6.4
213    */
removePropertyChangeListener(PropertyChangeListener listener)214   public void removePropertyChangeListener(PropertyChangeListener listener) {
215     this.propertyChangeSupport.removePropertyChangeListener(listener);
216   }
217 
218   /**
219    * Adds the <code>listener</code> in parameter to these preferences to listen
220    * to the changes of the given <code>property</code>.
221    * <br>Caution: a user preferences instance generally exists during all the application ;
222    * therefore you should take care of not bounding permanently listeners to this
223    * object (for example, do not create anonymous listeners on user preferences
224    * in classes depending on an edited home).
225    */
addPropertyChangeListener(Property property, PropertyChangeListener listener)226   public void addPropertyChangeListener(Property property,
227                                         PropertyChangeListener listener) {
228     this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
229   }
230 
231   /**
232    * Removes the <code>listener</code> in parameter from these preferences.
233    */
removePropertyChangeListener(Property property, PropertyChangeListener listener)234   public void removePropertyChangeListener(Property property,
235                                            PropertyChangeListener listener) {
236     this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
237   }
238 
239   /**
240    * Returns the furniture catalog.
241    */
getFurnitureCatalog()242   public FurnitureCatalog getFurnitureCatalog() {
243     return this.furnitureCatalog;
244   }
245 
246   /**
247    * Sets furniture catalog.
248    */
setFurnitureCatalog(FurnitureCatalog catalog)249   protected void setFurnitureCatalog(FurnitureCatalog catalog) {
250     this.furnitureCatalog = catalog;
251   }
252 
253   /**
254    * Returns the textures catalog.
255    */
getTexturesCatalog()256   public TexturesCatalog getTexturesCatalog() {
257     return this.texturesCatalog;
258   }
259 
260   /**
261    * Sets textures catalog.
262    */
setTexturesCatalog(TexturesCatalog catalog)263   protected void setTexturesCatalog(TexturesCatalog catalog) {
264     this.texturesCatalog = catalog;
265   }
266 
267   /**
268    * Returns the patterns catalog available to fill plan areas.
269    */
getPatternsCatalog()270   public PatternsCatalog getPatternsCatalog() {
271     return this.patternsCatalog;
272   }
273 
274   /**
275    * Sets the patterns available to fill plan areas.
276    */
setPatternsCatalog(PatternsCatalog catalog)277   protected void setPatternsCatalog(PatternsCatalog catalog) {
278     this.patternsCatalog = catalog;
279   }
280 
281   /**
282    * Returns the length unit currently in use.
283    */
getLengthUnit()284   public LengthUnit getLengthUnit() {
285     return this.unit;
286   }
287 
288   /**
289    * Changes the unit currently in use, and notifies listeners of this change.
290    * @param unit one of the values of Unit.
291    */
setUnit(LengthUnit unit)292   public void setUnit(LengthUnit unit) {
293     if (this.unit != unit) {
294       LengthUnit oldUnit = this.unit;
295       this.unit = unit;
296       this.propertyChangeSupport.firePropertyChange(Property.UNIT.name(), oldUnit, unit);
297     }
298   }
299 
300   /**
301    * Returns the preferred language to display information, noted with an ISO 639 code
302    * that may be followed by an underscore and an ISO 3166 code.
303    */
getLanguage()304   public String getLanguage() {
305     return this.language;
306   }
307 
308   /**
309    * If {@linkplain #isLanguageEditable() language can be changed}, sets the preferred language to display information,
310    * changes current default locale accordingly and notifies listeners of this change.
311    * @param language an ISO 639 code that may be followed by an underscore and an ISO 3166 code
312    *            (for example fr, de, it, en_US, zh_CN).
313    */
setLanguage(String language)314   public void setLanguage(String language) {
315     if (!language.equals(this.language)
316         && isLanguageEditable()) {
317       String oldLanguage = this.language;
318       this.language = language;
319       updateDefaultLocale();
320       this.classResourceBundles.clear();
321       this.resourceBundles.clear();
322       this.propertyChangeSupport.firePropertyChange(Property.LANGUAGE.name(),
323           oldLanguage, language);
324     }
325   }
326 
327   /**
328    * Returns <code>true</code> if the language in preferences can be set.
329    * @return <code>true</code> except if <code>user.language</code> System property isn't writable.
330    * @since 3.4
331    */
isLanguageEditable()332   public boolean isLanguageEditable() {
333     try {
334       SecurityManager securityManager = System.getSecurityManager();
335       if (securityManager != null) {
336         securityManager.checkPermission(new PropertyPermission("user.language", "write"));
337       }
338       return true;
339     } catch (AccessControlException ex) {
340       return false;
341     }
342   }
343 
344   /**
345    * Returns the array of default available languages in Sweet Home 3D.
346    */
getDefaultSupportedLanguages()347   public String [] getDefaultSupportedLanguages() {
348     return DEFAULT_SUPPORTED_LANGUAGES.clone();
349   }
350 
351   /**
352    * Returns the array of available languages in Sweet Home 3D including languages in libraries.
353    * @since 3.4
354    */
getSupportedLanguages()355   public String [] getSupportedLanguages() {
356     return this.supportedLanguages.clone();
357   }
358 
359   /**
360    * Sets the available languages in Sweet Home 3D.
361    * @since 3.4
362    */
setSupportedLanguages(String [] supportedLanguages)363   protected void setSupportedLanguages(String [] supportedLanguages) {
364     if (!Arrays.deepEquals(this.supportedLanguages, supportedLanguages)) {
365       String [] oldSupportedLanguages = this.supportedLanguages;
366       this.supportedLanguages = supportedLanguages.clone();
367       this.propertyChangeSupport.firePropertyChange(Property.SUPPORTED_LANGUAGES.name(),
368           oldSupportedLanguages, supportedLanguages);
369     }
370   }
371 
372   /**
373    * Returns the string matching <code>resourceKey</code> in current language in the
374    * context of <code>resourceClass</code>.
375    * If <code>resourceParameters</code> isn't empty the string is considered
376    * as a format string, and the returned string will be formatted with these parameters.
377    * This implementation searches first the key in a properties file named as
378    * <code>resourceClass</code>, then if this file doesn't exist, it searches
379    * the key prefixed by <code>resourceClass</code> name and a dot in a package.properties file
380    * in the folder matching the package of <code>resourceClass</code>.
381    * @throws IllegalArgumentException if no string for the given key can be found
382    */
getLocalizedString(Class<?> resourceClass, String resourceKey, Object ... resourceParameters)383   public String getLocalizedString(Class<?> resourceClass,
384                                    String   resourceKey,
385                                    Object ... resourceParameters) {
386     ResourceBundle classResourceBundle = this.classResourceBundles.get(resourceClass);
387     if (classResourceBundle == null) {
388       try {
389         classResourceBundle = getResourceBundle(resourceClass.getName());
390         this.classResourceBundles.put(resourceClass, classResourceBundle);
391       } catch (IOException ex) {
392         try {
393           String className = resourceClass.getName();
394           int lastIndex = className.lastIndexOf(".");
395           String resourceFamily;
396           if (lastIndex != -1) {
397             resourceFamily = className.substring(0, lastIndex) + ".package";
398           } else {
399             resourceFamily = "package";
400           }
401           classResourceBundle = new PrefixedResourceBundle(getResourceBundle(resourceFamily),
402               resourceClass.getSimpleName() + ".");
403           this.classResourceBundles.put(resourceClass, classResourceBundle);
404         } catch (IOException ex2) {
405           throw new IllegalArgumentException(
406               "Can't find resource bundle for " + resourceClass, ex);
407         }
408       }
409     }
410 
411     return getLocalizedString(classResourceBundle, resourceKey, resourceParameters);
412   }
413 
414   /**
415    * Returns the string matching <code>resourceKey</code> in current language
416    * for the given resource family.
417    * <code>resourceFamily</code> should match the absolute path of a .properties resource family,
418    * shouldn't start by a slash and may contain dots '.' or slash '/' as folder separators.
419    * If <code>resourceParameters</code> isn't empty the string is considered
420    * as a format string, and the returned string will be formatted with these parameters.
421    * This implementation searches the key in a properties file named as
422    * <code>resourceFamily</code>.
423    * @throws IllegalArgumentException if no string for the given key can be found
424    * @since 2.3
425    */
getLocalizedString(String resourceFamily, String resourceKey, Object ... resourceParameters)426   public String getLocalizedString(String resourceFamily,
427                                    String resourceKey,
428                                    Object ... resourceParameters) {
429     try {
430       ResourceBundle resourceBundle = getResourceBundle(resourceFamily);
431       return getLocalizedString(resourceBundle, resourceKey, resourceParameters);
432     } catch (IOException ex) {
433       throw new IllegalArgumentException(
434           "Can't find resource bundle for " + resourceFamily, ex);
435     }
436   }
437 
438   /**
439    * Returns a new resource bundle for the given <code>familyName</code>
440    * that matches current default locale. The search will be done
441    * only among .properties files.
442    * @throws IOException if no .properties file was found
443    */
getResourceBundle(String resourceFamily)444   private ResourceBundle getResourceBundle(String resourceFamily) throws IOException {
445     resourceFamily = resourceFamily.replace('.', '/');
446     ResourceBundle resourceBundle = this.resourceBundles.get(resourceFamily);
447     if (resourceBundle != null) {
448       return resourceBundle;
449     }
450     Locale defaultLocale = Locale.getDefault();
451     String language = defaultLocale.getLanguage();
452     String country = defaultLocale.getCountry();
453     String [] suffixes = {".properties",
454                           "_" + language + ".properties",
455                           "_" + language + "_" + country + ".properties"};
456     for (String suffix : suffixes) {
457       for (ClassLoader classLoader : getResourceClassLoaders()) {
458         InputStream in = classLoader.getResourceAsStream(resourceFamily + suffix);
459         if (in != null) {
460           final ResourceBundle parentResourceBundle = resourceBundle;
461           try {
462             resourceBundle = new PropertyResourceBundle(in) {
463               {
464                 setParent(parentResourceBundle);
465               }
466             };
467             break;
468           } catch (IllegalArgumentException ex) {
469             // May happen if the file contains some wrongly encoded characters
470             ex.printStackTrace();
471           } finally {
472             in.close();
473           }
474         }
475       }
476     }
477     if (resourceBundle == null) {
478       throw new IOException("No available resource bundle for " + resourceFamily);
479     }
480     this.resourceBundles.put(resourceFamily, resourceBundle);
481     return resourceBundle;
482   }
483 
484   /**
485    * Returns the string matching <code>resourceKey</code> for the given resource bundle.
486    */
getLocalizedString(ResourceBundle resourceBundle, String resourceKey, Object... resourceParameters)487   private String getLocalizedString(ResourceBundle resourceBundle,
488                                     String         resourceKey,
489                                     Object...      resourceParameters) {
490     try {
491       String localizedString = resourceBundle.getString(resourceKey);
492       if (resourceParameters.length > 0) {
493         localizedString = String.format(localizedString, resourceParameters);
494       }
495       return localizedString;
496     } catch (MissingResourceException ex) {
497       throw new IllegalArgumentException("Unknown key " + resourceKey);
498     }
499   }
500 
501   /**
502    * Returns the keys of the localized property strings of the given resource family.
503    * <code>resourceFamily</code> should match the absolute path of a .properties resource family,
504    * shouldn't start by a slash and may contain dots '.' or slash '/' as folder separators.
505    * @since 5.7
506    */
getLocalizedStringKeys(String resourceFamily)507   public Iterator<String> getLocalizedStringKeys(String resourceFamily) {
508     try {
509       final Enumeration<String> keys = getResourceBundle(resourceFamily).getKeys();
510       return new Iterator<String>() {
511           public boolean hasNext() {
512             return keys.hasMoreElements();
513           }
514 
515           public String next() {
516             return keys.nextElement();
517           }
518 
519           public void remove() {
520             throw new UnsupportedOperationException("Enumeration not modifiable");
521           }
522         };
523     } catch (IOException ex) {
524       return Collections.<String>emptyList().iterator();
525     }
526   }
527 
528   /**
529    * Returns the class loaders through which localized strings returned by
530    * {@link #getLocalizedString(Class, String, Object...) getLocalizedString} might be loaded.
531    * @since 2.3
532    */
533   public List<ClassLoader> getResourceClassLoaders() {
534     return DEFAULT_CLASS_LOADER;
535   }
536 
537   /**
538    * Returns the currency in use, noted with ISO 4217 code, or <code>null</code>
539    * if prices aren't used in application.
540    */
541   public String getCurrency() {
542     return this.currency;
543   }
544 
545   /**
546    * Sets the currency in use.
547    */
548   public void setCurrency(String currency) {
549     if (currency != this.currency
550         && (currency == null || !currency.equals(this.currency))) {
551       String oldCurrency = this.currency;
552       this.currency = currency;
553       this.propertyChangeSupport.firePropertyChange(Property.CURRENCY.name(), oldCurrency, currency);
554 
555     }
556   }
557 
558   /**
559    * Returns <code>true</code> if Value Added Tax should be taken in account in prices.
560    * @since 6.0
561    */
562   public boolean isValueAddedTaxEnabled() {
563     return this.valueAddedTaxEnabled;
564   }
565 
566   /**
567    * Sets whether Value Added Tax should be taken in account in prices.
568    * @param valueAddedTaxEnabled if <code>true</code> VAT will be added to prices.
569    * @since 6.0
570    */
571   public void setValueAddedTaxEnabled(boolean valueAddedTaxEnabled) {
572     if (this.valueAddedTaxEnabled != valueAddedTaxEnabled) {
573       this.valueAddedTaxEnabled = valueAddedTaxEnabled;
574       this.propertyChangeSupport.firePropertyChange(Property.VALUE_ADDED_TAX_ENABLED.name(),
575           !valueAddedTaxEnabled, valueAddedTaxEnabled);
576     }
577   }
578 
579   /**
580    * Returns the Value Added Tax percentage applied to prices by default, or <code>null</code>
581    * if VAT isn't taken into account in the application.
582    * @since 6.0
583    */
584   public BigDecimal getDefaultValueAddedTaxPercentage() {
585     return this.defaultValueAddedTaxPercentage;
586   }
587 
588   /**
589    * Sets the Value Added Tax percentage applied to prices by default.
590    * @param valueAddedTaxPercentage the default VAT percentage
591    * @since 6.0
592    */
593   public void setDefaultValueAddedTaxPercentage(BigDecimal valueAddedTaxPercentage) {
594     if (valueAddedTaxPercentage != this.defaultValueAddedTaxPercentage
595         && (valueAddedTaxPercentage == null || !valueAddedTaxPercentage.equals(this.defaultValueAddedTaxPercentage))) {
596       BigDecimal oldValueAddedTaxPercentage = this.defaultValueAddedTaxPercentage;
597       this.defaultValueAddedTaxPercentage = valueAddedTaxPercentage;
598       this.propertyChangeSupport.firePropertyChange(Property.DEFAULT_VALUE_ADDED_TAX_PERCENTAGE.name(), oldValueAddedTaxPercentage, valueAddedTaxPercentage);
599 
600     }
601   }
602 
603   /**
604    * Returns <code>true</code> if the furniture catalog should be viewed in a tree.
605    * @since 2.3
606    */
607   public boolean isFurnitureCatalogViewedInTree() {
608     return this.furnitureCatalogViewedInTree;
609   }
610 
611   /**
612    * Sets whether the furniture catalog should be viewed in a tree or a different way.
613    * @since 2.3
614    */
615   public void setFurnitureCatalogViewedInTree(boolean furnitureCatalogViewedInTree) {
616     if (this.furnitureCatalogViewedInTree != furnitureCatalogViewedInTree) {
617       this.furnitureCatalogViewedInTree = furnitureCatalogViewedInTree;
618       this.propertyChangeSupport.firePropertyChange(Property.FURNITURE_CATALOG_VIEWED_IN_TREE.name(),
619           !furnitureCatalogViewedInTree, furnitureCatalogViewedInTree);
620     }
621   }
622 
623   /**
624    * Returns <code>true</code> if the navigation panel should be displayed.
625    * @since 2.3
626    */
627   public boolean isNavigationPanelVisible() {
628     return this.navigationPanelVisible;
629   }
630 
631   /**
632    * Sets whether the navigation panel should be displayed or not.
633    * @since 2.3
634    */
635   public void setNavigationPanelVisible(boolean navigationPanelVisible) {
636     if (this.navigationPanelVisible != navigationPanelVisible) {
637       this.navigationPanelVisible = navigationPanelVisible;
638       this.propertyChangeSupport.firePropertyChange(Property.NAVIGATION_PANEL_VISIBLE.name(),
639           !navigationPanelVisible, navigationPanelVisible);
640     }
641   }
642 
643   /**
644    * Sets whether aerial view should be centered on selection or not.
645    * @since 4.0
646    */
647   public void setAerialViewCenteredOnSelectionEnabled(boolean aerialViewCenteredOnSelectionEnabled) {
648     if (aerialViewCenteredOnSelectionEnabled != this.aerialViewCenteredOnSelectionEnabled) {
649       this.aerialViewCenteredOnSelectionEnabled = aerialViewCenteredOnSelectionEnabled;
650       this.propertyChangeSupport.firePropertyChange(Property.AERIAL_VIEW_CENTERED_ON_SELECTION_ENABLED.name(),
651           !aerialViewCenteredOnSelectionEnabled, aerialViewCenteredOnSelectionEnabled);
652     }
653   }
654 
655   /**
656    * Returns whether aerial view should be centered on selection or not.
657    * @since 4.0
658    */
659   public boolean isAerialViewCenteredOnSelectionEnabled() {
660     return this.aerialViewCenteredOnSelectionEnabled;
661   }
662 
663   /**
664    * Sets whether the observer camera should be selected at each change.
665    * @since 5.5
666    */
667   public void setObserverCameraSelectedAtChange(boolean observerCameraSelectedAtChange) {
668     if (observerCameraSelectedAtChange != this.observerCameraSelectedAtChange) {
669       this.observerCameraSelectedAtChange = observerCameraSelectedAtChange;
670       this.propertyChangeSupport.firePropertyChange(Property.OBSERVER_CAMERA_SELECTED_AT_CHANGE.name(),
671           !observerCameraSelectedAtChange, observerCameraSelectedAtChange);
672     }
673   }
674 
675   /**
676    * Returns whether the observer camera should be selected at each change.
677    * @since 5.5
678    */
679   public boolean isObserverCameraSelectedAtChange() {
680     return this.observerCameraSelectedAtChange;
681   }
682 
683   /**
684    * Returns <code>true</code> if magnetism is enabled.
685    * @return <code>true</code> by default.
686    */
687   public boolean isMagnetismEnabled() {
688     return this.magnetismEnabled;
689   }
690 
691   /**
692    * Sets whether magnetism is enabled or not, and notifies
693    * listeners of this change.
694    * @param magnetismEnabled <code>true</code> if magnetism is enabled,
695    *          <code>false</code> otherwise.
696    */
697   public void setMagnetismEnabled(boolean magnetismEnabled) {
698     if (this.magnetismEnabled != magnetismEnabled) {
699       this.magnetismEnabled = magnetismEnabled;
700       this.propertyChangeSupport.firePropertyChange(Property.MAGNETISM_ENABLED.name(),
701           !magnetismEnabled, magnetismEnabled);
702     }
703   }
704 
705   /**
706    * Returns <code>true</code> if rulers are visible.
707    * @return <code>true</code> by default.
708    */
709   public boolean isRulersVisible() {
710     return this.rulersVisible;
711   }
712 
713   /**
714    * Sets whether rulers are visible or not, and notifies
715    * listeners of this change.
716    * @param rulersVisible <code>true</code> if rulers are visible,
717    *          <code>false</code> otherwise.
718    */
719   public void setRulersVisible(boolean rulersVisible) {
720     if (this.rulersVisible != rulersVisible) {
721       this.rulersVisible = rulersVisible;
722       this.propertyChangeSupport.firePropertyChange(Property.RULERS_VISIBLE.name(),
723           !rulersVisible, rulersVisible);
724     }
725   }
726 
727   /**
728    * Returns <code>true</code> if plan grid visible.
729    * @return <code>true</code> by default.
730    */
731   public boolean isGridVisible() {
732     return this.gridVisible;
733   }
734 
735   /**
736    * Sets whether plan grid is visible or not, and notifies
737    * listeners of this change.
738    * @param gridVisible <code>true</code> if grid is visible,
739    *          <code>false</code> otherwise.
740    */
741   public void setGridVisible(boolean gridVisible) {
742     if (this.gridVisible != gridVisible) {
743       this.gridVisible = gridVisible;
744       this.propertyChangeSupport.firePropertyChange(Property.GRID_VISIBLE.name(),
745           !gridVisible, gridVisible);
746     }
747   }
748 
749   /**
750    * Returns <code>true</code> is {@link HomeEnvironment#getDrawingMode() drawing mode}
751    * should be taken into account.
752    * @since 6.0
753    */
754   public boolean isDrawingModeEnabled() {
755     return this.drawingModeEnabled;
756   }
757 
758   /**
759    * Returns the name of the font that should be used by default or <code>null</code>
760    * if the default font should be the default one in the application.
761    * @since 5.0
762    */
763   public String getDefaultFontName() {
764     return this.defaultFontName;
765   }
766 
767   /**
768    * Sets the name of the font that should be used by default.
769    * @since 5.0
770    */
771   public void setDefaultFontName(String defaultFontName) {
772     if (defaultFontName != this.defaultFontName
773         && (defaultFontName == null || !defaultFontName.equals(this.defaultFontName))) {
774       String oldName = this.defaultFontName;
775       this.defaultFontName = defaultFontName;
776       this.propertyChangeSupport.firePropertyChange(Property.DEFAULT_FONT_NAME.name(), oldName, defaultFontName);
777     }
778   }
779 
780   /**
781    * Returns <code>true</code> if furniture should be viewed from its top in plan.
782    * @since 2.0
783    */
784   public boolean isFurnitureViewedFromTop() {
785     return this.furnitureViewedFromTop;
786   }
787 
788   /**
789    * Sets how furniture icon should be displayed in plan, and notifies
790    * listeners of this change.
791    * @param furnitureViewedFromTop if <code>true</code> the furniture
792    *    should be viewed from its top.
793    * @since 2.0
794    */
795   public void setFurnitureViewedFromTop(boolean furnitureViewedFromTop) {
796     if (this.furnitureViewedFromTop != furnitureViewedFromTop) {
797       this.furnitureViewedFromTop = furnitureViewedFromTop;
798       this.propertyChangeSupport.firePropertyChange(Property.FURNITURE_VIEWED_FROM_TOP.name(),
799           !furnitureViewedFromTop, furnitureViewedFromTop);
800     }
801   }
802 
803   /**
804    * Returns the size used to generate icons of furniture viewed from top.
805    * @since 5.5
806    */
807   public int getFurnitureModelIconSize() {
808     return this.furnitureModelIconSize;
809   }
810 
811   /**
812    * Sets the name of the font that should be used by default.
813    * @since 5.5
814    */
815   public void setFurnitureModelIconSize(int furnitureModelIconSize) {
816     if (furnitureModelIconSize != this.furnitureModelIconSize) {
817       int oldSize = this.furnitureModelIconSize;
818       this.furnitureModelIconSize = furnitureModelIconSize;
819       this.propertyChangeSupport.firePropertyChange(Property.FURNITURE_MODEL_ICON_SIZE.name(), oldSize, furnitureModelIconSize);
820     }
821   }
822 
823   /**
824    * Returns <code>true</code> if room floors should be rendered with color or texture
825    * in plan.
826    * @return <code>false</code> by default.
827    * @since 2.0
828    */
829   public boolean isRoomFloorColoredOrTextured() {
830     return this.roomFloorColoredOrTextured;
831   }
832 
833   /**
834    * Sets whether room floors should be rendered with color or texture,
835    * and notifies listeners of this change.
836    * @param roomFloorColoredOrTextured <code>true</code> if floor color
837    *          or texture is used, <code>false</code> otherwise.
838    * @since 2.0
839    */
840   public void setFloorColoredOrTextured(boolean roomFloorColoredOrTextured) {
841     if (this.roomFloorColoredOrTextured != roomFloorColoredOrTextured) {
842       this.roomFloorColoredOrTextured = roomFloorColoredOrTextured;
843       this.propertyChangeSupport.firePropertyChange(Property.ROOM_FLOOR_COLORED_OR_TEXTURED.name(),
844           !roomFloorColoredOrTextured, roomFloorColoredOrTextured);
845     }
846   }
847 
848   /**
849    * Returns the wall pattern in plan used by default.
850    * @since 2.0
851    */
852   public TextureImage getWallPattern() {
853     return this.wallPattern;
854   }
855 
856   /**
857    * Sets how walls should be displayed in plan by default, and notifies
858    * listeners of this change.
859    * @since 2.0
860    */
861   public void setWallPattern(TextureImage wallPattern) {
862     if (this.wallPattern != wallPattern) {
863       TextureImage oldWallPattern = this.wallPattern;
864       this.wallPattern = wallPattern;
865       this.propertyChangeSupport.firePropertyChange(Property.WALL_PATTERN.name(),
866           oldWallPattern, wallPattern);
867     }
868   }
869 
870   /**
871    * Returns the pattern used for new walls in plan or <code>null</code> if it's not set.
872    * @since 4.0
873    */
874   public TextureImage getNewWallPattern() {
875     return this.newWallPattern;
876   }
877 
878   /**
879    * Sets how new walls should be displayed in plan, and notifies
880    * listeners of this change.
881    * @since 4.0
882    */
883   public void setNewWallPattern(TextureImage newWallPattern) {
884     if (this.newWallPattern != newWallPattern) {
885       TextureImage oldWallPattern = this.newWallPattern;
886       this.newWallPattern = newWallPattern;
887       this.propertyChangeSupport.firePropertyChange(Property.NEW_WALL_PATTERN.name(),
888           oldWallPattern, newWallPattern);
889     }
890   }
891 
892   /**
893    * Returns default thickness of new walls in home.
894    */
895   public float getNewWallThickness() {
896     return this.newWallThickness;
897   }
898 
899   /**
900    * Sets default thickness of new walls in home, and notifies
901    * listeners of this change.
902    */
903   public void setNewWallThickness(float newWallThickness) {
904     if (this.newWallThickness != newWallThickness) {
905       float oldDefaultThickness = this.newWallThickness;
906       this.newWallThickness = newWallThickness;
907       this.propertyChangeSupport.firePropertyChange(Property.NEW_WALL_THICKNESS.name(),
908           oldDefaultThickness, newWallThickness);
909     }
910   }
911 
912   /**
913    * Returns default wall height of new home walls.
914    */
915   public float getNewWallHeight() {
916     return this.newWallHeight;
917   }
918 
919   /**
920    * Sets default wall height of new walls, and notifies
921    * listeners of this change.
922    */
923   public void setNewWallHeight(float newWallHeight) {
924     if (this.newWallHeight != newWallHeight) {
925       float oldWallHeight = this.newWallHeight;
926       this.newWallHeight = newWallHeight;
927       this.propertyChangeSupport.firePropertyChange(Property.NEW_WALL_HEIGHT.name(),
928           oldWallHeight, newWallHeight);
929     }
930   }
931 
932   /**
933    * Returns default baseboard thickness of new walls in home.
934    * @since 5.0
935    */
936   public float getNewWallBaseboardThickness() {
937     return this.newWallBaseboardThickness;
938   }
939 
940   /**
941    * Sets default baseboard thickness of new walls in home, and notifies
942    * listeners of this change.
943    * @since 5.0
944    */
945   public void setNewWallBaseboardThickness(float newWallBaseboardThickness) {
946     if (this.newWallBaseboardThickness != newWallBaseboardThickness) {
947       float oldThickness = this.newWallBaseboardThickness;
948       this.newWallBaseboardThickness = newWallBaseboardThickness;
949       this.propertyChangeSupport.firePropertyChange(Property.NEW_WALL_SIDEBOARD_THICKNESS.name(),
950           oldThickness, newWallBaseboardThickness);
951     }
952   }
953 
954   /**
955    * Returns default baseboard height of new home walls.
956    * @since 5.0
957    */
958   public float getNewWallBaseboardHeight() {
959     return this.newWallBaseboardHeight;
960   }
961 
962   /**
963    * Sets default baseboard height of new walls, and notifies
964    * listeners of this change.
965    * @since 5.0
966    */
967   public void setNewWallBaseboardHeight(float newWallBaseboardHeight) {
968     if (this.newWallBaseboardHeight != newWallBaseboardHeight) {
969       float oldHeight = this.newWallBaseboardHeight;
970       this.newWallBaseboardHeight = newWallBaseboardHeight;
971       this.propertyChangeSupport.firePropertyChange(Property.NEW_WALL_SIDEBOARD_HEIGHT.name(),
972           oldHeight, newWallBaseboardHeight);
973     }
974   }
975 
976   /**
977    * Returns the default color of new rooms in home.
978    * @since 6.4
979    */
980   public Integer getNewRoomFloorColor() {
981     return this.newRoomFloorColor;
982   }
983 
984   /**
985    * Sets the default color of new rooms in home, and notifies
986    * listeners of this change.
987    * @since 6.4
988    */
989   public void setNewRoomFloorColor(Integer newRoomFloorColor) {
990     if (this.newRoomFloorColor != newRoomFloorColor) {
991       Integer oldRoomFloorColor = this.newRoomFloorColor;
992       this.newRoomFloorColor = newRoomFloorColor;
993       this.propertyChangeSupport.firePropertyChange(Property.NEW_ROOM_FLOOR_COLOR.name(),
994           oldRoomFloorColor, newRoomFloorColor);
995     }
996   }
997 
998   /**
999    * Returns default thickness of the floor of new levels in home.
1000    * @since 3.4
1001    */
1002   public float getNewFloorThickness() {
1003     return this.newFloorThickness;
1004   }
1005 
1006   /**
1007    * Sets default thickness of the floor of new levels in home, and notifies
1008    * listeners of this change.
1009    * @since 3.4
1010    */
1011   public void setNewFloorThickness(float newFloorThickness) {
1012     if (this.newFloorThickness != newFloorThickness) {
1013       float oldFloorThickness = this.newFloorThickness;
1014       this.newFloorThickness = newFloorThickness;
1015       this.propertyChangeSupport.firePropertyChange(Property.NEW_FLOOR_THICKNESS.name(),
1016           oldFloorThickness, newFloorThickness);
1017     }
1018   }
1019 
1020   /**
1021    * Returns <code>true</code> if updates should be checked.
1022    * @since 4.0
1023    */
1024   public boolean isCheckUpdatesEnabled() {
1025     return this.checkUpdatesEnabled;
1026   }
1027 
1028   /**
1029    * Sets whether updates should be checked or not.
1030    * @since 4.0
1031    */
1032   public void setCheckUpdatesEnabled(boolean updatesChecked) {
1033     if (updatesChecked != this.checkUpdatesEnabled) {
1034       this.checkUpdatesEnabled = updatesChecked;
1035       this.propertyChangeSupport.firePropertyChange(Property.CHECK_UPDATES_ENABLED.name(),
1036           !updatesChecked, updatesChecked);
1037     }
1038   }
1039 
1040   /**
1041    * Returns the minimum date of updates that may interest user.
1042    * @return the date expressed in millis second since the epoch or <code>null</code> if not defined.
1043    * @since 4.0
1044    */
1045   public Long getUpdatesMinimumDate() {
1046     return this.updatesMinimumDate;
1047   }
1048 
1049   /**
1050    * Sets the minimum date of updates that may interest user, and notifies
1051    * listeners of this change.
1052    * @since 4.0
1053    */
1054   public void setUpdatesMinimumDate(Long updatesMinimumDate) {
1055     if (this.updatesMinimumDate != updatesMinimumDate
1056         && (updatesMinimumDate == null || !updatesMinimumDate.equals(this.updatesMinimumDate))) {
1057       Long oldUpdatesMinimumDate = this.updatesMinimumDate;
1058       this.updatesMinimumDate = updatesMinimumDate;
1059       this.propertyChangeSupport.firePropertyChange(Property.UPDATES_MINIMUM_DATE.name(),
1060           oldUpdatesMinimumDate, updatesMinimumDate);
1061     }
1062   }
1063 
1064   /**
1065    * Returns the delay between two automatic save operations of homes for recovery purpose.
1066    * @return a delay in milliseconds or 0 to disable auto save.
1067    * @since 3.0
1068    */
1069   public int getAutoSaveDelayForRecovery() {
1070     return this.autoSaveDelayForRecovery;
1071   }
1072 
1073   /**
1074    * Sets the delay between two automatic save operations of homes for recovery purpose.
1075    * @since 3.0
1076    */
1077   public void setAutoSaveDelayForRecovery(int autoSaveDelayForRecovery) {
1078     if (this.autoSaveDelayForRecovery != autoSaveDelayForRecovery) {
1079       float oldAutoSaveDelayForRecovery = this.autoSaveDelayForRecovery;
1080       this.autoSaveDelayForRecovery = autoSaveDelayForRecovery;
1081       this.propertyChangeSupport.firePropertyChange(Property.AUTO_SAVE_DELAY_FOR_RECOVERY.name(),
1082           oldAutoSaveDelayForRecovery, autoSaveDelayForRecovery);
1083     }
1084   }
1085 
1086   /**
1087    * Returns an unmodifiable list of the recent homes.
1088    */
1089   public List<String> getRecentHomes() {
1090     return Collections.unmodifiableList(this.recentHomes);
1091   }
1092 
1093   /**
1094    * Sets the recent homes list and notifies listeners of this change.
1095    */
1096   public void setRecentHomes(List<String> recentHomes) {
1097     if (!recentHomes.equals(this.recentHomes)) {
1098       List<String> oldRecentHomes = this.recentHomes;
1099       this.recentHomes = new ArrayList<String>(recentHomes);
1100       this.propertyChangeSupport.firePropertyChange(Property.RECENT_HOMES.name(),
1101           oldRecentHomes, getRecentHomes());
1102     }
1103   }
1104 
1105   /**
1106    * Returns the maximum count of homes that should be proposed to the user.
1107    */
1108   public int getRecentHomesMaxCount() {
1109     return 10;
1110   }
1111 
1112   /**
1113    * Returns the maximum count of stored cameras in homes that should be proposed to the user.
1114    * @since 4.5
1115    */
1116   public int getStoredCamerasMaxCount() {
1117     return 50;
1118   }
1119 
1120   /**
1121    * Sets which action tip should be ignored.
1122    * <br>This method should be overridden to store the ignore information.
1123    * By default it just notifies listeners of this change.
1124    */
1125   public void setActionTipIgnored(String actionKey) {
1126     this.propertyChangeSupport.firePropertyChange(Property.IGNORED_ACTION_TIP.name(), null, actionKey);
1127   }
1128 
1129   /**
1130    * Returns whether an action tip should be ignored or not.
1131    * <br>This method should be overridden to return the display information
1132    * stored in {@link #setActionTipIgnored(String) setActionTipIgnored}.
1133    * By default it returns <code>true</code>.
1134    */
1135   public boolean isActionTipIgnored(String actionKey) {
1136     return true;
1137   }
1138 
1139   /**
1140    * Resets the ignore flag of action tips.
1141    * <br>This method should be overridden to clear all the display flags.
1142    * By default it just notifies listeners of this change.
1143    */
1144   public void resetIgnoredActionTips() {
1145     this.propertyChangeSupport.firePropertyChange(Property.IGNORED_ACTION_TIP.name(), null, null);
1146   }
1147 
1148   /**
1149    * Returns the default text style of a class of selectable item.
1150    */
1151   public TextStyle getDefaultTextStyle(Class<? extends Selectable> selectableClass) {
1152     if (Room.class.isAssignableFrom(selectableClass)) {
1153       return DEFAULT_ROOM_TEXT_STYLE;
1154     } else {
1155       return DEFAULT_TEXT_STYLE;
1156     }
1157   }
1158 
1159   /**
1160    * Returns the strings that may be used for the auto completion of the given <code>property</code>.
1161    * @since 3.4
1162    */
1163   public List<String> getAutoCompletionStrings(String property) {
1164     List<String> propertyAutoCompletionStrings = this.autoCompletionStrings.get(property);
1165     if (propertyAutoCompletionStrings != null) {
1166       return Collections.unmodifiableList(propertyAutoCompletionStrings);
1167     } else {
1168       return Collections.emptyList();
1169     }
1170   }
1171 
1172   /**
1173    * Adds the given string to the list of the strings used in auto completion of a <code>property</code>
1174    * and notifies listeners of this change.
1175    * @since 3.4
1176    */
1177   public void addAutoCompletionString(String property, String autoCompletionString) {
1178     if (autoCompletionString != null
1179         && autoCompletionString.length() > 0) {
1180       List<String> propertyAutoCompletionStrings = this.autoCompletionStrings.get(property);
1181       if (propertyAutoCompletionStrings == null) {
1182         propertyAutoCompletionStrings = new ArrayList<String>();
1183       } else if (!propertyAutoCompletionStrings.contains(autoCompletionString)) {
1184         propertyAutoCompletionStrings = new ArrayList<String>(propertyAutoCompletionStrings);
1185       } else {
1186         return;
1187       }
1188       propertyAutoCompletionStrings.add(0, autoCompletionString);
1189       setAutoCompletionStrings(property, propertyAutoCompletionStrings);
1190     }
1191   }
1192 
1193   /**
1194    * Sets the auto completion strings list of the given <code>property</code> and notifies listeners of this change.
1195    * @since 3.4
1196    */
1197   public void setAutoCompletionStrings(String property, List<String> autoCompletionStrings) {
1198     List<String> propertyAutoCompletionStrings = this.autoCompletionStrings.get(property);
1199     if (!autoCompletionStrings.equals(propertyAutoCompletionStrings)) {
1200       this.autoCompletionStrings.put(property, new ArrayList<String>(autoCompletionStrings));
1201       this.propertyChangeSupport.firePropertyChange(Property.AUTO_COMPLETION_STRINGS.name(),
1202           null, property);
1203     }
1204   }
1205 
1206   /**
1207    * Returns the list of properties with auto completion strings.
1208    * @since 3.4
1209    */
1210   public List<String> getAutoCompletedProperties() {
1211     if (this.autoCompletionStrings != null) {
1212       return Arrays.asList(this.autoCompletionStrings.keySet().toArray(new String [this.autoCompletionStrings.size()]));
1213     } else {
1214       return Collections.emptyList();
1215     }
1216   }
1217 
1218   /**
1219    * Returns an unmodifiable list of the recent colors.
1220    * @since 4.0
1221    */
1222   public List<Integer> getRecentColors() {
1223     return Collections.unmodifiableList(this.recentColors);
1224   }
1225 
1226   /**
1227    * Sets the recent colors list and notifies listeners of this change.
1228    * @since 4.0
1229    */
1230   public void setRecentColors(List<Integer> recentColors) {
1231     if (!recentColors.equals(this.recentColors)) {
1232       List<Integer> oldRecentColors = this.recentColors;
1233       this.recentColors = new ArrayList<Integer>(recentColors);
1234       this.propertyChangeSupport.firePropertyChange(Property.RECENT_COLORS.name(),
1235           oldRecentColors, getRecentColors());
1236     }
1237   }
1238 
1239   /**
1240    * Returns an unmodifiable list of the recent textures.
1241    * @since 4.4
1242    */
1243   public List<TextureImage> getRecentTextures() {
1244     return Collections.unmodifiableList(this.recentTextures);
1245   }
1246 
1247   /**
1248    * Sets the recent colors list and notifies listeners of this change.
1249    * @since 4.4
1250    */
1251   public void setRecentTextures(List<TextureImage> recentTextures) {
1252     if (!recentTextures.equals(this.recentTextures)) {
1253       List<TextureImage> oldRecentTextures = this.recentTextures;
1254       this.recentTextures = new ArrayList<TextureImage>(recentTextures);
1255       this.propertyChangeSupport.firePropertyChange(Property.RECENT_TEXTURES.name(),
1256           oldRecentTextures, getRecentTextures());
1257     }
1258   }
1259 
1260   /**
1261    * Sets the home examples available for the user.
1262    * @since 5.5
1263    */
1264   protected void setHomeExamples(List<HomeDescriptor> homeExamples) {
1265     if (!homeExamples.equals(this.homeExamples)) {
1266       List<HomeDescriptor> oldExamples = this.homeExamples;
1267       this.homeExamples = new ArrayList<HomeDescriptor>(homeExamples);
1268       this.propertyChangeSupport.firePropertyChange(Property.HOME_EXAMPLES.name(),
1269           oldExamples, getHomeExamples());
1270     }
1271   }
1272 
1273   /**
1274    * Returns the home examples available for the user.
1275    * @since 5.5
1276    */
1277   public List<HomeDescriptor> getHomeExamples() {
1278     return Collections.unmodifiableList(this.homeExamples);
1279   }
1280 
1281   /**
1282    * Adds the language library to make the languages it contains available to supported languages.
1283    * @param languageLibraryLocation  the location where the library can be found.
1284    * @since 2.3
1285    */
1286   public abstract void addLanguageLibrary(String languageLibraryLocation) throws RecorderException;
1287 
1288   /**
1289    * Returns <code>true</code> if the language library at the given location exists.
1290    * @param languageLibraryLocation the name of the resource to check
1291    * @since 2.3
1292    */
1293   public abstract boolean languageLibraryExists(String languageLibraryLocation) throws RecorderException;
1294 
1295   /**
1296    * Adds <code>furnitureLibraryName</code> to furniture catalog
1297    * to make the furniture it contains available.
1298    * @param furnitureLibraryLocation  the location where the library can be found.
1299    */
1300   public abstract void addFurnitureLibrary(String furnitureLibraryLocation) throws RecorderException;
1301 
1302   /**
1303    * Returns <code>true</code> if the furniture library at the given location exists.
1304    * @param furnitureLibraryLocation the name of the resource to check
1305    */
1306   public abstract boolean furnitureLibraryExists(String furnitureLibraryLocation) throws RecorderException;
1307 
1308   /**
1309    * Adds the textures library at the given location to textures catalog
1310    * to make the textures it contains available.
1311    * @param texturesLibraryLocation  the location where the library can be found.
1312    * @since 2.3
1313    */
1314   public abstract void addTexturesLibrary(String texturesLibraryLocation) throws RecorderException;
1315 
1316   /**
1317    * Returns <code>true</code> if the textures library at the given location exists.
1318    * @param texturesLibraryLocation the name of the resource to check
1319    * @since 2.3
1320    */
1321   public abstract boolean texturesLibraryExists(String texturesLibraryLocation) throws RecorderException;
1322 
1323   /**
1324    * Returns the libraries available in user preferences.
1325    * @since 4.0
1326    */
1327   public abstract List<Library> getLibraries();
1328 
1329   /**
1330    * A resource bundle with a prefix added to resource key.
1331    */
1332   private static class PrefixedResourceBundle extends ResourceBundle {
1333     private ResourceBundle resourceBundle;
1334     private String         keyPrefix;
1335 
1336     public PrefixedResourceBundle(ResourceBundle resourceBundle,
1337                                   String keyPrefix) {
1338       this.resourceBundle = resourceBundle;
1339       this.keyPrefix = keyPrefix;
1340     }
1341 
1342     @Override
1343     public Locale getLocale() {
1344       return this.resourceBundle.getLocale();
1345     }
1346 
1347     @Override
1348     protected Object handleGetObject(String key) {
1349       key = this.keyPrefix + key;
1350       return this.resourceBundle.getObject(key);
1351     }
1352 
1353     @Override
1354     public Enumeration<String> getKeys() {
1355       return this.resourceBundle.getKeys();
1356     }
1357   }
1358 }
1359