1 package net.sf.openrocket.gui.main;
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;
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;
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;
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
61 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
64 public class ComponentAddButtons extends JPanel implements Scrollable {
66 private static final int ROWS = 3;
67 private static final int MAXCOLS = 6;
68 private static final String BUTTONPARAM = "grow, sizegroup buttons";
70 private static final int GAP = 5;
71 private static final int EXTRASPACE = 0;
73 private final ComponentButton[][] buttons;
75 private final OpenRocketDocument document;
76 private final TreeSelectionModel selectionModel;
77 private final JViewport viewport;
78 private final MigLayout layout;
80 private final int width, height;
83 public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model,
87 String constaint = "[min!]";
88 for (int i=1; i<MAXCOLS; i++)
89 constaint = constaint + GAP + "[min!]";
91 layout = new MigLayout("fill",constaint);
93 this.document = document;
94 this.selectionModel = model;
95 this.viewport = viewport;
97 buttons = new ComponentButton[ROWS][];
100 ////////////////////////////////////////////
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")
116 /////////////////////////////////////////////
118 addButtonRow("Inner component",row,
119 new ComponentButton(InnerTube.class, "Inner tube"),
120 new ComponentButton(TubeCoupler.class, "Coupler"),
121 new ComponentButton(CenteringRing.class, "Centering\nring"),
122 new ComponentButton(Bulkhead.class, "Bulkhead"),
123 new ComponentButton(EngineBlock.class, "Engine\nblock"));
127 ////////////////////////////////////////////
129 addButtonRow("Mass objects",row,
130 new ComponentButton(Parachute.class, "Parachute"),
131 new ComponentButton(Streamer.class, "Streamer"),
132 new ComponentButton(ShockCord.class, "Shock cord"),
133 // new ComponentButton("Motor clip"),
134 // new ComponentButton("Payload"),
135 new ComponentButton(MassComponent.class,"Mass\ncomponent")
139 // Get maximum button size
142 for (row=0; row < buttons.length; row++) {
143 for (int col=0; col < buttons[row].length; col++) {
144 Dimension d = buttons[row][col].getPreferredSize();
152 // Set all buttons to maximum size
153 System.out.println("Setting w="+w+" h="+h);
156 Dimension d = new Dimension(width,height);
157 for (row=0; row < buttons.length; row++) {
158 for (int col=0; col < buttons[row].length; col++) {
159 buttons[row][col].setMinimumSize(d);
160 buttons[row][col].setPreferredSize(d);
161 buttons[row][col].getComponent(0).validate();
165 // Add viewport listener if viewport provided
166 if (viewport != null) {
167 viewport.addChangeListener(new ChangeListener() {
168 private int oldWidth = -1;
169 public void stateChanged(ChangeEvent e) {
170 Dimension d = ComponentAddButtons.this.viewport.getExtentSize();
171 if (d.width != oldWidth) {
179 add(new JPanel(),"grow");
184 * Adds a row of buttons to the panel.
185 * @param label Label placed before the row
186 * @param row Row number
187 * @param b List of ComponentButtons to place on the row
189 private void addButtonRow(String label, int row, ComponentButton ... b) {
191 add(new JLabel(label),"span, gaptop unrel, wrap");
193 add(new JLabel(label),"span, gaptop 0, wrap");
196 buttons[row] = new ComponentButton[b.length];
198 for (int i=0; i<b.length; i++) {
199 buttons[row][col] = b[i];
201 add(b[i],BUTTONPARAM);
203 add(b[i],BUTTONPARAM+", wrap");
210 * Flows the buttons in all rows of the panel. If a button would come too close
211 * to the right edge of the viewport, "newline" is added to its constraints flowing
212 * it to the next line.
214 private void flowButtons() {
220 Dimension d = viewport.getExtentSize();
222 for (int row=0; row < buttons.length; row++) {
224 for (int col=0; col < buttons[row].length; col++) {
226 String param = BUTTONPARAM+",width "+width+"!,height "+height+"!";
228 if (w+EXTRASPACE > d.width) {
229 param = param + ",newline";
232 if (col == buttons[row].length-1)
233 param = param + ",wrap";
234 layout.setComponentConstraints(buttons[row][col], param);
243 * Class for a component button.
245 private class ComponentButton extends JButton implements TreeSelectionListener {
246 protected Class<? extends RocketComponent> componentClass = null;
247 private Constructor<? extends RocketComponent> constructor = null;
249 /** Only label, no icon. */
250 public ComponentButton(String text) {
251 this(text,null,null);
255 * Constructor with icon and label. The icon and label are placed into the button.
256 * The label may contain "\n" as a newline.
258 public ComponentButton(String text, Icon enabled, Icon disabled) {
260 setLayout(new MigLayout("fill, flowy, insets 0, gap 0","",""));
262 add(new JLabel(),"push, sizegroup spacing");
265 if (enabled != null) {
266 JLabel label = new JLabel(enabled);
267 if (disabled != null)
268 label.setDisabledIcon(disabled);
273 String[] l = text.split("\n");
274 for (int i=0; i<l.length; i++) {
275 add(new StyledLabel(l[i],SwingConstants.CENTER,-3.0f),"growx");
278 add(new JLabel(),"push, sizegroup spacing");
280 valueChanged(null); // Update enabled status
281 selectionModel.addTreeSelectionListener(this);
286 * Main constructor that should be used. The generated component type is specified
287 * and the text. The icons are fetched based on the component type.
289 public ComponentButton(Class<? extends RocketComponent> c, String text) {
290 this(text,ComponentIcons.getLargeIcon(c),ComponentIcons.getLargeDisabledIcon(c));
298 constructor = c.getConstructor();
299 } catch (NoSuchMethodException e) {
300 throw new IllegalArgumentException("Unable to get default "+
301 "constructor for class "+c,e);
307 * Return whether the current component is addable when the component c is selected.
308 * c is null if there is no selection. The default is to use c.isCompatible(class).
310 public boolean isAddable(RocketComponent c) {
313 if (componentClass==null)
315 return c.isCompatible(componentClass);
319 * Return the position to add the component if component c is selected currently.
320 * The first element of the returned array is the RocketComponent to add the component
321 * to, and the second (if non-null) an Integer telling the position of the component.
322 * A return value of null means that the user cancelled addition of the component.
323 * If the Integer is null, the component is added at the end of the sibling
324 * list. By default returns the end of the currently selected component.
326 * @param c The component currently selected
327 * @return The position to add the new component to, or null if should not add.
329 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
330 return new Pair<RocketComponent, Integer>(c, null);
334 * Updates the enabled status of the button.
335 * TODO: LOW: What about updates to the rocket tree?
337 public void valueChanged(TreeSelectionEvent e) {
342 * Sets the enabled status of the button and all subcomponents.
345 public void setEnabled(boolean enabled) {
346 super.setEnabled(enabled);
347 Component[] c = getComponents();
348 for (int i=0; i<c.length; i++)
349 c[i].setEnabled(enabled);
354 * Update the enabled status of the button.
356 private void updateEnabled() {
357 RocketComponent c=null;
358 TreePath p = selectionModel.getSelectionPath();
360 c = (RocketComponent)p.getLastPathComponent();
361 setEnabled(isAddable(c));
366 protected void fireActionPerformed(ActionEvent event) {
367 super.fireActionPerformed(event);
368 RocketComponent c = null;
369 Integer position = null;
371 TreePath p = selectionModel.getSelectionPath();
373 c = (RocketComponent)p.getLastPathComponent();
375 Pair<RocketComponent, Integer> pos = getAdditionPosition(c);
381 position = pos.getV();
386 ExceptionHandler.handleErrorCondition("ERROR: Could not place new component.");
391 if (constructor == null) {
392 System.err.println("ERROR: Construction of type not supported yet.");
396 RocketComponent component;
398 component = (RocketComponent)constructor.newInstance();
399 } catch (InstantiationException e) {
400 throw new BugException("Could not construct new instance of class "+
402 } catch (IllegalAccessException e) {
403 throw new BugException("Could not construct new instance of class "+
405 } catch (InvocationTargetException e) {
406 throw Reflection.handleWrappedException(e);
409 // Next undo position is set by opening the configuration dialog
410 document.addUndoPosition("Add " + component.getComponentName());
413 if (position == null)
414 c.addChild(component);
416 c.addChild(component, position);
418 // Select new component and open config dialog
419 selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
421 JFrame parent = null;
422 for (Component comp = ComponentAddButtons.this; comp != null;
423 comp = comp.getParent()) {
424 if (comp instanceof JFrame) {
425 parent = (JFrame) comp;
430 ComponentConfigDialog.showDialog(parent, document, component);
435 * A class suitable for BodyComponents. Addition is allowed ...
437 private class BodyComponentButton extends ComponentButton {
439 public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
443 public BodyComponentButton(String text, Icon enabled, Icon disabled) {
444 super(text, enabled, disabled);
447 public BodyComponentButton(String text) {
452 public boolean isAddable(RocketComponent c) {
453 if (super.isAddable(c))
455 // Handled separately:
456 if (c instanceof BodyComponent)
458 if (c == null || c instanceof Rocket)
464 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
465 if (super.isAddable(c)) // Handled automatically
466 return super.getAdditionPosition(c);
469 if (c == null || c instanceof Rocket) {
470 // Add as last body component of the last stage
471 Rocket rocket = document.getRocket();
472 return new Pair<RocketComponent,Integer>(rocket.getChild(rocket.getStageCount()-1),
476 if (!(c instanceof BodyComponent))
478 RocketComponent parent = c.getParent();
479 assert(parent != null);
481 // Check whether to insert between or at the end.
482 // 0 = ask, 1 = in between, 2 = at the end
483 int pos = Prefs.getChoise(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0);
485 if (parent.getChildPosition(c) == parent.getChildCount()-1)
486 pos = 2; // Selected component is the last component
496 // Insert after current position
497 return new Pair<RocketComponent,Integer>(parent, parent.getChildPosition(c)+1);
499 // Insert at the end of the parent
500 return new Pair<RocketComponent,Integer>(parent, null);
502 ExceptionHandler.handleErrorCondition("ERROR: Bad position type: "+pos);
507 private int askPosition() {
508 Object[] options = { "Insert here", "Add to the end", "Cancel" };
510 JPanel panel = new JPanel(new MigLayout());
511 JCheckBox check = new JCheckBox("Do not ask me again");
512 panel.add(check,"wrap");
513 panel.add(new StyledLabel("You can change the default operation in the " +
516 int sel = JOptionPane.showOptionDialog(null, // parent component
518 "Insert the component after the current component or as the last " +
521 "Select component position", // title
522 JOptionPane.DEFAULT_OPTION, // default selections
523 JOptionPane.QUESTION_MESSAGE, // dialog type
526 options[0]); // initial value
529 case JOptionPane.CLOSED_OPTION:
542 ExceptionHandler.handleErrorCondition("ERROR: JOptionPane returned "+sel);
546 if (check.isSelected()) {
547 // Save the preference
548 Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
558 * Class for fin sets, that attach only to BodyTubes.
560 private class FinButton extends ComponentButton {
561 public FinButton(Class<? extends RocketComponent> c, String text) {
565 public FinButton(String text, Icon enabled, Icon disabled) {
566 super(text, enabled, disabled);
569 public FinButton(String text) {
574 public boolean isAddable(RocketComponent c) {
577 return (c.getClass().equals(BodyTube.class));
583 ///////// Scrolling functionality
586 public Dimension getPreferredScrollableViewportSize() {
587 return getPreferredSize();
592 public int getScrollableBlockIncrement(Rectangle visibleRect,
593 int orientation, int direction) {
594 if (orientation == SwingConstants.VERTICAL)
595 return visibleRect.height * 8 / 10;
601 public boolean getScrollableTracksViewportHeight() {
607 public boolean getScrollableTracksViewportWidth() {
613 public int getScrollableUnitIncrement(Rectangle visibleRect,
614 int orientation, int direction) {