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