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.gui.main.componenttree.ComponentTreeModel;
33 import net.sf.openrocket.logging.LogHelper;
34 import net.sf.openrocket.rocketcomponent.BodyComponent;
35 import net.sf.openrocket.rocketcomponent.BodyTube;
36 import net.sf.openrocket.rocketcomponent.Bulkhead;
37 import net.sf.openrocket.rocketcomponent.CenteringRing;
38 import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
39 import net.sf.openrocket.rocketcomponent.EngineBlock;
40 import net.sf.openrocket.rocketcomponent.FreeformFinSet;
41 import net.sf.openrocket.rocketcomponent.InnerTube;
42 import net.sf.openrocket.rocketcomponent.LaunchLug;
43 import net.sf.openrocket.rocketcomponent.MassComponent;
44 import net.sf.openrocket.rocketcomponent.NoseCone;
45 import net.sf.openrocket.rocketcomponent.Parachute;
46 import net.sf.openrocket.rocketcomponent.Rocket;
47 import net.sf.openrocket.rocketcomponent.RocketComponent;
48 import net.sf.openrocket.rocketcomponent.ShockCord;
49 import net.sf.openrocket.rocketcomponent.Streamer;
50 import net.sf.openrocket.rocketcomponent.Transition;
51 import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
52 import net.sf.openrocket.rocketcomponent.TubeCoupler;
53 import net.sf.openrocket.startup.Application;
54 import net.sf.openrocket.util.BugException;
55 import net.sf.openrocket.util.Pair;
56 import net.sf.openrocket.util.Prefs;
57 import net.sf.openrocket.util.Reflection;
60 * A component that contains addition buttons to add different types of rocket components
61 * to a rocket. It enables and disables buttons according to the current selection of a
64 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
67 public class ComponentAddButtons extends JPanel implements Scrollable {
68 private static final LogHelper log = Application.getLogger();
70 private static final int ROWS = 3;
71 private static final int MAXCOLS = 6;
72 private static final String BUTTONPARAM = "grow, sizegroup buttons";
74 private static final int GAP = 5;
75 private static final int EXTRASPACE = 0;
77 private final ComponentButton[][] buttons;
79 private final OpenRocketDocument document;
80 private final TreeSelectionModel selectionModel;
81 private final JViewport viewport;
82 private final MigLayout layout;
84 private final int width, height;
87 public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model,
91 String constaint = "[min!]";
92 for (int i = 1; i < MAXCOLS; i++)
93 constaint = constaint + GAP + "[min!]";
95 layout = new MigLayout("fill", constaint);
97 this.document = document;
98 this.selectionModel = model;
99 this.viewport = viewport;
101 buttons = new ComponentButton[ROWS][];
104 ////////////////////////////////////////////
107 addButtonRow("Body components and fin sets", row,
108 new BodyComponentButton(NoseCone.class, "Nose cone"),
109 new BodyComponentButton(BodyTube.class, "Body tube"),
110 new BodyComponentButton(Transition.class, "Transition"),
111 new FinButton(TrapezoidFinSet.class, "Trapezoidal"), // TODO: MEDIUM: freer fin placing
112 new FinButton(EllipticalFinSet.class, "Elliptical"),
113 new FinButton(FreeformFinSet.class, "Freeform"),
114 new FinButton(LaunchLug.class, "Launch lug"));
119 /////////////////////////////////////////////
121 addButtonRow("Inner component", row,
122 new ComponentButton(InnerTube.class, "Inner tube"),
123 new ComponentButton(TubeCoupler.class, "Coupler"),
124 new ComponentButton(CenteringRing.class, "Centering\nring"),
125 new ComponentButton(Bulkhead.class, "Bulkhead"),
126 new ComponentButton(EngineBlock.class, "Engine\nblock"));
130 ////////////////////////////////////////////
132 addButtonRow("Mass objects", row,
133 new ComponentButton(Parachute.class, "Parachute"),
134 new ComponentButton(Streamer.class, "Streamer"),
135 new ComponentButton(ShockCord.class, "Shock cord"),
136 // new ComponentButton("Motor clip"),
137 // new ComponentButton("Payload"),
138 new ComponentButton(MassComponent.class, "Mass\ncomponent"));
141 // Get maximum button size
144 for (row = 0; row < buttons.length; row++) {
145 for (int col = 0; col < buttons[row].length; col++) {
146 Dimension d = buttons[row][col].getPreferredSize();
154 // Set all buttons to maximum size
157 Dimension d = new Dimension(width, height);
158 for (row = 0; row < buttons.length; row++) {
159 for (int col = 0; col < buttons[row].length; col++) {
160 buttons[row][col].setMinimumSize(d);
161 buttons[row][col].setPreferredSize(d);
162 buttons[row][col].getComponent(0).validate();
166 // Add viewport listener if viewport provided
167 if (viewport != null) {
168 viewport.addChangeListener(new ChangeListener() {
169 private int oldWidth = -1;
171 public void stateChanged(ChangeEvent e) {
172 Dimension d = ComponentAddButtons.this.viewport.getExtentSize();
173 if (d.width != oldWidth) {
181 add(new JPanel(), "grow");
186 * Adds a row of buttons to the panel.
187 * @param label Label placed before the row
188 * @param row Row number
189 * @param b List of ComponentButtons to place on the row
191 private void addButtonRow(String label, int row, ComponentButton... b) {
193 add(new JLabel(label), "span, gaptop unrel, wrap");
195 add(new JLabel(label), "span, gaptop 0, wrap");
198 buttons[row] = new ComponentButton[b.length];
200 for (int i = 0; i < b.length; i++) {
201 buttons[row][col] = b[i];
202 if (i < b.length - 1)
203 add(b[i], BUTTONPARAM);
205 add(b[i], BUTTONPARAM + ", wrap");
212 * Flows the buttons in all rows of the panel. If a button would come too close
213 * to the right edge of the viewport, "newline" is added to its constraints flowing
214 * it to the next line.
216 private void flowButtons() {
217 if (viewport == null)
222 Dimension d = viewport.getExtentSize();
224 for (int row = 0; row < buttons.length; row++) {
226 for (int col = 0; col < buttons[row].length; col++) {
228 String param = BUTTONPARAM + ",width " + width + "!,height " + height + "!";
230 if (w + EXTRASPACE > d.width) {
231 param = param + ",newline";
234 if (col == buttons[row].length - 1)
235 param = param + ",wrap";
236 layout.setComponentConstraints(buttons[row][col], param);
245 * Class for a component button.
247 private class ComponentButton extends JButton implements TreeSelectionListener {
248 protected Class<? extends RocketComponent> componentClass = null;
249 private Constructor<? extends RocketComponent> constructor = null;
251 /** Only label, no icon. */
252 public ComponentButton(String text) {
253 this(text, null, null);
257 * Constructor with icon and label. The icon and label are placed into the button.
258 * The label may contain "\n" as a newline.
260 public ComponentButton(String text, Icon enabled, Icon disabled) {
262 setLayout(new MigLayout("fill, flowy, insets 0, gap 0", "", ""));
264 add(new JLabel(), "push, sizegroup spacing");
267 if (enabled != null) {
268 JLabel label = new JLabel(enabled);
269 if (disabled != null)
270 label.setDisabledIcon(disabled);
275 String[] l = text.split("\n");
276 for (int i = 0; i < l.length; i++) {
277 add(new StyledLabel(l[i], SwingConstants.CENTER, -3.0f), "growx");
280 add(new JLabel(), "push, sizegroup spacing");
282 valueChanged(null); // Update enabled status
283 selectionModel.addTreeSelectionListener(this);
288 * Main constructor that should be used. The generated component type is specified
289 * and the text. The icons are fetched based on the component type.
291 public ComponentButton(Class<? extends RocketComponent> c, String text) {
292 this(text, ComponentIcons.getLargeIcon(c), ComponentIcons.getLargeDisabledIcon(c));
300 constructor = c.getConstructor();
301 } catch (NoSuchMethodException e) {
302 throw new IllegalArgumentException("Unable to get default " +
303 "constructor for class " + c, e);
309 * Return whether the current component is addable when the component c is selected.
310 * c is null if there is no selection. The default is to use c.isCompatible(class).
312 public boolean isAddable(RocketComponent c) {
315 if (componentClass == null)
317 return c.isCompatible(componentClass);
321 * Return the position to add the component if component c is selected currently.
322 * The first element of the returned array is the RocketComponent to add the component
323 * to, and the second (if non-null) an Integer telling the position of the component.
324 * A return value of null means that the user cancelled addition of the component.
325 * If the Integer is null, the component is added at the end of the sibling
326 * list. By default returns the end of the currently selected component.
328 * @param c The component currently selected
329 * @return The position to add the new component to, or null if should not add.
331 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
332 return new Pair<RocketComponent, Integer>(c, null);
336 * Updates the enabled status of the button.
337 * TODO: LOW: What about updates to the rocket tree?
339 public void valueChanged(TreeSelectionEvent e) {
344 * Sets the enabled status of the button and all subcomponents.
347 public void setEnabled(boolean enabled) {
348 super.setEnabled(enabled);
349 Component[] c = getComponents();
350 for (int i = 0; i < c.length; i++)
351 c[i].setEnabled(enabled);
356 * Update the enabled status of the button.
358 private void updateEnabled() {
359 RocketComponent c = null;
360 TreePath p = selectionModel.getSelectionPath();
362 c = (RocketComponent) p.getLastPathComponent();
363 setEnabled(isAddable(c));
368 protected void fireActionPerformed(ActionEvent event) {
369 super.fireActionPerformed(event);
370 log.user("Adding component of type " + componentClass.getSimpleName());
371 RocketComponent c = null;
372 Integer position = null;
374 TreePath p = selectionModel.getSelectionPath();
376 c = (RocketComponent) p.getLastPathComponent();
378 Pair<RocketComponent, Integer> pos = getAdditionPosition(c);
381 log.info("No position to add component");
385 position = pos.getV();
390 ExceptionHandler.handleErrorCondition("ERROR: Could not place new component.");
395 if (constructor == null) {
396 ExceptionHandler.handleErrorCondition("ERROR: Construction of type not supported yet.");
400 RocketComponent component;
402 component = (RocketComponent) constructor.newInstance();
403 } catch (InstantiationException e) {
404 throw new BugException("Could not construct new instance of class " + constructor, e);
405 } catch (IllegalAccessException e) {
406 throw new BugException("Could not construct new instance of class " + constructor, e);
407 } catch (InvocationTargetException e) {
408 throw Reflection.handleWrappedException(e);
411 // Next undo position is set by opening the configuration dialog
412 document.addUndoPosition("Add " + component.getComponentName());
414 log.info("Adding component " + component.getComponentName() + " to component " + c.getComponentName() +
415 " position=" + position);
417 if (position == null)
418 c.addChild(component);
420 c.addChild(component, position);
422 // Select new component and open config dialog
423 selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
425 JFrame parent = null;
426 for (Component comp = ComponentAddButtons.this; comp != null; comp = comp.getParent()) {
427 if (comp instanceof JFrame) {
428 parent = (JFrame) comp;
433 ComponentConfigDialog.showDialog(parent, document, component);
438 * A class suitable for BodyComponents. Addition is allowed ...
440 private class BodyComponentButton extends ComponentButton {
442 public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
446 public BodyComponentButton(String text, Icon enabled, Icon disabled) {
447 super(text, enabled, disabled);
450 public BodyComponentButton(String text) {
455 public boolean isAddable(RocketComponent c) {
456 if (super.isAddable(c))
458 // Handled separately:
459 if (c instanceof BodyComponent)
461 if (c == null || c instanceof Rocket)
467 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
468 if (super.isAddable(c)) // Handled automatically
469 return super.getAdditionPosition(c);
472 if (c == null || c instanceof Rocket) {
473 // Add as last body component of the last stage
474 Rocket rocket = document.getRocket();
475 return new Pair<RocketComponent, Integer>(rocket.getChild(rocket.getStageCount() - 1),
479 if (!(c instanceof BodyComponent))
481 RocketComponent parent = c.getParent();
482 if (parent == null) {
483 throw new BugException("Component " + c.getComponentName() + " is the root component, " +
484 "componentClass=" + componentClass);
487 // Check whether to insert between or at the end.
488 // 0 = ask, 1 = in between, 2 = at the end
489 int pos = Prefs.getChoise(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0);
491 if (parent.getChildPosition(c) == parent.getChildCount() - 1)
492 pos = 2; // Selected component is the last component
502 // Insert after current position
503 return new Pair<RocketComponent, Integer>(parent, parent.getChildPosition(c) + 1);
505 // Insert at the end of the parent
506 return new Pair<RocketComponent, Integer>(parent, null);
508 ExceptionHandler.handleErrorCondition("ERROR: Bad position type: " + pos);
513 private int askPosition() {
514 Object[] options = { "Insert here", "Add to the end", "Cancel" };
516 JPanel panel = new JPanel(new MigLayout());
517 JCheckBox check = new JCheckBox("Do not ask me again");
518 panel.add(check, "wrap");
519 panel.add(new StyledLabel("You can change the default operation in the " +
520 "preferences.", -2));
522 int sel = JOptionPane.showOptionDialog(null, // parent component
524 "Insert the component after the current component or as the last " +
527 "Select component position", // title
528 JOptionPane.DEFAULT_OPTION, // default selections
529 JOptionPane.QUESTION_MESSAGE, // dialog type
532 options[0]); // initial value
535 case JOptionPane.CLOSED_OPTION:
548 ExceptionHandler.handleErrorCondition("ERROR: JOptionPane returned " + sel);
552 if (check.isSelected()) {
553 // Save the preference
554 Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
564 * Class for fin sets, that attach only to BodyTubes.
566 private class FinButton extends ComponentButton {
567 public FinButton(Class<? extends RocketComponent> c, String text) {
571 public FinButton(String text, Icon enabled, Icon disabled) {
572 super(text, enabled, disabled);
575 public FinButton(String text) {
580 public boolean isAddable(RocketComponent c) {
583 return (c.getClass().equals(BodyTube.class));
589 ///////// Scrolling functionality
592 public Dimension getPreferredScrollableViewportSize() {
593 return getPreferredSize();
598 public int getScrollableBlockIncrement(Rectangle visibleRect,
599 int orientation, int direction) {
600 if (orientation == SwingConstants.VERTICAL)
601 return visibleRect.height * 8 / 10;
607 public boolean getScrollableTracksViewportHeight() {
613 public boolean getScrollableTracksViewportWidth() {
619 public int getScrollableUnitIncrement(Rectangle visibleRect,
620 int orientation, int direction) {