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.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;
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
65 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
68 public class ComponentAddButtons extends JPanel implements Scrollable {
69 private static final LogHelper log = Application.getLogger();
70 private static final Translator trans = Application.getTranslator();
72 private static final int ROWS = 3;
73 private static final int MAXCOLS = 6;
74 private static final String BUTTONPARAM = "grow, sizegroup buttons";
76 private static final int GAP = 5;
77 private static final int EXTRASPACE = 0;
79 private final ComponentButton[][] buttons;
81 private final OpenRocketDocument document;
82 private final TreeSelectionModel selectionModel;
83 private final JViewport viewport;
84 private final MigLayout layout;
86 private final int width, height;
89 public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model,
93 String constaint = "[min!]";
94 for (int i = 1; i < MAXCOLS; i++)
95 constaint = constaint + GAP + "[min!]";
97 layout = new MigLayout("fill", constaint);
99 this.document = document;
100 this.selectionModel = model;
101 this.viewport = viewport;
103 buttons = new ComponentButton[ROWS][];
106 ////////////////////////////////////////////
108 //// Body components and fin sets
109 addButtonRow(trans.get("compaddbuttons.Bodycompandfinsets"), row,
111 new BodyComponentButton(NoseCone.class, trans.get("compaddbuttons.Nosecone")),
113 new BodyComponentButton(BodyTube.class, trans.get("compaddbuttons.Bodytube")),
115 new BodyComponentButton(Transition.class, trans.get("compaddbuttons.Transition")),
117 new FinButton(TrapezoidFinSet.class, trans.get("compaddbuttons.Trapezoidal")), // TODO: MEDIUM: freer fin placing
119 new FinButton(EllipticalFinSet.class, trans.get("compaddbuttons.Elliptical")),
121 new FinButton(FreeformFinSet.class, trans.get("compaddbuttons.Freeform")),
123 new FinButton(LaunchLug.class, trans.get("compaddbuttons.Launchlug")));
128 /////////////////////////////////////////////
131 addButtonRow(trans.get("compaddbuttons.Innercomponent"), row,
133 new ComponentButton(InnerTube.class, trans.get("compaddbuttons.Innertube")),
135 new ComponentButton(TubeCoupler.class, trans.get("compaddbuttons.Coupler")),
137 new ComponentButton(CenteringRing.class, trans.get("compaddbuttons.Centeringring")),
139 new ComponentButton(Bulkhead.class, trans.get("compaddbuttons.Bulkhead")),
141 new ComponentButton(EngineBlock.class, trans.get("compaddbuttons.Engineblock")));
145 ////////////////////////////////////////////
148 addButtonRow(trans.get("compaddbuttons.Massobjects"), row,
150 new ComponentButton(Parachute.class, trans.get("compaddbuttons.Parachute")),
152 new ComponentButton(Streamer.class, trans.get("compaddbuttons.Streamer")),
154 new ComponentButton(ShockCord.class, trans.get("compaddbuttons.Shockcord")),
155 // new ComponentButton("Motor clip"),
156 // new ComponentButton("Payload"),
158 new ComponentButton(MassComponent.class, trans.get("compaddbuttons.Masscomponent")));
161 // Get maximum button size
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();
174 // Set all buttons to maximum size
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();
186 // Add viewport listener if viewport provided
187 if (viewport != null) {
188 viewport.addChangeListener(new ChangeListener() {
189 private int oldWidth = -1;
191 public void stateChanged(ChangeEvent e) {
192 Dimension d = ComponentAddButtons.this.viewport.getExtentSize();
193 if (d.width != oldWidth) {
201 add(new JPanel(), "grow");
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
211 private void addButtonRow(String label, int row, ComponentButton... b) {
213 add(new JLabel(label), "span, gaptop unrel, wrap");
215 add(new JLabel(label), "span, gaptop 0, wrap");
218 buttons[row] = new ComponentButton[b.length];
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);
225 add(b[i], BUTTONPARAM + ", wrap");
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.
236 private void flowButtons() {
237 if (viewport == null)
242 Dimension d = viewport.getExtentSize();
244 for (int row = 0; row < buttons.length; row++) {
246 for (int col = 0; col < buttons[row].length; col++) {
248 String param = BUTTONPARAM + ",width " + width + "!,height " + height + "!";
250 if (w + EXTRASPACE > d.width) {
251 param = param + ",newline";
254 if (col == buttons[row].length - 1)
255 param = param + ",wrap";
256 layout.setComponentConstraints(buttons[row][col], param);
265 * Class for a component button.
267 private class ComponentButton extends JButton implements TreeSelectionListener {
268 protected Class<? extends RocketComponent> componentClass = null;
269 private Constructor<? extends RocketComponent> constructor = null;
271 /** Only label, no icon. */
272 public ComponentButton(String text) {
273 this(text, null, null);
277 * Constructor with icon and label. The icon and label are placed into the button.
278 * The label may contain "\n" as a newline.
280 public ComponentButton(String text, Icon enabled, Icon disabled) {
282 setLayout(new MigLayout("fill, flowy, insets 0, gap 0", "", ""));
284 add(new JLabel(), "push, sizegroup spacing");
287 if (enabled != null) {
288 JLabel label = new JLabel(enabled);
289 if (disabled != null)
290 label.setDisabledIcon(disabled);
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");
300 add(new JLabel(), "push, sizegroup spacing");
302 valueChanged(null); // Update enabled status
303 selectionModel.addTreeSelectionListener(this);
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.
311 public ComponentButton(Class<? extends RocketComponent> c, String text) {
312 this(text, ComponentIcons.getLargeIcon(c), ComponentIcons.getLargeDisabledIcon(c));
320 constructor = c.getConstructor();
321 } catch (NoSuchMethodException e) {
322 throw new IllegalArgumentException("Unable to get default " +
323 "constructor for class " + c, e);
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).
332 public boolean isAddable(RocketComponent c) {
335 if (componentClass == null)
337 return c.isCompatible(componentClass);
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.
348 * @param c The component currently selected
349 * @return The position to add the new component to, or null if should not add.
351 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
352 return new Pair<RocketComponent, Integer>(c, null);
356 * Updates the enabled status of the button.
357 * TODO: LOW: What about updates to the rocket tree?
359 public void valueChanged(TreeSelectionEvent e) {
364 * Sets the enabled status of the button and all subcomponents.
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);
376 * Update the enabled status of the button.
378 private void updateEnabled() {
379 RocketComponent c = null;
380 TreePath p = selectionModel.getSelectionPath();
382 c = (RocketComponent) p.getLastPathComponent();
383 setEnabled(isAddable(c));
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;
394 TreePath p = selectionModel.getSelectionPath();
396 c = (RocketComponent) p.getLastPathComponent();
398 Pair<RocketComponent, Integer> pos = getAdditionPosition(c);
401 log.info("No position to add component");
405 position = pos.getV();
410 Application.getExceptionHandler().handleErrorCondition("ERROR: Could not place new component.");
415 if (constructor == null) {
416 Application.getExceptionHandler().handleErrorCondition("ERROR: Construction of type not supported yet.");
420 RocketComponent component;
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);
431 // Next undo position is set by opening the configuration dialog
432 document.addUndoPosition("Add " + component.getComponentName());
434 log.info("Adding component " + component.getComponentName() + " to component " + c.getComponentName() +
435 " position=" + position);
437 if (position == null)
438 c.addChild(component);
440 c.addChild(component, position);
442 // Select new component and open config dialog
443 selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
445 JFrame parent = null;
446 for (Component comp = ComponentAddButtons.this; comp != null; comp = comp.getParent()) {
447 if (comp instanceof JFrame) {
448 parent = (JFrame) comp;
453 ComponentConfigDialog.showDialog(parent, document, component);
458 * A class suitable for BodyComponents. Addition is allowed ...
460 private class BodyComponentButton extends ComponentButton {
462 public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
466 public BodyComponentButton(String text, Icon enabled, Icon disabled) {
467 super(text, enabled, disabled);
470 public BodyComponentButton(String text) {
475 public boolean isAddable(RocketComponent c) {
476 if (super.isAddable(c))
478 // Handled separately:
479 if (c instanceof BodyComponent)
481 if (c == null || c instanceof Rocket)
487 public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) {
488 if (super.isAddable(c)) // Handled automatically
489 return super.getAdditionPosition(c);
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),
499 if (!(c instanceof BodyComponent))
501 RocketComponent parent = c.getParent();
502 if (parent == null) {
503 throw new BugException("Component " + c.getComponentName() + " is the root component, " +
504 "componentClass=" + componentClass);
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);
511 if (parent.getChildPosition(c) == parent.getChildCount() - 1)
512 pos = 2; // Selected component is the last component
522 // Insert after current position
523 return new Pair<RocketComponent, Integer>(parent, parent.getChildPosition(c) + 1);
525 // Insert at the end of the parent
526 return new Pair<RocketComponent, Integer>(parent, null);
528 Application.getExceptionHandler().handleErrorCondition("ERROR: Bad position type: " + pos);
533 private int askPosition() {
537 Object[] options = { trans.get("compaddbuttons.askPosition.Inserthere"),
538 trans.get("compaddbuttons.askPosition.Addtotheend"),
539 trans.get("compaddbuttons.askPosition.Cancel") };
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));
548 int sel = JOptionPane.showOptionDialog(null, // parent component
549 //// Insert the component after the current component or as the last component?
551 trans.get("compaddbuttons.lbl.insertcomp"),
553 //// Select component position
554 trans.get("compaddbuttons.Selectcomppos"), // title
555 JOptionPane.DEFAULT_OPTION, // default selections
556 JOptionPane.QUESTION_MESSAGE, // dialog type
559 options[0]); // initial value
562 case JOptionPane.CLOSED_OPTION:
575 Application.getExceptionHandler().handleErrorCondition("ERROR: JOptionPane returned " + sel);
579 if (check.isSelected()) {
580 // Save the preference
581 Application.getPreferences().putInt(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
591 * Class for fin sets, that attach only to BodyTubes.
593 private class FinButton extends ComponentButton {
594 public FinButton(Class<? extends RocketComponent> c, String text) {
598 public FinButton(String text, Icon enabled, Icon disabled) {
599 super(text, enabled, disabled);
602 public FinButton(String text) {
607 public boolean isAddable(RocketComponent c) {
610 return (c.getClass().equals(BodyTube.class));
616 ///////// Scrolling functionality
619 public Dimension getPreferredScrollableViewportSize() {
620 return getPreferredSize();
625 public int getScrollableBlockIncrement(Rectangle visibleRect,
626 int orientation, int direction) {
627 if (orientation == SwingConstants.VERTICAL)
628 return visibleRect.height * 8 / 10;
634 public boolean getScrollableTracksViewportHeight() {
640 public boolean getScrollableTracksViewportWidth() {
646 public int getScrollableUnitIncrement(Rectangle visibleRect,
647 int orientation, int direction) {