1 /*
2  * HomeController.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.viewcontroller;
21 
22 import java.beans.PropertyChangeEvent;
23 import java.beans.PropertyChangeListener;
24 import java.io.IOException;
25 import java.io.InterruptedIOException;
26 import java.lang.ref.WeakReference;
27 import java.net.MalformedURLException;
28 import java.net.URL;
29 import java.net.URLConnection;
30 import java.security.AccessControlException;
31 import java.text.DateFormat;
32 import java.text.DecimalFormat;
33 import java.text.ParseException;
34 import java.text.SimpleDateFormat;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.Collections;
38 import java.util.Comparator;
39 import java.util.Date;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.LinkedHashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.Set;
46 import java.util.TimeZone;
47 import java.util.concurrent.Callable;
48 
49 import javax.swing.event.UndoableEditEvent;
50 import javax.swing.event.UndoableEditListener;
51 import javax.swing.undo.CannotRedoException;
52 import javax.swing.undo.CannotUndoException;
53 import javax.swing.undo.CompoundEdit;
54 import javax.swing.undo.UndoManager;
55 import javax.swing.undo.UndoableEdit;
56 import javax.swing.undo.UndoableEditSupport;
57 import javax.xml.parsers.ParserConfigurationException;
58 import javax.xml.parsers.SAXParser;
59 import javax.xml.parsers.SAXParserFactory;
60 
61 import org.xml.sax.Attributes;
62 import org.xml.sax.SAXException;
63 import org.xml.sax.helpers.DefaultHandler;
64 
65 import com.eteks.sweethome3d.model.AspectRatio;
66 import com.eteks.sweethome3d.model.BackgroundImage;
67 import com.eteks.sweethome3d.model.Camera;
68 import com.eteks.sweethome3d.model.CatalogPieceOfFurniture;
69 import com.eteks.sweethome3d.model.CatalogTexture;
70 import com.eteks.sweethome3d.model.CollectionEvent;
71 import com.eteks.sweethome3d.model.CollectionListener;
72 import com.eteks.sweethome3d.model.Compass;
73 import com.eteks.sweethome3d.model.Content;
74 import com.eteks.sweethome3d.model.DamagedHomeRecorderException;
75 import com.eteks.sweethome3d.model.DimensionLine;
76 import com.eteks.sweethome3d.model.Elevatable;
77 import com.eteks.sweethome3d.model.FurnitureCatalog;
78 import com.eteks.sweethome3d.model.FurnitureCategory;
79 import com.eteks.sweethome3d.model.Home;
80 import com.eteks.sweethome3d.model.HomeApplication;
81 import com.eteks.sweethome3d.model.HomeDoorOrWindow;
82 import com.eteks.sweethome3d.model.HomeEnvironment;
83 import com.eteks.sweethome3d.model.HomeFurnitureGroup;
84 import com.eteks.sweethome3d.model.HomeMaterial;
85 import com.eteks.sweethome3d.model.HomePieceOfFurniture;
86 import com.eteks.sweethome3d.model.HomeRecorder;
87 import com.eteks.sweethome3d.model.HomeTexture;
88 import com.eteks.sweethome3d.model.InterruptedRecorderException;
89 import com.eteks.sweethome3d.model.Label;
90 import com.eteks.sweethome3d.model.Level;
91 import com.eteks.sweethome3d.model.Library;
92 import com.eteks.sweethome3d.model.NotEnoughSpaceRecorderException;
93 import com.eteks.sweethome3d.model.Polyline;
94 import com.eteks.sweethome3d.model.RecorderException;
95 import com.eteks.sweethome3d.model.Room;
96 import com.eteks.sweethome3d.model.Selectable;
97 import com.eteks.sweethome3d.model.SelectionEvent;
98 import com.eteks.sweethome3d.model.SelectionListener;
99 import com.eteks.sweethome3d.model.TextStyle;
100 import com.eteks.sweethome3d.model.TextureImage;
101 import com.eteks.sweethome3d.model.TexturesCatalog;
102 import com.eteks.sweethome3d.model.UserPreferences;
103 import com.eteks.sweethome3d.model.Wall;
104 import com.eteks.sweethome3d.tools.OperatingSystem;
105 import com.eteks.sweethome3d.tools.ResourceURLContent;
106 
107 /**
108  * A MVC controller for the home view.
109  * @author Emmanuel Puybaret
110  */
111 public class HomeController implements Controller {
112   private final Home                  home;
113   private final UserPreferences       preferences;
114   private final HomeApplication       application;
115   private final ViewFactory           viewFactory;
116   private final ContentManager        contentManager;
117   private final UndoableEditSupport   undoSupport;
118   private final UndoManager           undoManager;
119   private HomeView                    homeView;
120   private List<Controller>            childControllers;
121   private FurnitureCatalogController  furnitureCatalogController;
122   private FurnitureController         furnitureController;
123   private PlanController              planController;
124   private HomeController3D            homeController3D;
125   private static HelpController       helpController;  // Only one help controller
126   private int                         saveUndoLevel;
127   private boolean                     notUndoableModifications;
128   private View                        focusedView;
129 
130   private static final Content REPAIRED_IMAGE_CONTENT = new ResourceURLContent(HomeController.class, "resources/repairedImage.png");
131   private static final Content REPAIRED_ICON_CONTENT = new ResourceURLContent(HomeController.class, "resources/repairedIcon.png");
132   private static final Content REPAIRED_MODEL_CONTENT = new ResourceURLContent(HomeController.class, "resources/repairedModel.obj");
133 
134   /**
135    * Creates the controller of home view.
136    * @param home the home edited by this controller and its view.
137    * @param application the instance of current application.
138    * @param viewFactory a factory able to create views.
139    * @param contentManager the content manager of the application.
140    */
HomeController(Home home, HomeApplication application, ViewFactory viewFactory, ContentManager contentManager)141   public HomeController(Home home,
142                         HomeApplication application,
143                         ViewFactory    viewFactory,
144                         ContentManager contentManager) {
145     this(home, application, application.getUserPreferences(),
146         viewFactory, contentManager);
147   }
148 
149   /**
150    * Creates the controller of home view.
151    * @param home the home edited by this controller and its view.
152    * @param application the instance of current application.
153    * @param viewFactory a factory able to create views.
154    */
HomeController(Home home, HomeApplication application, ViewFactory viewFactory)155   public HomeController(Home home,
156                         HomeApplication application,
157                         ViewFactory viewFactory) {
158     this(home, application, application.getUserPreferences(), viewFactory, null);
159   }
160 
161   /**
162    * Creates the controller of home view.
163    * @param home        the home edited by this controller and its view.
164    * @param preferences the preferences of the application.
165    * @param viewFactory a factory able to create views.
166    */
HomeController(Home home, UserPreferences preferences, ViewFactory viewFactory)167   public HomeController(Home home,
168                         UserPreferences preferences,
169                         ViewFactory viewFactory) {
170     this(home, null, preferences, viewFactory, null);
171   }
172 
173   /**
174    * Creates the controller of home view.
175    * @param home        the home edited by this controller and its view.
176    * @param preferences the preferences of the application.
177    * @param viewFactory a factory able to create views.
178    * @param contentManager the content manager of the application.
179    */
HomeController(Home home, UserPreferences preferences, ViewFactory viewFactory, ContentManager contentManager)180   public HomeController(Home home,
181                         UserPreferences preferences,
182                         ViewFactory    viewFactory,
183                         ContentManager contentManager) {
184     this(home, null, preferences, viewFactory, contentManager);
185   }
186 
HomeController(final Home home, HomeApplication application, final UserPreferences preferences, ViewFactory viewFactory, ContentManager contentManager)187   private HomeController(final Home home,
188                          HomeApplication application,
189                          final UserPreferences preferences,
190                          ViewFactory    viewFactory,
191                          ContentManager contentManager) {
192     this.home = home;
193     this.preferences = preferences;
194     this.viewFactory = viewFactory;
195     this.contentManager = contentManager;
196     this.application = application;
197 
198     this.undoSupport = new UndoableEditSupport() {
199         @Override
200         protected void _postEdit(UndoableEdit edit) {
201           // Ignore not significant compound edit
202           if (!(edit instanceof CompoundEdit)
203               || edit.isSignificant()) {
204             super._postEdit(edit);
205           }
206         }
207       };
208     this.undoManager = new UndoManager();
209     this.undoSupport.addUndoableEditListener(this.undoManager);
210 
211     // Update recent homes list
212     if (home.getName() != null) {
213       List<String> recentHomes = new ArrayList<String>(this.preferences.getRecentHomes());
214       recentHomes.remove(home.getName());
215       recentHomes.add(0, home.getName());
216       updateUserPreferencesRecentHomes(recentHomes);
217     }
218   }
219 
220   /**
221    * Enables actions at controller instantiation.
222    */
enableDefaultActions(HomeView homeView)223   private void enableDefaultActions(HomeView homeView) {
224     boolean applicationExists = this.application != null;
225 
226     homeView.setEnabled(HomeView.ActionType.NEW_HOME, applicationExists);
227     homeView.setEnabled(HomeView.ActionType.NEW_HOME_FROM_EXAMPLE, applicationExists);
228     homeView.setEnabled(HomeView.ActionType.OPEN, applicationExists);
229     homeView.setEnabled(HomeView.ActionType.DELETE_RECENT_HOMES,
230         applicationExists && !this.preferences.getRecentHomes().isEmpty());
231     homeView.setEnabled(HomeView.ActionType.CLOSE, applicationExists);
232     homeView.setEnabled(HomeView.ActionType.SAVE, applicationExists);
233     homeView.setEnabled(HomeView.ActionType.SAVE_AS, applicationExists);
234     homeView.setEnabled(HomeView.ActionType.SAVE_AND_COMPRESS, applicationExists);
235     homeView.setEnabled(HomeView.ActionType.PAGE_SETUP, true);
236     homeView.setEnabled(HomeView.ActionType.PRINT_PREVIEW, true);
237     homeView.setEnabled(HomeView.ActionType.PRINT, true);
238     homeView.setEnabled(HomeView.ActionType.PRINT_TO_PDF, true);
239     homeView.setEnabled(HomeView.ActionType.PREFERENCES, true);
240     homeView.setEnabled(HomeView.ActionType.EXIT, applicationExists);
241     homeView.setEnabled(HomeView.ActionType.IMPORT_FURNITURE, true);
242     homeView.setEnabled(HomeView.ActionType.IMPORT_FURNITURE_LIBRARY, true);
243     homeView.setEnabled(HomeView.ActionType.IMPORT_TEXTURE, true);
244     homeView.setEnabled(HomeView.ActionType.IMPORT_TEXTURES_LIBRARY, true);
245     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_CATALOG_ID, true);
246     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_NAME, true);
247     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_CREATOR, true);
248     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_WIDTH, true);
249     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_HEIGHT, true);
250     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_DEPTH, true);
251     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_X, true);
252     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_Y, true);
253     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_ELEVATION, true);
254     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_ANGLE, true);
255     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_LEVEL, true);
256     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_MODEL_SIZE, true);
257     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_COLOR, true);
258     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_TEXTURE, true);
259     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_MOVABILITY, true);
260     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_TYPE, true);
261     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_VISIBILITY, true);
262     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_PRICE, true);
263     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_VALUE_ADDED_TAX_PERCENTAGE, true);
264     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_VALUE_ADDED_TAX, true);
265     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_PRICE_VALUE_ADDED_TAX_INCLUDED, true);
266     homeView.setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_DESCENDING_ORDER,
267         this.home.getFurnitureSortedProperty() != null);
268     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_CATALOG_ID, true);
269     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_NAME, true);
270     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_CREATOR, true);
271     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_WIDTH, true);
272     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_DEPTH, true);
273     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_HEIGHT, true);
274     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_X, true);
275     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_Y, true);
276     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_ELEVATION, true);
277     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_ANGLE, true);
278     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_LEVEL, true);
279     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_MODEL_SIZE, true);
280     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_COLOR, true);
281     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_TEXTURE, true);
282     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_MOVABLE, true);
283     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_DOOR_OR_WINDOW, true);
284     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_VISIBLE, true);
285     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_PRICE, true);
286     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_VALUE_ADDED_TAX_PERCENTAGE, true);
287     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_VALUE_ADDED_TAX, true);
288     homeView.setEnabled(HomeView.ActionType.DISPLAY_HOME_FURNITURE_PRICE_VALUE_ADDED_TAX_INCLUDED, true);
289     homeView.setEnabled(HomeView.ActionType.EXPORT_TO_CSV, true);
290     homeView.setEnabled(HomeView.ActionType.SELECT, true);
291     homeView.setEnabled(HomeView.ActionType.PAN, true);
292     homeView.setEnabled(HomeView.ActionType.LOCK_BASE_PLAN, true);
293     homeView.setEnabled(HomeView.ActionType.UNLOCK_BASE_PLAN, true);
294     homeView.setEnabled(HomeView.ActionType.ENABLE_MAGNETISM, true);
295     homeView.setEnabled(HomeView.ActionType.DISABLE_MAGNETISM, true);
296     homeView.setEnabled(HomeView.ActionType.MODIFY_COMPASS, true);
297     Level selectedLevel = this.home.getSelectedLevel();
298     enableBackgroungImageActions(homeView, selectedLevel != null
299         ? selectedLevel.getBackgroundImage()
300         : this.home.getBackgroundImage());
301     enableLevelActions(homeView);
302     homeView.setEnabled(HomeView.ActionType.ZOOM_IN, true);
303     homeView.setEnabled(HomeView.ActionType.ZOOM_OUT, true);
304     homeView.setEnabled(HomeView.ActionType.EXPORT_TO_SVG, true);
305     homeView.setEnabled(HomeView.ActionType.VIEW_FROM_TOP, true);
306     homeView.setEnabled(HomeView.ActionType.VIEW_FROM_OBSERVER, true);
307     homeView.setEnabled(HomeView.ActionType.MODIFY_OBSERVER, this.home.getCamera() == this.home.getObserverCamera());
308     homeView.setEnabled(HomeView.ActionType.STORE_POINT_OF_VIEW, true);
309     boolean emptyStoredCameras = home.getStoredCameras().isEmpty();
310     homeView.setEnabled(HomeView.ActionType.DELETE_POINTS_OF_VIEW, !emptyStoredCameras);
311     homeView.setEnabled(HomeView.ActionType.CREATE_PHOTOS_AT_POINTS_OF_VIEW, !emptyStoredCameras);
312     homeView.setEnabled(HomeView.ActionType.DETACH_3D_VIEW, true);
313     homeView.setEnabled(HomeView.ActionType.ATTACH_3D_VIEW, true);
314     homeView.setEnabled(HomeView.ActionType.VIEW_FROM_OBSERVER, true);
315     homeView.setEnabled(HomeView.ActionType.MODIFY_3D_ATTRIBUTES, true);
316     homeView.setEnabled(HomeView.ActionType.CREATE_PHOTO, true);
317     homeView.setEnabled(HomeView.ActionType.CREATE_VIDEO, true);
318     homeView.setEnabled(HomeView.ActionType.EXPORT_TO_OBJ, true);
319     homeView.setEnabled(HomeView.ActionType.HELP, true);
320     homeView.setEnabled(HomeView.ActionType.ABOUT, true);
321     enableCreationToolsActions(homeView);
322     homeView.setTransferEnabled(true);
323   }
324 
325   /**
326    * Enables actions handling levels.
327    */
enableLevelActions(HomeView homeView)328   private void enableLevelActions(HomeView homeView) {
329     boolean modificationState = getPlanController().isModificationState();
330     homeView.setEnabled(HomeView.ActionType.ADD_LEVEL, !modificationState);
331     homeView.setEnabled(HomeView.ActionType.ADD_LEVEL_AT_SAME_ELEVATION, !modificationState);
332     List<Level> levels = this.home.getLevels();
333     Level selectedLevel = this.home.getSelectedLevel();
334     boolean homeContainsOneSelectedLevel = levels.size() > 1 && selectedLevel != null;
335     homeView.setEnabled(HomeView.ActionType.SELECT_ALL_AT_ALL_LEVELS, !modificationState && levels.size() > 1);
336     homeView.setEnabled(HomeView.ActionType.MAKE_LEVEL_VIEWABLE, !modificationState && homeContainsOneSelectedLevel);
337     homeView.setEnabled(HomeView.ActionType.MAKE_LEVEL_UNVIEWABLE, !modificationState && homeContainsOneSelectedLevel);
338     homeView.setEnabled(HomeView.ActionType.MAKE_LEVEL_ONLY_VIEWABLE_ONE, homeContainsOneSelectedLevel);
339     homeView.setEnabled(HomeView.ActionType.MAKE_ALL_LEVELS_VIEWABLE, levels.size() > 1);
340     homeView.setEnabled(HomeView.ActionType.MODIFY_LEVEL, homeContainsOneSelectedLevel);
341     homeView.setEnabled(HomeView.ActionType.DELETE_LEVEL, !modificationState && homeContainsOneSelectedLevel);
342     homeView.setEnabled(HomeView.ActionType.DISPLAY_ALL_LEVELS, levels.size() > 1);
343     homeView.setEnabled(HomeView.ActionType.DISPLAY_SELECTED_LEVEL, levels.size() > 1);
344   }
345 
346   /**
347    * Enables plan actions depending on the selected level is viewable or not.
348    */
enableCreationToolsActions(HomeView homeView)349   private void enableCreationToolsActions(HomeView homeView) {
350     Level selectedLevel = this.home.getSelectedLevel();
351     boolean viewableLevel = selectedLevel == null || selectedLevel.isViewable();
352     homeView.setEnabled(HomeView.ActionType.CREATE_WALLS, viewableLevel);
353     homeView.setEnabled(HomeView.ActionType.CREATE_ROOMS, viewableLevel);
354     homeView.setEnabled(HomeView.ActionType.CREATE_POLYLINES, viewableLevel);
355     homeView.setEnabled(HomeView.ActionType.CREATE_DIMENSION_LINES, viewableLevel);
356     homeView.setEnabled(HomeView.ActionType.CREATE_LABELS, viewableLevel);
357   }
358 
359   /**
360    * Returns the view associated with this controller.
361    */
getView()362   public HomeView getView() {
363     if (this.homeView == null) {
364       this.homeView = this.viewFactory.createHomeView(this.home, this.preferences, this);
365       enableDefaultActions(this.homeView);
366       addListeners();
367 
368       // If home version is more recent than current version
369       if (this.home.getName() != null
370           && this.home.getVersion() > Home.CURRENT_VERSION) {
371         // Warn the user that view will display a home created with a more recent version
372         this.homeView.invokeLater(new Runnable() {
373             public void run() {
374               String message = preferences.getLocalizedString(HomeController.class,
375                   "moreRecentVersionHome", home.getName());
376               getView().showMessage(message);
377             }
378           });
379       }
380     }
381     return this.homeView;
382   }
383 
384   /**
385    * Returns the content manager of this controller.
386    */
getContentManager()387   public ContentManager getContentManager() {
388     return this.contentManager;
389   }
390 
391   /**
392    * Returns the furniture catalog controller managed by this controller.
393    */
getFurnitureCatalogController()394   public FurnitureCatalogController getFurnitureCatalogController() {
395     // Create sub controller lazily only once it's needed
396     if (this.furnitureCatalogController == null) {
397       this.furnitureCatalogController = new FurnitureCatalogController(
398           this.preferences.getFurnitureCatalog(), this.preferences, this.viewFactory, this.contentManager);
399     }
400     return this.furnitureCatalogController;
401   }
402 
403   /**
404    * Returns the furniture controller managed by this controller.
405    */
getFurnitureController()406   public FurnitureController getFurnitureController() {
407     // Create sub controller lazily only once it's needed
408     if (this.furnitureController == null) {
409       this.furnitureController = new FurnitureController(
410           this.home, this.preferences, this.viewFactory, this.contentManager, getUndoableEditSupport());
411     }
412     return this.furnitureController;
413   }
414 
415   /**
416    * Returns the controller of home plan.
417    */
getPlanController()418   public PlanController getPlanController() {
419     // Create sub controller lazily only once it's needed
420     if (this.planController == null) {
421       this.planController = new PlanController(
422           this.home, this.preferences, this.viewFactory, this.contentManager, getUndoableEditSupport());
423     }
424     return this.planController;
425   }
426 
427   /**
428    * Returns the controller of home 3D view.
429    */
getHomeController3D()430   public HomeController3D getHomeController3D() {
431     // Create sub controller lazily only once it's needed
432     if (this.homeController3D == null) {
433       this.homeController3D = new HomeController3D(
434           this.home, this.preferences, this.viewFactory, this.contentManager, getUndoableEditSupport());
435     }
436     return this.homeController3D;
437   }
438 
439   /**
440    * Returns the undoable edit support managed by this controller.
441    */
getUndoableEditSupport()442   protected final UndoableEditSupport getUndoableEditSupport() {
443     return this.undoSupport;
444   }
445 
446   /**
447    * Adds listeners that updates the enabled / disabled state of actions.
448    */
addListeners()449   private void addListeners() {
450     // Save preferences when they change
451     this.preferences.getFurnitureCatalog().addFurnitureListener(
452         new FurnitureCatalogChangeListener(this));
453     this.preferences.getTexturesCatalog().addTexturesListener(
454         new TexturesCatalogChangeListener(this));
455     // Listen to all property changes to save them when any of them change
456     this.preferences.addPropertyChangeListener(new UserPreferencesPropertiesChangeListener(this));
457 
458     addCatalogSelectionListener();
459     addHomeBackgroundImageListener();
460     addNotUndoableModificationListeners();
461     addHomeSelectionListener();
462     addFurnitureSortListener();
463     addUndoSupportListener();
464     addHomeItemsListener();
465     addLevelListeners();
466     addStoredCamerasListener();
467     addPlanControllerListeners();
468     addLanguageListener();
469   }
470 
471   /**
472    * Super class of catalog listeners that writes preferences each time a piece of furniture or a texture
473    * is deleted or added in furniture or textures catalog.
474    */
475   private abstract static class UserPreferencesChangeListener {
476     // Stores the currently writing preferences
477     private static Set<UserPreferences> writingPreferences = new HashSet<UserPreferences>();
478 
writePreferences(final HomeController controller)479     public void writePreferences(final HomeController controller) {
480       if (!writingPreferences.contains(controller.preferences)) {
481         writingPreferences.add(controller.preferences);
482         // Write preferences later once all catalog modifications are notified
483         controller.getView().invokeLater(new Runnable() {
484             public void run() {
485               try {
486                 controller.preferences.write();
487                 writingPreferences.remove(controller.preferences);
488               } catch (RecorderException ex) {
489                 controller.getView().showError(controller.preferences.getLocalizedString(
490                     HomeController.class, "savePreferencesError"));
491               }
492             }
493           });
494       }
495     }
496   }
497 
498   /**
499    * Furniture catalog listener that writes preferences each time a piece of furniture
500    * is deleted or added in furniture catalog. This listener is bound to this controller
501    * with a weak reference to avoid strong link between catalog and this controller.
502    */
503   private static class FurnitureCatalogChangeListener extends UserPreferencesChangeListener
504                                                       implements CollectionListener<CatalogPieceOfFurniture> {
505     private WeakReference<HomeController> homeController;
506 
FurnitureCatalogChangeListener(HomeController homeController)507     public FurnitureCatalogChangeListener(HomeController homeController) {
508       this.homeController = new WeakReference<HomeController>(homeController);
509     }
510 
collectionChanged(CollectionEvent<CatalogPieceOfFurniture> ev)511     public void collectionChanged(CollectionEvent<CatalogPieceOfFurniture> ev) {
512       // If controller was garbage collected, remove this listener from catalog
513       final HomeController controller = this.homeController.get();
514       if (controller == null) {
515         ((FurnitureCatalog)ev.getSource()).removeFurnitureListener(this);
516       } else {
517         writePreferences(controller);
518       }
519     }
520   }
521 
522   /**
523    * Textures catalog listener that writes preferences each time a texture
524    * is deleted or added in textures catalog. This listener is bound to this controller
525    * with a weak reference to avoid strong link between catalog and this controller.
526    */
527   private static class TexturesCatalogChangeListener extends UserPreferencesChangeListener
528                                                      implements CollectionListener<CatalogTexture> {
529     private WeakReference<HomeController> homeController;
530 
TexturesCatalogChangeListener(HomeController homeController)531     public TexturesCatalogChangeListener(HomeController homeController) {
532       this.homeController = new WeakReference<HomeController>(homeController);
533     }
534 
collectionChanged(CollectionEvent<CatalogTexture> ev)535     public void collectionChanged(CollectionEvent<CatalogTexture> ev) {
536       // If controller was garbage collected, remove this listener from catalog
537       final HomeController controller = this.homeController.get();
538       if (controller == null) {
539         ((TexturesCatalog)ev.getSource()).removeTexturesListener(this);
540       } else {
541         writePreferences(controller);
542       }
543     }
544   }
545 
546   /**
547    * Properties listener that writes preferences each time the value of one of its properties changes.
548    * This listener is bound to this controller with a weak reference to avoid strong link
549    * between catalog and this controller.
550    */
551   private static class UserPreferencesPropertiesChangeListener extends UserPreferencesChangeListener
552                                                                implements PropertyChangeListener {
553     private WeakReference<HomeController> homeController;
554 
UserPreferencesPropertiesChangeListener(HomeController homeController)555     public UserPreferencesPropertiesChangeListener(HomeController homeController) {
556       this.homeController = new WeakReference<HomeController>(homeController);
557     }
558 
propertyChange(PropertyChangeEvent ev)559     public void propertyChange(PropertyChangeEvent ev) {
560       // If controller was garbage collected, remove this listener from catalog
561       final HomeController controller = this.homeController.get();
562       if (controller == null) {
563         ((UserPreferences)ev.getSource()).removePropertyChangeListener(
564             UserPreferences.Property.valueOf(ev.getPropertyName()), this);
565       } else {
566         writePreferences(controller);
567       }
568     }
569   }
570 
571   /**
572    * Adds a selection listener to catalog that enables / disables Add Furniture action.
573    */
addCatalogSelectionListener()574   private void addCatalogSelectionListener() {
575     getFurnitureCatalogController().addSelectionListener(new SelectionListener() {
576           public void selectionChanged(SelectionEvent ev) {
577             enableActionsBoundToSelection();
578           }
579         });
580   }
581 
582   /**
583    * Adds a property change listener to <code>preferences</code> to update
584    * undo and redo presentation names when preferred language changes.
585    */
addLanguageListener()586   private void addLanguageListener() {
587     this.preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE,
588         new LanguageChangeListener(this));
589   }
590 
591   /**
592    * Preferences property listener bound to this component with a weak reference to avoid
593    * strong link between preferences and this component.
594    */
595   private static class LanguageChangeListener implements PropertyChangeListener {
596     private WeakReference<HomeController> homeController;
597 
LanguageChangeListener(HomeController homeController)598     public LanguageChangeListener(HomeController homeController) {
599       this.homeController = new WeakReference<HomeController>(homeController);
600     }
601 
propertyChange(PropertyChangeEvent ev)602     public void propertyChange(PropertyChangeEvent ev) {
603       // If home pane was garbage collected, remove this listener from preferences
604       HomeController homeController = this.homeController.get();
605       if (homeController == null) {
606         ((UserPreferences)ev.getSource()).removePropertyChangeListener(
607             UserPreferences.Property.LANGUAGE, this);
608       } else {
609         // Update undo and redo name
610         homeController.getView().setUndoRedoName(
611             homeController.undoManager.canUndo()
612                 ? homeController.undoManager.getUndoPresentationName()
613                 : null,
614             homeController.undoManager.canRedo()
615                 ? homeController.undoManager.getRedoPresentationName()
616                 : null);
617       }
618     }
619   }
620 
621   /**
622    *  Adds a selection listener to home that enables / disables actions on selection.
623    */
addHomeSelectionListener()624   private void addHomeSelectionListener() {
625     if (this.home != null) {
626       this.home.addSelectionListener(new SelectionListener() {
627         public void selectionChanged(SelectionEvent ev) {
628           enableActionsBoundToSelection();
629         }
630       });
631     }
632   }
633 
634   /**
635    *  Adds a property change listener to home that enables / disables sort order action.
636    */
addFurnitureSortListener()637   private void addFurnitureSortListener() {
638     if (this.home != null) {
639       this.home.addPropertyChangeListener(Home.Property.FURNITURE_SORTED_PROPERTY,
640         new PropertyChangeListener() {
641           public void propertyChange(PropertyChangeEvent ev) {
642             getView().setEnabled(HomeView.ActionType.SORT_HOME_FURNITURE_BY_DESCENDING_ORDER,
643                 ev.getNewValue() != null);
644           }
645         });
646     }
647   }
648 
649   /**
650    *  Adds a property change listener to home that enables / disables background image actions.
651    */
addHomeBackgroundImageListener()652   private void addHomeBackgroundImageListener() {
653     if (this.home != null) {
654       this.home.addPropertyChangeListener(Home.Property.BACKGROUND_IMAGE,
655           new PropertyChangeListener() {
656             public void propertyChange(PropertyChangeEvent ev) {
657               enableBackgroungImageActions(getView(), (BackgroundImage)ev.getNewValue());
658             }
659           });
660     }
661   }
662 
663   /**
664    * Enables background image actions.
665    */
enableBackgroungImageActions(HomeView homeView, BackgroundImage backgroundImage)666   private void enableBackgroungImageActions(HomeView homeView, BackgroundImage backgroundImage) {
667     Level selectedLevel = this.home.getSelectedLevel();
668     boolean homeHasBackgroundImage = backgroundImage != null
669         && (selectedLevel == null || selectedLevel.isViewable());
670     getView().setEnabled(HomeView.ActionType.IMPORT_BACKGROUND_IMAGE, !homeHasBackgroundImage);
671     getView().setEnabled(HomeView.ActionType.MODIFY_BACKGROUND_IMAGE, homeHasBackgroundImage);
672     getView().setEnabled(HomeView.ActionType.HIDE_BACKGROUND_IMAGE,
673         homeHasBackgroundImage && backgroundImage.isVisible());
674     getView().setEnabled(HomeView.ActionType.SHOW_BACKGROUND_IMAGE,
675         homeHasBackgroundImage && !backgroundImage.isVisible());
676     getView().setEnabled(HomeView.ActionType.DELETE_BACKGROUND_IMAGE, homeHasBackgroundImage);
677   }
678 
679   /**
680    * Adds listeners to track property changes that are not undoable.
681    */
addNotUndoableModificationListeners()682   private void addNotUndoableModificationListeners() {
683     if (this.home != null) {
684       final PropertyChangeListener notUndoableModificationListener = new PropertyChangeListener() {
685           public void propertyChange(PropertyChangeEvent ev) {
686             notUndoableModifications = true;
687             home.setModified(true);
688           }
689         };
690       this.home.addPropertyChangeListener(Home.Property.STORED_CAMERAS, notUndoableModificationListener);
691       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.OBSERVER_CAMERA_ELEVATION_ADJUSTED, notUndoableModificationListener);
692       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.VIDEO_WIDTH, notUndoableModificationListener);
693       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.VIDEO_ASPECT_RATIO, notUndoableModificationListener);
694       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.VIDEO_FRAME_RATE, notUndoableModificationListener);
695       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.VIDEO_QUALITY, notUndoableModificationListener);
696       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.VIDEO_CAMERA_PATH, notUndoableModificationListener);
697       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.CEILING_LIGHT_COLOR, notUndoableModificationListener);
698       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.PHOTO_QUALITY, notUndoableModificationListener);
699       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.PHOTO_ASPECT_RATIO, notUndoableModificationListener);
700       PropertyChangeListener photoSizeModificationListener = new PropertyChangeListener() {
701           public void propertyChange(PropertyChangeEvent ev) {
702             if (home.getEnvironment().getPhotoAspectRatio() != AspectRatio.VIEW_3D_RATIO) {
703               // Ignore photo size modification with 3D view aspect ratio since it can change for various reasons
704               notUndoableModificationListener.propertyChange(ev);
705             }
706           }
707         };
708       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.PHOTO_WIDTH, photoSizeModificationListener);
709       this.home.getEnvironment().addPropertyChangeListener(HomeEnvironment.Property.PHOTO_HEIGHT, photoSizeModificationListener);
710       PropertyChangeListener timeOrLensModificationListener = new PropertyChangeListener() {
711           public void propertyChange(PropertyChangeEvent ev) {
712             if (ev.getPropertyName().equals(Camera.Property.TIME.name())
713                 || ev.getPropertyName().equals(Camera.Property.LENS.name())) {
714               notUndoableModificationListener.propertyChange(ev);
715             }
716           }
717         };
718       this.home.getObserverCamera().addPropertyChangeListener(timeOrLensModificationListener);
719       this.home.getTopCamera().addPropertyChangeListener(timeOrLensModificationListener);
720     }
721   }
722 
723   /**
724    * Enables or disables action bound to selection.
725    * This method will be called when selection in plan or in catalog changes and when
726    * focused component or modification state in plan changes.
727    */
enableActionsBoundToSelection()728   protected void enableActionsBoundToSelection() {
729     boolean modificationState = getPlanController().isModificationState();
730 
731     // Search if catalog selection contains at least one piece
732     List<CatalogPieceOfFurniture> catalogSelectedItems =
733         getFurnitureCatalogController().getSelectedFurniture();
734     boolean catalogSelectionContainsFurniture = !catalogSelectedItems.isEmpty();
735     boolean catalogSelectionContainsOneModifiablePiece = catalogSelectedItems.size() == 1
736         && catalogSelectedItems.get(0).isModifiable();
737 
738     // Search if home selection contains at least one piece, one wall or one dimension line
739     List<Selectable> selectedItems = this.home.getSelectedItems();
740     boolean homeSelectionContainsDeletableItems = false;
741     boolean homeSelectionContainsFurniture = false;
742     boolean homeSelectionContainsDeletableFurniture = false;
743     boolean homeSelectionContainsOneCopiableItemOrMore = false;
744     boolean homeSelectionContainsOneMovablePieceOfFurnitureOrMore = false;
745     boolean homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore = false;
746     boolean homeSelectionContainsTwoMovableGroupablePiecesOfFurnitureOrMore = false;
747     boolean homeSelectionContainsThreeMovablePiecesOfFurnitureOrMore = false;
748     boolean homeSelectionContainsOnlyOneGroup = selectedItems.size() == 1
749         && selectedItems.get(0) instanceof HomeFurnitureGroup;
750     boolean homeSelectionContainsFurnitureGroup = false;
751     boolean homeSelectionContainsWalls = false;
752     boolean homeSelectionContainsOneWall = false;
753     boolean homeSelectionContainsOneOrTwoWallsWithOneFreeEnd = false;
754     boolean homeSelectionContainsRooms = false;
755     boolean homeSelectionContainsPolylines = false;
756     boolean homeSelectionContainsOnlyOneRoom = false;
757     boolean homeSelectionContainsOnlyOneRoomWithFourPointsOrMore = false;
758     boolean homeSelectionContainsLabels = false;
759     boolean homeSelectionContainsItemsWithText = false;
760     boolean homeSelectionContainsCompass = false;
761     FurnitureController furnitureController = getFurnitureController();
762     if (!modificationState) {
763       for (Selectable item : selectedItems) {
764         // Check item is deletable
765         if (getPlanController().isItemDeletable(item)) {
766           homeSelectionContainsDeletableItems = true;
767           break;
768         }
769       }
770       List<HomePieceOfFurniture> selectedFurniture = Home.getFurnitureSubList(selectedItems);
771       homeSelectionContainsFurniture = !selectedFurniture.isEmpty();
772       for (HomePieceOfFurniture piece : selectedFurniture) {
773         // Check piece is deletable
774         if (furnitureController.isPieceOfFurnitureDeletable(piece)) {
775           homeSelectionContainsDeletableFurniture = true;
776           break;
777         }
778       }
779       for (HomePieceOfFurniture piece : selectedFurniture) {
780         if (piece instanceof HomeFurnitureGroup) {
781           homeSelectionContainsFurnitureGroup = true;
782           break;
783         }
784       }
785       int movablePiecesOfFurnitureCount = 0;
786       for (HomePieceOfFurniture piece : selectedFurniture) {
787         if (furnitureController.isPieceOfFurnitureMovable(piece)) {
788           homeSelectionContainsOneMovablePieceOfFurnitureOrMore = true;
789           movablePiecesOfFurnitureCount++;
790           if (movablePiecesOfFurnitureCount >= 2) {
791             homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore = true;
792           }
793           if (movablePiecesOfFurnitureCount >= 3) {
794             homeSelectionContainsThreeMovablePiecesOfFurnitureOrMore = true;
795             break;
796           }
797         }
798       }
799       if (homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore) {
800         homeSelectionContainsTwoMovableGroupablePiecesOfFurnitureOrMore = true;
801         List<HomePieceOfFurniture> furniture = this.home.getFurniture();
802         // Allow to group only furniture that are not in subgroups
803         for (HomePieceOfFurniture piece : selectedFurniture) {
804           if (!furnitureController.isPieceOfFurnitureMovable(piece)
805               || !furniture.contains(piece)) {
806             homeSelectionContainsTwoMovableGroupablePiecesOfFurnitureOrMore = false;
807             break;
808           }
809         }
810       }
811       List<Wall> selectedWalls = Home.getWallsSubList(selectedItems);
812       homeSelectionContainsWalls = !selectedWalls.isEmpty();
813       homeSelectionContainsOneWall = selectedWalls.size() == 1;
814       if (selectedWalls.size() >= 2) {
815         Wall [] wallsWithFreeEnd = {null, null, null};
816         for (Wall wall : selectedWalls) {
817           if ((wall.getArcExtent() == null
818                   || wall.getArcExtent() == 0f)
819               && (wall.getWallAtStart() == null
820                   || wall.getWallAtEnd() == null)) {
821             for (int i = 0; i < wallsWithFreeEnd.length; i++) {
822               if (wallsWithFreeEnd [i] == null) {
823                 wallsWithFreeEnd [i] = wall;
824                 break;
825               }
826             }
827             if (wallsWithFreeEnd [2] != null) {
828               break;
829             }
830           }
831         }
832         homeSelectionContainsOneOrTwoWallsWithOneFreeEnd =
833             wallsWithFreeEnd [2] == null
834             && wallsWithFreeEnd [0] != null
835             && (wallsWithFreeEnd [1] == null
836                   && !selectedWalls.contains(wallsWithFreeEnd [0].getWallAtStart())
837                   && !selectedWalls.contains(wallsWithFreeEnd [0].getWallAtEnd())
838                || wallsWithFreeEnd [0].getWallAtEnd() != wallsWithFreeEnd [1]
839                    && wallsWithFreeEnd [0].getWallAtStart() != wallsWithFreeEnd [1]);
840       }
841       List<Room> selectedRooms = Home.getRoomsSubList(selectedItems);
842       homeSelectionContainsRooms = !selectedRooms.isEmpty();
843       homeSelectionContainsOnlyOneRoom = selectedItems.size() == 1
844           && selectedRooms.size() == 1;
845       homeSelectionContainsOnlyOneRoomWithFourPointsOrMore = homeSelectionContainsOnlyOneRoom
846           && selectedRooms.get(0).getPointCount() >= 4;
847       boolean homeSelectionContainsDimensionLines = !Home.getDimensionLinesSubList(selectedItems).isEmpty();
848       homeSelectionContainsPolylines = !Home.getPolylinesSubList(selectedItems).isEmpty();
849       homeSelectionContainsLabels = !Home.getLabelsSubList(selectedItems).isEmpty();
850       homeSelectionContainsCompass = selectedItems.contains(this.home.getCompass());
851       homeSelectionContainsOneCopiableItemOrMore =
852           homeSelectionContainsFurniture || homeSelectionContainsWalls
853           || homeSelectionContainsRooms || homeSelectionContainsDimensionLines
854           || homeSelectionContainsPolylines || homeSelectionContainsLabels
855           || homeSelectionContainsCompass;
856       homeSelectionContainsItemsWithText =
857           homeSelectionContainsFurniture || homeSelectionContainsRooms
858           || homeSelectionContainsDimensionLines || homeSelectionContainsLabels;
859     }
860 
861     HomeView view = getView();
862     if (this.focusedView == getFurnitureCatalogController().getView()) {
863       view.setEnabled(HomeView.ActionType.COPY,
864           !modificationState && catalogSelectionContainsFurniture);
865       view.setEnabled(HomeView.ActionType.CUT, false);
866       view.setEnabled(HomeView.ActionType.DELETE, false);
867       for (CatalogPieceOfFurniture piece : catalogSelectedItems) {
868         if (piece.isModifiable()) {
869           // Only modifiable catalog furniture may be deleted
870           view.setEnabled(HomeView.ActionType.DELETE, true);
871           break;
872         }
873       }
874     } else if (this.focusedView == furnitureController.getView()) {
875       view.setEnabled(HomeView.ActionType.COPY, homeSelectionContainsFurniture);
876       view.setEnabled(HomeView.ActionType.CUT, homeSelectionContainsDeletableFurniture);
877       view.setEnabled(HomeView.ActionType.DELETE, homeSelectionContainsDeletableFurniture);
878     } else if (this.focusedView == getPlanController().getView()) {
879       view.setEnabled(HomeView.ActionType.COPY, homeSelectionContainsOneCopiableItemOrMore);
880       view.setEnabled(HomeView.ActionType.CUT, homeSelectionContainsDeletableItems);
881       view.setEnabled(HomeView.ActionType.DELETE, homeSelectionContainsDeletableItems);
882     } else {
883       view.setEnabled(HomeView.ActionType.COPY, false);
884       view.setEnabled(HomeView.ActionType.CUT, false);
885       view.setEnabled(HomeView.ActionType.DELETE, false);
886     }
887     enablePasteToGroupAction();
888     enablePasteStyleAction();
889 
890     Level selectedLevel = this.home.getSelectedLevel();
891     boolean viewableLevel = selectedLevel == null || selectedLevel.isViewable();
892     view.setEnabled(HomeView.ActionType.ADD_HOME_FURNITURE, catalogSelectionContainsFurniture
893         && viewableLevel);
894     view.setEnabled(HomeView.ActionType.ADD_FURNITURE_TO_GROUP, catalogSelectionContainsFurniture
895         && viewableLevel && homeSelectionContainsOnlyOneGroup);
896     // In creation mode all actions bound to selection are disabled
897     view.setEnabled(HomeView.ActionType.DELETE_HOME_FURNITURE,
898         homeSelectionContainsDeletableFurniture);
899     view.setEnabled(HomeView.ActionType.DELETE_SELECTION,
900         (catalogSelectionContainsFurniture
901             && this.focusedView == getFurnitureCatalogController().getView())
902         || (homeSelectionContainsDeletableItems
903             && (this.focusedView == furnitureController.getView()
904                 || this.focusedView == getPlanController().getView()
905                 || this.focusedView == getHomeController3D().getView())));
906     view.setEnabled(HomeView.ActionType.MODIFY_FURNITURE,
907         (catalogSelectionContainsOneModifiablePiece
908              && this.focusedView == getFurnitureCatalogController().getView())
909         || (homeSelectionContainsFurniture
910              && (this.focusedView == furnitureController.getView()
911                  || this.focusedView == getPlanController().getView()
912                  || this.focusedView == getHomeController3D().getView())));
913     view.setEnabled(HomeView.ActionType.MODIFY_WALL,
914         homeSelectionContainsWalls);
915     view.setEnabled(HomeView.ActionType.FLIP_HORIZONTALLY,
916         homeSelectionContainsOneCopiableItemOrMore);
917     view.setEnabled(HomeView.ActionType.FLIP_VERTICALLY,
918         homeSelectionContainsOneCopiableItemOrMore);
919     view.setEnabled(HomeView.ActionType.JOIN_WALLS,
920         homeSelectionContainsOneOrTwoWallsWithOneFreeEnd);
921     view.setEnabled(HomeView.ActionType.REVERSE_WALL_DIRECTION,
922         homeSelectionContainsWalls);
923     view.setEnabled(HomeView.ActionType.SPLIT_WALL,
924         homeSelectionContainsOneWall);
925     view.setEnabled(HomeView.ActionType.MODIFY_ROOM,
926         homeSelectionContainsRooms);
927     view.setEnabled(HomeView.ActionType.MODIFY_POLYLINE,
928         homeSelectionContainsPolylines);
929     view.setEnabled(HomeView.ActionType.MODIFY_LABEL,
930         homeSelectionContainsLabels);
931     view.setEnabled(HomeView.ActionType.TOGGLE_BOLD_STYLE,
932         homeSelectionContainsItemsWithText);
933     view.setEnabled(HomeView.ActionType.TOGGLE_ITALIC_STYLE,
934         homeSelectionContainsItemsWithText);
935     view.setEnabled(HomeView.ActionType.INCREASE_TEXT_SIZE,
936         homeSelectionContainsItemsWithText);
937     view.setEnabled(HomeView.ActionType.DECREASE_TEXT_SIZE,
938         homeSelectionContainsItemsWithText);
939     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_TOP,
940         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
941     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_BOTTOM,
942         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
943     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_LEFT,
944         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
945     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_RIGHT,
946         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
947     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_FRONT_SIDE,
948         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
949     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_BACK_SIDE,
950         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
951     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_LEFT_SIDE,
952         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
953     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_ON_RIGHT_SIDE,
954         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
955     view.setEnabled(HomeView.ActionType.ALIGN_FURNITURE_SIDE_BY_SIDE,
956         homeSelectionContainsTwoMovablePiecesOfFurnitureOrMore);
957     view.setEnabled(HomeView.ActionType.DISTRIBUTE_FURNITURE_HORIZONTALLY,
958         homeSelectionContainsThreeMovablePiecesOfFurnitureOrMore);
959     view.setEnabled(HomeView.ActionType.DISTRIBUTE_FURNITURE_VERTICALLY,
960         homeSelectionContainsThreeMovablePiecesOfFurnitureOrMore);
961     view.setEnabled(HomeView.ActionType.RESET_FURNITURE_ELEVATION,
962         homeSelectionContainsOneMovablePieceOfFurnitureOrMore);
963     view.setEnabled(HomeView.ActionType.GROUP_FURNITURE,
964         homeSelectionContainsTwoMovableGroupablePiecesOfFurnitureOrMore);
965     view.setEnabled(HomeView.ActionType.UNGROUP_FURNITURE,
966         homeSelectionContainsFurnitureGroup);
967     boolean selectionMode = getPlanController() != null
968         && getPlanController().getMode() == PlanController.Mode.SELECTION;
969     view.setEnabled(HomeView.ActionType.ADD_ROOM_POINT, homeSelectionContainsOnlyOneRoom && selectionMode);
970     // Check minimum requirement for DELETE_ROOM_POINT action
971     // and let home view check the coordinates of the deleted point
972     view.setEnabled(HomeView.ActionType.DELETE_ROOM_POINT,
973         homeSelectionContainsOnlyOneRoomWithFourPointsOrMore && selectionMode);
974   }
975 
976   /**
977    * Enables clipboard paste action if clipboard isn't empty.
978    */
enablePasteAction()979   public void enablePasteAction() {
980     HomeView view = getView();
981     boolean pasteEnabled = false;
982     if (this.focusedView == getFurnitureController().getView()
983         || this.focusedView == getPlanController().getView()) {
984       Level selectedLevel = this.home.getSelectedLevel();
985       pasteEnabled = (selectedLevel == null || selectedLevel.isViewable())
986           && !getPlanController().isModificationState() && !view.isClipboardEmpty();
987     }
988     view.setEnabled(HomeView.ActionType.PASTE, pasteEnabled);
989     enablePasteToGroupAction();
990     enablePasteStyleAction();
991   }
992 
993   /**
994    * Enables paste to group action if clipboard contains furniture and
995    * home selected item is a furniture group.
996    */
enablePasteToGroupAction()997   private void enablePasteToGroupAction() {
998     HomeView view = getView();
999     boolean pasteToGroupEnabled = false;
1000     if (this.focusedView == getFurnitureController().getView()
1001         || this.focusedView == getPlanController().getView()) {
1002       Level selectedLevel = this.home.getSelectedLevel();
1003       if ((selectedLevel == null || selectedLevel.isViewable())
1004           && !getPlanController().isModificationState()) {
1005         List<Selectable> selectedItems = this.home.getSelectedItems();
1006         if (selectedItems.size() == 1
1007             && selectedItems.get(0) instanceof HomeFurnitureGroup) {
1008           List<Selectable> clipboardItems = view.getClipboardItems();
1009           if (clipboardItems != null) {
1010             pasteToGroupEnabled = true;
1011             for (Selectable item : clipboardItems) {
1012               if (!(item instanceof HomePieceOfFurniture)) {
1013                 pasteToGroupEnabled = false;
1014                 break;
1015               }
1016             }
1017           }
1018         }
1019       }
1020     }
1021     view.setEnabled(HomeView.ActionType.PASTE_TO_GROUP, pasteToGroupEnabled);
1022   }
1023 
1024   /**
1025    * Enables clipboard paste style action if selection contains some items of a class
1026    * compatible with the clipboard item.
1027    */
enablePasteStyleAction()1028   private void enablePasteStyleAction() {
1029     HomeView view = getView();
1030     boolean pasteStyleEnabled = false;
1031     if ((this.focusedView == getFurnitureController().getView()
1032           || this.focusedView == getPlanController().getView())
1033         && !getPlanController().isModificationState()) {
1034       List<Selectable> clipboardItems = view.getClipboardItems();
1035       if (clipboardItems != null
1036           && clipboardItems.size() == 1) {
1037         Selectable clipboardItem = clipboardItems.get(0);
1038         for (Selectable item : this.home.getSelectedItems()) {
1039           if (item instanceof HomePieceOfFurniture && clipboardItem instanceof HomePieceOfFurniture
1040               || item instanceof Wall && clipboardItem instanceof Wall
1041               || item instanceof Room && clipboardItem instanceof Room
1042               || item instanceof Polyline && clipboardItem instanceof Polyline
1043               || item instanceof Label && clipboardItem instanceof Label) {
1044             pasteStyleEnabled = true;
1045             break;
1046           }
1047         }
1048       }
1049     }
1050     view.setEnabled(HomeView.ActionType.PASTE_STYLE, pasteStyleEnabled);
1051   }
1052 
1053   /**
1054    * Enables select all action if home isn't empty.
1055    */
enableSelectAllAction()1056   protected void enableSelectAllAction() {
1057     HomeView view = getView();
1058     boolean modificationState = getPlanController().isModificationState();
1059     if (this.focusedView == getFurnitureController().getView()) {
1060       view.setEnabled(HomeView.ActionType.SELECT_ALL,
1061           !modificationState
1062           && this.home.getFurniture().size() > 0);
1063     } else if (this.focusedView == getPlanController().getView()
1064                || this.focusedView == getHomeController3D().getView()) {
1065       boolean homeContainsOneSelectableItemOrMore = !this.home.isEmpty()
1066           || this.home.getCompass().isVisible();
1067       view.setEnabled(HomeView.ActionType.SELECT_ALL,
1068           !modificationState && homeContainsOneSelectableItemOrMore);
1069     } else {
1070       view.setEnabled(HomeView.ActionType.SELECT_ALL, false);
1071     }
1072   }
1073 
1074   /**
1075    * Enables zoom actions depending on current scale.
1076    */
enableZoomActions()1077   private void enableZoomActions() {
1078     PlanController planController = getPlanController();
1079     float scale = planController.getScale();
1080     HomeView view = getView();
1081     view.setEnabled(HomeView.ActionType.ZOOM_IN, scale < planController.getMaximumScale());
1082     view.setEnabled(HomeView.ActionType.ZOOM_OUT, scale > planController.getMinimumScale());
1083   }
1084 
1085   /**
1086    * Adds undoable edit listener to undo support that enables Undo action.
1087    */
addUndoSupportListener()1088   private void addUndoSupportListener() {
1089     getUndoableEditSupport().addUndoableEditListener(
1090       new UndoableEditListener () {
1091         public void undoableEditHappened(UndoableEditEvent ev) {
1092           HomeView view = getView();
1093           view.setEnabled(HomeView.ActionType.UNDO,
1094               !getPlanController().isModificationState());
1095           view.setEnabled(HomeView.ActionType.REDO, false);
1096           view.setUndoRedoName(ev.getEdit().getUndoPresentationName(), null);
1097           saveUndoLevel++;
1098           home.setModified(true);
1099         }
1100       });
1101    home.addPropertyChangeListener(Home.Property.MODIFIED, new PropertyChangeListener() {
1102       public void propertyChange(PropertyChangeEvent ev) {
1103         if (!home.isModified()) {
1104           // Change undo level and modification flag if home is set as unmodified
1105           saveUndoLevel = 0;
1106           notUndoableModifications = false;
1107         }
1108       }
1109     });
1110   }
1111 
1112   /**
1113    * Adds a furniture listener to home that enables / disables actions on furniture list change.
1114    */
1115   @SuppressWarnings("unchecked")
addHomeItemsListener()1116   private void addHomeItemsListener() {
1117     CollectionListener homeItemsListener =
1118         new CollectionListener() {
1119           public void collectionChanged(CollectionEvent ev) {
1120             if (ev.getType() == CollectionEvent.Type.ADD
1121                 || ev.getType() == CollectionEvent.Type.DELETE) {
1122               enableSelectAllAction();
1123             }
1124           }
1125         };
1126     this.home.addFurnitureListener((CollectionListener<HomePieceOfFurniture>)homeItemsListener);
1127     this.home.addWallsListener((CollectionListener<Wall>)homeItemsListener);
1128     this.home.addRoomsListener((CollectionListener<Room>)homeItemsListener);
1129     this.home.addPolylinesListener((CollectionListener<Polyline>)homeItemsListener);
1130     this.home.addDimensionLinesListener((CollectionListener<DimensionLine>)homeItemsListener);
1131     this.home.addLabelsListener((CollectionListener<Label>)homeItemsListener);
1132     this.home.getCompass().addPropertyChangeListener(new PropertyChangeListener() {
1133         public void propertyChange(PropertyChangeEvent ev) {
1134           if (Compass.Property.VISIBLE.name().equals(ev.getPropertyName())) {
1135             enableSelectAllAction();
1136           }
1137         }
1138       });
1139     this.home.addPropertyChangeListener(Home.Property.CAMERA, new PropertyChangeListener() {
1140         public void propertyChange(PropertyChangeEvent ev) {
1141           getView().setEnabled(HomeView.ActionType.MODIFY_OBSERVER, home.getCamera() == home.getObserverCamera());
1142         }
1143       });
1144   }
1145 
1146   /**
1147    * Adds a property change listener to home to
1148    * enable/disable authorized actions according to selected level.
1149    */
addLevelListeners()1150   private void addLevelListeners() {
1151     final PropertyChangeListener selectedLevelListener = new PropertyChangeListener() {
1152         public void propertyChange(PropertyChangeEvent ev) {
1153           Level selectedLevel = home.getSelectedLevel();
1154           if (!home.isAllLevelsSelection()) {
1155             // Keep in selection only items that are at this level
1156             List<Selectable> selectedItemsAtLevel = new ArrayList<Selectable>();
1157             for (Selectable item : home.getSelectedItems()) {
1158               if (!(item instanceof Elevatable)
1159                   || ((Elevatable)item).isAtLevel(selectedLevel)) {
1160                 selectedItemsAtLevel.add(item);
1161               }
1162             }
1163             home.setSelectedItems(selectedItemsAtLevel);
1164           }
1165           HomeView view = getView();
1166           enableCreationToolsActions(view);
1167           enableBackgroungImageActions(view, selectedLevel == null
1168               ? home.getBackgroundImage()
1169               : selectedLevel.getBackgroundImage());
1170           enableLevelActions(view);
1171         }
1172       };
1173     this.home.addPropertyChangeListener(Home.Property.SELECTED_LEVEL, selectedLevelListener);
1174     final PropertyChangeListener backgroundImageChangeListener = new PropertyChangeListener() {
1175         public void propertyChange(PropertyChangeEvent ev) {
1176           if (Level.Property.BACKGROUND_IMAGE.name().equals(ev.getPropertyName())) {
1177             enableBackgroungImageActions(getView(), (BackgroundImage)ev.getNewValue());
1178           } else if (Level.Property.VIEWABLE.name().equals(ev.getPropertyName())) {
1179             enableCreationToolsActions(getView());
1180             if (!(Boolean)ev.getNewValue()) {
1181               PlanController.Mode mode = getPlanController().getMode();
1182               if (mode != PlanController.Mode.SELECTION
1183                   && mode != PlanController.Mode.PANNING) {
1184                 getPlanController().setMode(PlanController.Mode.SELECTION);
1185               }
1186             }
1187           }
1188         }
1189       };
1190     for (Level level : home.getLevels()) {
1191       level.addPropertyChangeListener(backgroundImageChangeListener);
1192     }
1193     this.home.addLevelsListener(new CollectionListener<Level>() {
1194         public void collectionChanged(CollectionEvent<Level> ev) {
1195           switch (ev.getType()) {
1196             case ADD :
1197               home.setSelectedLevel(ev.getItem());
1198               ev.getItem().addPropertyChangeListener(backgroundImageChangeListener);
1199               break;
1200             case DELETE :
1201               selectedLevelListener.propertyChange(null);
1202               ev.getItem().removePropertyChangeListener(backgroundImageChangeListener);
1203               break;
1204           }
1205         }
1206       });
1207   }
1208 
1209   /**
1210    * Adds a property change listener to home to
1211    * enable/disable authorized actions according to stored cameras change.
1212    */
addStoredCamerasListener()1213   private void addStoredCamerasListener() {
1214     this.home.addPropertyChangeListener(Home.Property.STORED_CAMERAS, new PropertyChangeListener() {
1215         public void propertyChange(PropertyChangeEvent ev) {
1216           boolean emptyStoredCameras = home.getStoredCameras().isEmpty();
1217           getView().setEnabled(HomeView.ActionType.DELETE_POINTS_OF_VIEW, !emptyStoredCameras);
1218           getView().setEnabled(HomeView.ActionType.CREATE_PHOTOS_AT_POINTS_OF_VIEW, !emptyStoredCameras);
1219         }
1220       });
1221   }
1222 
1223   /**
1224    * Adds a property change listener to plan controller to
1225    * enable/disable authorized actions according to its modification state and the plan scale.
1226    */
addPlanControllerListeners()1227   private void addPlanControllerListeners() {
1228     getPlanController().addPropertyChangeListener(PlanController.Property.MODIFICATION_STATE,
1229         new PropertyChangeListener() {
1230           public void propertyChange(PropertyChangeEvent ev) {
1231             enableActionsBoundToSelection();
1232             enableSelectAllAction();
1233             HomeView view = getView();
1234             enableLevelActions(view);
1235             boolean modificationState = getPlanController().isModificationState();
1236             if (modificationState) {
1237               view.setEnabled(HomeView.ActionType.PASTE, false);
1238               view.setEnabled(HomeView.ActionType.UNDO, false);
1239               view.setEnabled(HomeView.ActionType.REDO, false);
1240             } else {
1241               enablePasteAction();
1242               view.setEnabled(HomeView.ActionType.UNDO, undoManager.canUndo());
1243               view.setEnabled(HomeView.ActionType.REDO, undoManager.canRedo());
1244             }
1245             view.setEnabled(HomeView.ActionType.LOCK_BASE_PLAN, !modificationState);
1246             view.setEnabled(HomeView.ActionType.UNLOCK_BASE_PLAN, !modificationState);
1247           }
1248         });
1249     getPlanController().addPropertyChangeListener(PlanController.Property.MODE,
1250         new PropertyChangeListener() {
1251           public void propertyChange(PropertyChangeEvent ev) {
1252             enableActionsBoundToSelection();
1253           }
1254         });
1255     getPlanController().addPropertyChangeListener(PlanController.Property.SCALE,
1256         new PropertyChangeListener() {
1257           public void propertyChange(PropertyChangeEvent ev) {
1258             enableZoomActions();
1259           }
1260         });
1261   }
1262 
1263   /**
1264    * Adds the selected furniture in catalog to home and selects it.
1265    */
addHomeFurniture()1266   public void addHomeFurniture() {
1267     addFurniture(null);
1268   }
1269 
1270   /**
1271    * Adds the selected furniture in catalog to the selected group and selects it.
1272    * @since 5.0
1273    */
addFurnitureToGroup()1274   public void addFurnitureToGroup() {
1275     addFurniture((HomeFurnitureGroup)this.home.getSelectedItems().get(0));
1276   }
1277 
addFurniture(HomeFurnitureGroup group)1278   private void addFurniture(HomeFurnitureGroup group) {
1279     // Use automatically selection mode
1280     getPlanController().setMode(PlanController.Mode.SELECTION);
1281     List<CatalogPieceOfFurniture> selectedFurniture =
1282       getFurnitureCatalogController().getSelectedFurniture();
1283     if (!selectedFurniture.isEmpty()) {
1284       List<HomePieceOfFurniture> addedFurniture = new ArrayList<HomePieceOfFurniture>();
1285       for (CatalogPieceOfFurniture piece : selectedFurniture) {
1286         addedFurniture.add(getFurnitureController().createHomePieceOfFurniture(piece));
1287       }
1288       // Add furniture to home with furnitureController
1289       if (group != null) {
1290         getFurnitureController().addFurnitureToGroup(addedFurniture, group);
1291       } else {
1292         getFurnitureController().addFurniture(addedFurniture);
1293       }
1294       adjustFurnitureSizeAndElevation(addedFurniture, false);
1295     }
1296   }
1297 
1298   /**
1299    * Modifies the selected furniture of the focused view.
1300    */
modifySelectedFurniture()1301   public void modifySelectedFurniture() {
1302     if (this.focusedView == getFurnitureCatalogController().getView()) {
1303       getFurnitureCatalogController().modifySelectedFurniture();
1304     } else if (this.focusedView == getFurnitureController().getView()
1305                || this.focusedView == getPlanController().getView()
1306                || this.focusedView == getHomeController3D().getView()) {
1307       getFurnitureController().modifySelectedFurniture();
1308     }
1309   }
1310 
1311   /**
1312    * Imports a language library chosen by the user.
1313    */
importLanguageLibrary()1314   public void importLanguageLibrary() {
1315     getView().invokeLater(new Runnable() {
1316         public void run() {
1317           final String languageLibraryName = getView().showImportLanguageLibraryDialog();
1318           if (languageLibraryName != null) {
1319             importLanguageLibrary(languageLibraryName);
1320           }
1321         }
1322       });
1323   }
1324 
1325   /**
1326    * Imports a given language library.
1327    */
importLanguageLibrary(String languageLibraryName)1328   public void importLanguageLibrary(String languageLibraryName) {
1329     try {
1330       if (!this.preferences.languageLibraryExists(languageLibraryName)
1331           || getView().confirmReplaceLanguageLibrary(languageLibraryName)) {
1332         this.preferences.addLanguageLibrary(languageLibraryName);
1333       }
1334     } catch (RecorderException ex) {
1335       String message = this.preferences.getLocalizedString(HomeController.class,
1336           "importLanguageLibraryError", languageLibraryName);
1337       getView().showError(message);
1338     }
1339   }
1340 
1341   /**
1342    * Imports furniture to the catalog or home depending on the focused view.
1343    */
importFurniture()1344   public void importFurniture() {
1345     // Always use selection mode after an import furniture operation
1346     getPlanController().setMode(PlanController.Mode.SELECTION);
1347     if (this.focusedView == getFurnitureCatalogController().getView()) {
1348       getFurnitureCatalogController().importFurniture();
1349     } else {
1350       getFurnitureController().importFurniture();
1351     }
1352   }
1353 
1354   /**
1355    * Imports a furniture library chosen by the user.
1356    */
importFurnitureLibrary()1357   public void importFurnitureLibrary() {
1358     getView().invokeLater(new Runnable() {
1359         public void run() {
1360           final String furnitureLibraryName = getView().showImportFurnitureLibraryDialog();
1361           if (furnitureLibraryName != null) {
1362             importFurnitureLibrary(furnitureLibraryName);
1363           }
1364         }
1365       });
1366   }
1367 
1368   /**
1369    * Imports a given furniture library.
1370    */
importFurnitureLibrary(String furnitureLibraryName)1371   public void importFurnitureLibrary(String furnitureLibraryName) {
1372     try {
1373       if (!this.preferences.furnitureLibraryExists(furnitureLibraryName)
1374           || getView().confirmReplaceFurnitureLibrary(furnitureLibraryName)) {
1375         this.preferences.addFurnitureLibrary(furnitureLibraryName);
1376         getView().showMessage(this.preferences.getLocalizedString(HomeController.class, "importedFurnitureLibraryMessage",
1377             this.contentManager != null
1378                 ? this.contentManager.getPresentationName(furnitureLibraryName, ContentManager.ContentType.FURNITURE_LIBRARY)
1379                 : furnitureLibraryName));
1380       }
1381     } catch (RecorderException ex) {
1382       String message = this.preferences.getLocalizedString(HomeController.class,
1383           "importFurnitureLibraryError", furnitureLibraryName);
1384       getView().showError(message);
1385     }
1386   }
1387 
1388   /**
1389    * Imports a texture to the texture catalog.
1390    * @since 4.0
1391    */
importTexture()1392   public void importTexture() {
1393     new ImportedTextureWizardController(this.preferences,
1394         this.viewFactory, this.contentManager).displayView(getView());
1395   }
1396 
1397   /**
1398    * Imports a textures library chosen by the user.
1399    */
importTexturesLibrary()1400   public void importTexturesLibrary() {
1401     getView().invokeLater(new Runnable() {
1402         public void run() {
1403           final String texturesLibraryName = getView().showImportTexturesLibraryDialog();
1404           if (texturesLibraryName != null) {
1405             importTexturesLibrary(texturesLibraryName);
1406           }
1407         }
1408       });
1409   }
1410 
1411   /**
1412    * Imports a given textures library.
1413    */
importTexturesLibrary(String texturesLibraryName)1414   public void importTexturesLibrary(String texturesLibraryName) {
1415     try {
1416       if (!this.preferences.texturesLibraryExists(texturesLibraryName)
1417           || getView().confirmReplaceTexturesLibrary(texturesLibraryName)) {
1418         this.preferences.addTexturesLibrary(texturesLibraryName);
1419         getView().showMessage(this.preferences.getLocalizedString(HomeController.class, "importedTexturesLibraryMessage",
1420             this.contentManager != null
1421                 ? this.contentManager.getPresentationName(texturesLibraryName, ContentManager.ContentType.TEXTURES_LIBRARY)
1422                 : texturesLibraryName));
1423       }
1424     } catch (RecorderException ex) {
1425       String message = this.preferences.getLocalizedString(HomeController.class,
1426           "importTexturesLibraryError", texturesLibraryName);
1427       getView().showError(message);
1428     }
1429   }
1430 
1431   /**
1432    * Undoes last operation.
1433    */
undo()1434   public void undo() {
1435     this.undoManager.undo();
1436     HomeView view = getView();
1437     boolean moreUndo = this.undoManager.canUndo();
1438     view.setEnabled(HomeView.ActionType.UNDO, moreUndo);
1439     view.setEnabled(HomeView.ActionType.REDO, true);
1440     if (moreUndo) {
1441       view.setUndoRedoName(this.undoManager.getUndoPresentationName(),
1442           this.undoManager.getRedoPresentationName());
1443     } else {
1444       view.setUndoRedoName(null, this.undoManager.getRedoPresentationName());
1445     }
1446     this.saveUndoLevel--;
1447     this.home.setModified(this.saveUndoLevel != 0 || this.notUndoableModifications);
1448   }
1449 
1450   /**
1451    * Redoes last undone operation.
1452    */
redo()1453   public void redo() {
1454     this.undoManager.redo();
1455     HomeView view = getView();
1456     boolean moreRedo = this.undoManager.canRedo();
1457     view.setEnabled(HomeView.ActionType.UNDO, true);
1458     view.setEnabled(HomeView.ActionType.REDO, moreRedo);
1459     if (moreRedo) {
1460       view.setUndoRedoName(this.undoManager.getUndoPresentationName(),
1461           this.undoManager.getRedoPresentationName());
1462     } else {
1463       view.setUndoRedoName(this.undoManager.getUndoPresentationName(), null);
1464     }
1465     this.saveUndoLevel++;
1466     this.home.setModified(this.saveUndoLevel != 0 || this.notUndoableModifications);
1467   }
1468 
1469   /**
1470    * Deletes items and post a cut operation to undo support.
1471    */
cut(List<? extends Selectable> items)1472   public void cut(List<? extends Selectable> items) {
1473     // Start a compound edit that deletes items and changes presentation name
1474     UndoableEditSupport undoSupport = getUndoableEditSupport();
1475     undoSupport.beginUpdate();
1476     getPlanController().deleteItems(items);
1477     // Add a undoable edit to change presentation name
1478     undoSupport.postEdit(new LocalizedUndoableEdit(preferences, HomeController.class, "undoCutName"));
1479     // End compound edit
1480     undoSupport.endUpdate();
1481   }
1482 
1483   /**
1484    * Adds items to home and posts a paste operation to undo support.
1485    */
paste(final List<? extends Selectable> items)1486   public void paste(final List<? extends Selectable> items) {
1487     // Check if pasted items and currently selected items overlap
1488     List<Selectable> selectedItems = this.home.getSelectedItems();
1489     float pastedItemsDelta = 0;
1490     if (items.size() == selectedItems.size()) {
1491       // The default delta used to be able to distinguish dropped items from previous selection
1492       pastedItemsDelta = 20;
1493       for (Selectable pastedItem : items) {
1494         // Search which item of selected items it may overlap
1495         float [][] pastedItemPoints = pastedItem.getPoints();
1496         boolean pastedItemOverlapSelectedItem = false;
1497         for (Selectable selectedItem : selectedItems) {
1498           if (Arrays.deepEquals(pastedItemPoints, selectedItem.getPoints())) {
1499             pastedItemOverlapSelectedItem = true;
1500             break;
1501           }
1502         }
1503         if (!pastedItemOverlapSelectedItem) {
1504           pastedItemsDelta = 0;
1505           break;
1506         }
1507       }
1508     }
1509     addPastedItems(items, null, pastedItemsDelta, pastedItemsDelta, null, "undoPasteName");
1510   }
1511 
1512   /**
1513    * Adds items to home, moves them of (dx, dy)
1514    * and posts a drop operation to undo support.
1515    */
drop(final List<? extends Selectable> items, float dx, float dy)1516   public void drop(final List<? extends Selectable> items, float dx, float dy) {
1517     drop(items, null, dx, dy);
1518   }
1519 
1520   /**
1521    * Adds items to home, moves them of (dx, dy)
1522    * and posts a drop operation to undo support.
1523    */
drop(final List<? extends Selectable> items, View destinationView, float dx, float dy)1524   public void drop(final List<? extends Selectable> items, View destinationView, float dx, float dy) {
1525     addPastedItems(items, destinationView, dx, dy, null, "undoDropName");
1526   }
1527 
1528   /**
1529    * Adds items to home before the given item
1530    * and posts a drop operation to undo support.
1531    * @since 6.3
1532    */
drop(List<? extends Selectable> items, View destinationView, Selectable beforeItem)1533   public void drop(List<? extends Selectable> items,  View destinationView, Selectable beforeItem) {
1534     addPastedItems(items, destinationView, 0, 0, beforeItem, "undoDropName");
1535   }
1536 
1537   /**
1538    * Adds items to home.
1539    */
addPastedItems(List<? extends Selectable> items, final View destinationView, float dx, float dy, Selectable beforeItem, final String presentationNameKey)1540   private void addPastedItems(List<? extends Selectable> items,
1541                               final View destinationView,
1542                               float dx, float dy, Selectable beforeItem,
1543                               final String presentationNameKey) {
1544     if (items.size() > 1
1545         || (items.size() == 1
1546             && !(items.get(0) instanceof Compass))) {
1547       // Remove Compass instance from copied items
1548       List<Compass> compassList = Home.getSubList(items, Compass.class);
1549       if (compassList.size() != 0) {
1550         items = new ArrayList<Selectable>(items);
1551         items.removeAll(compassList);
1552       }
1553       // Always use selection mode after a drop or a paste operation
1554       getPlanController().setMode(PlanController.Mode.SELECTION);
1555       // Start a compound edit that adds walls, furniture, rooms, dimension lines, polylines and labels to home
1556       UndoableEditSupport undoSupport = getUndoableEditSupport();
1557       undoSupport.beginUpdate();
1558       if (destinationView != null
1559           && destinationView == getFurnitureController().getView()) {
1560         getFurnitureController().addFurniture(Home.getFurnitureSubList(items), (HomePieceOfFurniture)beforeItem);
1561       } else {
1562         getPlanController().addItems(items);
1563       }
1564       List<HomePieceOfFurniture> addedFurniture = Home.getFurnitureSubList(items);
1565       adjustFurnitureSizeAndElevation(addedFurniture, dx == 0 && dy == 0 && destinationView == null);
1566       getPlanController().moveItems(items, dx, dy);
1567       if (destinationView == getPlanController().getView()) {
1568         if (this.preferences.isMagnetismEnabled()
1569             && items.size() == 1
1570             && addedFurniture.size() == 1) {
1571           // Adjust piece when it's dropped in plan view
1572           getPlanController().adjustMagnetizedPieceOfFurniture((HomePieceOfFurniture)items.get(0), dx, dy);
1573         }
1574       }
1575       undoSupport.postEdit(new LocalizedUndoableEdit(this.preferences, HomeController.class, presentationNameKey));
1576 
1577       // End compound edit
1578       undoSupport.endUpdate();
1579     }
1580   }
1581 
1582   /**
1583    * Adjusts furniture size and elevation if magnetism is enabled.
1584    * This method should be called after the given furniture is added to the plan,
1585    * to ensure its size in plan is adjusted too.
1586    */
adjustFurnitureSizeAndElevation(List<HomePieceOfFurniture> furniture, boolean keepDoorAndWindowDepth)1587   private void adjustFurnitureSizeAndElevation(List<HomePieceOfFurniture> furniture, boolean keepDoorAndWindowDepth) {
1588     if (this.preferences.isMagnetismEnabled()) {
1589       for (HomePieceOfFurniture piece : furniture) {
1590         if (!(piece instanceof HomeFurnitureGroup)
1591             && piece.isResizable()) {
1592           piece.setWidth(this.preferences.getLengthUnit().getMagnetizedLength(piece.getWidth(), 0.1f));
1593           // Don't adjust depth of doors or windows otherwise they may be misplaced in a wall
1594           if (!(piece instanceof HomeDoorOrWindow) || !keepDoorAndWindowDepth) {
1595             piece.setDepth(this.preferences.getLengthUnit().getMagnetizedLength(piece.getDepth(), 0.1f));
1596           }
1597           piece.setHeight(this.preferences.getLengthUnit().getMagnetizedLength(piece.getHeight(), 0.1f));
1598         }
1599         piece.setElevation(this.preferences.getLengthUnit().getMagnetizedLength(piece.getElevation(), 0.1f));
1600       }
1601     }
1602   }
1603 
1604   /**
1605    * Adds imported models to home, moves them of (dx, dy)
1606    * and post a drop operation to undo support.
1607    */
dropFiles(final List<String> importableModels, float dx, float dy)1608   public void dropFiles(final List<String> importableModels, float dx, float dy) {
1609     // Always use selection mode after a drop operation
1610     getPlanController().setMode(PlanController.Mode.SELECTION);
1611     // Add to home a listener to track imported furniture
1612     final List<HomePieceOfFurniture> importedFurniture =
1613         new ArrayList<HomePieceOfFurniture>(importableModels.size());
1614     CollectionListener<HomePieceOfFurniture> addedFurnitureListener =
1615         new CollectionListener<HomePieceOfFurniture>() {
1616           public void collectionChanged(CollectionEvent<HomePieceOfFurniture> ev) {
1617             importedFurniture.add(ev.getItem());
1618           }
1619         };
1620     this.home.addFurnitureListener(addedFurnitureListener);
1621 
1622     // Start a compound edit that adds furniture to home
1623     UndoableEditSupport undoSupport = getUndoableEditSupport();
1624     undoSupport.beginUpdate();
1625     // Import furniture
1626     for (String model : importableModels) {
1627       getFurnitureController().importFurniture(model);
1628     }
1629     this.home.removeFurnitureListener(addedFurnitureListener);
1630 
1631     if (importedFurniture.size() > 0) {
1632       getPlanController().moveItems(importedFurniture, dx, dy);
1633       this.home.setSelectedItems(importedFurniture);
1634 
1635       // Add a undoable edit that will select the imported furniture at redo
1636       undoSupport.postEdit(new DroppingEndUndoableEdit(this.home, this.preferences,
1637           importedFurniture.toArray(new HomePieceOfFurniture [importedFurniture.size()])));
1638     }
1639 
1640     // End compound edit
1641     undoSupport.endUpdate();
1642   }
1643 
1644   /**
1645    * Undoable edit for dropping end.
1646    */
1647   private static class DroppingEndUndoableEdit extends LocalizedUndoableEdit {
1648     private final Home                    home;
1649     private final HomePieceOfFurniture [] importedFurniture;
1650 
DroppingEndUndoableEdit(Home home, UserPreferences preferences, HomePieceOfFurniture [] importedFurniture)1651     public DroppingEndUndoableEdit(Home home, UserPreferences preferences,
1652                                    HomePieceOfFurniture [] importedFurniture) {
1653       super(preferences, HomeController.class, "undoDropName");
1654       this.home = home;
1655       this.importedFurniture = importedFurniture;
1656     }
1657 
1658     @Override
redo()1659     public void redo() throws CannotRedoException {
1660       super.redo();
1661       home.setSelectedItems(Arrays.asList(this.importedFurniture));
1662     }
1663   }
1664 
1665   /**
1666    * Paste the furniture in clipboard to the selected group in home.
1667    * @since 5.0
1668    */
pasteToGroup()1669   public void pasteToGroup() {
1670     // Start a compound edit that adds furniture
1671     UndoableEditSupport undoSupport = getUndoableEditSupport();
1672     undoSupport.beginUpdate();
1673     List<HomePieceOfFurniture> addedFurniture = Home.getFurnitureSubList(getView().getClipboardItems());
1674     getFurnitureController().addFurnitureToGroup(addedFurniture,
1675         (HomeFurnitureGroup)this.home.getSelectedItems().get(0));
1676     adjustFurnitureSizeAndElevation(addedFurniture, true);
1677     undoSupport.postEdit(new LocalizedUndoableEdit(preferences, HomeController.class, "undoPasteToGroupName"));
1678     // End compound edit
1679     undoSupport.endUpdate();
1680   }
1681 
1682   /**
1683    * Paste the style of the item in clipboard on selected items compatible with it.
1684    * @since 5.0
1685    */
pasteStyle()1686   public void pasteStyle() {
1687     // Start a compound edit that modifies items with their controller
1688     UndoableEditSupport undoSupport = getUndoableEditSupport();
1689     undoSupport.beginUpdate();
1690     Selectable clipboardItem = getView().getClipboardItems().get(0);
1691     final List<Selectable> selectedItems = this.home.getSelectedItems();
1692     if (clipboardItem instanceof HomePieceOfFurniture) {
1693       HomePieceOfFurniture clipboardPiece = (HomePieceOfFurniture)clipboardItem;
1694       HomeFurnitureController furnitureController = new HomeFurnitureController(
1695           this.home, this.preferences, this.viewFactory, this.contentManager, undoSupport);
1696       HomeMaterial [] materials = clipboardPiece.getModelMaterials();
1697       if (materials != null) {
1698         furnitureController.getModelMaterialsController().setMaterials(clipboardPiece.getModelMaterials());
1699         furnitureController.setPaint(HomeFurnitureController.FurniturePaint.MODEL_MATERIALS);
1700       } else if (clipboardPiece.getTexture() != null) {
1701         furnitureController.getTextureController().setTexture(clipboardPiece.getTexture());
1702         furnitureController.setPaint(HomeFurnitureController.FurniturePaint.TEXTURED);
1703       } else if (clipboardPiece.getColor() != null) {
1704         furnitureController.setColor(clipboardPiece.getColor());
1705         furnitureController.setPaint(HomeFurnitureController.FurniturePaint.COLORED);
1706       } else {
1707         furnitureController.setPaint(HomeFurnitureController.FurniturePaint.DEFAULT);
1708       }
1709       Float shininess = clipboardPiece.getShininess();
1710       furnitureController.setShininess(shininess == null
1711           ? HomeFurnitureController.FurnitureShininess.DEFAULT
1712           : (shininess.floatValue() == 0
1713               ? HomeFurnitureController.FurnitureShininess.MATT
1714               : HomeFurnitureController.FurnitureShininess.SHINY));
1715       furnitureController.modifyFurniture();
1716     } else if (clipboardItem instanceof Wall) {
1717       Wall clipboardWall = (Wall)clipboardItem;
1718       WallController wallController = new WallController(this.home, this.preferences, this.viewFactory, this.contentManager, undoSupport);
1719       if (clipboardWall.getLeftSideColor() != null) {
1720         wallController.setLeftSideColor(clipboardWall.getLeftSideColor());
1721         wallController.setLeftSidePaint(WallController.WallPaint.COLORED);
1722       } else if (clipboardWall.getLeftSideTexture() != null) {
1723         wallController.getLeftSideTextureController().setTexture(clipboardWall.getLeftSideTexture());
1724         wallController.setLeftSidePaint(WallController.WallPaint.TEXTURED);
1725       } else {
1726         wallController.setLeftSidePaint(WallController.WallPaint.DEFAULT);
1727       }
1728       wallController.setLeftSideShininess(clipboardWall.getLeftSideShininess());
1729       wallController.getLeftSideBaseboardController().setBaseboard(clipboardWall.getLeftSideBaseboard());
1730       if (clipboardWall.getRightSideColor() != null) {
1731         wallController.setRightSideColor(clipboardWall.getRightSideColor());
1732         wallController.setRightSidePaint(WallController.WallPaint.COLORED);
1733       } else if (clipboardWall.getRightSideTexture() != null) {
1734         wallController.getRightSideTextureController().setTexture(clipboardWall.getRightSideTexture());
1735         wallController.setRightSidePaint(WallController.WallPaint.TEXTURED);
1736       } else {
1737         wallController.setRightSidePaint(WallController.WallPaint.DEFAULT);
1738       }
1739       wallController.setRightSideShininess(clipboardWall.getRightSideShininess());
1740       wallController.getRightSideBaseboardController().setBaseboard(clipboardWall.getRightSideBaseboard());
1741       wallController.setPattern(clipboardWall.getPattern());
1742       wallController.setTopColor(clipboardWall.getTopColor());
1743       wallController.setTopPaint(clipboardWall.getTopColor() != null
1744           ? WallController.WallPaint.COLORED
1745           : WallController.WallPaint.DEFAULT);
1746       wallController.modifyWalls();
1747     } else if (clipboardItem instanceof Room) {
1748       Room clipboardRoom = (Room)clipboardItem;
1749       RoomController roomController = new RoomController(this.home, this.preferences, this.viewFactory, this.contentManager, undoSupport);
1750       if (clipboardRoom.getFloorColor() != null) {
1751         roomController.setFloorColor(clipboardRoom.getFloorColor());
1752         roomController.setFloorPaint(RoomController.RoomPaint.COLORED);
1753       } else if (clipboardRoom.getFloorTexture() != null) {
1754         roomController.getFloorTextureController().setTexture(clipboardRoom.getFloorTexture());
1755         roomController.setFloorPaint(RoomController.RoomPaint.TEXTURED);
1756       } else {
1757         roomController.setFloorPaint(RoomController.RoomPaint.DEFAULT);
1758       }
1759       roomController.setFloorShininess(clipboardRoom.getFloorShininess());
1760       if (clipboardRoom.getCeilingColor() != null) {
1761         roomController.setCeilingColor(clipboardRoom.getCeilingColor());
1762         roomController.setCeilingPaint(RoomController.RoomPaint.COLORED);
1763       } else if (clipboardRoom.getCeilingTexture() != null) {
1764         roomController.getCeilingTextureController().setTexture(clipboardRoom.getCeilingTexture());
1765         roomController.setCeilingPaint(RoomController.RoomPaint.TEXTURED);
1766       } else {
1767         roomController.setCeilingPaint(RoomController.RoomPaint.DEFAULT);
1768       }
1769       roomController.setCeilingShininess(clipboardRoom.getCeilingShininess());
1770       roomController.modifyRooms();
1771     } else if (clipboardItem instanceof Polyline) {
1772       Polyline clipboardPolyline = (Polyline)clipboardItem;
1773       PolylineController polylineController = new PolylineController(
1774           this.home, this.preferences, this.viewFactory, this.contentManager, undoSupport);
1775       polylineController.setThickness(clipboardPolyline.getThickness());
1776       polylineController.setJoinStyle(clipboardPolyline.getJoinStyle());
1777       polylineController.setCapStyle(clipboardPolyline.getCapStyle());
1778       polylineController.setStartArrowStyle(clipboardPolyline.getStartArrowStyle());
1779       polylineController.setEndArrowStyle(clipboardPolyline.getEndArrowStyle());
1780       polylineController.setDashStyle(clipboardPolyline.getDashStyle());
1781       polylineController.setDashPattern(clipboardPolyline.getDashPattern());
1782       polylineController.setDashOffset(clipboardPolyline.getDashOffset());
1783       polylineController.setColor(clipboardPolyline.getColor());
1784       polylineController.modifyPolylines();
1785     } else if (clipboardItem instanceof Label) {
1786       Label clipboardLabel = (Label)clipboardItem;
1787       LabelController labelController = new LabelController(this.home, this.preferences, this.viewFactory, undoSupport);
1788       labelController.setColor(clipboardLabel.getColor());
1789       TextStyle labelStyle = clipboardLabel.getStyle();
1790       if (labelStyle != null) {
1791         labelController.setAlignment(labelStyle.getAlignment());
1792         labelController.setFontName(labelStyle.getFontName());
1793         labelController.setFontSize(labelStyle.getFontSize());
1794       } else {
1795         labelController.setAlignment(null);
1796         labelController.setFontName(null);
1797         labelController.setFontSize(this.preferences.getDefaultTextStyle(Label.class).getFontSize());
1798       }
1799       labelController.modifyLabels();
1800     }
1801 
1802     // Add a undoable edit to change presentation name
1803     undoSupport.postEdit(new PastingStyleEndUndoableEdit(this.home, this.preferences,
1804         selectedItems.toArray(new Selectable [selectedItems.size()])));
1805     // End compound edit
1806     undoSupport.endUpdate();
1807   }
1808 
1809   /**
1810    * Undoable edit for pasting style end.
1811    */
1812   private static class PastingStyleEndUndoableEdit extends LocalizedUndoableEdit {
1813     private final Home home;
1814     private final Selectable [] selectedItems;
1815 
PastingStyleEndUndoableEdit(Home home, UserPreferences preferences, Selectable [] selectedItems)1816     public PastingStyleEndUndoableEdit(Home home, UserPreferences preferences, Selectable [] selectedItems) {
1817       super(preferences, HomeController.class, "undoPasteStyleName");
1818       this.home = home;
1819       this.selectedItems = selectedItems;
1820     }
1821 
1822     @Override
redo()1823     public void redo() throws CannotRedoException {
1824       super.redo();
1825       this.home.setSelectedItems(Arrays.asList(this.selectedItems));
1826     }
1827   }
1828 
1829   /**
1830    * Returns the transfer data matching the requested types.
1831    */
createTransferData(final TransferableView.TransferObserver observer, final TransferableView.DataType ... dataTypes)1832    public void createTransferData(final TransferableView.TransferObserver observer,
1833                                   final TransferableView.DataType ... dataTypes) {
1834      final List<Object> data = new ArrayList<Object>();
1835      for (int i = 0; i < dataTypes.length; i++) {
1836        if (this.childControllers == null) {
1837          this.childControllers = new ArrayList<Controller>();
1838          this.childControllers.add(getFurnitureCatalogController());
1839          this.childControllers.add(getFurnitureController());
1840          this.childControllers.add(getPlanController());
1841          this.childControllers.add(getHomeController3D());
1842        }
1843        for (Controller childController : this.childControllers) {
1844          if (childController.getView() instanceof TransferableView) {
1845            data.add(((TransferableView)childController.getView()).createTransferData(dataTypes [i]));
1846          }
1847        }
1848      }
1849      observer.dataReady(data.toArray());
1850    }
1851 
1852   /**
1853    * Deletes the selection in the focused component.
1854    */
delete()1855   public void delete() {
1856     if (this.focusedView == getFurnitureCatalogController().getView()) {
1857       if (getView().confirmDeleteCatalogSelection()) {
1858         getFurnitureCatalogController().deleteSelection();
1859       }
1860     } else if (this.focusedView == getFurnitureController().getView()) {
1861       getFurnitureController().deleteSelection();
1862     } else if (this.focusedView == getPlanController().getView()) {
1863       getPlanController().deleteSelection();
1864     }
1865   }
1866 
1867   /**
1868    * Updates actions when focused view changed.
1869    */
focusedViewChanged(View focusedView)1870   public void focusedViewChanged(View focusedView) {
1871     this.focusedView = focusedView;
1872     enableActionsBoundToSelection();
1873     enablePasteAction();
1874     enablePasteToGroupAction();
1875     enablePasteStyleAction();
1876     enableSelectAllAction();
1877   }
1878 
1879   /**
1880    * Selects everything in the focused component.
1881    */
selectAll()1882   public void selectAll() {
1883     if (this.focusedView == getFurnitureController().getView()) {
1884       getFurnitureController().selectAll();
1885     } else if (this.focusedView == getPlanController().getView()
1886                || this.focusedView == getHomeController3D().getView()) {
1887       getPlanController().selectAll();
1888     }
1889   }
1890 
1891   /**
1892    * Creates a new home and adds it to application home list.
1893    */
newHome()1894   public void newHome() {
1895     Home home;
1896     if (this.application != null) {
1897       home = this.application.createHome();
1898     } else {
1899       home = new Home(this.preferences.getNewWallHeight());
1900     }
1901     this.application.addHome(home);
1902   }
1903 
1904   /**
1905    * Creates a new home from an example chosen by the user.
1906    */
newHomeFromExample()1907   public void newHomeFromExample() {
1908     final String exampleName = getView().showNewHomeFromExampleDialog();
1909     if (exampleName != null) {
1910       // Read home in a threaded task
1911       Callable<Void> openTask = new Callable<Void>() {
1912             public Void call() throws RecorderException {
1913               // Read home with application recorder
1914               Home openedHome = application.getHomeRecorder().readHome(exampleName);
1915               // Reset furniture names to their catalog one to simulate translation
1916               final Map<String, String> furnitureNames = getCatalogFurnitureNames(preferences.getFurnitureCatalog());
1917               String groupName = preferences.getLocalizedString(HomeController.class, "defaultGroupName");
1918               for (HomePieceOfFurniture piece : openedHome.getFurniture()) {
1919                 renameToCatalogName(piece, furnitureNames, groupName);
1920               }
1921               openedHome.setName(null);
1922               addHomeToApplication(openedHome);
1923               return null;
1924             }
1925           };
1926       ThreadedTaskController.ExceptionHandler exceptionHandler =
1927           new ThreadedTaskController.ExceptionHandler() {
1928             public void handleException(Exception ex) {
1929               if (!(ex instanceof InterruptedRecorderException)) {
1930                 ex.printStackTrace();
1931                 if (ex instanceof RecorderException) {
1932                   String message = preferences.getLocalizedString(HomeController.class,
1933                       "openError", exampleName, ex);
1934                   getView().showError(message);
1935                 }
1936               }
1937             }
1938           };
1939       new ThreadedTaskController(openTask,
1940           this.preferences.getLocalizedString(HomeController.class, "openMessage"), exceptionHandler,
1941           this.preferences, this.viewFactory).executeTask(getView());
1942     }
1943   }
1944 
1945   /**
1946    * Returns a map with entries containing furniture name associated to their id.
1947    */
getCatalogFurnitureNames(FurnitureCatalog catalog)1948   private Map<String, String> getCatalogFurnitureNames(FurnitureCatalog catalog) {
1949     Map<String, String> furnitureNames = new HashMap<String, String>();
1950     for (FurnitureCategory category : catalog.getCategories()) {
1951       for (CatalogPieceOfFurniture piece : category.getFurniture()) {
1952         if (piece.getId() != null) {
1953           furnitureNames.put(piece.getId(), piece.getName());
1954         }
1955       }
1956     }
1957     return furnitureNames;
1958   }
1959 
1960   /**
1961    * Renames the given <code>piece</code> from the piece name with the same id in <code>furnitureNames</code>.
1962    */
renameToCatalogName(HomePieceOfFurniture piece, Map<String, String> furnitureNames, String groupName)1963   private void renameToCatalogName(HomePieceOfFurniture piece,
1964                                    Map<String, String> furnitureNames,
1965                                    String groupName) {
1966     if (piece instanceof HomeFurnitureGroup) {
1967       piece.setName(groupName);
1968       for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
1969         renameToCatalogName(groupPiece, furnitureNames, groupName);
1970       }
1971     } else {
1972       String id = piece.getCatalogId();
1973       if (id != null) {
1974         piece.setName(furnitureNames.get(id));
1975       }
1976     }
1977   }
1978 
1979   /**
1980    * Opens a home. This method displays an {@link HomeView#showOpenDialog() open dialog}
1981    * in view, reads the home from the chosen name and adds it to application home list.
1982    */
open()1983   public void open() {
1984     getView().invokeLater(new Runnable() {
1985       public void run() {
1986         final String homeName = getView().showOpenDialog();
1987         if (homeName != null) {
1988           open(homeName);
1989         }
1990       }
1991     });
1992   }
1993 
1994   /**
1995    * Opens a given <code>homeName</code>home.
1996    */
open(final String homeName)1997   public void open(final String homeName) {
1998     // Check if requested home isn't already opened
1999     for (Home home : this.application.getHomes()) {
2000       if (homeName.equals(home.getName())) {
2001         String message = this.preferences.getLocalizedString(
2002             HomeController.class, "alreadyOpen", homeName);
2003         getView().showMessage(message);
2004         return;
2005       }
2006     }
2007 
2008     // Read home in a threaded task
2009     Callable<Void> openTask = new Callable<Void>() {
2010           public Void call() throws RecorderException {
2011             // Read home with application recorder
2012             Home openedHome = application.getHomeRecorder().readHome(homeName);
2013             openedHome.setName(homeName);
2014             addHomeToApplication(openedHome);
2015             if (openedHome.isRepaired()) {
2016               getView().invokeLater(new Runnable() {
2017                   public void run() {
2018                     String message = preferences.getLocalizedString(HomeController.class, "openRepairedHomeMessage", homeName);
2019                     getView().showMessage(message);
2020                   }
2021                 });
2022             }
2023             return null;
2024           }
2025         };
2026     ThreadedTaskController.ExceptionHandler exceptionHandler =
2027         new ThreadedTaskController.ExceptionHandler() {
2028           public void handleException(Exception ex) {
2029             if (!(ex instanceof InterruptedRecorderException)) {
2030               if (ex instanceof DamagedHomeRecorderException) {
2031                 DamagedHomeRecorderException ex2 = (DamagedHomeRecorderException)ex;
2032                 openDamagedHome(homeName, ex2.getDamagedHome(), ex2.getInvalidContent());
2033               } else {
2034                 ex.printStackTrace();
2035                 if (ex instanceof RecorderException) {
2036                   String message = preferences.getLocalizedString(HomeController.class,
2037                       "openError", homeName, ex);
2038                   getView().showError(message);
2039                 }
2040               }
2041             }
2042           }
2043         };
2044     new ThreadedTaskController(openTask,
2045         this.preferences.getLocalizedString(HomeController.class, "openMessage"), exceptionHandler,
2046         this.preferences, this.viewFactory).executeTask(getView());
2047   }
2048 
2049   /**
2050    * Adds the given home to application.
2051    */
addHomeToApplication(final Home home)2052   private void addHomeToApplication(final Home home) {
2053     getView().invokeLater(new Runnable() {
2054         public void run() {
2055           application.addHome(home);
2056         }
2057       });
2058   }
2059 
2060   /**
2061    * Prompts the user to choose an option to open the given damaged home,
2062    * fixes the damaged home accordingly and shows it.
2063    */
openDamagedHome(final String homeName, Home damagedHome, List<Content> invalidContent)2064   private void openDamagedHome(final String homeName, Home damagedHome, List<Content> invalidContent) {
2065     HomeView.OpenDamagedHomeAnswer answer = getView().confirmOpenDamagedHome(
2066         homeName, damagedHome, invalidContent);
2067     switch (answer) {
2068       case REMOVE_DAMAGED_ITEMS:
2069         removeDamagedItems(damagedHome, invalidContent);
2070         break;
2071       case REPLACE_DAMAGED_ITEMS:
2072         replaceDamagedItems(damagedHome, invalidContent);
2073         break;
2074     }
2075     if (answer != HomeView.OpenDamagedHomeAnswer.DO_NOT_OPEN_HOME) {
2076       damagedHome.setName(homeName);
2077       damagedHome.setRepaired(true);
2078       addHomeToApplication(damagedHome);
2079     }
2080   }
2081 
2082   /**
2083    * Removes from the given <code>home</code> all the objects that reference the invalid content.
2084    */
removeDamagedItems(Home home, List<Content> invalidContent)2085   private void removeDamagedItems(Home home, List<Content> invalidContent) {
2086     for (HomePieceOfFurniture piece : home.getFurniture()) {
2087       if (referencesInvalidContent(piece, invalidContent)) {
2088         home.deletePieceOfFurniture(piece);
2089       } else {
2090         removeInvalidTextures(piece, invalidContent);
2091       }
2092     }
2093     for (Wall wall : home.getWalls()) {
2094       if (referencesInvalidContent(wall.getLeftSideTexture(), invalidContent)) {
2095         wall.setLeftSideTexture(null);
2096       }
2097       if (referencesInvalidContent(wall.getRightSideTexture(), invalidContent)) {
2098         wall.setRightSideTexture(null);
2099       }
2100     }
2101     for (Room room : home.getRooms()) {
2102       if (referencesInvalidContent(room.getFloorTexture(), invalidContent)) {
2103         room.setFloorTexture(null);
2104       }
2105       if (referencesInvalidContent(room.getCeilingTexture(), invalidContent)) {
2106         room.setCeilingTexture(null);
2107       }
2108     }
2109     HomeEnvironment environment = home.getEnvironment();
2110     if (referencesInvalidContent(environment.getGroundTexture(), invalidContent)) {
2111       environment.setGroundTexture(null);
2112     }
2113     if (referencesInvalidContent(environment.getSkyTexture(), invalidContent)) {
2114       environment.setSkyTexture(null);
2115     }
2116     BackgroundImage backgroundImage = home.getBackgroundImage();
2117     if (backgroundImage != null && invalidContent.contains(backgroundImage.getImage())) {
2118       home.setBackgroundImage(null);
2119     }
2120     for (Level level : home.getLevels()) {
2121       backgroundImage = level.getBackgroundImage();
2122       if (backgroundImage != null && invalidContent.contains(backgroundImage.getImage())) {
2123         level.setBackgroundImage(null);
2124       }
2125     }
2126   }
2127 
2128   /**
2129    * Returns <code>true</code> if the model of the given <code>piece</code> and its icons are not valid.
2130    */
referencesInvalidContent(HomePieceOfFurniture piece, List<Content> invalidContent)2131   private boolean referencesInvalidContent(HomePieceOfFurniture piece, List<Content> invalidContent) {
2132     if (invalidContent.contains(piece.getIcon())
2133         || invalidContent.contains(piece.getPlanIcon())
2134         || invalidContent.contains(piece.getModel())) {
2135       return true;
2136     } else if (piece instanceof HomeFurnitureGroup) {
2137       for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
2138         if (referencesInvalidContent(groupPiece, invalidContent)) {
2139           return true;
2140         }
2141       }
2142     }
2143     return false;
2144   }
2145 
2146   /**
2147    * Sets to <code>null</code> the invalid textures used by the given <code>piece</code>.
2148    */
removeInvalidTextures(HomePieceOfFurniture piece, List<Content> invalidContent)2149   private void removeInvalidTextures(HomePieceOfFurniture piece, List<Content> invalidContent) {
2150     if (referencesInvalidContent(piece.getTexture(), invalidContent)) {
2151       piece.setTexture(null);
2152     }
2153     HomeMaterial [] materials = piece.getModelMaterials();
2154     if (materials != null) {
2155       for (int i = 0; i < materials.length; i++) {
2156         if (materials [i] != null
2157             && referencesInvalidContent(materials [i].getTexture(), invalidContent)) {
2158           materials [i] = null;
2159         }
2160         piece.setModelMaterials(materials);
2161       }
2162     }
2163     if (piece instanceof HomeFurnitureGroup) {
2164       for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
2165         removeInvalidTextures(groupPiece, invalidContent);
2166       }
2167     }
2168   }
2169 
2170   /**
2171    * Returns <code>true</code> if the given <code>texture</code> is not valid.
2172    */
referencesInvalidContent(TextureImage texture, List<Content> invalidContent)2173   private boolean referencesInvalidContent(TextureImage texture, List<Content> invalidContent) {
2174     return texture != null && invalidContent.contains(texture.getImage());
2175   }
2176 
2177   /**
2178    * Replaces all the objects that reference an invalid content in the given <code>home</code>.
2179    */
replaceDamagedItems(Home home, List<Content> invalidContent)2180   private void replaceDamagedItems(Home home, List<Content> invalidContent) {
2181     List<HomePieceOfFurniture> furniture = home.getFurniture();
2182     for (int i = furniture.size() - 1; i >= 0; i--) {
2183       HomePieceOfFurniture piece = furniture.get(i);
2184       if (referencesInvalidContent(piece, invalidContent)) {
2185         HomePieceOfFurniture replacingPiece = getFurnitureController().createHomePieceOfFurniture(
2186             new CatalogPieceOfFurniture(piece.getCatalogId(), piece.getName(), piece.getDescription(),
2187                 piece.getInformation(), new String [0], null, null,
2188                 REPAIRED_ICON_CONTENT, REPAIRED_IMAGE_CONTENT, REPAIRED_MODEL_CONTENT,
2189                 piece.getWidth(), piece.getDepth(), piece.getHeight(), piece.getElevation(), 1f,
2190                 piece.isMovable(), piece.getStaircaseCutOutShape(), null, false, null, piece.getCreator(),
2191                 piece.isResizable(), piece.isDeformable(), piece.isTexturable(), piece.isHorizontallyRotatable(),
2192                 piece.getPrice(), piece.getValueAddedTaxPercentage(), piece.getCurrency()));
2193         replacingPiece.setNameVisible(piece.isNameVisible());
2194         replacingPiece.setNameXOffset(piece.getNameXOffset());
2195         replacingPiece.setNameYOffset(piece.getNameYOffset());
2196         replacingPiece.setNameStyle(piece.getNameStyle());
2197         replacingPiece.setVisible(piece.isVisible());
2198         replacingPiece.setAngle(piece.getAngle());
2199         replacingPiece.setX(piece.getX());
2200         replacingPiece.setY(piece.getY());
2201         home.addPieceOfFurniture(replacingPiece, i);
2202         if (replacingPiece.isHorizontallyRotatable()) {
2203           replacingPiece.setPitch(piece.getPitch());
2204           replacingPiece.setRoll(piece.getRoll());
2205         }
2206         replacingPiece.setLevel(piece.getLevel());
2207         home.deletePieceOfFurniture(piece);
2208       } else {
2209         replaceInvalidTextures(piece, invalidContent);
2210       }
2211     }
2212     for (Wall wall : home.getWalls()) {
2213       if (referencesInvalidContent(wall.getLeftSideTexture(), invalidContent)) {
2214         wall.setLeftSideTexture(getErrorTexture(wall.getLeftSideTexture()));
2215       }
2216       if (referencesInvalidContent(wall.getRightSideTexture(), invalidContent)) {
2217         wall.setRightSideTexture(getErrorTexture(wall.getRightSideTexture()));
2218       }
2219     }
2220     for (Room room : home.getRooms()) {
2221       if (referencesInvalidContent(room.getFloorTexture(), invalidContent)) {
2222         room.setFloorTexture(getErrorTexture(room.getFloorTexture()));
2223       }
2224       if (referencesInvalidContent(room.getCeilingTexture(), invalidContent)) {
2225         room.setCeilingTexture(getErrorTexture(room.getCeilingTexture()));
2226       }
2227     }
2228     HomeEnvironment environment = home.getEnvironment();
2229     if (referencesInvalidContent(environment.getGroundTexture(), invalidContent)) {
2230       environment.setGroundTexture(getErrorTexture(environment.getGroundTexture()));
2231     }
2232     if (referencesInvalidContent(environment.getSkyTexture(), invalidContent)) {
2233       environment.setSkyTexture(getErrorTexture(environment.getSkyTexture()));
2234     }
2235     BackgroundImage backgroundImage = home.getBackgroundImage();
2236     if (backgroundImage != null && invalidContent.contains(backgroundImage.getImage())) {
2237       home.setBackgroundImage(getErrorBackgroundImage(backgroundImage));
2238     }
2239     for (Level level : home.getLevels()) {
2240       backgroundImage = level.getBackgroundImage();
2241       if (backgroundImage != null && invalidContent.contains(backgroundImage.getImage())) {
2242         level.setBackgroundImage(getErrorBackgroundImage(backgroundImage));
2243       }
2244     }
2245   }
2246 
2247   /**
2248    * Replaces the invalid textures used by the given <code>piece</code>.
2249    */
replaceInvalidTextures(HomePieceOfFurniture piece, List<Content> invalidContent)2250   private void replaceInvalidTextures(HomePieceOfFurniture piece, List<Content> invalidContent) {
2251     if (referencesInvalidContent(piece.getTexture(), invalidContent)) {
2252       piece.setTexture(getErrorTexture(piece.getTexture()));
2253     }
2254     HomeMaterial [] materials = piece.getModelMaterials();
2255     if (materials != null) {
2256       for (int i = 0; i < materials.length; i++) {
2257         HomeMaterial material = materials [i];
2258         if (material != null
2259             && referencesInvalidContent(material.getTexture(), invalidContent)) {
2260           materials [i] = new HomeMaterial(material.getName(), material.getColor(),
2261               getErrorTexture(material.getTexture()), material.getShininess());
2262         }
2263         piece.setModelMaterials(materials);
2264       }
2265     }
2266     if (piece instanceof HomeFurnitureGroup) {
2267       for (HomePieceOfFurniture groupPiece : ((HomeFurnitureGroup)piece).getFurniture()) {
2268         replaceInvalidTextures(groupPiece, invalidContent);
2269       }
2270     }
2271   }
2272 
2273   /**
2274    * Returns a texture referencing a correct image.
2275    */
getErrorTexture(HomeTexture texture)2276   private HomeTexture getErrorTexture(HomeTexture texture) {
2277     return new HomeTexture(new CatalogTexture(texture.getName(),
2278         REPAIRED_IMAGE_CONTENT, texture.getWidth(), texture.getHeight()));
2279   }
2280 
2281   /**
2282    * Returns a background image referencing a correct image.
2283    */
getErrorBackgroundImage(BackgroundImage image)2284   private BackgroundImage getErrorBackgroundImage(BackgroundImage image) {
2285     return new BackgroundImage(REPAIRED_IMAGE_CONTENT,
2286         image.getScaleDistance(), image.getScaleDistanceXStart(), image.getScaleDistanceYStart(),
2287         image.getScaleDistanceXEnd(), image.getScaleDistanceYEnd(),
2288         image.getXOrigin(), image.getYOrigin(), image.isVisible());
2289   }
2290 
2291   /**
2292    * Updates user preferences <code>recentHomes</code> and write preferences.
2293    */
updateUserPreferencesRecentHomes(List<String> recentHomes)2294   private void updateUserPreferencesRecentHomes(List<String> recentHomes) {
2295     if (this.application != null) {
2296       // Check every recent home exists
2297       for (int i = recentHomes.size() - 1; i >= 0; i--) {
2298         try {
2299           if (!this.application.getHomeRecorder().exists(recentHomes.get(i))) {
2300             recentHomes.remove(i);
2301           }
2302         } catch (RecorderException ex) {
2303           // If homeName can't be checked ignore it
2304         }
2305       }
2306       this.preferences.setRecentHomes(recentHomes);
2307     }
2308   }
2309 
2310   /**
2311    * Returns a list of displayable recent homes.
2312    */
getRecentHomes()2313   public List<String> getRecentHomes() {
2314     if (this.application != null) {
2315       List<String> recentHomes = new ArrayList<String>();
2316       for (String homeName : this.preferences.getRecentHomes()) {
2317         try {
2318           if (this.application.getHomeRecorder().exists(homeName)) {
2319             recentHomes.add(homeName);
2320             if (recentHomes.size() == this.preferences.getRecentHomesMaxCount()) {
2321               break;
2322             }
2323           }
2324         } catch (RecorderException ex) {
2325           // If homeName can't be checked ignore it
2326         }
2327       }
2328       getView().setEnabled(HomeView.ActionType.DELETE_RECENT_HOMES,
2329           !recentHomes.isEmpty());
2330       return Collections.unmodifiableList(recentHomes);
2331     } else {
2332       return new ArrayList<String>();
2333     }
2334   }
2335 
2336   /**
2337    * Returns the version of the application for display purpose.
2338    */
getVersion()2339   public String getVersion() {
2340     if (this.application != null) {
2341       String applicationVersion = this.application.getVersion();
2342       try {
2343         String deploymentInformation = System.getProperty("com.eteks.sweethome3d.deploymentInformation");
2344         if (deploymentInformation != null) {
2345           applicationVersion += " " + deploymentInformation;
2346         }
2347       } catch (AccessControlException ex) {
2348         // Ignore com.eteks.sweethome3d.deploymentInformation property since it can't be read
2349       }
2350       return applicationVersion;
2351     } else {
2352       return "";
2353     }
2354   }
2355 
2356   /**
2357    * Deletes the list of recent homes in user preferences.
2358    */
deleteRecentHomes()2359   public void deleteRecentHomes() {
2360     updateUserPreferencesRecentHomes(new ArrayList<String>());
2361     getView().setEnabled(HomeView.ActionType.DELETE_RECENT_HOMES, false);
2362   }
2363 
2364   /**
2365    * Manages home close operation. If the home managed by this controller is modified,
2366    * this method will {@link HomeView#confirmSave(String) confirm}
2367    * in view whether home should be saved. Once home is actually saved,
2368    * home is removed from application homes list.
2369    */
close()2370   public void close() {
2371     close(null);
2372   }
2373 
2374 
2375   /**
2376    * Manages home close operation. If the home managed by this controller is modified,
2377    * this method will {@link HomeView#confirmSave(String) confirm}
2378    * in view whether home should be saved. Once home is actually saved,
2379    * home is removed from application homes list and <code>postCloseTask</code>
2380    * is called if it's not <code>null</code>.
2381    * @since 5.0
2382    */
close(final Runnable postCloseTask)2383   public void close(final Runnable postCloseTask) {
2384     // Create a task that deletes home and run postCloseTask
2385     Runnable closeTask = new Runnable() {
2386         public void run() {
2387           home.setRecovered(false);
2388           application.deleteHome(home);
2389           if (postCloseTask != null) {
2390             postCloseTask.run();
2391           }
2392         }
2393       };
2394 
2395     if (this.home.isModified() || this.home.isRecovered() || this.home.isRepaired()) {
2396       switch (getView().confirmSave(this.home.getName())) {
2397         case SAVE   : save(HomeRecorder.Type.DEFAULT, closeTask); // Falls through
2398         case CANCEL : return;
2399       }
2400     }
2401     closeTask.run();
2402   }
2403 
2404   /**
2405    * Saves the home managed by this controller. If home name doesn't exist,
2406    * this method will act as {@link #saveAs() saveAs} method.
2407    */
save()2408   public void save() {
2409     save(HomeRecorder.Type.DEFAULT, null);
2410   }
2411 
2412   /**
2413    * Saves the home managed by this controller and executes <code>postSaveTask</code>
2414    * if it's not <code>null</code>.
2415    */
save(HomeRecorder.Type recorderType, Runnable postSaveTask)2416   private void save(HomeRecorder.Type recorderType, Runnable postSaveTask) {
2417     if (this.home.getName() == null
2418         || this.home.isRepaired()) {
2419       saveAs(recorderType, postSaveTask);
2420     } else {
2421       save(this.home.getName(), recorderType, postSaveTask);
2422     }
2423   }
2424 
2425   /**
2426    * Saves the home managed by this controller with a different name.
2427    * This method displays a {@link HomeView#showSaveDialog(String) save dialog} in  view,
2428    * and saves home with the chosen name if any.
2429    */
saveAs()2430   public void saveAs() {
2431     saveAs(HomeRecorder.Type.DEFAULT, null);
2432   }
2433 
2434   /**
2435    * Saves the home managed by this controller with a different name.
2436    * Once home is actually saved, home is removed from application homes list
2437    * and <code>postCloseTask</code> is called if it's not <code>null</code>.
2438    * @since 4.4
2439    */
saveAs(HomeRecorder.Type recorderType, Runnable postSaveTask)2440   protected void saveAs(HomeRecorder.Type recorderType, Runnable postSaveTask) {
2441     String newName = getView().showSaveDialog(this.home.getName());
2442     if (newName != null) {
2443       save(newName, recorderType, postSaveTask);
2444     }
2445   }
2446 
2447   /**
2448    * Saves the home managed by this controller and compresses it. If home name doesn't exist,
2449    * this method will prompt user to choose a home name.
2450    */
saveAndCompress()2451   public void saveAndCompress() {
2452     save(HomeRecorder.Type.COMPRESSED, null);
2453   }
2454 
2455   /**
2456    * Saves the home managed by this controller with a different name and compresses it.
2457    * This method displays a {@link HomeView#showSaveDialog(String) save dialog} in  view,
2458    * and saves home with the chosen name if any.
2459    * @since 4.2
2460    */
saveAsAndCompress()2461   public void saveAsAndCompress() {
2462     saveAs(HomeRecorder.Type.COMPRESSED, null);
2463   }
2464 
2465   /**
2466    * Actually saves the home managed by this controller and executes <code>postSaveTask</code>
2467    * if it's not <code>null</code>.
2468    */
save(final String homeName, final HomeRecorder.Type recorderType, final Runnable postSaveTask)2469   private void save(final String homeName,
2470                     final HomeRecorder.Type recorderType,
2471                     final Runnable postSaveTask) {
2472     // If home version is older than current version
2473     // or if home name is changed
2474     // or if user confirms to save a home created with a newer version
2475     if (this.home.getVersion() <= Home.CURRENT_VERSION
2476         || !homeName.equals(this.home.getName())
2477         || getView().confirmSaveNewerHome(homeName)) {
2478       final Home savedHome;
2479       try {
2480         // Clone home to save it safely in a threaded task
2481         savedHome = this.home.clone();
2482       } catch (RuntimeException ex) {
2483         // If home data is corrupted some way and couldn't be cloned
2484         // warn the user his home couldn't be saved
2485         getView().showError(preferences.getLocalizedString(
2486             HomeController.class, "saveError", homeName, ex));
2487         throw ex;
2488       }
2489       Callable<Void> saveTask = new Callable<Void>() {
2490             public Void call() throws RecorderException {
2491               savedHome.setName(contentManager.getPresentationName(homeName, ContentManager.ContentType.SWEET_HOME_3D));
2492               // Write home with application recorder
2493               application.getHomeRecorder(recorderType).writeHome(savedHome, homeName);
2494               updateSavedHome(homeName, savedHome.getVersion(), postSaveTask);
2495               return null;
2496             }
2497           };
2498       ThreadedTaskController.ExceptionHandler exceptionHandler =
2499           new ThreadedTaskController.ExceptionHandler() {
2500             public void handleException(Exception ex) {
2501               if (!(ex instanceof InterruptedRecorderException)) {
2502                 String cause = ex.toString();
2503                 if (ex instanceof NotEnoughSpaceRecorderException) {
2504                   long missingSpace = ((NotEnoughSpaceRecorderException)ex).getMissingSpace();
2505                   float missingSpaceMegaByte = Math.max(0.1f, missingSpace / 1048576f);
2506                   cause = "Missing " + new DecimalFormat("#.#").format(missingSpaceMegaByte) + " MB to save home";
2507                 } else if (ex instanceof RecorderException) {
2508                   cause = "RecorderException";
2509                   String message = ex.getMessage();
2510                   if (message != null) {
2511                     cause += ": " + message;
2512                   }
2513                   if (ex.getCause() != null) {
2514                     cause += "<br>" + ex.getCause();
2515                   }
2516                 }
2517                 ex.printStackTrace();
2518                 getView().showError(preferences.getLocalizedString(
2519                     HomeController.class, "saveError", homeName, cause));
2520               }
2521             }
2522           };
2523       new ThreadedTaskController(saveTask,
2524           this.preferences.getLocalizedString(HomeController.class, "saveMessage"), exceptionHandler,
2525           this.preferences, this.viewFactory).executeTask(getView());
2526     }
2527   }
2528 
2529   /**
2530    * Updates the saved home and executes <code>postSaveTask</code>
2531    * if it's not <code>null</code>.
2532    */
updateSavedHome(final String homeName, final long savedVersion, final Runnable postSaveTask)2533   private void updateSavedHome(final String homeName,
2534                                final long savedVersion,
2535                                final Runnable postSaveTask) {
2536     getView().invokeLater(new Runnable() {
2537         public void run() {
2538           home.setName(homeName);
2539           home.setModified(false);
2540           home.setRecovered(false);
2541           home.setRepaired(false);
2542           home.setVersion(savedVersion);
2543           // Update recent homes list
2544           List<String> recentHomes = new ArrayList<String>(preferences.getRecentHomes());
2545           int homeNameIndex = recentHomes.indexOf(homeName);
2546           if (homeNameIndex >= 0) {
2547             recentHomes.remove(homeNameIndex);
2548           }
2549           recentHomes.add(0, homeName);
2550           updateUserPreferencesRecentHomes(recentHomes);
2551 
2552           if (postSaveTask != null) {
2553             postSaveTask.run();
2554           }
2555         }
2556       });
2557   }
2558 
2559   /**
2560    * Controls the export of the furniture list of current home to a CSV file.
2561    * @since 4.0
2562    */
exportToCSV()2563   public void exportToCSV() {
2564     final String csvName = getView().showExportToCSVDialog(this.home.getName());
2565     if (csvName != null) {
2566       // Export furniture list in a threaded task
2567       Callable<Void> exportToCsvTask = new Callable<Void>() {
2568             public Void call() throws RecorderException {
2569               getView().exportToCSV(csvName);
2570               return null;
2571             }
2572           };
2573       ThreadedTaskController.ExceptionHandler exceptionHandler =
2574           new ThreadedTaskController.ExceptionHandler() {
2575             public void handleException(Exception ex) {
2576               if (!(ex instanceof InterruptedRecorderException)) {
2577                 if (ex instanceof RecorderException) {
2578                   String message = preferences.getLocalizedString(
2579                       HomeController.class, "exportToCSVError", csvName);
2580                   getView().showError(message);
2581                 } else {
2582                   ex.printStackTrace();
2583                 }
2584               }
2585             }
2586           };
2587       new ThreadedTaskController(exportToCsvTask,
2588           this.preferences.getLocalizedString(HomeController.class, "exportToCSVMessage"), exceptionHandler,
2589           this.preferences, this.viewFactory).executeTask(getView());
2590     }
2591   }
2592 
2593   /**
2594    * Controls the export of the current home plan to a SVG file.
2595    */
exportToSVG()2596   public void exportToSVG() {
2597     final String svgName = getView().showExportToSVGDialog(this.home.getName());
2598     if (svgName != null) {
2599       // Export plan in a threaded task
2600       Callable<Void> exportToSvgTask = new Callable<Void>() {
2601             public Void call() throws RecorderException {
2602               getView().exportToSVG(svgName);
2603               return null;
2604             }
2605           };
2606       ThreadedTaskController.ExceptionHandler exceptionHandler =
2607           new ThreadedTaskController.ExceptionHandler() {
2608             public void handleException(Exception ex) {
2609               if (!(ex instanceof InterruptedRecorderException)) {
2610                 if (ex instanceof RecorderException) {
2611                   String message = preferences.getLocalizedString(
2612                       HomeController.class, "exportToSVGError", svgName);
2613                   getView().showError(message);
2614                 } else {
2615                   ex.printStackTrace();
2616                 }
2617               }
2618             }
2619           };
2620       new ThreadedTaskController(exportToSvgTask,
2621           this.preferences.getLocalizedString(HomeController.class, "exportToSVGMessage"), exceptionHandler,
2622           this.preferences, this.viewFactory).executeTask(getView());
2623     }
2624   }
2625 
2626   /**
2627    * Controls the export of the 3D view of current home to an OBJ file.
2628    */
exportToOBJ()2629   public void exportToOBJ() {
2630     final String objName = getView().showExportToOBJDialog(this.home.getName());
2631     if (objName != null) {
2632       // Export 3D view in a threaded task
2633       Callable<Void> exportToObjTask = new Callable<Void>() {
2634             public Void call() throws RecorderException {
2635               getView().exportToOBJ(objName);
2636               return null;
2637             }
2638           };
2639       ThreadedTaskController.ExceptionHandler exceptionHandler =
2640           new ThreadedTaskController.ExceptionHandler() {
2641             public void handleException(Exception ex) {
2642               if (!(ex instanceof InterruptedRecorderException)) {
2643                 if (ex instanceof RecorderException) {
2644                   String message = preferences.getLocalizedString(
2645                       HomeController.class, "exportToOBJError", objName);
2646                   getView().showError(message);
2647                 } else {
2648                   ex.printStackTrace();
2649                 }
2650               }
2651             }
2652           };
2653       new ThreadedTaskController(exportToObjTask,
2654           this.preferences.getLocalizedString(HomeController.class, "exportToOBJMessage"), exceptionHandler,
2655           this.preferences, this.viewFactory).executeTask(getView());
2656     }
2657   }
2658 
2659   /**
2660    * Controls the creation of multiple photo-realistic images at the stored cameras locations.
2661    */
createPhotos()2662   public void createPhotos() {
2663     PhotosController photosController = new PhotosController(this.home, this.preferences,
2664         getHomeController3D().getView(), this.viewFactory, this.contentManager);
2665     photosController.displayView(getView());
2666   }
2667 
2668   /**
2669    * Controls the creation of photo-realistic images.
2670    */
createPhoto()2671   public void createPhoto() {
2672     PhotoController photoController = new PhotoController(this.home, this.preferences,
2673         getHomeController3D().getView(), this.viewFactory, this.contentManager);
2674     photoController.displayView(getView());
2675   }
2676 
2677   /**
2678    * Controls the creation of 3D videos.
2679    */
createVideo()2680   public void createVideo() {
2681     getPlanController().setMode(PlanController.Mode.SELECTION);
2682     getHomeController3D().viewFromObserver();
2683     VideoController videoController = new VideoController(this.home, this.preferences,
2684         this.viewFactory, this.contentManager);
2685     videoController.displayView(getView());
2686   }
2687 
2688   /**
2689    * Controls page setup.
2690    */
setupPage()2691   public void setupPage() {
2692     new PageSetupController(this.home, this.preferences,
2693         this.viewFactory, getUndoableEditSupport()).displayView(getView());
2694   }
2695 
2696   /**
2697    * Controls the print preview.
2698    */
previewPrint()2699   public void previewPrint() {
2700     new PrintPreviewController(this.home, this.preferences,
2701         this, this.viewFactory).displayView(getView());
2702   }
2703 
2704   /**
2705    * Controls the print of this home.
2706    */
print()2707   public void print() {
2708     final Callable<Void> printTask = getView().showPrintDialog();
2709     if (printTask != null) {
2710       // Print in a threaded task
2711       ThreadedTaskController.ExceptionHandler exceptionHandler =
2712           new ThreadedTaskController.ExceptionHandler() {
2713             public void handleException(Exception ex) {
2714               if (!(ex instanceof InterruptedRecorderException)) {
2715                 if (ex instanceof RecorderException) {
2716                   String message = preferences.getLocalizedString(
2717                       HomeController.class, "printError", home.getName());
2718                   getView().showError(message);
2719                 } else {
2720                   ex.printStackTrace();
2721                 }
2722               }
2723             }
2724           };
2725       new ThreadedTaskController(printTask,
2726           this.preferences.getLocalizedString(HomeController.class, "printMessage"), exceptionHandler,
2727           this.preferences, this.viewFactory).executeTask(getView());
2728     }
2729   }
2730 
2731   /**
2732    * Controls the print of this home in a PDF file.
2733    */
printToPDF()2734   public void printToPDF() {
2735     final String pdfName = getView().showPrintToPDFDialog(this.home.getName());
2736     if (pdfName != null) {
2737       // Print to PDF in a threaded task
2738       Callable<Void> printToPdfTask = new Callable<Void>() {
2739           public Void call() throws RecorderException {
2740             getView().printToPDF(pdfName);
2741             return null;
2742           }
2743         };
2744       ThreadedTaskController.ExceptionHandler exceptionHandler =
2745           new ThreadedTaskController.ExceptionHandler() {
2746             public void handleException(Exception ex) {
2747               if (!(ex instanceof InterruptedRecorderException)) {
2748                 if (ex instanceof RecorderException) {
2749                   String message = preferences.getLocalizedString(
2750                       HomeController.class, "printToPDFError", pdfName);
2751                   getView().showError(message);
2752                 } else {
2753                   ex.printStackTrace();
2754                 }
2755               }
2756             }
2757           };
2758       new ThreadedTaskController(printToPdfTask,
2759           preferences.getLocalizedString(HomeController.class, "printToPDFMessage"), exceptionHandler,
2760           this.preferences, this.viewFactory).executeTask(getView());
2761     }
2762   }
2763 
2764   /**
2765    * Controls application exit. If any home in application homes list is modified,
2766    * the user will be {@link HomeView#confirmExit() prompted} in view whether he wants
2767    * to discard his modifications or not.
2768    */
exit()2769   public void exit() {
2770     for (Home home : this.application.getHomes()) {
2771       if (home.isModified() || home.isRecovered() || home.isRepaired()) {
2772         if (getView().confirmExit()) {
2773           break;
2774         } else {
2775           return;
2776         }
2777       }
2778     }
2779     // Remove all homes from application
2780     for (Home home : this.application.getHomes()) {
2781       home.setRecovered(false);
2782       this.application.deleteHome(home);
2783     }
2784     // Let application decide what to do when there's no more home
2785   }
2786 
2787   /**
2788    * Edits preferences and changes them if user agrees.
2789    */
editPreferences()2790   public void editPreferences() {
2791     new UserPreferencesController(this.preferences,
2792         this.viewFactory, this.contentManager, this).displayView(getView());
2793   }
2794 
2795   /**
2796    * Enables magnetism in preferences.
2797    */
enableMagnetism()2798   public void enableMagnetism() {
2799     this.preferences.setMagnetismEnabled(true);
2800   }
2801 
2802   /**
2803    * Disables magnetism in preferences.
2804    */
disableMagnetism()2805   public void disableMagnetism() {
2806     this.preferences.setMagnetismEnabled(false);
2807   }
2808 
2809   /**
2810    * Displays a tip message dialog depending on the given mode and
2811    * sets the active mode of the plan controller.
2812    */
setMode(PlanController.Mode mode)2813   public void setMode(PlanController.Mode mode) {
2814     if (getPlanController().getMode() != mode) {
2815       final String actionKey;
2816       if (mode == PlanController.Mode.WALL_CREATION) {
2817         actionKey = HomeView.ActionType.CREATE_WALLS.name();
2818       } else if (mode == PlanController.Mode.ROOM_CREATION) {
2819         actionKey = HomeView.ActionType.CREATE_ROOMS.name();
2820       } else if (mode == PlanController.Mode.POLYLINE_CREATION) {
2821         actionKey = HomeView.ActionType.CREATE_POLYLINES.name();
2822       } else if (mode == PlanController.Mode.DIMENSION_LINE_CREATION) {
2823         actionKey = HomeView.ActionType.CREATE_DIMENSION_LINES.name();
2824       } else if (mode == PlanController.Mode.LABEL_CREATION) {
2825         actionKey = HomeView.ActionType.CREATE_LABELS.name();
2826       } else {
2827         actionKey = null;
2828       }
2829       // Display the tip message dialog matching mode
2830       if (actionKey != null
2831           && !this.preferences.isActionTipIgnored(actionKey)) {
2832         getView().invokeLater(new Runnable() {
2833             public void run() {
2834               // Show tip later to let the mode switch finish first
2835               if (getView().showActionTipMessage(actionKey)) {
2836                 preferences.setActionTipIgnored(actionKey);
2837               }
2838             }
2839           });
2840       }
2841       getPlanController().setMode(mode);
2842     }
2843   }
2844 
2845   /**
2846    * Displays the wizard that helps to import home background image.
2847    */
importBackgroundImage()2848   public void importBackgroundImage() {
2849     new BackgroundImageWizardController(this.home, this.preferences,
2850         this.viewFactory, this.contentManager, getUndoableEditSupport()).displayView(getView());
2851   }
2852 
2853   /**
2854    * Displays the wizard that helps to change home background image.
2855    */
modifyBackgroundImage()2856   public void modifyBackgroundImage() {
2857     importBackgroundImage();
2858   }
2859 
2860   /**
2861    * Hides the home background image.
2862    */
hideBackgroundImage()2863   public void hideBackgroundImage() {
2864     toggleBackgroundImageVisibility("undoHideBackgroundImageName");
2865   }
2866 
2867   /**
2868    * Shows the home background image.
2869    */
showBackgroundImage()2870   public void showBackgroundImage() {
2871     toggleBackgroundImageVisibility("undoShowBackgroundImageName");
2872   }
2873 
2874   /**
2875    * Toggles visibility of the background image and posts an undoable operation.
2876    */
toggleBackgroundImageVisibility(final String presentationName)2877   private void toggleBackgroundImageVisibility(final String presentationName) {
2878     final Level selectedLevel = this.home.getSelectedLevel();
2879     doToggleBackgroundImageVisibility(this.home);
2880     getUndoableEditSupport().postEdit(new BackgroundImageVisibilityTogglingUndoableEdit(
2881         this.home, this.preferences, presentationName, selectedLevel));
2882   }
2883 
2884   /**
2885    * Undoable edit for toggling background image visibility.
2886    */
2887   private static class BackgroundImageVisibilityTogglingUndoableEdit extends LocalizedUndoableEdit {
2888     private final Home  home;
2889     private final Level selectedLevel;
2890 
BackgroundImageVisibilityTogglingUndoableEdit(Home home, UserPreferences preferences, String presentationName, Level selectedLevel)2891     private BackgroundImageVisibilityTogglingUndoableEdit(Home home, UserPreferences preferences, String presentationName,
2892                                                           Level selectedLevel) {
2893       super(preferences, HomeController.class, presentationName);
2894       this.home = home;
2895       this.selectedLevel = selectedLevel;
2896     }
2897 
2898     @Override
undo()2899     public void undo() throws CannotUndoException {
2900       super.undo();
2901       this.home.setSelectedLevel(this.selectedLevel);
2902       doToggleBackgroundImageVisibility(this.home);
2903     }
2904 
2905     @Override
redo()2906     public void redo() throws CannotRedoException {
2907       super.redo();
2908       this.home.setSelectedLevel(this.selectedLevel);
2909       doToggleBackgroundImageVisibility(this.home);
2910     }
2911   }
2912 
2913   /**
2914    * Toggles visibility of the background image.
2915    */
doToggleBackgroundImageVisibility(Home home)2916   private static void doToggleBackgroundImageVisibility(Home home) {
2917     BackgroundImage backgroundImage = home.getSelectedLevel() != null
2918         ? home.getSelectedLevel().getBackgroundImage()
2919         : home.getBackgroundImage();
2920     backgroundImage = new BackgroundImage(backgroundImage.getImage(),
2921         backgroundImage.getScaleDistance(),
2922         backgroundImage.getScaleDistanceXStart(), backgroundImage.getScaleDistanceYStart(),
2923         backgroundImage.getScaleDistanceXEnd(), backgroundImage.getScaleDistanceYEnd(),
2924         backgroundImage.getXOrigin(), backgroundImage.getYOrigin(), !backgroundImage.isVisible());
2925     if (home.getSelectedLevel() != null) {
2926       home.getSelectedLevel().setBackgroundImage(backgroundImage);
2927     } else {
2928       home.setBackgroundImage(backgroundImage);
2929     }
2930   }
2931 
2932   /**
2933    * Deletes home background image and posts and posts an undoable operation.
2934    */
deleteBackgroundImage()2935   public void deleteBackgroundImage() {
2936     final Level selectedLevel = this.home.getSelectedLevel();
2937     final BackgroundImage oldImage;
2938     if (selectedLevel != null) {
2939       oldImage = selectedLevel.getBackgroundImage();
2940       selectedLevel.setBackgroundImage(null);
2941     } else {
2942       oldImage = this.home.getBackgroundImage();
2943       this.home.setBackgroundImage(null);
2944     }
2945     getUndoableEditSupport().postEdit(new BackgroundImageDeletionUndoableEdit(this.home, this.preferences,
2946         selectedLevel, oldImage));
2947   }
2948 
2949   /**
2950    * Undoable edit for background image deletion.
2951    */
2952   private static class BackgroundImageDeletionUndoableEdit extends LocalizedUndoableEdit {
2953     private final Home            home;
2954     private final Level           selectedLevel;
2955     private final BackgroundImage oldImage;
2956 
BackgroundImageDeletionUndoableEdit(Home home, UserPreferences preferences, Level selectedLevel, BackgroundImage oldImage)2957     private BackgroundImageDeletionUndoableEdit(Home home, UserPreferences preferences,
2958                                                 Level selectedLevel, BackgroundImage oldImage) {
2959       super(preferences, HomeController.class, "undoDeleteBackgroundImageName");
2960       this.home = home;
2961       this.oldImage = oldImage;
2962       this.selectedLevel = selectedLevel;
2963     }
2964 
2965     @Override
undo()2966     public void undo() throws CannotUndoException {
2967       super.undo();
2968       this.home.setSelectedLevel(this.selectedLevel);
2969       if (this.selectedLevel != null) {
2970         this.selectedLevel.setBackgroundImage(this.oldImage);
2971       } else {
2972         this.home.setBackgroundImage(this.oldImage);
2973       }
2974     }
2975 
2976     @Override
redo()2977     public void redo() throws CannotRedoException {
2978       super.redo();
2979       this.home.setSelectedLevel(this.selectedLevel);
2980       if (this.selectedLevel != null) {
2981         this.selectedLevel.setBackgroundImage(null);
2982       } else {
2983         this.home.setBackgroundImage(null);
2984       }
2985     }
2986   }
2987 
2988   /**
2989    * Zooms out in plan.
2990    */
zoomOut()2991   public void zoomOut() {
2992     PlanController planController = getPlanController();
2993     float newScale = planController.getScale() / 1.5f;
2994     planController.setScale(newScale);
2995     planController.getView().makeSelectionVisible();
2996   }
2997 
2998   /**
2999    * Zooms in in plan.
3000    */
zoomIn()3001   public void zoomIn() {
3002     PlanController planController = getPlanController();
3003     float newScale = planController.getScale() * 1.5f;
3004     planController.setScale(newScale);
3005     planController.getView().makeSelectionVisible();
3006   }
3007 
3008   /**
3009    * Prompts a name for the current camera and stores it in home.
3010    */
storeCamera()3011   public void storeCamera() {
3012     String now = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.MEDIUM).format(new Date());
3013     String name = getView().showStoreCameraDialog(now);
3014     if (name != null) {
3015       getHomeController3D().storeCamera(name);
3016     }
3017   }
3018 
3019   /**
3020    * Prompts stored cameras in home to be deleted and deletes the ones selected by the user.
3021    */
deleteCameras()3022   public void deleteCameras() {
3023     List<Camera> deletedCameras = getView().showDeletedCamerasDialog();
3024     if (deletedCameras != null) {
3025       getHomeController3D().deleteCameras(deletedCameras);
3026     }
3027   }
3028 
3029   /**
3030    * Detaches the given <code>view</code> from home view.
3031    */
detachView(View view)3032   public void detachView(View view) {
3033     if (view != null) {
3034       getView().detachView(view);
3035       this.notUndoableModifications = true;
3036       home.setModified(true);
3037     }
3038   }
3039 
3040   /**
3041    * Attaches the given <code>view</code> to home view.
3042    */
attachView(View view)3043   public void attachView(View view) {
3044     if (view != null) {
3045       getView().attachView(view);
3046       this.notUndoableModifications = true;
3047       home.setModified(true);
3048     }
3049   }
3050 
3051   /**
3052    * Displays help window.
3053    */
help()3054   public void help() {
3055     if (helpController == null) {
3056       helpController = new HelpController(this.preferences, this.viewFactory);
3057     }
3058     helpController.displayView();
3059   }
3060 
3061   /**
3062    * Displays about dialog.
3063    */
about()3064   public void about() {
3065     getView().showAboutDialog();
3066   }
3067 
3068   /**
3069    * Controls the change of value of a visual property in home.
3070    * @deprecated {@link #setVisualProperty(String, Object) setVisualProperty} should be replaced by a call to
3071    * {@link #setHomeProperty(String, String)} to ensure the property can be easily saved and read.
3072    */
setVisualProperty(String propertyName, Object propertyValue)3073   public void setVisualProperty(String propertyName,
3074                                 Object propertyValue) {
3075     this.home.setVisualProperty(propertyName, propertyValue);
3076   }
3077 
3078   /**
3079    * Controls the change of value of a property in home.
3080    * @since 5.2
3081    */
setHomeProperty(String propertyName, String propertyValue)3082   public void setHomeProperty(String propertyName,
3083                                 String propertyValue) {
3084     this.home.setProperty(propertyName, propertyValue);
3085   }
3086 
3087   /**
3088    * Checks if some application or libraries updates are available.
3089    * @since 4.0
3090    */
checkUpdates(final boolean displayOnlyIfNewUpdates)3091   public void checkUpdates(final boolean displayOnlyIfNewUpdates) {
3092     String updatesUrl = getPropertyValue("com.eteks.sweethome3d.updatesUrl", "updatesUrl");
3093     if (updatesUrl != null && updatesUrl.length() > 0) {
3094       final URL url;
3095       try {
3096         url = new URL(updatesUrl);
3097       } catch (MalformedURLException ex) {
3098         ex.printStackTrace();
3099         return;
3100       }
3101 
3102       final List<Library> libraries = this.preferences.getLibraries();
3103       final Long updatesMinimumDate = displayOnlyIfNewUpdates
3104           ? this.preferences.getUpdatesMinimumDate()
3105           : null;
3106 
3107       // Read updates from XML content in updatesUrl in a threaded task
3108       Callable<Void> checkUpdatesTask = new Callable<Void>() {
3109           public Void call() throws IOException, SAXException {
3110             final Map<Library, List<Update>> availableUpdates = readAvailableUpdates(url, libraries, updatesMinimumDate,
3111                 displayOnlyIfNewUpdates ? 3000 : -1);
3112             getView().invokeLater(new Runnable () {
3113                 public void run() {
3114                   if (availableUpdates.isEmpty()) {
3115                     if (!displayOnlyIfNewUpdates) {
3116                       getView().showMessage(preferences.getLocalizedString(HomeController.class, "noUpdateMessage"));
3117                     }
3118                   } else if (!getView().showUpdatesMessage(getUpdatesMessage(availableUpdates), !displayOnlyIfNewUpdates)) {
3119                     // Search the latest date among updates
3120                     long latestUpdateDate = Long.MIN_VALUE;
3121                     for (List<Update> libraryAvailableUpdates : availableUpdates.values()) {
3122                       for (Update update : libraryAvailableUpdates) {
3123                         latestUpdateDate = Math.max(latestUpdateDate, update.getDate().getTime());
3124                       }
3125                     }
3126                     preferences.setUpdatesMinimumDate(latestUpdateDate + 1);
3127                   }
3128                 }
3129               });
3130             return null;
3131           }
3132         };
3133       ThreadedTaskController.ExceptionHandler exceptionHandler =
3134           new ThreadedTaskController.ExceptionHandler() {
3135             public void handleException(Exception ex) {
3136               if (!displayOnlyIfNewUpdates && !(ex instanceof InterruptedIOException)) {
3137                 if (ex instanceof IOException) {
3138                   getView().showError(preferences.getLocalizedString(HomeController.class, "checkUpdatesIOError", ex));
3139                 } else if (ex instanceof SAXException) {
3140                   getView().showError(preferences.getLocalizedString(HomeController.class, "checkUpdatesXMLError", ex.getMessage()));
3141                 } else {
3142                   ex.printStackTrace();
3143                 }
3144               }
3145             }
3146           };
3147 
3148       ViewFactory dummyThreadedTaskViewFactory = new ViewFactoryAdapter() {
3149           @Override
3150           public ThreadedTaskView createThreadedTaskView(String taskMessage, UserPreferences preferences,
3151                                                          ThreadedTaskController controller) {
3152             // Return a dummy view that doesn't do anything
3153             return new ThreadedTaskView() {
3154               public void setTaskRunning(boolean taskRunning, View executingView) {
3155               }
3156 
3157               public void invokeLater(Runnable runnable) {
3158                 getView().invokeLater(runnable);
3159               }
3160             };
3161           }
3162         };
3163       new ThreadedTaskController(checkUpdatesTask,
3164           this.preferences.getLocalizedString(HomeController.class, "checkUpdatesMessage"), exceptionHandler,
3165           this.preferences, displayOnlyIfNewUpdates
3166             ? dummyThreadedTaskViewFactory
3167             : this.viewFactory).executeTask(getView());
3168     }
3169   }
3170 
3171   /**
3172    * Returns the System property value of the given <code>propertyKey</code>, or the
3173    * the resource property value matching <code>resourceKey</code> or <code>null</code>
3174    * if none are defined.
3175    */
getPropertyValue(String propertyKey, String resourceKey)3176   private String getPropertyValue(String propertyKey, String resourceKey) {
3177     String propertyValue = System.getProperty(propertyKey);
3178     if (propertyValue != null) {
3179       return propertyValue;
3180     } else {
3181       try {
3182         return this.preferences.getLocalizedString(HomeController.class, resourceKey);
3183       } catch (IllegalArgumentException ex) {
3184         return null;
3185       }
3186     }
3187   }
3188 
3189   /**
3190    * Reads the available updates from the XML stream contained in the given <code>url</code>.
3191    * Caution : this method is called from a separate thread.
3192    */
readAvailableUpdates(URL url, List<Library> libraries, Long minDate, int timeout)3193   private Map<Library, List<Update>> readAvailableUpdates(URL url, List<Library> libraries, Long minDate, int timeout) throws IOException, SAXException {
3194     try {
3195       SAXParserFactory factory = SAXParserFactory.newInstance();
3196       factory.setValidating(false);
3197       SAXParser saxParser = factory.newSAXParser();
3198       UpdatesHandler updatesHandler = new UpdatesHandler(url);
3199       URLConnection connection = url.openConnection();
3200       if (timeout > 0) {
3201         connection.setConnectTimeout(timeout);
3202         connection.setReadTimeout(timeout);
3203       }
3204       saxParser.parse(connection.getInputStream(), updatesHandler);
3205 
3206       // Filter updates according to application version and libraries version
3207       Map<Library, List<Update>> availableUpdates = new LinkedHashMap<Library, List<Update>>();
3208       long now = System.currentTimeMillis();
3209       if (this.application != null) {
3210         String applicationId = this.application.getId();
3211         List<Update> applicationUpdates = getAvailableUpdates(updatesHandler.getUpdates(applicationId),
3212             this.application.getVersion(), minDate, now);
3213         if (!applicationUpdates.isEmpty()) {
3214           availableUpdates.put(null, applicationUpdates);
3215         }
3216       }
3217       Set<String> updatedLibraryIds = new HashSet<String>();
3218       for (Library library : libraries) {
3219         if (Thread.interrupted()) {
3220           throw new InterruptedIOException();
3221         }
3222         String libraryId = library.getId();
3223         if (libraryId != null
3224             && !updatedLibraryIds.contains(libraryId)) {
3225           List<Update> libraryUpdates = getAvailableUpdates(updatesHandler.getUpdates(libraryId),
3226               library.getVersion(), minDate, now);
3227           if (!libraryUpdates.isEmpty()) {
3228             availableUpdates.put(library, libraryUpdates);
3229           }
3230           // Ignore older libraries with same ID
3231           updatedLibraryIds.add(libraryId);
3232         }
3233       }
3234       return availableUpdates;
3235     } catch (ParserConfigurationException ex) {
3236       throw new SAXException(ex);
3237     } catch (SAXException ex) {
3238       // If task was interrupted (see UpdatesHandler implementation), report the interruption
3239       if (ex.getCause() instanceof InterruptedIOException) {
3240         throw (InterruptedIOException)ex.getCause();
3241       } else {
3242         throw ex;
3243       }
3244     }
3245   }
3246 
3247   /**
3248    * Returns the updates sublist which match the given <code>version</code>.
3249    * If no update has a date greater that <code>minDate</code>, an empty list is returned.
3250    * Caution : this method is called from a separate thread.
3251    */
getAvailableUpdates(List<Update> updates, String version, Long minDate, long maxDate)3252   private List<Update> getAvailableUpdates(List<Update> updates, String version, Long minDate, long maxDate) {
3253     if (updates != null) {
3254       boolean recentUpdates = false;
3255       List<Update> availableUpdates = new ArrayList<Update>();
3256       for (Update update : updates) {
3257         String minVersion = update.getMinVersion();
3258         String maxVersion = update.getMaxVersion();
3259         String operatingSystem = update.getOperatingSystem();
3260         if (OperatingSystem.compareVersions(version, update.getVersion()) < 0
3261             && (minVersion == null || OperatingSystem.compareVersions(minVersion, version) <= 0)
3262             && (maxVersion == null || OperatingSystem.compareVersions(version, maxVersion) < 0)
3263             && (operatingSystem == null || System.getProperty("os.name").matches(operatingSystem))) {
3264           Date date = update.getDate();
3265           if (date == null
3266               || ((minDate == null || date.getTime() >= minDate)
3267                   && date.getTime() < maxDate)) {
3268             availableUpdates.add(update);
3269             recentUpdates = true;
3270           }
3271         }
3272       }
3273       if (recentUpdates) {
3274         Collections.sort(availableUpdates, new Comparator<Update>() {
3275             public int compare(Update update1, Update update2) {
3276               return -OperatingSystem.compareVersions(update1.getVersion(), update2.getVersion());
3277             }
3278           });
3279         return availableUpdates;
3280       }
3281     }
3282     return Collections.emptyList();
3283   }
3284 
3285   /**
3286    * Returns the message for the given updates.
3287    */
getUpdatesMessage(Map<Library, List<Update>> updates)3288   private String getUpdatesMessage(Map<Library, List<Update>> updates) {
3289     if (updates.isEmpty()) {
3290       return this.preferences.getLocalizedString(HomeController.class, "noUpdateMessage");
3291     } else {
3292       String message = "<html><head><style>"
3293           + this.preferences.getLocalizedString(HomeController.class, "updatesMessageStyleSheet")
3294           + " .separator { margin: 0px;}</style></head><body>"
3295           + this.preferences.getLocalizedString(HomeController.class, "updatesMessageTitle");
3296       String applicationUpdateMessage = this.preferences.getLocalizedString(HomeController.class, "applicationUpdateMessage");
3297       String libraryUpdateMessage = this.preferences.getLocalizedString(HomeController.class, "libraryUpdateMessage");
3298       String sizeUpdateMessage = this.preferences.getLocalizedString(HomeController.class, "sizeUpdateMessage");
3299       String downloadUpdateMessage = this.preferences.getLocalizedString(HomeController.class, "downloadUpdateMessage");
3300       String updatesMessageSeparator = this.preferences.getLocalizedString(HomeController.class, "updatesMessageSeparator");
3301       boolean firstUpdate = true;
3302       for (Map.Entry<Library, List<Update>> updateEntry : updates.entrySet()) {
3303         if (firstUpdate) {
3304           firstUpdate = false;
3305         } else {
3306           message += updatesMessageSeparator;
3307         }
3308         Library library = updateEntry.getKey();
3309         if (library == null) {
3310           // Application itself
3311           if (this.application != null) {
3312             message += getApplicationOrLibraryUpdateMessage(updateEntry.getValue(), this.application.getName(),
3313                 applicationUpdateMessage, sizeUpdateMessage, downloadUpdateMessage);
3314           }
3315         } else {
3316           String name = library.getName();
3317           if (name == null) {
3318             name = library.getDescription();
3319             if (name == null) {
3320               name = library.getLocation();
3321             }
3322           }
3323           message += getApplicationOrLibraryUpdateMessage(updateEntry.getValue(), name,
3324               libraryUpdateMessage, sizeUpdateMessage, downloadUpdateMessage);
3325         }
3326       }
3327 
3328       message += "</body></html>";
3329       return message;
3330     }
3331   }
3332 
3333   /**
3334    * Returns the message for the update of the application or a library.
3335    */
getApplicationOrLibraryUpdateMessage(List<Update> updates, String applicationOrLibraryName, String applicationOrLibraryUpdateMessage, String sizeUpdateMessage, String downloadUpdateMessage)3336   private String getApplicationOrLibraryUpdateMessage(List<Update> updates,
3337                                                       String applicationOrLibraryName,
3338                                                       String applicationOrLibraryUpdateMessage,
3339                                                       String sizeUpdateMessage,
3340                                                       String downloadUpdateMessage) {
3341     String message = "";
3342     boolean first = true;
3343     DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG);
3344     DecimalFormat megabyteSizeFormat = new DecimalFormat("#,##0.#");
3345     for (Update update : updates) {
3346       String size;
3347       if (update.getSize() != null) {
3348         // Format at MB format
3349         size = String.format(sizeUpdateMessage,
3350             megabyteSizeFormat.format(update.getSize() / (1024. * 1024.)));
3351       } else {
3352         size = "";
3353       }
3354       message += String.format(applicationOrLibraryUpdateMessage,
3355           applicationOrLibraryName, update.getVersion(), dateFormat.format(update.getDate()), size);
3356       if (first) {
3357         first = false;
3358         URL downloadPage = update.getDownloadPage();
3359         if (downloadPage == null) {
3360           downloadPage = update.getDefaultDownloadPage();
3361         }
3362         if (downloadPage != null) {
3363           message += String.format(downloadUpdateMessage, downloadPage);
3364         }
3365       }
3366       String comment = update.getComment();
3367       if (comment == null) {
3368         comment = update.getDefaultComment();
3369       }
3370       if (comment != null) {
3371         message += "<p class='separator'/>";
3372         message += comment;
3373         message += "<p class='separator'/>";
3374       }
3375     }
3376     return message;
3377   }
3378 
3379   /**
3380    * SAX handler used to parse updates XML files.
3381    * DTD used in updated files:<pre>
3382    * &lt;!ELEMENT updates (update*)>
3383    *
3384    * &lt;!ELEMENT update (downloadPage*, comment*)>
3385    * &lt;!ATTLIST update id CDATA #REQUIRED>
3386    * &lt;!ATTLIST update version CDATA #REQUIRED>
3387    * &lt;!ATTLIST update operatingSystem CDATA #IMPLIED>
3388    * &lt;!ATTLIST update date CDATA #REQUIRED>
3389    * &lt;!ATTLIST update minVersion CDATA #IMPLIED>
3390    * &lt;!ATTLIST update maxVersion CDATA #IMPLIED>
3391    * &lt;!ATTLIST update size CDATA #IMPLIED>
3392    * &lt;!ATTLIST update inherits CDATA #IMPLIED>
3393    *
3394    * &lt;!ELEMENT downloadPage EMPTY>
3395    * &lt;!ATTLIST downloadPage url CDATA #REQUIRED>
3396    * &lt;!ATTLIST downloadPage lang CDATA #IMPLIED>
3397    *
3398    * &lt;!ELEMENT comment (#PCDATA)>
3399    * &lt;!ATTLIST comment lang CDATA #IMPLIED>
3400    * </pre>
3401    * with <code>updates</code> as root element,
3402    * <code>operatingSystem</code> an optional regular expression for the target OS,
3403    * <code>inherits</code> the id of an other <code>update</code> element with the same version,
3404    * <code>date</code> using <code>yyyy-MM-ddThh:mm:ss<code> or <code>yyyy-MM-dd</code> format
3405    * at GMT and <code>comment</code> element possibly containing XHTML.
3406    */
3407   private class UpdatesHandler extends DefaultHandler {
3408     private final URL                       baseUrl;
3409     private final StringBuilder             comment = new StringBuilder();
3410     private final SimpleDateFormat          dateTimeFormat;
3411     private final SimpleDateFormat          dateFormat;
3412     private final Map<String, List<Update>> updates = new HashMap<String, List<Update>>();
3413     private Update                          update;
3414     private boolean                         inComment;
3415     private boolean                         inUpdate;
3416     private String                          language;
3417 
UpdatesHandler(URL baseUrl)3418     public UpdatesHandler(URL baseUrl) {
3419       this.baseUrl = baseUrl;
3420       TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
3421       this.dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss");
3422       this.dateTimeFormat.setTimeZone(gmtTimeZone);
3423       this.dateFormat = new SimpleDateFormat("yyyy-MM-dd");
3424       this.dateFormat.setTimeZone(gmtTimeZone);
3425     }
3426 
3427     /**
3428      * Returns the update matching the given <code>id</code>.
3429      */
getUpdates(String id)3430     private List<Update> getUpdates(String id) {
3431       return this.updates.get(id);
3432     }
3433 
3434     /**
3435      * Throws a <code>SAXException</code> exception initialized with a <code>InterruptedRecorderException</code>
3436      * cause if current thread is interrupted. The interrupted status of the current thread
3437      * is cleared when an exception is thrown.
3438      */
checkCurrentThreadIsntInterrupted()3439     private void checkCurrentThreadIsntInterrupted() throws SAXException {
3440       if (Thread.interrupted()) {
3441         throw new SAXException(new InterruptedIOException());
3442       }
3443     }
3444 
3445     @Override
startElement(String uri, String localName, String name, Attributes attributes)3446     public void startElement(String uri, String localName, String name, Attributes attributes) throws SAXException {
3447       checkCurrentThreadIsntInterrupted();
3448       if (this.inComment) {
3449         // Reproduce comment content
3450         this.comment.append("<" + name);
3451         for (int i = 0; i < attributes.getLength(); i++) {
3452           this.comment.append(" " + attributes.getQName(i) + "=\"" + attributes.getValue(i) + "\"");
3453         }
3454         this.comment.append(">");
3455       } else if (this.inUpdate && "comment".equals(name)) {
3456         this.comment.setLength(0);
3457         this.language = attributes.getValue("lang");
3458         if (this.language == null || preferences.getLanguage().equals(this.language)) {
3459           this.inComment = true;
3460         }
3461       } else if (this.inUpdate && "downloadPage".equals(name)) {
3462         String url = attributes.getValue("url");
3463         if (url != null) {
3464           try {
3465             String language = attributes.getValue("lang");
3466             if (language == null) {
3467               this.update.setDefaultDownloadPage(new URL(this.baseUrl, url));
3468             } else if (preferences.getLanguage().equals(language)) {
3469               this.update.setDownloadPage(new URL(this.baseUrl, url));
3470             }
3471           } catch (MalformedURLException ex) {
3472             // Ignore bad URLs
3473           }
3474         }
3475       } else if (!this.inUpdate && "update".equals(name)) {
3476         String id = attributes.getValue("id");
3477         String version = attributes.getValue("version");
3478         if (id != null
3479             && version != null) {
3480           this.update = new Update(id, version);
3481 
3482           String inheritedUpdate = attributes.getValue("inherits");
3483           // If update inherits from an other update, search the update with the same id and version
3484           if (inheritedUpdate != null) {
3485             List<Update> updates = this.updates.get(inheritedUpdate);
3486             if (updates != null) {
3487               for (Update update : updates) {
3488                 if (version.equals(update.getVersion())) {
3489                   this.update = update.clone();
3490                   this.update.setId(id);
3491                   break;
3492                 }
3493               }
3494             }
3495           }
3496 
3497           String dateAttibute = attributes.getValue("date");
3498           if (dateAttibute != null) {
3499             try {
3500               this.update.setDate(this.dateTimeFormat.parse(dateAttibute));
3501             } catch (ParseException ex) {
3502               try {
3503                 this.update.setDate(this.dateFormat.parse(dateAttibute));
3504               } catch (ParseException ex1) {
3505               }
3506             }
3507           }
3508 
3509           String minVersion = attributes.getValue("minVersion");
3510           if (minVersion != null) {
3511             this.update.setMinVersion(minVersion);
3512           }
3513 
3514           String maxVersion = attributes.getValue("maxVersion");
3515           if (maxVersion != null) {
3516             this.update.setMaxVersion(maxVersion);
3517           }
3518 
3519           String size = attributes.getValue("size");
3520           if (size != null) {
3521             try {
3522               this.update.setSize(new Long (size));
3523             } catch (NumberFormatException ex) {
3524               // Ignore malformed number
3525             }
3526           }
3527 
3528           String operatingSystem = attributes.getValue("operatingSystem");
3529           if (operatingSystem != null) {
3530             this.update.setOperatingSystem(operatingSystem);
3531           }
3532 
3533           List<Update> updates = this.updates.get(id);
3534           if (updates == null) {
3535             updates = new ArrayList<Update>();
3536             this.updates.put(id, updates);
3537           }
3538           updates.add(this.update);
3539           this.inUpdate = true;
3540         }
3541       }
3542     }
3543 
3544     @Override
characters(char [] ch, int start, int length)3545     public void characters(char [] ch, int start, int length) throws SAXException {
3546       checkCurrentThreadIsntInterrupted();
3547       if (this.inComment) {
3548         // Reproduce comment content
3549         this.comment.append(ch, start, length);
3550       }
3551     }
3552 
3553     @Override
endElement(String uri, String localName, String name)3554     public void endElement(String uri, String localName, String name) throws SAXException {
3555       if (this.inComment) {
3556         if ("comment".equals(name)) {
3557           String comment = this.comment.toString().trim().replace('\n', ' ');
3558           if (comment.length() == 0) {
3559             comment = null;
3560           }
3561           if (this.language == null) {
3562             this.update.setDefaultComment(comment);
3563           } else {
3564             this.update.setComment(comment);
3565           }
3566           this.inComment = false;
3567         } else {
3568           // Reproduce comment content
3569           this.comment.append("</" + name + ">");
3570         }
3571       } else if (this.inUpdate && "update".equals(name)) {
3572         this.inUpdate = false;
3573       }
3574     }
3575   }
3576 
3577   /**
3578    * Update info.
3579    */
3580   private static class Update implements Cloneable {
3581     private String id;
3582     private final String version;
3583     private Date   date;
3584     private String minVersion;
3585     private String maxVersion;
3586     private Long   size;
3587     private String operatingSystem;
3588     private URL    defaultDownloadPage;
3589     private URL    downloadPage;
3590     private String defaultComment;
3591     private String comment;
3592 
Update(String id, String version)3593     public Update(String id, String version) {
3594       this.id = id;
3595       this.version = version;
3596     }
3597 
getId()3598     public String getId() {
3599       return this.id;
3600     }
3601 
setId(String id)3602     public void setId(String id) {
3603       this.id = id;
3604     }
3605 
getVersion()3606     public String getVersion() {
3607       return this.version;
3608     }
3609 
getDate()3610     public Date getDate() {
3611       return this.date;
3612     }
3613 
setDate(Date date)3614     public void setDate(Date date) {
3615       this.date = date;
3616     }
3617 
getMinVersion()3618     public String getMinVersion() {
3619       return this.minVersion;
3620     }
3621 
setMinVersion(String minVersion)3622     public void setMinVersion(String minVersion) {
3623       this.minVersion = minVersion;
3624     }
3625 
getMaxVersion()3626     public String getMaxVersion() {
3627       return this.maxVersion;
3628     }
3629 
setMaxVersion(String maxVersion)3630     public void setMaxVersion(String maxVersion) {
3631       this.maxVersion = maxVersion;
3632     }
3633 
getSize()3634     public Long getSize() {
3635       return this.size;
3636     }
3637 
setSize(Long size)3638     public void setSize(Long size) {
3639       this.size = size;
3640     }
3641 
getOperatingSystem()3642     public String getOperatingSystem() {
3643       return this.operatingSystem;
3644     }
3645 
setOperatingSystem(String system)3646     public void setOperatingSystem(String system) {
3647       this.operatingSystem = system;
3648     }
3649 
getDefaultDownloadPage()3650     public URL getDefaultDownloadPage() {
3651       return this.defaultDownloadPage;
3652     }
3653 
setDefaultDownloadPage(URL defaultDownloadPage)3654     public void setDefaultDownloadPage(URL defaultDownloadPage) {
3655       this.defaultDownloadPage = defaultDownloadPage;
3656     }
3657 
getDownloadPage()3658     public URL getDownloadPage() {
3659       return this.downloadPage;
3660     }
3661 
setDownloadPage(URL downloadPage)3662     public void setDownloadPage(URL downloadPage) {
3663       this.downloadPage = downloadPage;
3664     }
3665 
getDefaultComment()3666     public String getDefaultComment() {
3667       return this.defaultComment;
3668     }
3669 
setDefaultComment(String defaultComment)3670     public void setDefaultComment(String defaultComment) {
3671       this.defaultComment = defaultComment;
3672     }
3673 
getComment()3674     public String getComment() {
3675       return this.comment;
3676     }
3677 
setComment(String comment)3678     public void setComment(String comment) {
3679       this.comment = comment;
3680     }
3681 
3682     @Override
clone()3683     protected Update clone() {
3684       try {
3685         return (Update)super.clone();
3686       } catch (CloneNotSupportedException ex) {
3687         throw new InternalError();
3688       }
3689     }
3690   }
3691 }
3692