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