1 /*
2  * LevelController.java 27 oct 2011
3  *
4  * Sweet Home 3D, Copyright (c) 2011 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.PropertyChangeListener;
23 import java.beans.PropertyChangeSupport;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.List;
27 
28 import javax.swing.undo.CannotRedoException;
29 import javax.swing.undo.CannotUndoException;
30 import javax.swing.undo.UndoableEdit;
31 import javax.swing.undo.UndoableEditSupport;
32 
33 import com.eteks.sweethome3d.model.Elevatable;
34 import com.eteks.sweethome3d.model.Home;
35 import com.eteks.sweethome3d.model.Level;
36 import com.eteks.sweethome3d.model.Selectable;
37 import com.eteks.sweethome3d.model.UserPreferences;
38 
39 /**
40  * A MVC controller for home levels view.
41  * @author Emmanuel Puybaret
42  */
43 public class LevelController implements Controller {
44   /**
45    * The properties that may be edited by the view associated to this controller.
46    */
47   public enum Property {VIEWABLE, NAME, ELEVATION, ELEVATION_INDEX, FLOOR_THICKNESS, HEIGHT, LEVELS, SELECT_LEVEL_INDEX}
48 
49   private final Home                  home;
50   private final UserPreferences       preferences;
51   private final ViewFactory           viewFactory;
52   private final UndoableEditSupport   undoSupport;
53   private final PropertyChangeSupport propertyChangeSupport;
54   private DialogView                  homeLevelView;
55 
56   private String   name;
57   private Boolean  viewable;
58   private Float    elevation;
59   private Integer  elevationIndex;
60   private Float    floorThickness;
61   private Float    height;
62   private Level [] levels;
63   private Integer  selectedLevelIndex;
64 
65   /**
66    * Creates the controller of home levels view with undo support.
67    */
LevelController(Home home, UserPreferences preferences, ViewFactory viewFactory, UndoableEditSupport undoSupport)68   public LevelController(Home home,
69                          UserPreferences preferences,
70                          ViewFactory viewFactory,
71                          UndoableEditSupport undoSupport) {
72     this.home = home;
73     this.preferences = preferences;
74     this.viewFactory = viewFactory;
75     this.undoSupport = undoSupport;
76     this.propertyChangeSupport = new PropertyChangeSupport(this);
77 
78     updateProperties();
79   }
80 
81   /**
82    * Returns the view associated with this controller.
83    */
getView()84   public DialogView getView() {
85     // Create view lazily only once it's needed
86     if (this.homeLevelView == null) {
87       this.homeLevelView = this.viewFactory.createLevelView(this.preferences, this);
88     }
89     return this.homeLevelView;
90   }
91 
92   /**
93    * Displays the view controlled by this controller.
94    */
displayView(View parentView)95   public void displayView(View parentView) {
96     getView().displayView(parentView);
97   }
98 
99   /**
100    * Adds the property change <code>listener</code> in parameter to this controller.
101    */
addPropertyChangeListener(Property property, PropertyChangeListener listener)102   public void addPropertyChangeListener(Property property, PropertyChangeListener listener) {
103     this.propertyChangeSupport.addPropertyChangeListener(property.name(), listener);
104   }
105 
106   /**
107    * Removes the property change <code>listener</code> in parameter from this controller.
108    */
removePropertyChangeListener(Property property, PropertyChangeListener listener)109   public void removePropertyChangeListener(Property property, PropertyChangeListener listener) {
110     this.propertyChangeSupport.removePropertyChangeListener(property.name(), listener);
111   }
112 
113   /**
114    * Updates edited properties from selected level in the home edited by this controller.
115    */
updateProperties()116   protected void updateProperties() {
117     Level selectedLevel = this.home.getSelectedLevel();
118     setLevels(clone(this.home.getLevels().toArray(new Level [0])));
119     if (selectedLevel == null) {
120       setSelectedLevelIndex(null);
121       setName(null); // Nothing to edit
122       setViewable(Boolean.TRUE);
123       setElevation(null, false);
124       setFloorThickness(null);
125       setHeight(null);
126       setElevationIndex(null, false);
127     } else {
128       setSelectedLevelIndex(this.home.getLevels().indexOf(selectedLevel));
129       setName(selectedLevel.getName());
130       setViewable(selectedLevel.isViewable());
131       setElevation(selectedLevel.getElevation(), false);
132       setFloorThickness(selectedLevel.getFloorThickness());
133       setHeight(selectedLevel.getHeight());
134       setElevationIndex(selectedLevel.getElevationIndex(), false);
135     }
136   }
137 
clone(Level[] levels)138   private Level [] clone(Level[] levels) {
139     for (int i = 0; i < levels.length; i++) {
140       levels [i] = levels [i].clone();
141     }
142     return levels;
143   }
144 
145   /**
146    * Returns <code>true</code> if the given <code>property</code> is editable.
147    * Depending on whether a property is editable or not, the view associated to this controller
148    * may render it differently.
149    * The implementation of this method always returns <code>true</code>.
150    */
isPropertyEditable(Property property)151   public boolean isPropertyEditable(Property property) {
152     return true;
153   }
154 
155   /**
156    * Sets the edited name.
157    */
setName(String name)158   public void setName(String name) {
159     if (name != this.name) {
160       String oldName = this.name;
161       this.name = name;
162       this.propertyChangeSupport.firePropertyChange(Property.NAME.name(), oldName, name);
163       if (this.selectedLevelIndex != null) {
164         this.levels [this.selectedLevelIndex].setName(name);
165         this.propertyChangeSupport.firePropertyChange(Property.LEVELS.name(), null, this.levels);
166       }
167     }
168   }
169 
170   /**
171    * Returns the edited name.
172    */
getName()173   public String getName() {
174     return this.name;
175   }
176 
177   /**
178    * Sets the edited viewable attribute.
179    * @since 5.0
180    */
setViewable(Boolean viewable)181   public void setViewable(Boolean viewable) {
182     if (viewable != this.viewable) {
183       Boolean oldViewable = viewable;
184       this.viewable = viewable;
185       this.propertyChangeSupport.firePropertyChange(Property.VIEWABLE.name(), oldViewable, viewable);
186       if (viewable != null && this.selectedLevelIndex != null) {
187         this.levels [this.selectedLevelIndex].setViewable(viewable);
188         this.propertyChangeSupport.firePropertyChange(Property.LEVELS.name(), null, this.levels);
189       }
190     }
191   }
192 
193   /**
194    * Returns the edited viewable attribute.
195    * @since 5.0
196    */
getViewable()197   public Boolean getViewable() {
198     return this.viewable;
199   }
200 
201   /**
202    * Sets the edited elevation.
203    */
setElevation(Float elevation)204   public void setElevation(Float elevation) {
205     setElevation(elevation, true);
206   }
207 
setElevation(Float elevation, boolean updateLevels)208   private void setElevation(Float elevation, boolean updateLevels) {
209     if (elevation != this.elevation) {
210       Float oldElevation = this.elevation;
211       this.elevation = elevation;
212       this.propertyChangeSupport.firePropertyChange(Property.ELEVATION.name(), oldElevation, elevation);
213 
214       if (updateLevels
215           && elevation != null
216           && this.selectedLevelIndex != null) {
217         int elevationIndex = updateLevelElevation(this.levels [this.selectedLevelIndex],
218             elevation, Arrays.asList(this.levels));
219         setElevationIndex(elevationIndex, false);
220         updateLevels();
221       }
222     }
223   }
224 
225   /**
226    * Updates the elevation of the given <code>level</code> and modifies the
227    * elevation index of other levels if necessary.
228    */
updateLevelElevation(Level level, float elevation, List<Level> levels)229   private static int updateLevelElevation(Level level, float elevation, List<Level> levels) {
230     // Search biggest elevation index at the given elevation
231     // and update elevation index of other levels at the current elevation of the modified level
232     int levelIndex = levels.size();
233     int elevationIndex = 0;
234     for (int i = 0; i < levels.size(); i++) {
235       Level homeLevel = levels.get(i);
236       if (homeLevel == level) {
237         levelIndex = i;
238       } else {
239         if (homeLevel.getElevation() == elevation) {
240           elevationIndex = homeLevel.getElevationIndex() + 1;
241         } else if (i > levelIndex
242             && homeLevel.getElevation() == level.getElevation()) {
243           homeLevel.setElevationIndex(homeLevel.getElevationIndex() - 1);
244         }
245       }
246     }
247     level.setElevation(elevation);
248     level.setElevationIndex(elevationIndex);
249     return elevationIndex;
250   }
251 
252   /**
253    * Returns the edited elevation.
254    */
getElevation()255   public Float getElevation() {
256     return this.elevation;
257   }
258 
259   /**
260    * Sets the edited elevation index.
261    * @since 5.0
262    */
setElevationIndex(Integer elevationIndex)263   public void setElevationIndex(Integer elevationIndex) {
264     setElevationIndex(elevationIndex, true);
265   }
266 
setElevationIndex(Integer elevationIndex, boolean updateLevels)267   private void setElevationIndex(Integer elevationIndex, boolean updateLevels) {
268     if (elevationIndex != this.elevationIndex) {
269       Integer oldElevationIndex = this.elevationIndex;
270       this.elevationIndex = elevationIndex;
271       this.propertyChangeSupport.firePropertyChange(Property.ELEVATION_INDEX.name(), oldElevationIndex, elevationIndex);
272 
273       if (updateLevels
274           && elevationIndex != null
275           && this.selectedLevelIndex != null) {
276         updateLevelElevationIndex(this.levels [this.selectedLevelIndex], elevationIndex, Arrays.asList(this.levels));
277         updateLevels();
278       }
279     }
280   }
281 
282   /**
283    * Updates the elevation index of the given <code>level</code> and modifies the
284    * elevation index of other levels at same elevation if necessary.
285    */
updateLevelElevationIndex(Level level, int elevationIndex, List<Level> levels)286   private static void updateLevelElevationIndex(Level level, int elevationIndex, List<Level> levels) {
287     // Update elevation index of levels with a value between selected level index and new index
288     float elevationIndexSignum = Math.signum(elevationIndex - level.getElevationIndex());
289     for (Level homeLevel : levels) {
290       if (homeLevel != level
291           && homeLevel.getElevation() == level.getElevation()
292           && Math.signum(homeLevel.getElevationIndex() - level.getElevationIndex()) == elevationIndexSignum
293           && Math.signum(homeLevel.getElevationIndex() - elevationIndex) != elevationIndexSignum) {
294         homeLevel.setElevationIndex(homeLevel.getElevationIndex() - (int)elevationIndexSignum);
295       } else if (homeLevel.getElevation() > level.getElevation()) {
296         break;
297       }
298     }
299     level.setElevationIndex(elevationIndex);
300   }
301 
updateLevels()302   private void updateLevels() {
303     // Create a temporary home with levels to update their order
304     Home tempHome = new Home();
305     Level selectedLevel = this.levels [this.selectedLevelIndex];
306     for (Level homeLevel : this.levels) {
307       tempHome.addLevel(homeLevel);
308     }
309     List<Level> updatedLevels = tempHome.getLevels();
310     setLevels(updatedLevels.toArray(new Level [updatedLevels.size()]));
311     setSelectedLevelIndex(updatedLevels.indexOf(selectedLevel));
312   }
313 
314   /**
315    * Returns the edited elevation index.
316    * @since 5.0
317    */
getElevationIndex()318   public Integer getElevationIndex() {
319     return this.elevationIndex;
320   }
321 
322   /**
323    * Sets the edited floor thickness.
324    */
setFloorThickness(Float floorThickness)325   public void setFloorThickness(Float floorThickness) {
326     if (floorThickness != this.floorThickness) {
327       Float oldFloorThickness = this.floorThickness;
328       this.floorThickness = floorThickness;
329       this.propertyChangeSupport.firePropertyChange(Property.FLOOR_THICKNESS.name(), oldFloorThickness, floorThickness);
330       if (floorThickness != null && this.selectedLevelIndex != null) {
331         this.levels [this.selectedLevelIndex].setFloorThickness(floorThickness);
332         this.propertyChangeSupport.firePropertyChange(Property.LEVELS.name(), null, this.levels);
333       }
334     }
335   }
336 
337   /**
338    * Returns the edited floor thickness.
339    */
getFloorThickness()340   public Float getFloorThickness() {
341     return this.floorThickness;
342   }
343 
344   /**
345    * Sets the edited height.
346    */
setHeight(Float height)347   public void setHeight(Float height) {
348     if (height != this.height) {
349       Float oldHeight = this.height;
350       this.height = height;
351       this.propertyChangeSupport.firePropertyChange(Property.HEIGHT.name(), oldHeight, height);
352       if (height != null && this.selectedLevelIndex != null) {
353         this.levels [this.selectedLevelIndex].setHeight(height);
354         this.propertyChangeSupport.firePropertyChange(Property.LEVELS.name(), null, this.levels);
355       }
356     }
357   }
358 
359   /**
360    * Returns the edited height.
361    */
getHeight()362   public Float getHeight() {
363     return this.height;
364   }
365 
366   /**
367    * Sets home levels.
368    */
setLevels(Level [] levels)369   private void setLevels(Level [] levels) {
370     if (levels != this.levels) {
371       Level [] oldLevels = this.levels;
372       this.levels = levels;
373       this.propertyChangeSupport.firePropertyChange(Property.LEVELS.name(), oldLevels, levels);
374     }
375   }
376 
377   /**
378    * Returns a copy of home levels.
379    */
getLevels()380   public Level [] getLevels() {
381     return this.levels.clone();
382   }
383 
384   /**
385    * Sets the selected level index.
386    */
setSelectedLevelIndex(Integer selectedLevelIndex)387   private void setSelectedLevelIndex(Integer selectedLevelIndex) {
388     if (selectedLevelIndex != this.selectedLevelIndex) {
389       Integer oldSelectedLevelIndex = this.selectedLevelIndex;
390       this.selectedLevelIndex = selectedLevelIndex;
391       this.propertyChangeSupport.firePropertyChange(Property.SELECT_LEVEL_INDEX.name(), oldSelectedLevelIndex, selectedLevelIndex);
392     }
393   }
394 
395   /**
396    * Returns the selected level index.
397    */
getSelectedLevelIndex()398   public Integer getSelectedLevelIndex() {
399     return this.selectedLevelIndex;
400   }
401 
402   /**
403    * Controls the modification of selected level in the edited home.
404    */
modifyLevels()405   public void modifyLevels() {
406     Level selectedLevel = this.home.getSelectedLevel();
407     if (selectedLevel != null) {
408       List<Selectable> oldSelection = this.home.getSelectedItems();
409       String name = getName();
410       Boolean viewable = getViewable();
411       Float elevation = getElevation();
412       Float floorThickness = getFloorThickness();
413       Float height = getHeight();
414       Integer elevationIndex = getElevationIndex();
415 
416       ModifiedLevel modifiedLevel = new ModifiedLevel(selectedLevel);
417       // Apply modification
418       doModifyLevel(this.home, modifiedLevel, name, viewable, elevation, floorThickness, height, elevationIndex);
419       if (this.undoSupport != null) {
420         UndoableEdit undoableEdit = new LevelModificationUndoableEdit(
421             this.home, this.preferences, oldSelection.toArray(new Selectable [oldSelection.size()]),
422             modifiedLevel, name, viewable,  elevation, floorThickness, height, elevationIndex);
423         this.undoSupport.postEdit(undoableEdit);
424       }
425       if (name != null) {
426         this.preferences.addAutoCompletionString("LevelName", name);
427       }
428     }
429   }
430 
431   /**
432    * Undoable edit for level modification. This class isn't anonymous to avoid
433    * being bound to controller and its view.
434    */
435   private static class LevelModificationUndoableEdit extends LocalizedUndoableEdit {
436     private final Home          home;
437     private final Selectable [] oldSelection;
438     private final ModifiedLevel modifiedLevel;
439     private final String        name;
440     private final Boolean       viewable;
441     private final Float         elevation;
442     private final Float         floorThickness;
443     private final Float         height;
444     private final Integer       elevationIndex;
445 
LevelModificationUndoableEdit(Home home, UserPreferences preferences, Selectable [] oldSelection, ModifiedLevel modifiedLevel, String name, Boolean viewable, Float elevation, Float floorThickness, Float height, Integer elevationIndex)446     private LevelModificationUndoableEdit(Home home,
447                                           UserPreferences preferences,
448                                           Selectable [] oldSelection,
449                                           ModifiedLevel modifiedLevel,
450                                           String name,
451                                           Boolean viewable,
452                                           Float elevation,
453                                           Float floorThickness,
454                                           Float height,
455                                           Integer elevationIndex) {
456       super(preferences, LevelController.class, "undoModifyLevelName");
457       this.home = home;
458       this.oldSelection = oldSelection;
459       this.modifiedLevel = modifiedLevel;
460       this.name = name;
461       this.viewable = viewable;
462       this.elevation = elevation;
463       this.floorThickness = floorThickness;
464       this.height = height;
465       this.elevationIndex = elevationIndex;
466     }
467 
468     @Override
undo()469     public void undo() throws CannotUndoException {
470       super.undo();
471       undoModifyLevel(this.home, this.modifiedLevel);
472       this.home.setSelectedLevel(this.modifiedLevel.getLevel());
473       this.home.setSelectedItems(Arrays.asList(this.oldSelection));
474     }
475 
476     @Override
redo()477     public void redo() throws CannotRedoException {
478       super.redo();
479       this.home.setSelectedLevel(this.modifiedLevel.getLevel());
480       doModifyLevel(this.home, this.modifiedLevel, this.name, this.viewable,
481           this.elevation, this.floorThickness, this.height, this.elevationIndex);
482     }
483   }
484 
485   /**
486    * Modifies level properties with the values in parameter.
487    */
doModifyLevel(Home home, ModifiedLevel modifiedLevel, String name, Boolean viewable, Float elevation, Float floorThickness, Float height, Integer elevationIndex)488   private static void doModifyLevel(Home home, ModifiedLevel modifiedLevel,
489                                     String name, Boolean viewable, Float elevation,
490                                     Float floorThickness, Float height,
491                                     Integer elevationIndex) {
492     Level level = modifiedLevel.getLevel();
493     if (name != null) {
494       level.setName(name);
495     }
496     if (viewable != null) {
497       List<Selectable> selectedItems = home.getSelectedItems();
498       level.setViewable(viewable);
499       home.setSelectedItems(getViewableSublist(selectedItems));
500     }
501     if (elevation != null
502         && elevation != level.getElevation()) {
503       updateLevelElevation(level, elevation, home.getLevels());
504     }
505     if (elevationIndex != null) {
506       updateLevelElevationIndex(level, elevationIndex, home.getLevels());
507     }
508     if (!home.getEnvironment().isAllLevelsVisible()) {
509       // Update visibility of levels
510       Level selectedLevel = home.getSelectedLevel();
511       boolean visible = true;
512       for (Level homeLevel : home.getLevels()) {
513         homeLevel.setVisible(visible);
514         if (homeLevel == selectedLevel) {
515           visible = false;
516         }
517       }
518     }
519     if (floorThickness != null) {
520       level.setFloorThickness(floorThickness);
521     }
522     if (height != null) {
523       level.setHeight(height);
524     }
525   }
526 
527   /**
528    * Returns a sub list of <code>items</code> that are at a viewable level.
529    */
getViewableSublist(List<? extends Selectable> items)530   private static List<Selectable> getViewableSublist(List<? extends Selectable> items) {
531     List<Selectable> viewableItems = new ArrayList<Selectable>(items.size());
532     for (Selectable item : items) {
533       if (!(item instanceof Elevatable)
534           || ((Elevatable)item).getLevel().isViewable()) {
535         viewableItems.add(item);
536       }
537     }
538     return viewableItems;
539   }
540 
541   /**
542    * Restores level properties from the values stored in <code>modifiedLevel</code>.
543    */
undoModifyLevel(Home home, ModifiedLevel modifiedLevel)544   private static void undoModifyLevel(Home home, ModifiedLevel modifiedLevel) {
545     modifiedLevel.reset();
546     Level level = modifiedLevel.getLevel();
547     if (modifiedLevel.getElevation() != level.getElevation()) {
548       updateLevelElevation(level, modifiedLevel.getElevation(), home.getLevels());
549     }
550     if (modifiedLevel.getElevationIndex() != level.getElevationIndex()) {
551       updateLevelElevationIndex(level, modifiedLevel.getElevationIndex(), home.getLevels());
552     }
553   }
554 
555   /**
556    * Stores the current properties values of a modified level.
557    */
558   private static class ModifiedLevel {
559     private final Level   level;
560     private final String  name;
561     private final boolean viewable;
562     private final float   elevation;
563     private final float   floorThickness;
564     private final float   height;
565     private final int     elevationIndex;
566 
ModifiedLevel(Level level)567     public ModifiedLevel(Level level) {
568       this.level = level;
569       this.name = level.getName();
570       this.viewable = level.isViewable();
571       this.elevation = level.getElevation();
572       this.floorThickness = level.getFloorThickness();
573       this.height = level.getHeight();
574       this.elevationIndex = level.getElevationIndex();
575     }
576 
getLevel()577     public Level getLevel() {
578       return this.level;
579     }
580 
getElevation()581     public float getElevation() {
582       return this.elevation;
583     }
584 
getElevationIndex()585     public int getElevationIndex() {
586       return this.elevationIndex;
587     }
588 
reset()589     public void reset() {
590       this.level.setName(this.name);
591       this.level.setViewable(this.viewable);
592       this.level.setFloorThickness(this.floorThickness);
593       this.level.setHeight(this.height);
594     }
595   }
596 }
597