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