bug fix + more logging
[debian/openrocket] / src / net / sf / openrocket / gui / main / ComponentAddButtons.java
1 package net.sf.openrocket.gui.main;
2
3
4 import java.awt.Component;
5 import java.awt.Dimension;
6 import java.awt.Rectangle;
7 import java.awt.event.ActionEvent;
8 import java.lang.reflect.Constructor;
9 import java.lang.reflect.InvocationTargetException;
10
11 import javax.swing.Icon;
12 import javax.swing.JButton;
13 import javax.swing.JCheckBox;
14 import javax.swing.JFrame;
15 import javax.swing.JLabel;
16 import javax.swing.JOptionPane;
17 import javax.swing.JPanel;
18 import javax.swing.JViewport;
19 import javax.swing.Scrollable;
20 import javax.swing.SwingConstants;
21 import javax.swing.event.ChangeEvent;
22 import javax.swing.event.ChangeListener;
23 import javax.swing.event.TreeSelectionEvent;
24 import javax.swing.event.TreeSelectionListener;
25 import javax.swing.tree.TreePath;
26 import javax.swing.tree.TreeSelectionModel;
27
28 import net.miginfocom.swing.MigLayout;
29 import net.sf.openrocket.document.OpenRocketDocument;
30 import net.sf.openrocket.gui.components.StyledLabel;
31 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
32 import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel;
33 import net.sf.openrocket.logging.LogHelper;
34 import net.sf.openrocket.rocketcomponent.BodyComponent;
35 import net.sf.openrocket.rocketcomponent.BodyTube;
36 import net.sf.openrocket.rocketcomponent.Bulkhead;
37 import net.sf.openrocket.rocketcomponent.CenteringRing;
38 import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
39 import net.sf.openrocket.rocketcomponent.EngineBlock;
40 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
41 import net.sf.openrocket.rocketcomponent.InnerTube;
42 import net.sf.openrocket.rocketcomponent.LaunchLug;
43 import net.sf.openrocket.rocketcomponent.MassComponent;
44 import net.sf.openrocket.rocketcomponent.NoseCone;
45 import net.sf.openrocket.rocketcomponent.Parachute;
46 import net.sf.openrocket.rocketcomponent.Rocket;
47 import net.sf.openrocket.rocketcomponent.RocketComponent;
48 import net.sf.openrocket.rocketcomponent.ShockCord;
49 import net.sf.openrocket.rocketcomponent.Streamer;
50 import net.sf.openrocket.rocketcomponent.Transition;
51 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
52 import net.sf.openrocket.rocketcomponent.TubeCoupler;
53 import net.sf.openrocket.startup.Application;
54 import net.sf.openrocket.util.BugException;
55 import net.sf.openrocket.util.Pair;
56 import net.sf.openrocket.util.Prefs;
57 import net.sf.openrocket.util.Reflection;
58
59 /**
60  * A component that contains addition buttons to add different types of rocket components
61  * to a rocket.  It enables and disables buttons according to the current selection of a 
62  * TreeSelectionModel. 
63  * 
64  * @author Sampo Niskanen <sampo.niskanen@iki.fi>
65  */
66
67 public class ComponentAddButtons extends JPanel implements Scrollable {
68         private static final LogHelper log = Application.getLogger();
69         
70         private static final int ROWS = 3;
71         private static final int MAXCOLS = 6;
72         private static final String BUTTONPARAM = "grow, sizegroup buttons";
73         
74         private static final int GAP = 5;
75         private static final int EXTRASPACE = 0;
76         
77         private final ComponentButton[][] buttons;
78         
79         private final OpenRocketDocument document;
80         private final TreeSelectionModel selectionModel;
81         private final JViewport viewport;
82         private final MigLayout layout;
83         
84         private final int width, height;
85         
86         
87         public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model,
88                         JViewport viewport) {
89                 
90                 super();
91                 String constaint = "[min!]";
92                 for (int i = 1; i < MAXCOLS; i++)
93                         constaint = constaint + GAP + "[min!]";
94                 
95                 layout = new MigLayout("fill", constaint);
96                 setLayout(layout);
97                 this.document = document;
98                 this.selectionModel = model;
99                 this.viewport = viewport;
100                 
101                 buttons = new ComponentButton[ROWS][];
102                 int row = 0;
103                 
104                 ////////////////////////////////////////////
105                 
106
107                 addButtonRow("Body components and fin sets", row,
108                                 new BodyComponentButton(NoseCone.class, "Nose cone"),
109                                 new BodyComponentButton(BodyTube.class, "Body tube"),
110                                 new BodyComponentButton(Transition.class, "Transition"),
111                                 new FinButton(TrapezoidFinSet.class, "Trapezoidal"), // TODO: MEDIUM: freer fin placing
112                                 new FinButton(EllipticalFinSet.class, "Elliptical"),
113                                 new FinButton(FreeformFinSet.class, "Freeform"),
114                                 new FinButton(LaunchLug.class, "Launch lug"));
115                 
116                 row++;
117                 
118
119                 /////////////////////////////////////////////
120                 
121                 addButtonRow("Inner component", row,
122                                 new ComponentButton(InnerTube.class, "Inner tube"),
123                                 new ComponentButton(TubeCoupler.class, "Coupler"),
124                                 new ComponentButton(CenteringRing.class, "Centering\nring"),
125                                 new ComponentButton(Bulkhead.class, "Bulkhead"),
126                                 new ComponentButton(EngineBlock.class, "Engine\nblock"));
127                 
128                 row++;
129                 
130                 ////////////////////////////////////////////
131                 
132                 addButtonRow("Mass objects", row,
133                                 new ComponentButton(Parachute.class, "Parachute"),
134                                 new ComponentButton(Streamer.class, "Streamer"),
135                                 new ComponentButton(ShockCord.class, "Shock cord"),
136                                 //                              new ComponentButton("Motor clip"),
137                                 //                              new ComponentButton("Payload"),
138                                 new ComponentButton(MassComponent.class, "Mass\ncomponent"));
139                 
140
141                 // Get maximum button size
142                 int w = 0, h = 0;
143                 
144                 for (row = 0; row < buttons.length; row++) {
145                         for (int col = 0; col < buttons[row].length; col++) {
146                                 Dimension d = buttons[row][col].getPreferredSize();
147                                 if (d.width > w)
148                                         w = d.width;
149                                 if (d.height > h)
150                                         h = d.height;
151                         }
152                 }
153                 
154                 // Set all buttons to maximum size
155                 width = w;
156                 height = h;
157                 Dimension d = new Dimension(width, height);
158                 for (row = 0; row < buttons.length; row++) {
159                         for (int col = 0; col < buttons[row].length; col++) {
160                                 buttons[row][col].setMinimumSize(d);
161                                 buttons[row][col].setPreferredSize(d);
162                                 buttons[row][col].getComponent(0).validate();
163                         }
164                 }
165                 
166                 // Add viewport listener if viewport provided
167                 if (viewport != null) {
168                         viewport.addChangeListener(new ChangeListener() {
169                                 private int oldWidth = -1;
170                                 
171                                 public void stateChanged(ChangeEvent e) {
172                                         Dimension d = ComponentAddButtons.this.viewport.getExtentSize();
173                                         if (d.width != oldWidth) {
174                                                 oldWidth = d.width;
175                                                 flowButtons();
176                                         }
177                                 }
178                         });
179                 }
180                 
181                 add(new JPanel(), "grow");
182         }
183         
184         
185         /**
186          * Adds a row of buttons to the panel.
187          * @param label  Label placed before the row
188          * @param row    Row number
189          * @param b      List of ComponentButtons to place on the row
190          */
191         private void addButtonRow(String label, int row, ComponentButton... b) {
192                 if (row > 0)
193                         add(new JLabel(label), "span, gaptop unrel, wrap");
194                 else
195                         add(new JLabel(label), "span, gaptop 0, wrap");
196                 
197                 int col = 0;
198                 buttons[row] = new ComponentButton[b.length];
199                 
200                 for (int i = 0; i < b.length; i++) {
201                         buttons[row][col] = b[i];
202                         if (i < b.length - 1)
203                                 add(b[i], BUTTONPARAM);
204                         else
205                                 add(b[i], BUTTONPARAM + ", wrap");
206                         col++;
207                 }
208         }
209         
210         
211         /**
212          * Flows the buttons in all rows of the panel.  If a button would come too close
213          * to the right edge of the viewport, "newline" is added to its constraints flowing 
214          * it to the next line.
215          */
216         private void flowButtons() {
217                 if (viewport == null)
218                         return;
219                 
220                 int w;
221                 
222                 Dimension d = viewport.getExtentSize();
223                 
224                 for (int row = 0; row < buttons.length; row++) {
225                         w = 0;
226                         for (int col = 0; col < buttons[row].length; col++) {
227                                 w += GAP + width;
228                                 String param = BUTTONPARAM + ",width " + width + "!,height " + height + "!";
229                                 
230                                 if (w + EXTRASPACE > d.width) {
231                                         param = param + ",newline";
232                                         w = GAP + width;
233                                 }
234                                 if (col == buttons[row].length - 1)
235                                         param = param + ",wrap";
236                                 layout.setComponentConstraints(buttons[row][col], param);
237                         }
238                 }
239                 revalidate();
240         }
241         
242         
243
244         /**
245          * Class for a component button.
246          */
247         private class ComponentButton extends JButton implements TreeSelectionListener {
248                 protected Class<? extends RocketComponent> componentClass = null;
249                 private Constructor<? extends RocketComponent> constructor = null;
250                 
251                 /** Only label, no icon. */
252                 public ComponentButton(String text) {
253                         this(text, null, null);
254                 }
255                 
256                 /**
257                  * Constructor with icon and label.  The icon and label are placed into the button.
258                  * The label may contain "\n" as a newline.
259                  */
260                 public ComponentButton(String text, Icon enabled, Icon disabled) {
261                         super();
262                         setLayout(new MigLayout("fill, flowy, insets 0, gap 0", "", ""));
263                         
264                         add(new JLabel(), "push, sizegroup spacing");
265                         
266                         // Add Icon
267                         if (enabled != null) {
268                                 JLabel label = new JLabel(enabled);
269                                 if (disabled != null)
270                                         label.setDisabledIcon(disabled);
271                                 add(label, "growx");
272                         }
273                         
274                         // Add labels
275                         String[] l = text.split("\n");
276                         for (int i = 0; i < l.length; i++) {
277                                 add(new StyledLabel(l[i], SwingConstants.CENTER, -3.0f), "growx");
278                         }
279                         
280                         add(new JLabel(), "push, sizegroup spacing");
281                         
282                         valueChanged(null); // Update enabled status
283                         selectionModel.addTreeSelectionListener(this);
284                 }
285                 
286                 
287                 /**
288                  * Main constructor that should be used.  The generated component type is specified
289                  * and the text.  The icons are fetched based on the component type.
290                  */
291                 public ComponentButton(Class<? extends RocketComponent> c, String text) {
292                         this(text, ComponentIcons.getLargeIcon(c), ComponentIcons.getLargeDisabledIcon(c));
293                         
294                         if (c == null)
295                                 return;
296                         
297                         componentClass = c;
298                         
299                         try {
300                                 constructor = c.getConstructor();
301                         } catch (NoSuchMethodException e) {
302                                 throw new IllegalArgumentException("Unable to get default " +
303                                                 "constructor for class " + c, e);
304                         }
305                 }
306                 
307                 
308                 /**
309                  * Return whether the current component is addable when the component c is selected.
310                  * c is null if there is no selection.  The default is to use c.isCompatible(class).
311                  */
312                 public boolean isAddable(RocketComponent c) {
313                         if (c == null)
314                                 return false;
315                         if (componentClass == null)
316                                 return false;
317                         return c.isCompatible(componentClass);
318                 }
319                 
320                 /**
321                  * Return the position to add the component if component c is selected currently.
322                  * The first element of the returned array is the RocketComponent to add the component
323                  * to, and the second (if non-null) an Integer telling the position of the component.
324                  * A return value of null means that the user cancelled addition of the component.
325                  * If the Integer is null, the component is added at the end of the sibling 
326                  * list.  By default returns the end of the currently selected component.
327                  * 
328                  * @param c  The component currently selected
329                  * @return   The position to add the new component to, or null if should not add.
330                  */
331                 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
332                         return new Pair<RocketComponent, Integer>(c, null);
333                 }
334                 
335                 /**
336                  * Updates the enabled status of the button.
337                  * TODO: LOW: What about updates to the rocket tree?
338                  */
339                 public void valueChanged(TreeSelectionEvent e) {
340                         updateEnabled();
341                 }
342                 
343                 /**
344                  * Sets the enabled status of the button and all subcomponents.
345                  */
346                 @Override
347                 public void setEnabled(boolean enabled) {
348                         super.setEnabled(enabled);
349                         Component[] c = getComponents();
350                         for (int i = 0; i < c.length; i++)
351                                 c[i].setEnabled(enabled);
352                 }
353                 
354                 
355                 /**
356                  * Update the enabled status of the button.
357                  */
358                 private void updateEnabled() {
359                         RocketComponent c = null;
360                         TreePath p = selectionModel.getSelectionPath();
361                         if (p != null)
362                                 c = (RocketComponent) p.getLastPathComponent();
363                         setEnabled(isAddable(c));
364                 }
365                 
366                 
367                 @Override
368                 protected void fireActionPerformed(ActionEvent event) {
369                         super.fireActionPerformed(event);
370                         log.user("Adding component of type " + componentClass.getSimpleName());
371                         RocketComponent c = null;
372                         Integer position = null;
373                         
374                         TreePath p = selectionModel.getSelectionPath();
375                         if (p != null)
376                                 c = (RocketComponent) p.getLastPathComponent();
377                         
378                         Pair<RocketComponent, Integer> pos = getAdditionPosition(c);
379                         if (pos == null) {
380                                 // Cancel addition
381                                 log.info("No position to add component");
382                                 return;
383                         }
384                         c = pos.getU();
385                         position = pos.getV();
386                         
387
388                         if (c == null) {
389                                 // Should not occur
390                                 ExceptionHandler.handleErrorCondition("ERROR:  Could not place new component.");
391                                 updateEnabled();
392                                 return;
393                         }
394                         
395                         if (constructor == null) {
396                                 ExceptionHandler.handleErrorCondition("ERROR:  Construction of type not supported yet.");
397                                 return;
398                         }
399                         
400                         RocketComponent component;
401                         try {
402                                 component = (RocketComponent) constructor.newInstance();
403                         } catch (InstantiationException e) {
404                                 throw new BugException("Could not construct new instance of class " + constructor, e);
405                         } catch (IllegalAccessException e) {
406                                 throw new BugException("Could not construct new instance of class " + constructor, e);
407                         } catch (InvocationTargetException e) {
408                                 throw Reflection.handleWrappedException(e);
409                         }
410                         
411                         // Next undo position is set by opening the configuration dialog
412                         document.addUndoPosition("Add " + component.getComponentName());
413                         
414                         log.info("Adding component " + component.getComponentName() + " to component " + c.getComponentName() +
415                                         " position=" + position);
416                         
417                         if (position == null)
418                                 c.addChild(component);
419                         else
420                                 c.addChild(component, position);
421                         
422                         // Select new component and open config dialog
423                         selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
424                         
425                         JFrame parent = null;
426                         for (Component comp = ComponentAddButtons.this; comp != null; comp = comp.getParent()) {
427                                 if (comp instanceof JFrame) {
428                                         parent = (JFrame) comp;
429                                         break;
430                                 }
431                         }
432                         
433                         ComponentConfigDialog.showDialog(parent, document, component);
434                 }
435         }
436         
437         /**
438          * A class suitable for BodyComponents.  Addition is allowed ...  
439          */
440         private class BodyComponentButton extends ComponentButton {
441                 
442                 public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
443                         super(c, text);
444                 }
445                 
446                 public BodyComponentButton(String text, Icon enabled, Icon disabled) {
447                         super(text, enabled, disabled);
448                 }
449                 
450                 public BodyComponentButton(String text) {
451                         super(text);
452                 }
453                 
454                 @Override
455                 public boolean isAddable(RocketComponent c) {
456                         if (super.isAddable(c))
457                                 return true;
458                         // Handled separately:
459                         if (c instanceof BodyComponent)
460                                 return true;
461                         if (c == null || c instanceof Rocket)
462                                 return true;
463                         return false;
464                 }
465                 
466                 @Override
467                 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
468                         if (super.isAddable(c)) // Handled automatically
469                                 return super.getAdditionPosition(c);
470                         
471
472                         if (c == null || c instanceof Rocket) {
473                                 // Add as last body component of the last stage
474                                 Rocket rocket = document.getRocket();
475                                 return new Pair<RocketComponent, Integer>(rocket.getChild(rocket.getStageCount() - 1),
476                                                 null);
477                         }
478                         
479                         if (!(c instanceof BodyComponent))
480                                 return null;
481                         RocketComponent parent = c.getParent();
482                         if (parent == null) {
483                                 throw new BugException("Component " + c.getComponentName() + " is the root component, " +
484                                                 "componentClass=" + componentClass);
485                         }
486                         
487                         // Check whether to insert between or at the end.
488                         // 0 = ask, 1 = in between, 2 = at the end
489                         int pos = Prefs.getChoise(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0);
490                         if (pos == 0) {
491                                 if (parent.getChildPosition(c) == parent.getChildCount() - 1)
492                                         pos = 2; // Selected component is the last component
493                                 else
494                                         pos = askPosition();
495                         }
496                         
497                         switch (pos) {
498                         case 0:
499                                 // Cancel
500                                 return null;
501                         case 1:
502                                 // Insert after current position
503                                 return new Pair<RocketComponent, Integer>(parent, parent.getChildPosition(c) + 1);
504                         case 2:
505                                 // Insert at the end of the parent
506                                 return new Pair<RocketComponent, Integer>(parent, null);
507                         default:
508                                 ExceptionHandler.handleErrorCondition("ERROR:  Bad position type: " + pos);
509                                 return null;
510                         }
511                 }
512                 
513                 private int askPosition() {
514                         Object[] options = { "Insert here", "Add to the end", "Cancel" };
515                         
516                         JPanel panel = new JPanel(new MigLayout());
517                         JCheckBox check = new JCheckBox("Do not ask me again");
518                         panel.add(check, "wrap");
519                         panel.add(new StyledLabel("You can change the default operation in the " +
520                                         "preferences.", -2));
521                         
522                         int sel = JOptionPane.showOptionDialog(null, // parent component 
523                                         new Object[] {
524                                                         "Insert the component after the current component or as the last " +
525                                                                         "component?",
526                                                         panel },
527                                         "Select component position", // title
528                                         JOptionPane.DEFAULT_OPTION, // default selections
529                                         JOptionPane.QUESTION_MESSAGE, // dialog type
530                                         null, // icon
531                                         options, // options
532                                         options[0]); // initial value
533                         
534                         switch (sel) {
535                         case JOptionPane.CLOSED_OPTION:
536                         case 2:
537                                 // Cancel
538                                 return 0;
539                         case 0:
540                                 // Insert
541                                 sel = 1;
542                                 break;
543                         case 1:
544                                 // Add
545                                 sel = 2;
546                                 break;
547                         default:
548                                 ExceptionHandler.handleErrorCondition("ERROR:  JOptionPane returned " + sel);
549                                 return 0;
550                         }
551                         
552                         if (check.isSelected()) {
553                                 // Save the preference
554                                 Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
555                         }
556                         return sel;
557                 }
558                 
559         }
560         
561         
562
563         /**
564          * Class for fin sets, that attach only to BodyTubes.
565          */
566         private class FinButton extends ComponentButton {
567                 public FinButton(Class<? extends RocketComponent> c, String text) {
568                         super(c, text);
569                 }
570                 
571                 public FinButton(String text, Icon enabled, Icon disabled) {
572                         super(text, enabled, disabled);
573                 }
574                 
575                 public FinButton(String text) {
576                         super(text);
577                 }
578                 
579                 @Override
580                 public boolean isAddable(RocketComponent c) {
581                         if (c == null)
582                                 return false;
583                         return (c.getClass().equals(BodyTube.class));
584                 }
585         }
586         
587         
588
589         /////////  Scrolling functionality
590         
591         @Override
592         public Dimension getPreferredScrollableViewportSize() {
593                 return getPreferredSize();
594         }
595         
596         
597         @Override
598         public int getScrollableBlockIncrement(Rectangle visibleRect,
599                         int orientation, int direction) {
600                 if (orientation == SwingConstants.VERTICAL)
601                         return visibleRect.height * 8 / 10;
602                 return 10;
603         }
604         
605         
606         @Override
607         public boolean getScrollableTracksViewportHeight() {
608                 return false;
609         }
610         
611         
612         @Override
613         public boolean getScrollableTracksViewportWidth() {
614                 return true;
615         }
616         
617         
618         @Override
619         public int getScrollableUnitIncrement(Rectangle visibleRect,
620                         int orientation, int direction) {
621                 return 10;
622         }
623         
624 }