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"));
115 /////////////////////////////////////////////
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"));
126 ////////////////////////////////////////////
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"));
137 // Get maximum button size
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();
150 // Set all buttons to maximum size
151 System.out.println("Setting w=" + w + " h=" + 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();
163 // Add viewport listener if viewport provided
164 if (viewport != null) {
165 viewport.addChangeListener(new ChangeListener() {
166 private int oldWidth = -1;
168 public void stateChanged(ChangeEvent e) {
169 Dimension d = ComponentAddButtons.this.viewport.getExtentSize();
170 if (d.width != oldWidth) {
178 add(new JPanel(), "grow");
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
188 private void addButtonRow(String label, int row, ComponentButton... b) {
190 add(new JLabel(label), "span, gaptop unrel, wrap");
192 add(new JLabel(label), "span, gaptop 0, wrap");
195 buttons[row] = new ComponentButton[b.length];
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);
202 add(b[i], BUTTONPARAM + ", wrap");
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.
213 private void flowButtons() {
214 if (viewport == null)
219 Dimension d = viewport.getExtentSize();
221 for (int row = 0; row < buttons.length; row++) {
223 for (int col = 0; col < buttons[row].length; col++) {
225 String param = BUTTONPARAM + ",width " + width + "!,height " + height + "!";
227 if (w + EXTRASPACE > d.width) {
228 param = param + ",newline";
231 if (col == buttons[row].length - 1)
232 param = param + ",wrap";
233 layout.setComponentConstraints(buttons[row][col], param);
242 * Class for a component button.
244 private class ComponentButton extends JButton implements TreeSelectionListener {
245 protected Class<? extends RocketComponent> componentClass = null;
246 private Constructor<? extends RocketComponent> constructor = null;
248 /** Only label, no icon. */
249 public ComponentButton(String text) {
250 this(text, null, null);
254 * Constructor with icon and label. The icon and label are placed into the button.
255 * The label may contain "\n" as a newline.
257 public ComponentButton(String text, Icon enabled, Icon disabled) {
259 setLayout(new MigLayout("fill, flowy, insets 0, gap 0", "", ""));
261 add(new JLabel(), "push, sizegroup spacing");
264 if (enabled != null) {
265 JLabel label = new JLabel(enabled);
266 if (disabled != null)
267 label.setDisabledIcon(disabled);
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");
277 add(new JLabel(), "push, sizegroup spacing");
279 valueChanged(null); // Update enabled status
280 selectionModel.addTreeSelectionListener(this);
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.
288 public ComponentButton(Class<? extends RocketComponent> c, String text) {
289 this(text, ComponentIcons.getLargeIcon(c), ComponentIcons.getLargeDisabledIcon(c));
297 constructor = c.getConstructor();
298 } catch (NoSuchMethodException e) {
299 throw new IllegalArgumentException("Unable to get default " +
300 "constructor for class " + c, e);
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).
309 public boolean isAddable(RocketComponent c) {
312 if (componentClass == null)
314 return c.isCompatible(componentClass);
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.
325 * @param c The component currently selected
326 * @return The position to add the new component to, or null if should not add.
328 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
329 return new Pair<RocketComponent, Integer>(c, null);
333 * Updates the enabled status of the button.
334 * TODO: LOW: What about updates to the rocket tree?
336 public void valueChanged(TreeSelectionEvent e) {
341 * Sets the enabled status of the button and all subcomponents.
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);
353 * Update the enabled status of the button.
355 private void updateEnabled() {
356 RocketComponent c = null;
357 TreePath p = selectionModel.getSelectionPath();
359 c = (RocketComponent) p.getLastPathComponent();
360 setEnabled(isAddable(c));
365 protected void fireActionPerformed(ActionEvent event) {
366 super.fireActionPerformed(event);
367 RocketComponent c = null;
368 Integer position = null;
370 TreePath p = selectionModel.getSelectionPath();
372 c = (RocketComponent) p.getLastPathComponent();
374 Pair<RocketComponent, Integer> pos = getAdditionPosition(c);
380 position = pos.getV();
385 ExceptionHandler.handleErrorCondition("ERROR: Could not place new component.");
390 if (constructor == null) {
391 ExceptionHandler.handleErrorCondition("ERROR: Construction of type not supported yet.");
395 RocketComponent component;
397 component = (RocketComponent) constructor.newInstance();
398 } catch (InstantiationException e) {
399 throw new BugException("Could not construct new instance of class " +
401 } catch (IllegalAccessException e) {
402 throw new BugException("Could not construct new instance of class " +
404 } catch (InvocationTargetException e) {
405 throw Reflection.handleWrappedException(e);
408 // Next undo position is set by opening the configuration dialog
409 document.addUndoPosition("Add " + component.getComponentName());
412 if (position == null)
413 c.addChild(component);
415 c.addChild(component, position);
417 // Select new component and open config dialog
418 selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
420 JFrame parent = null;
421 for (Component comp = ComponentAddButtons.this; comp != null; comp = comp.getParent()) {
422 if (comp instanceof JFrame) {
423 parent = (JFrame) comp;
428 ComponentConfigDialog.showDialog(parent, document, component);
433 * A class suitable for BodyComponents. Addition is allowed ...
435 private class BodyComponentButton extends ComponentButton {
437 public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
441 public BodyComponentButton(String text, Icon enabled, Icon disabled) {
442 super(text, enabled, disabled);
445 public BodyComponentButton(String text) {
450 public boolean isAddable(RocketComponent c) {
451 if (super.isAddable(c))
453 // Handled separately:
454 if (c instanceof BodyComponent)
456 if (c == null || c instanceof Rocket)
462 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
463 if (super.isAddable(c)) // Handled automatically
464 return super.getAdditionPosition(c);
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),
474 if (!(c instanceof BodyComponent))
476 RocketComponent parent = c.getParent();
477 if (parent == null) {
478 throw new BugException("Component " + c.getComponentName() + " is the root component, " +
479 "componentClass=" + componentClass);
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);
486 if (parent.getChildPosition(c) == parent.getChildCount() - 1)
487 pos = 2; // Selected component is the last component
497 // Insert after current position
498 return new Pair<RocketComponent, Integer>(parent, parent.getChildPosition(c) + 1);
500 // Insert at the end of the parent
501 return new Pair<RocketComponent, Integer>(parent, null);
503 ExceptionHandler.handleErrorCondition("ERROR: Bad position type: " + pos);
508 private int askPosition() {
509 Object[] options = { "Insert here", "Add to the end", "Cancel" };
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));
517 int sel = JOptionPane.showOptionDialog(null, // parent component
519 "Insert the component after the current component or as the last " +
522 "Select component position", // title
523 JOptionPane.DEFAULT_OPTION, // default selections
524 JOptionPane.QUESTION_MESSAGE, // dialog type
527 options[0]); // initial value
530 case JOptionPane.CLOSED_OPTION:
543 ExceptionHandler.handleErrorCondition("ERROR: JOptionPane returned " + sel);
547 if (check.isSelected()) {
548 // Save the preference
549 Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
559 * Class for fin sets, that attach only to BodyTubes.
561 private class FinButton extends ComponentButton {
562 public FinButton(Class<? extends RocketComponent> c, String text) {
566 public FinButton(String text, Icon enabled, Icon disabled) {
567 super(text, enabled, disabled);
570 public FinButton(String text) {
575 public boolean isAddable(RocketComponent c) {
578 return (c.getClass().equals(BodyTube.class));
584 ///////// Scrolling functionality
587 public Dimension getPreferredScrollableViewportSize() {
588 return getPreferredSize();
593 public int getScrollableBlockIncrement(Rectangle visibleRect,
594 int orientation, int direction) {
595 if (orientation == SwingConstants.VERTICAL)
596 return visibleRect.height * 8 / 10;
602 public boolean getScrollableTracksViewportHeight() {
608 public boolean getScrollableTracksViewportWidth() {
614 public int getScrollableUnitIncrement(Rectangle visibleRect,
615 int orientation, int direction) {