package net.sf.openrocket.gui.configdialog; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.geom.Ellipse2D; import java.util.EventObject; import java.util.List; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.SwingUtilities; import javax.swing.border.BevelBorder; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.Resettable; import net.sf.openrocket.gui.SpinnerEditor; import net.sf.openrocket.gui.adaptors.DoubleModel; import net.sf.openrocket.gui.adaptors.EnumModel; import net.sf.openrocket.gui.components.BasicSlider; import net.sf.openrocket.gui.components.DescriptionArea; import net.sf.openrocket.gui.components.UnitSelector; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.material.Material; import net.sf.openrocket.rocketcomponent.ClusterConfiguration; import net.sf.openrocket.rocketcomponent.Clusterable; import net.sf.openrocket.rocketcomponent.InnerTube; import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.rocketcomponent.RingComponent; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.StateChangeListener; public class InnerTubeConfig extends RocketComponentConfig { private static final Translator trans = Application.getTranslator(); public InnerTubeConfig(OpenRocketDocument d, RocketComponent c) { super(d, c); //// General and General properties JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); DoubleModel m; JSpinner spin; DoubleModel od = null; //// Outer diameter panel.add(new JLabel(trans.get("ThicknessRingCompCfg.tab.Outerdiam"))); //// OuterRadius od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); // Diameter = 2*Radius spin = new JSpinner(od.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); panel.add(spin, "growx"); panel.add(new UnitSelector(od), "growx"); panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); if (od.isAutomaticAvailable()) { JCheckBox check = new JCheckBox(od.getAutomaticAction()); //// Automatic check.setText(trans.get("ringcompcfg.Automatic")); panel.add(check, "skip, span 2, wrap"); } //// Inner diameter panel.add(new JLabel(trans.get("ThicknessRingCompCfg.tab.Innerdiam"))); //// InnerRadius m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); panel.add(spin, "growx"); panel.add(new UnitSelector(m), "growx"); if (od == null) panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); else panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), "w 100lp, wrap"); if (m.isAutomaticAvailable()) { JCheckBox check = new JCheckBox(m.getAutomaticAction()); //// Automatic check.setText(trans.get("ringcompcfg.Automatic")); panel.add(check, "skip, span 2, wrap"); } //// Wall thickness panel.add(new JLabel(trans.get("ThicknessRingCompCfg.tab.Wallthickness"))); //// Thickness m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); panel.add(spin, "growx"); panel.add(new UnitSelector(m), "growx"); panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap"); //// Inner tube length panel.add(new JLabel(trans.get("ThicknessRingCompCfg.tab.Length"))); //// Length m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); panel.add(spin, "growx"); panel.add(new UnitSelector(m), "growx"); panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); //// Position //// Position relative to: panel.add(new JLabel(trans.get("ringcompcfg.Positionrelativeto"))); JComboBox combo = new JComboBox( new EnumModel<RocketComponent.Position>(component, "RelativePosition", new RocketComponent.Position[] { RocketComponent.Position.TOP, RocketComponent.Position.MIDDLE, RocketComponent.Position.BOTTOM, RocketComponent.Position.ABSOLUTE })); panel.add(combo, "spanx 3, growx, wrap"); //// plus panel.add(new JLabel(trans.get("ringcompcfg.plus")), "right"); //// PositionValue m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); panel.add(spin, "growx"); panel.add(new UnitSelector(m), "growx"); panel.add(new BasicSlider(m.getSliderModel( new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), new DoubleModel(component.getParent(), "Length"))), "w 100lp, wrap"); //// Material panel.add(materialPanel(Material.Type.BULK), "cell 4 0, gapleft paragraph, aligny 0%, spany"); tabbedPane.insertTab(trans.get("ThicknessRingCompCfg.tab.General"), null, panel, trans.get("ThicknessRingCompCfg.tab.Generalprop"), 0); MotorConfig motorConfig = new MotorConfig((MotorMount)c); tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Motor"), null, motorConfig, trans.get("InnerTubeCfg.tab.ttip.Motor"), 1); JPanel tab = clusterTab(); //// Cluster and Cluster configuration tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Cluster"), null, tab, trans.get("InnerTubeCfg.tab.ttip.Cluster"), 2); tab = positionTab(); //// Radial position tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Radialpos"), null, tab, trans.get("InnerTubeCfg.tab.ttip.Radialpos"), 3); tabbedPane.setSelectedIndex(0); } protected JPanel positionTab() { JPanel panel = new JPanel(new MigLayout("align 20% 20%, gap rel unrel", "[][65lp::][30lp::]", "")); //// Radial position JLabel l = new JLabel(trans.get("ringcompcfg.Radialdistance")); //// Distance from the rocket centerline l.setToolTipText(trans.get("ringcompcfg.Distancefrom")); panel.add(l); DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); JSpinner spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); //// Distance from the rocket centerline spin.setToolTipText(trans.get("ringcompcfg.Distancefrom")); panel.add(spin, "growx"); panel.add(new UnitSelector(m), "growx"); BasicSlider bs = new BasicSlider(m.getSliderModel(0, 0.1, 1.0)); //// Distance from the rocket centerline bs.setToolTipText(trans.get("ringcompcfg.Distancefrom")); panel.add(bs, "w 100lp, wrap"); //// Radial direction l = new JLabel(trans.get("ringcompcfg.Radialdirection")); //// The radial direction from the rocket centerline l.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); panel.add(l); m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE); spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); //// The radial direction from the rocket centerline spin.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); panel.add(spin, "growx"); panel.add(new UnitSelector(m), "growx"); bs = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)); //// The radial direction from the rocket centerline bs.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); panel.add(bs, "w 100lp, wrap"); //// Reset button JButton button = new JButton(trans.get("ringcompcfg.but.Reset")); //// Reset the component to the rocket centerline button.setToolTipText(trans.get("ringcompcfg.but.Resetcomponant")); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { ((RingComponent) component).setRadialDirection(0.0); ((RingComponent) component).setRadialPosition(0.0); } }); panel.add(button, "spanx, right, wrap para"); DescriptionArea note = new DescriptionArea(3); //// Note: An inner tube will not affect the aerodynamics of the rocket even if it is located outside of the body tube. note.setText(trans.get("ringcompcfg.note.desc")); panel.add(note, "spanx, growx"); return panel; } private JPanel clusterTab() { JPanel panel = new JPanel(new MigLayout()); JPanel subPanel = new JPanel(new MigLayout()); // Cluster type selection //// Select cluster configuration: subPanel.add(new JLabel(trans.get("InnerTubeCfg.lbl.Selectclustercfg")), "spanx, wrap"); subPanel.add(new ClusterSelectionPanel((InnerTube) component), "spanx, wrap"); // JPanel clusterSelection = new ClusterSelectionPanel((InnerTube)component); // clusterSelection.setBackground(Color.blue); // subPanel.add(clusterSelection); panel.add(subPanel); subPanel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]")); // Tube separation scale //// Tube separation: JLabel l = new JLabel(trans.get("InnerTubeCfg.lbl.TubeSep")); //// The separation of the tubes, 1.0 = touching each other l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); subPanel.add(l); DoubleModel dm = new DoubleModel(component, "ClusterScale", 1, UnitGroup.UNITS_NONE, 0); JSpinner spin = new JSpinner(dm.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); //// The separation of the tubes, 1.0 = touching each other spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); subPanel.add(spin, "growx"); BasicSlider bs = new BasicSlider(dm.getSliderModel(0, 1, 4)); //// The separation of the tubes, 1.0 = touching each other bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); subPanel.add(bs, "skip,w 100lp, wrap"); // Rotation: l = new JLabel(trans.get("InnerTubeCfg.lbl.Rotation")); //// Rotation angle of the cluster configuration l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); subPanel.add(l); dm = new DoubleModel(component, "ClusterRotation", 1, UnitGroup.UNITS_ANGLE, -Math.PI, Math.PI); spin = new JSpinner(dm.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); //// Rotation angle of the cluster configuration spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); subPanel.add(spin, "growx"); subPanel.add(new UnitSelector(dm), "growx"); bs = new BasicSlider(dm.getSliderModel(-Math.PI, 0, Math.PI)); //// Rotation angle of the cluster configuration bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); subPanel.add(bs, "w 100lp, wrap para"); // Split button //// Split cluster JButton split = new JButton(trans.get("InnerTubeCfg.but.Splitcluster")); //// <html>Split the cluster into separate components.<br> //// This also duplicates all components attached to this inner tube. split.setToolTipText(trans.get("InnerTubeCfg.lbl.longA1") + trans.get("InnerTubeCfg.lbl.longA2")); split.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // Do change in future for overall safety SwingUtilities.invokeLater(new Runnable() { @Override public void run() { RocketComponent parent = component.getParent(); int index = parent.getChildPosition(component); if (index < 0) { throw new BugException("Inconsistent state: component=" + component + " parent=" + parent + " parent.children=" + parent.getChildren()); } InnerTube tube = (InnerTube) component; if (tube.getClusterCount() <= 1) return; document.addUndoPosition("Split cluster"); Coordinate[] coords = { Coordinate.NUL }; coords = component.shiftCoordinates(coords); parent.removeChild(index); for (int i = 0; i < coords.length; i++) { InnerTube copy = InnerTube.makeIndividualClusterComponent(coords[i], component.getName() + " #" + (i + 1), component); parent.addChild(copy, index + i); } } }); } }); subPanel.add(split, "spanx, split 2, gapright para, sizegroup buttons, right"); // Reset button ///// Reset settings JButton reset = new JButton(trans.get("InnerTubeCfg.but.Resetsettings")); //// Reset the separation and rotation to the default values reset.setToolTipText(trans.get("InnerTubeCfg.but.ttip.Resetsettings")); reset.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { ((InnerTube) component).setClusterScale(1.0); ((InnerTube) component).setClusterRotation(0.0); } }); subPanel.add(reset, "sizegroup buttons, right"); panel.add(subPanel, "grow"); return panel; } } class ClusterSelectionPanel extends JPanel { private static final int BUTTON_SIZE = 50; private static final int MOTOR_DIAMETER = 10; private static final Color SELECTED_COLOR = Color.RED; private static final Color UNSELECTED_COLOR = Color.WHITE; private static final Color MOTOR_FILL_COLOR = Color.GREEN; private static final Color MOTOR_BORDER_COLOR = Color.BLACK; public ClusterSelectionPanel(Clusterable component) { super(new MigLayout("gap 0 0", "[" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!]", "[" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!]")); for (int i = 0; i < ClusterConfiguration.CONFIGURATIONS.length; i++) { ClusterConfiguration config = ClusterConfiguration.CONFIGURATIONS[i]; JComponent button = new ClusterButton(component, config); if (i % 4 == 3) add(button, "wrap"); else add(button); } } private class ClusterButton extends JPanel implements StateChangeListener, MouseListener, Resettable { private Clusterable component; private ClusterConfiguration config; public ClusterButton(Clusterable c, ClusterConfiguration config) { component = c; this.config = config; setMinimumSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); setMaximumSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); // setBorder(BorderFactory.createLineBorder(Color.BLACK, 1)); component.addChangeListener(this); addMouseListener(this); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; Rectangle area = g2.getClipBounds(); if (component.getClusterConfiguration() == config) g2.setColor(SELECTED_COLOR); else g2.setColor(UNSELECTED_COLOR); g2.fillRect(area.x, area.y, area.width, area.height); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); List<Double> points = config.getPoints(); Ellipse2D.Float circle = new Ellipse2D.Float(); for (int i = 0; i < points.size() / 2; i++) { double x = points.get(i * 2); double y = points.get(i * 2 + 1); double px = BUTTON_SIZE / 2 + x * MOTOR_DIAMETER; double py = BUTTON_SIZE / 2 - y * MOTOR_DIAMETER; circle.setFrameFromCenter(px, py, px + MOTOR_DIAMETER / 2, py + MOTOR_DIAMETER / 2); g2.setColor(MOTOR_FILL_COLOR); g2.fill(circle); g2.setColor(MOTOR_BORDER_COLOR); g2.draw(circle); } } @Override public void stateChanged(EventObject e) { repaint(); } @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { component.setClusterConfiguration(config); } } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void resetModel() { component.removeChangeListener(this); removeMouseListener(this); } } }