a8072de47e72be4d0ac68b6dbd28b75d7e2d3be1
[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.StyledLabel;
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 StyledLabel(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                                 ExceptionHandler.handleErrorCondition("ERROR:  Could not place new component.");
384                                 updateEnabled();
385                                 return;
386                         }
387                         
388                         if (constructor == null) {
389                                 System.err.println("ERROR:  Construction of type not supported yet.");
390                                 return;
391                         }
392                         
393                         RocketComponent component;
394                         try {
395                                 component = (RocketComponent)constructor.newInstance();
396                         } catch (Exception e) {
397                                 throw new RuntimeException("Could not construct new instance of class "+
398                                                 constructor,e);
399                         }
400                         
401                         // Next undo position is set by opening the configuration dialog
402                         document.addUndoPosition("Add " + component.getComponentName());
403                         
404                         
405                         if (position == null)
406                                 c.addChild(component);
407                         else
408                                 c.addChild(component, position);
409                         
410                         // Select new component and open config dialog
411                         selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
412                         
413                         JFrame parent = null;
414                         for (Component comp = ComponentAddButtons.this; comp != null; 
415                                  comp = comp.getParent()) {
416                                 if (comp instanceof JFrame) {
417                                         parent = (JFrame) comp;
418                                         break;
419                                 }
420                         }
421                                 
422                         ComponentConfigDialog.showDialog(parent, document, component);
423                 }
424         }
425         
426         /**
427          * A class suitable for BodyComponents.  Addition is allowed ...  
428          */
429         private class BodyComponentButton extends ComponentButton {
430                 
431                 public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
432                         super(c, text);
433                 }
434
435                 public BodyComponentButton(String text, Icon enabled, Icon disabled) {
436                         super(text, enabled, disabled);
437                 }
438
439                 public BodyComponentButton(String text) {
440                         super(text);
441                 }
442
443                 @Override
444                 public boolean isAddable(RocketComponent c) {
445                         if (super.isAddable(c))
446                                 return true;
447                         // Handled separately:
448                         if (c instanceof BodyComponent)
449                                 return true;
450                         if (c == null || c instanceof Rocket)
451                                 return true;
452                         return false;
453                 }
454                 
455                 @Override
456                 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
457                         if (super.isAddable(c))     // Handled automatically
458                                 return super.getAdditionPosition(c);
459                         
460                         
461                         if (c == null || c instanceof Rocket) {
462                                 // Add as last body component of the last stage
463                                 Rocket rocket = document.getRocket();
464                                 return new Pair<RocketComponent,Integer>(rocket.getChild(rocket.getStageCount()-1),
465                                                 null);
466                         }
467                         
468                         if (!(c instanceof BodyComponent))
469                                 return null;
470                         RocketComponent parent = c.getParent();
471                         assert(parent != null);
472                         
473                         // Check whether to insert between or at the end.
474                         // 0 = ask, 1 = in between, 2 = at the end
475                         int pos = Prefs.getChoise(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0);
476                         if (pos==0) {
477                                 if (parent.getChildPosition(c) == parent.getChildCount()-1)
478                                         pos = 2;  // Selected component is the last component
479                                 else
480                                         pos = askPosition();
481                         }
482                         
483                         switch (pos) {
484                         case 0:
485                                 // Cancel
486                                 return null;
487                         case 1:
488                                 // Insert after current position
489                                 return new Pair<RocketComponent,Integer>(parent, parent.getChildPosition(c)+1);
490                         case 2:
491                                 // Insert at the end of the parent
492                                 return new Pair<RocketComponent,Integer>(parent, null);
493                         default:
494                                 ExceptionHandler.handleErrorCondition("ERROR:  Bad position type: "+pos);
495                                 return null;
496                         }
497                 }
498                 
499                 private int askPosition() {
500                         Object[] options = { "Insert here", "Add to the end", "Cancel" };
501                         
502                         JPanel panel = new JPanel(new MigLayout());
503                         JCheckBox check = new JCheckBox("Do not ask me again");
504                         panel.add(check,"wrap");
505                         panel.add(new StyledLabel("You can change the default operation in the " +
506                                         "preferences.",-2));
507                         
508                         int sel = JOptionPane.showOptionDialog(null,  // parent component 
509                                         new Object[] {
510                                         "Insert the component after the current component or as the last " +
511                                         "component?",
512                                         panel },
513                                         "Select component position",   // title
514                                         JOptionPane.DEFAULT_OPTION,    // default selections
515                                         JOptionPane.QUESTION_MESSAGE,  // dialog type
516                                         null,         // icon
517                                         options,      // options
518                                         options[0]);  // initial value
519
520                         switch (sel) {
521                         case JOptionPane.CLOSED_OPTION:
522                         case 2:
523                                 // Cancel
524                                 return 0;
525                         case 0:
526                                 // Insert
527                                 sel = 1;
528                                 break;
529                         case 1:
530                                 // Add
531                                 sel = 2;
532                                 break;
533                         default:
534                                 ExceptionHandler.handleErrorCondition("ERROR:  JOptionPane returned "+sel);
535                                 return 0;
536                         }
537                         
538                         if (check.isSelected()) {
539                                 // Save the preference
540                                 Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
541                         }
542                         return sel;
543                 }
544                 
545         }
546         
547
548
549         /**
550          * Class for fin sets, that attach only to BodyTubes.
551          */
552         private class FinButton extends ComponentButton {
553                 public FinButton(Class<? extends RocketComponent> c, String text) {
554                         super(c, text);
555                 }
556
557                 public FinButton(String text, Icon enabled, Icon disabled) {
558                         super(text, enabled, disabled);
559                 }
560
561                 public FinButton(String text) {
562                         super(text);
563                 }
564
565                 @Override
566                 public boolean isAddable(RocketComponent c) {
567                         if (c==null)
568                                 return false;
569                         return (c.getClass().equals(BodyTube.class));
570                 }
571         }
572
573
574         
575         /////////  Scrolling functionality
576
577         @Override
578         public Dimension getPreferredScrollableViewportSize() {
579                 return getPreferredSize();
580         }
581
582
583         @Override
584         public int getScrollableBlockIncrement(Rectangle visibleRect,
585                         int orientation, int direction) {
586                 if (orientation == SwingConstants.VERTICAL)
587                         return visibleRect.height * 8 / 10;
588                 return 10;
589         }
590
591
592         @Override
593         public boolean getScrollableTracksViewportHeight() {
594                 return false;
595         }
596
597
598         @Override
599         public boolean getScrollableTracksViewportWidth() {
600                 return true;
601         }
602
603
604         @Override
605         public int getScrollableUnitIncrement(Rectangle visibleRect,
606                         int orientation, int direction) {
607                 return 10;
608         }
609         
610 }
611