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