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