package net.sf.openrocket.gui.main; import java.awt.Component; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JViewport; import javax.swing.Scrollable; import javax.swing.SwingConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.components.StyledLabel; import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.Markers; import net.sf.openrocket.rocketcomponent.BodyComponent; import net.sf.openrocket.rocketcomponent.BodyTube; import net.sf.openrocket.rocketcomponent.Bulkhead; import net.sf.openrocket.rocketcomponent.CenteringRing; import net.sf.openrocket.rocketcomponent.EllipticalFinSet; import net.sf.openrocket.rocketcomponent.EngineBlock; import net.sf.openrocket.rocketcomponent.FreeformFinSet; import net.sf.openrocket.rocketcomponent.InnerTube; import net.sf.openrocket.rocketcomponent.LaunchLug; import net.sf.openrocket.rocketcomponent.MassComponent; import net.sf.openrocket.rocketcomponent.NoseCone; import net.sf.openrocket.rocketcomponent.Parachute; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.ShockCord; import net.sf.openrocket.rocketcomponent.Streamer; import net.sf.openrocket.rocketcomponent.Transition; import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; import net.sf.openrocket.rocketcomponent.TubeCoupler; import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.startup.Application; import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Pair; import net.sf.openrocket.util.Reflection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A component that contains addition buttons to add different types of rocket components * to a rocket. It enables and disables buttons according to the current selection of a * TreeSelectionModel. * * @author Sampo Niskanen <sampo.niskanen@iki.fi> */ public class ComponentAddButtons extends JPanel implements Scrollable { private static final Logger log = LoggerFactory.getLogger(ComponentAddButtons.class); private static final Translator trans = Application.getTranslator(); private static final int ROWS = 3; private static final int MAXCOLS = 6; private static final String BUTTONPARAM = "grow, sizegroup buttons"; private static final int GAP = 5; private static final int EXTRASPACE = 0; private final ComponentButton[][] buttons; private final OpenRocketDocument document; private final TreeSelectionModel selectionModel; private final JViewport viewport; private final MigLayout layout; private final int width, height; public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model, JViewport viewport) { super(); String constaint = "[min!]"; for (int i = 1; i < MAXCOLS; i++) constaint = constaint + GAP + "[min!]"; layout = new MigLayout("fill", constaint); setLayout(layout); this.document = document; this.selectionModel = model; this.viewport = viewport; buttons = new ComponentButton[ROWS][]; int row = 0; //////////////////////////////////////////// //// Body components and fin sets addButtonRow(trans.get("compaddbuttons.Bodycompandfinsets"), row, //// Nose cone new BodyComponentButton(NoseCone.class, trans.get("compaddbuttons.Nosecone")), //// Body tube new BodyComponentButton(BodyTube.class, trans.get("compaddbuttons.Bodytube")), //// Transition new BodyComponentButton(Transition.class, trans.get("compaddbuttons.Transition")), //// Trapezoidal new FinButton(TrapezoidFinSet.class, trans.get("compaddbuttons.Trapezoidal")), // TODO: MEDIUM: freer fin placing //// Elliptical new FinButton(EllipticalFinSet.class, trans.get("compaddbuttons.Elliptical")), //// Freeform new FinButton(FreeformFinSet.class, trans.get("compaddbuttons.Freeform")), //// Freeform new FinButton(TubeFinSet.class, trans.get("compaddbuttons.Tubefin")), //// Launch lug new FinButton(LaunchLug.class, trans.get("compaddbuttons.Launchlug"))); row++; ///////////////////////////////////////////// //// Inner component addButtonRow(trans.get("compaddbuttons.Innercomponent"), row, //// Inner tube new ComponentButton(InnerTube.class, trans.get("compaddbuttons.Innertube")), //// Coupler new ComponentButton(TubeCoupler.class, trans.get("compaddbuttons.Coupler")), //// Centering\nring new ComponentButton(CenteringRing.class, trans.get("compaddbuttons.Centeringring")), //// Bulkhead new ComponentButton(Bulkhead.class, trans.get("compaddbuttons.Bulkhead")), //// Engine\nblock new ComponentButton(EngineBlock.class, trans.get("compaddbuttons.Engineblock"))); row++; //////////////////////////////////////////// //// Mass objects addButtonRow(trans.get("compaddbuttons.Massobjects"), row, //// Parachute new ComponentButton(Parachute.class, trans.get("compaddbuttons.Parachute")), //// Streamer new ComponentButton(Streamer.class, trans.get("compaddbuttons.Streamer")), //// Shock cord new ComponentButton(ShockCord.class, trans.get("compaddbuttons.Shockcord")), // new ComponentButton("Motor clip"), // new ComponentButton("Payload"), //// Mass\ncomponent new ComponentButton(MassComponent.class, trans.get("compaddbuttons.Masscomponent"))); // Get maximum button size int w = 0, h = 0; for (row = 0; row < buttons.length; row++) { for (int col = 0; col < buttons[row].length; col++) { Dimension d = buttons[row][col].getPreferredSize(); if (d.width > w) w = d.width; if (d.height > h) h = d.height; } } // Set all buttons to maximum size width = w; height = h; Dimension d = new Dimension(width, height); for (row = 0; row < buttons.length; row++) { for (int col = 0; col < buttons[row].length; col++) { buttons[row][col].setMinimumSize(d); buttons[row][col].setPreferredSize(d); buttons[row][col].getComponent(0).validate(); } } // Add viewport listener if viewport provided if (viewport != null) { viewport.addChangeListener(new ChangeListener() { private int oldWidth = -1; @Override public void stateChanged(ChangeEvent e) { Dimension d1 = ComponentAddButtons.this.viewport.getExtentSize(); if (d1.width != oldWidth) { oldWidth = d1.width; flowButtons(); } } }); } add(new JPanel(), "grow"); } /** * Adds a row of buttons to the panel. * @param label Label placed before the row * @param row Row number * @param b List of ComponentButtons to place on the row */ private void addButtonRow(String label, int row, ComponentButton... b) { if (row > 0) add(new JLabel(label), "span, gaptop unrel, wrap"); else add(new JLabel(label), "span, gaptop 0, wrap"); int col = 0; buttons[row] = new ComponentButton[b.length]; for (int i = 0; i < b.length; i++) { buttons[row][col] = b[i]; if (i < b.length - 1) add(b[i], BUTTONPARAM); else add(b[i], BUTTONPARAM + ", wrap"); col++; } } /** * Flows the buttons in all rows of the panel. If a button would come too close * to the right edge of the viewport, "newline" is added to its constraints flowing * it to the next line. */ private void flowButtons() { if (viewport == null) return; int w; Dimension d = viewport.getExtentSize(); for (int row = 0; row < buttons.length; row++) { w = 0; for (int col = 0; col < buttons[row].length; col++) { w += GAP + width; String param = BUTTONPARAM + ",width " + width + "!,height " + height + "!"; if (w + EXTRASPACE > d.width) { param = param + ",newline"; w = GAP + width; } if (col == buttons[row].length - 1) param = param + ",wrap"; layout.setComponentConstraints(buttons[row][col], param); } } revalidate(); } /** * Class for a component button. */ private class ComponentButton extends JButton implements TreeSelectionListener { protected Class<? extends RocketComponent> componentClass = null; private Constructor<? extends RocketComponent> constructor = null; /** Only label, no icon. */ public ComponentButton(String text) { this(text, null, null); } /** * Constructor with icon and label. The icon and label are placed into the button. * The label may contain "\n" as a newline. */ public ComponentButton(String text, Icon enabled, Icon disabled) { super(); setLayout(new MigLayout("fill, flowy, insets 0, gap 0", "", "")); add(new JLabel(), "push, sizegroup spacing"); // Add Icon if (enabled != null) { JLabel label = new JLabel(enabled); if (disabled != null) label.setDisabledIcon(disabled); add(label, "growx"); } // Add labels String[] l = text.split("\n"); for (int i = 0; i < l.length; i++) { add(new StyledLabel(l[i], SwingConstants.CENTER, -3.0f), "growx"); } add(new JLabel(), "push, sizegroup spacing"); valueChanged(null); // Update enabled status selectionModel.addTreeSelectionListener(this); } /** * Main constructor that should be used. The generated component type is specified * and the text. The icons are fetched based on the component type. */ public ComponentButton(Class<? extends RocketComponent> c, String text) { this(text, ComponentIcons.getLargeIcon(c), ComponentIcons.getLargeDisabledIcon(c)); if (c == null) return; componentClass = c; try { constructor = c.getConstructor(); } catch (NoSuchMethodException e) { throw new IllegalArgumentException("Unable to get default " + "constructor for class " + c, e); } } /** * Return whether the current component is addable when the component c is selected. * c is null if there is no selection. The default is to use c.isCompatible(class). */ public boolean isAddable(RocketComponent c) { if (c == null) return false; if (componentClass == null) return false; return c.isCompatible(componentClass); } /** * Return the position to add the component if component c is selected currently. * The first element of the returned array is the RocketComponent to add the component * to, and the second (if non-null) an Integer telling the position of the component. * A return value of null means that the user cancelled addition of the component. * If the Integer is null, the component is added at the end of the sibling * list. By default returns the end of the currently selected component. * * @param c The component currently selected * @return The position to add the new component to, or null if should not add. */ public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) { return new Pair<RocketComponent, Integer>(c, null); } /** * Updates the enabled status of the button. * TODO: LOW: What about updates to the rocket tree? */ @Override public void valueChanged(TreeSelectionEvent e) { updateEnabled(); } /** * Sets the enabled status of the button and all subcomponents. */ @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); Component[] c = getComponents(); for (int i = 0; i < c.length; i++) c[i].setEnabled(enabled); } /** * Update the enabled status of the button. */ private void updateEnabled() { RocketComponent c = null; TreePath p = selectionModel.getSelectionPath(); if (p != null) c = (RocketComponent) p.getLastPathComponent(); setEnabled(isAddable(c)); } @Override protected void fireActionPerformed(ActionEvent event) { super.fireActionPerformed(event); log.info(Markers.USER_MARKER, "Adding component of type " + componentClass.getSimpleName()); RocketComponent c = null; Integer position = null; TreePath p = selectionModel.getSelectionPath(); if (p != null) c = (RocketComponent) p.getLastPathComponent(); Pair<RocketComponent, Integer> pos = getAdditionPosition(c); if (pos == null) { // Cancel addition log.info("No position to add component"); return; } c = pos.getU(); position = pos.getV(); if (c == null) { // Should not occur Application.getExceptionHandler().handleErrorCondition("ERROR: Could not place new component."); updateEnabled(); return; } if (constructor == null) { Application.getExceptionHandler().handleErrorCondition("ERROR: Construction of type not supported yet."); return; } RocketComponent component; try { component = (RocketComponent) constructor.newInstance(); } catch (InstantiationException e) { throw new BugException("Could not construct new instance of class " + constructor, e); } catch (IllegalAccessException e) { throw new BugException("Could not construct new instance of class " + constructor, e); } catch (InvocationTargetException e) { throw Reflection.handleWrappedException(e); } // Next undo position is set by opening the configuration dialog document.addUndoPosition("Add " + component.getComponentName()); log.info("Adding component " + component.getComponentName() + " to component " + c.getComponentName() + " position=" + position); if (position == null) c.addChild(component); else c.addChild(component, position); // Select new component and open config dialog selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component)); JFrame parent = null; for (Component comp = ComponentAddButtons.this; comp != null; comp = comp.getParent()) { if (comp instanceof JFrame) { parent = (JFrame) comp; break; } } ComponentConfigDialog.showDialog(parent, document, component); } } /** * A class suitable for BodyComponents. Addition is allowed ... */ private class BodyComponentButton extends ComponentButton { public BodyComponentButton(Class<? extends RocketComponent> c, String text) { super(c, text); } public BodyComponentButton(String text, Icon enabled, Icon disabled) { super(text, enabled, disabled); } public BodyComponentButton(String text) { super(text); } @Override public boolean isAddable(RocketComponent c) { if (super.isAddable(c)) return true; // Handled separately: if (c instanceof BodyComponent) return true; if (c == null || c instanceof Rocket) return true; return false; } @Override public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) { if (super.isAddable(c)) // Handled automatically return super.getAdditionPosition(c); if (c == null || c instanceof Rocket) { // Add as last body component of the last stage Rocket rocket = document.getRocket(); return new Pair<RocketComponent, Integer>(rocket.getChild(rocket.getStageCount() - 1), null); } if (!(c instanceof BodyComponent)) return null; RocketComponent parent = c.getParent(); if (parent == null) { throw new BugException("Component " + c.getComponentName() + " is the root component, " + "componentClass=" + componentClass); } // Check whether to insert between or at the end. // 0 = ask, 1 = in between, 2 = at the end int pos = Application.getPreferences().getChoice(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0); if (pos == 0) { if (parent.getChildPosition(c) == parent.getChildCount() - 1) pos = 2; // Selected component is the last component else pos = askPosition(); } switch (pos) { case 0: // Cancel return null; case 1: // Insert after current position return new Pair<RocketComponent, Integer>(parent, parent.getChildPosition(c) + 1); case 2: // Insert at the end of the parent return new Pair<RocketComponent, Integer>(parent, null); default: Application.getExceptionHandler().handleErrorCondition("ERROR: Bad position type: " + pos); return null; } } private int askPosition() { //// Insert here //// Add to the end //// Cancel Object[] options = { trans.get("compaddbuttons.askPosition.Inserthere"), trans.get("compaddbuttons.askPosition.Addtotheend"), trans.get("compaddbuttons.askPosition.Cancel") }; JPanel panel = new JPanel(new MigLayout()); //// Do not ask me again JCheckBox check = new JCheckBox(trans.get("compaddbuttons.Donotaskmeagain")); panel.add(check, "wrap"); //// You can change the default operation in the preferences. panel.add(new StyledLabel(trans.get("compaddbuttons.lbl.Youcanchange"), -2)); int sel = JOptionPane.showOptionDialog(null, // parent component //// Insert the component after the current component or as the last component? new Object[] { trans.get("compaddbuttons.lbl.insertcomp"), panel }, //// Select component position trans.get("compaddbuttons.Selectcomppos"), // title JOptionPane.DEFAULT_OPTION, // default selections JOptionPane.QUESTION_MESSAGE, // dialog type null, // icon options, // options options[0]); // initial value switch (sel) { case JOptionPane.CLOSED_OPTION: case 2: // Cancel return 0; case 0: // Insert sel = 1; break; case 1: // Add sel = 2; break; default: Application.getExceptionHandler().handleErrorCondition("ERROR: JOptionPane returned " + sel); return 0; } if (check.isSelected()) { // Save the preference Application.getPreferences().putInt(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, sel); } return sel; } } /** * Class for fin sets, that attach only to BodyTubes. */ private class FinButton extends ComponentButton { public FinButton(Class<? extends RocketComponent> c, String text) { super(c, text); } public FinButton(String text, Icon enabled, Icon disabled) { super(text, enabled, disabled); } public FinButton(String text) { super(text); } @Override public boolean isAddable(RocketComponent c) { if (c == null) return false; return (c.getClass().equals(BodyTube.class)); } } ///////// Scrolling functionality @Override public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { if (orientation == SwingConstants.VERTICAL) return visibleRect.height * 8 / 10; return 10; } @Override public boolean getScrollableTracksViewportHeight() { return false; } @Override public boolean getScrollableTracksViewportWidth() { return true; } @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { return 10; } }