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;
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;
27 import net.miginfocom.swing.MigLayout;
28 import net.sf.openrocket.document.OpenRocketDocument;
29 import net.sf.openrocket.gui.components.ResizeLabel;
30 import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
31 import net.sf.openrocket.rocketcomponent.BodyComponent;
32 import net.sf.openrocket.rocketcomponent.BodyTube;
33 import net.sf.openrocket.rocketcomponent.Bulkhead;
34 import net.sf.openrocket.rocketcomponent.CenteringRing;
35 import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
36 import net.sf.openrocket.rocketcomponent.EngineBlock;
37 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
38 import net.sf.openrocket.rocketcomponent.InnerTube;
39 import net.sf.openrocket.rocketcomponent.LaunchLug;
40 import net.sf.openrocket.rocketcomponent.MassComponent;
41 import net.sf.openrocket.rocketcomponent.NoseCone;
42 import net.sf.openrocket.rocketcomponent.Parachute;
43 import net.sf.openrocket.rocketcomponent.Rocket;
44 import net.sf.openrocket.rocketcomponent.RocketComponent;
45 import net.sf.openrocket.rocketcomponent.ShockCord;
46 import net.sf.openrocket.rocketcomponent.Streamer;
47 import net.sf.openrocket.rocketcomponent.Transition;
48 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
49 import net.sf.openrocket.rocketcomponent.TubeCoupler;
50 import net.sf.openrocket.util.Pair;
51 import net.sf.openrocket.util.Prefs;
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
58 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
61 public class ComponentAddButtons extends JPanel implements Scrollable {
63 private static final int ROWS = 3;
64 private static final int MAXCOLS = 6;
65 private static final String BUTTONPARAM = "grow, sizegroup buttons";
67 private static final int GAP = 5;
68 private static final int EXTRASPACE = 0;
70 private final ComponentButton[][] buttons;
72 private final OpenRocketDocument document;
73 private final TreeSelectionModel selectionModel;
74 private final JViewport viewport;
75 private final MigLayout layout;
77 private final int width, height;
80 public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model,
84 String constaint = "[min!]";
85 for (int i=1; i<MAXCOLS; i++)
86 constaint = constaint + GAP + "[min!]";
88 layout = new MigLayout("fill",constaint);
90 this.document = document;
91 this.selectionModel = model;
92 this.viewport = viewport;
94 buttons = new ComponentButton[ROWS][];
97 ////////////////////////////////////////////
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")
113 /////////////////////////////////////////////
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"));
124 ////////////////////////////////////////////
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")
136 // Get maximum button size
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();
149 // Set all buttons to maximum size
150 System.out.println("Setting w="+w+" h="+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();
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) {
176 add(new JPanel(),"grow");
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
186 private void addButtonRow(String label, int row, ComponentButton ... b) {
188 add(new JLabel(label),"span, gaptop unrel, wrap");
190 add(new JLabel(label),"span, gaptop 0, wrap");
193 buttons[row] = new ComponentButton[b.length];
195 for (int i=0; i<b.length; i++) {
196 buttons[row][col] = b[i];
198 add(b[i],BUTTONPARAM);
200 add(b[i],BUTTONPARAM+", wrap");
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.
211 private void flowButtons() {
217 Dimension d = viewport.getExtentSize();
219 for (int row=0; row < buttons.length; row++) {
221 for (int col=0; col < buttons[row].length; col++) {
223 String param = BUTTONPARAM+",width "+width+"!,height "+height+"!";
225 if (w+EXTRASPACE > d.width) {
226 param = param + ",newline";
229 if (col == buttons[row].length-1)
230 param = param + ",wrap";
231 layout.setComponentConstraints(buttons[row][col], param);
240 * Class for a component button.
242 private class ComponentButton extends JButton implements TreeSelectionListener {
243 protected Class<? extends RocketComponent> componentClass = null;
244 private Constructor<? extends RocketComponent> constructor = null;
246 /** Only label, no icon. */
247 public ComponentButton(String text) {
248 this(text,null,null);
252 * Constructor with icon and label. The icon and label are placed into the button.
253 * The label may contain "\n" as a newline.
255 public ComponentButton(String text, Icon enabled, Icon disabled) {
257 setLayout(new MigLayout("fill, flowy, insets 0, gap 0","",""));
259 add(new JLabel(),"push, sizegroup spacing");
262 if (enabled != null) {
263 JLabel label = new JLabel(enabled);
264 if (disabled != null)
265 label.setDisabledIcon(disabled);
270 String[] l = text.split("\n");
271 for (int i=0; i<l.length; i++) {
272 add(new ResizeLabel(l[i],SwingConstants.CENTER,-3.0f),"growx");
275 add(new JLabel(),"push, sizegroup spacing");
277 valueChanged(null); // Update enabled status
278 selectionModel.addTreeSelectionListener(this);
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.
286 public ComponentButton(Class<? extends RocketComponent> c, String text) {
287 this(text,ComponentIcons.getLargeIcon(c),ComponentIcons.getLargeDisabledIcon(c));
295 constructor = c.getConstructor();
296 } catch (NoSuchMethodException e) {
297 throw new IllegalArgumentException("Unable to get default "+
298 "constructor for class "+c,e);
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).
307 public boolean isAddable(RocketComponent c) {
310 if (componentClass==null)
312 return c.isCompatible(componentClass);
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.
323 * @param c The component currently selected
324 * @return The position to add the new component to, or null if should not add.
326 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
327 return new Pair<RocketComponent, Integer>(c, null);
331 * Updates the enabled status of the button.
332 * TODO: LOW: What about updates to the rocket tree?
334 public void valueChanged(TreeSelectionEvent e) {
339 * Sets the enabled status of the button and all subcomponents.
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);
351 * Update the enabled status of the button.
353 private void updateEnabled() {
354 RocketComponent c=null;
355 TreePath p = selectionModel.getSelectionPath();
357 c = (RocketComponent)p.getLastPathComponent();
358 setEnabled(isAddable(c));
363 protected void fireActionPerformed(ActionEvent event) {
364 super.fireActionPerformed(event);
365 RocketComponent c = null;
366 Integer position = null;
368 TreePath p = selectionModel.getSelectionPath();
370 c = (RocketComponent)p.getLastPathComponent();
372 Pair<RocketComponent, Integer> pos = getAdditionPosition(c);
378 position = pos.getV();
383 ExceptionHandler.handleErrorCondition("ERROR: Could not place new component.");
388 if (constructor == null) {
389 System.err.println("ERROR: Construction of type not supported yet.");
393 RocketComponent component;
395 component = (RocketComponent)constructor.newInstance();
396 } catch (Exception e) {
397 throw new RuntimeException("Could not construct new instance of class "+
401 // Next undo position is set by opening the configuration dialog
402 document.addUndoPosition("Add " + component.getComponentName());
405 if (position == null)
406 c.addChild(component);
408 c.addChild(component, position);
410 // Select new component and open config dialog
411 selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
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;
422 ComponentConfigDialog.showDialog(parent, document, component);
427 * A class suitable for BodyComponents. Addition is allowed ...
429 private class BodyComponentButton extends ComponentButton {
431 public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
435 public BodyComponentButton(String text, Icon enabled, Icon disabled) {
436 super(text, enabled, disabled);
439 public BodyComponentButton(String text) {
444 public boolean isAddable(RocketComponent c) {
445 if (super.isAddable(c))
447 // Handled separately:
448 if (c instanceof BodyComponent)
450 if (c == null || c instanceof Rocket)
456 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
457 if (super.isAddable(c)) // Handled automatically
458 return super.getAdditionPosition(c);
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),
468 if (!(c instanceof BodyComponent))
470 RocketComponent parent = c.getParent();
471 assert(parent != null);
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);
477 if (parent.getChildPosition(c) == parent.getChildCount()-1)
478 pos = 2; // Selected component is the last component
488 // Insert after current position
489 return new Pair<RocketComponent,Integer>(parent, parent.getChildPosition(c)+1);
491 // Insert at the end of the parent
492 return new Pair<RocketComponent,Integer>(parent, null);
494 ExceptionHandler.handleErrorCondition("ERROR: Bad position type: "+pos);
499 private int askPosition() {
500 Object[] options = { "Insert here", "Add to the end", "Cancel" };
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 ResizeLabel("You can change the default operation in the " +
508 int sel = JOptionPane.showOptionDialog(null, // parent component
510 "Insert the component after the current component or as the last " +
513 "Select component position", // title
514 JOptionPane.DEFAULT_OPTION, // default selections
515 JOptionPane.QUESTION_MESSAGE, // dialog type
518 options[0]); // initial value
521 case JOptionPane.CLOSED_OPTION:
534 ExceptionHandler.handleErrorCondition("ERROR: JOptionPane returned "+sel);
538 if (check.isSelected()) {
539 // Save the preference
540 Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
550 * Class for fin sets, that attach only to BodyTubes.
552 private class FinButton extends ComponentButton {
553 public FinButton(Class<? extends RocketComponent> c, String text) {
557 public FinButton(String text, Icon enabled, Icon disabled) {
558 super(text, enabled, disabled);
561 public FinButton(String text) {
566 public boolean isAddable(RocketComponent c) {
569 return (c.getClass().equals(BodyTube.class));
575 ///////// Scrolling functionality
578 public Dimension getPreferredScrollableViewportSize() {
579 return getPreferredSize();
584 public int getScrollableBlockIncrement(Rectangle visibleRect,
585 int orientation, int direction) {
586 if (orientation == SwingConstants.VERTICAL)
587 return visibleRect.height * 8 / 10;
593 public boolean getScrollableTracksViewportHeight() {
599 public boolean getScrollableTracksViewportWidth() {
605 public int getScrollableUnitIncrement(Rectangle visibleRect,
606 int orientation, int direction) {