diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 7d169196e..82dbfbc26 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -1087,10 +1087,13 @@ StorageOptChooser.lbl.Saveopt = Save options ! ThrustCurveMotorSelectionPanel TCMotorSelPan.lbl.Selrocketmotor = Select rocket motor: TCMotorSelPan.checkbox.hideSimilar = Hide very similar thrust curves -TCMotorSelPan.SHOW_DESCRIPTIONS.desc1 = Show all motors -TCMotorSelPan.SHOW_DESCRIPTIONS.desc2 = Show motors with diameter less than that of the motor mount -TCMotorSelPan.SHOW_DESCRIPTIONS.desc3 = Show motors with diameter equal to that of the motor mount -TCMotorSelPan.lbl.Motormountdia = Motor mount diameter: +TCMotorSelPan.checkbox.hideUsed = Hide motors already used in the mount +TCMotorSelPan.btn.details = Show Details +TCMotorSelPan.btn.filter = Filter Motors +TCMotorSelPan.MotorSize = Motor Dimensions +TCMotorSelPan.Diameter = Daimeter +TCMotorSelPan.Length = Length +TCMotorSelPan.MotorMountDimensions = Motor mount dimensions: TCMotorSelPan.lbl.Search = Search: TCMotorSelPan.lbl.Selectthrustcurve = Select thrust curve: TCMotorSelPan.lbl.Ejectionchargedelay = Ejection charge delay: @@ -1108,6 +1111,10 @@ TCMotorSelPan.title.Thrustcurve = Thrust curve: TCMotorSelPan.title.Thrust = Thrust TCMotorSelPan.delayBox.None = None TCMotorSelPan.noDescription = No description available. +TCMotorSelPan.btn.checkAll = Select All +TCMotorSelPan.btn.checkNone = Clear All +TCMotorSelPan.btn.close = Close + ! PlotDialog diff --git a/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java b/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java index d99c81b9e..054f50cf3 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java +++ b/core/src/net/sf/openrocket/rocketcomponent/FlightConfiguration.java @@ -11,7 +11,7 @@ import net.sf.openrocket.util.ChangeSource; * * @param the parameter type */ -public interface FlightConfiguration extends FlightConfigurableComponent { +public interface FlightConfiguration extends FlightConfigurableComponent, Iterable { /** * Return the default parameter value for this FlightConfiguration. diff --git a/core/src/net/sf/openrocket/rocketcomponent/FlightConfigurationImpl.java b/core/src/net/sf/openrocket/rocketcomponent/FlightConfigurationImpl.java index f69fa6b22..91fa99d60 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/FlightConfigurationImpl.java +++ b/core/src/net/sf/openrocket/rocketcomponent/FlightConfigurationImpl.java @@ -2,6 +2,7 @@ package net.sf.openrocket.rocketcomponent; import java.util.EventObject; import java.util.HashMap; +import java.util.Iterator; import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.Utils; @@ -79,6 +80,10 @@ class FlightConfigurationImpl> implemen fireEvent(); } + @Override + public Iterator iterator() { + return map.values().iterator(); + } @Override diff --git a/core/src/net/sf/openrocket/unit/Unit.java b/core/src/net/sf/openrocket/unit/Unit.java index 7ac073a8d..a0522c2ba 100644 --- a/core/src/net/sf/openrocket/unit/Unit.java +++ b/core/src/net/sf/openrocket/unit/Unit.java @@ -103,6 +103,10 @@ public abstract class Unit { } val = roundForDecimalFormat(val); + // Check for approximate integer + if (Math.abs(val - Math.floor(val)) < 0.001) { + return intFormat.format(val); + } return decFormat.format(val); } diff --git a/swing/src/net/sf/openrocket/gui/configdialog/MotorConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/MotorConfig.java index 03882cce5..564ba3954 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/MotorConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/MotorConfig.java @@ -184,8 +184,7 @@ public class MotorConfig extends JPanel { public void actionPerformed(ActionEvent e) { String id = configuration.getFlightConfigurationID(); - MotorChooserDialog dialog = new MotorChooserDialog(mount.getMotor(id), - mount.getMotorDelay(id), mount.getMotorMountDiameter(), + MotorChooserDialog dialog = new MotorChooserDialog(mount, id, SwingUtilities.getWindowAncestor(MotorConfig.this)); dialog.setVisible(true); Motor m = dialog.getSelectedMotor(); diff --git a/swing/src/net/sf/openrocket/gui/dialogs/flightconfiguration/MotorConfigurationPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/flightconfiguration/MotorConfigurationPanel.java index 811169dd0..b47d910f7 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/flightconfiguration/MotorConfigurationPanel.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/flightconfiguration/MotorConfigurationPanel.java @@ -43,9 +43,11 @@ public class MotorConfigurationPanel extends JPanel { private final MotorConfigurationTableModel configurationTableModel; private final JButton selectMotorButton, removeMotorButton, selectIgnitionButton, resetIgnitionButton; + private final MotorChooserDialog dialog; MotorConfigurationPanel(FlightConfigurationDialog flightConfigurationDialog, Rocket rocket) { super(new MigLayout("fill")); + dialog = new MotorChooserDialog(flightConfigurationDialog); this.flightConfigurationDialog = flightConfigurationDialog; this.rocket = rocket; @@ -207,11 +209,7 @@ public class MotorConfigurationPanel extends JPanel { MotorConfiguration config = mount.getMotorConfiguration().get(id); - MotorChooserDialog dialog = new MotorChooserDialog( - config.getMotor(), - config.getEjectionDelay(), - mount.getMotorMountDiameter(), - flightConfigurationDialog); + dialog.setMotorMountAndConfig(mount, id); dialog.setVisible(true); Motor m = dialog.getSelectedMotor(); double d = dialog.getSelectedDelay(); diff --git a/swing/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java index 761d4dab6..5ff870e1b 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java @@ -16,7 +16,7 @@ import net.sf.openrocket.gui.dialogs.motor.thrustcurve.ThrustCurveMotorSelection import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.startup.Application; public class MotorChooserDialog extends JDialog implements CloseableDialog { @@ -26,16 +26,20 @@ public class MotorChooserDialog extends JDialog implements CloseableDialog { private boolean okClicked = false; private static final Translator trans = Application.getTranslator(); + public MotorChooserDialog(MotorMount mount, String currentConfig, Window owner) { + this(owner); + setMotorMountAndConfig(mount, currentConfig); + } - public MotorChooserDialog(Motor current, double delay, double diameter, Window owner) { + public MotorChooserDialog(Window owner) { super(owner, trans.get("MotorChooserDialog.title"), Dialog.ModalityType.APPLICATION_MODAL); JPanel panel = new JPanel(new MigLayout("fill")); - selectionPanel = new ThrustCurveMotorSelectionPanel((ThrustCurveMotor) current, delay, diameter); + selectionPanel = new ThrustCurveMotorSelectionPanel(); - panel.add(selectionPanel, "grow, wrap para"); + panel.add(selectionPanel, "grow, wrap"); // OK / Cancel buttons @@ -74,6 +78,9 @@ public class MotorChooserDialog extends JDialog implements CloseableDialog { selectionPanel.setCloseableDialog(this); } + public void setMotorMountAndConfig( MotorMount mount, String currentConfig ) { + selectionPanel.setMotorMountAndConfig(mount, currentConfig); + } /** * Return the motor selected by this chooser dialog, or null if the selection has been aborted. diff --git a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ImpulseClass.java b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ImpulseClass.java new file mode 100644 index 000000000..8f639c893 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ImpulseClass.java @@ -0,0 +1,44 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + + +public enum ImpulseClass { + + A("A",0.0, 2.5 ), + B("B",2.5, 5.0 ), + C("C",5.0, 10.0), + D("D",10.0, 20.0), + E("E",20.0, 40.0), + F("F", 40.0, 80.0), + G("G", 80.0, 160.0), + H("H", 160.0, 320.0), + I("I", 320.0, 640.0), + J("J", 640.0, 1280.0), + K("K", 1280.0, 2560.0), + L("L", 2560.0, 5120.0), + M("M", 5120.0, 10240.0), + N("N", 10240.0, 20480.0), + O("O", 20480.0, Double.MAX_VALUE); + + private ImpulseClass( String name, double low, double high ) { + this.name = name; + this.low = low; + this.high = high; + } + + public String toString() { + return name; + } + + public double getLow() { + return low; + } + + public double getHigh() { + return high; + } + + private double low; + private double high; + private String name; + +} diff --git a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorFilterPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorFilterPanel.java new file mode 100644 index 000000000..e23216b68 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorFilterPanel.java @@ -0,0 +1,317 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Hashtable; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.util.CheckList; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.gui.widgets.MultiSlider; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +import com.itextpdf.text.Font; + +public abstract class MotorFilterPanel extends JPanel { + + private static final Translator trans = Application.getTranslator(); + + private static Hashtable diameterLabels = new Hashtable(); + private static double[] diameterValues = new double[] { + 0, + .013, + .018, + .024, + .029, + .038, + .054, + .075, + .098, + 1.000 + }; + static { + for( int i = 0; i< diameterValues.length; i++ ) { + if( i == diameterValues.length-1) { + diameterLabels.put( i, new JLabel("+")); + } else { + diameterLabels.put( i, new JLabel(UnitGroup.UNITS_MOTOR_DIMENSIONS.toString(diameterValues[i]))); + } + } + } + + private static Hashtable impulseLabels = new Hashtable(); + static { + int i =0; + for( ImpulseClass impulseClass : ImpulseClass.values() ) { + impulseLabels.put(i, new JLabel( impulseClass.name() )); + i++; + } + } + + private final CheckList manufacturerCheckList; + + private final MotorRowFilter filter; + + // Things we change the label on based on the MotorMount. + private final JLabel motorMountDimension; + private final MultiSlider lengthSlider; + private final MultiSlider diameterSlider; + + public MotorFilterPanel(Collection allManufacturers, MotorRowFilter filter ) { + super(new MigLayout("fill", "[grow]")); + this.filter = filter; + + List unselectedManusFromPreferences = ((SwingPreferences) Application.getPreferences()).getExcludedMotorManufacturers(); + filter.setExcludedManufacturers(unselectedManusFromPreferences); + + //// Hide used motor files + { + final JCheckBox hideUsedBox = new JCheckBox(trans.get("TCMotorSelPan.checkbox.hideUsed")); + GUIUtil.changeFontSize(hideUsedBox, -1); + hideUsedBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + MotorFilterPanel.this.filter.setHideUsedMotors(hideUsedBox.isSelected()); + onSelectionChanged(); + } + }); + this.add(hideUsedBox, "gapleft para, spanx, growx, wrap"); + } + + + // Manufacturer selection + JPanel sub = new JPanel(new MigLayout("fill")); + TitledBorder border = BorderFactory.createTitledBorder(trans.get("TCurveMotorCol.MANUFACTURER")); + GUIUtil.changeFontStyle(border, Font.BOLD); + sub.setBorder(border); + + this.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1)); + + List manufacturers = new ArrayList(); + for (Manufacturer m : allManufacturers) { + manufacturers.add(m); + } + + Collections.sort(manufacturers, new Comparator() { + @Override + public int compare(Manufacturer o1, Manufacturer o2) { + return o1.getSimpleName().compareTo( o2.getSimpleName()); + } + + }); + + manufacturerCheckList = new CheckList.Builder().build(); + manufacturerCheckList.setData(manufacturers); + + manufacturerCheckList.setUncheckedItems(unselectedManusFromPreferences); + manufacturerCheckList.getModel().addListDataListener( new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent e) { + } + @Override + public void intervalRemoved(ListDataEvent e) { + } + + @Override + public void contentsChanged(ListDataEvent e) { + MotorFilterPanel.this.filter.setExcludedManufacturers( manufacturerCheckList.getUncheckedItems() ); + onSelectionChanged(); + } + }); + + sub.add(new JScrollPane(manufacturerCheckList.getList()), "grow,wrap"); + + JButton clearMotors = new JButton(trans.get("TCMotorSelPan.btn.checkNone")); + clearMotors.addActionListener( new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + MotorFilterPanel.this.manufacturerCheckList.clearAll(); + + } + }); + + sub.add(clearMotors,"split 2"); + + JButton selectMotors = new JButton(trans.get("TCMotorSelPan.btn.checkAll")); + selectMotors.addActionListener( new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + MotorFilterPanel.this.manufacturerCheckList.checkAll(); + + } + }); + + sub.add(selectMotors,"wrap"); + + this.add(sub,"grow, wrap"); + + // Impulse selection + { + sub = new JPanel(new MigLayout("fill")); + border = BorderFactory.createTitledBorder(trans.get("TCurveMotorCol.TOTAL_IMPULSE")); + GUIUtil.changeFontStyle(border, Font.BOLD); + sub.setBorder(border); + + final MultiSlider impulseSlider = new MultiSlider(MultiSlider.HORIZONTAL,0, ImpulseClass.values().length-1,0, ImpulseClass.values().length-1); + impulseSlider.setBounded(true); // thumbs cannot cross + impulseSlider.setMajorTickSpacing(1); + impulseSlider.setPaintTicks(true); + impulseSlider.setLabelTable(impulseLabels); + impulseSlider.setPaintLabels(true); + impulseSlider.addChangeListener( new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + int minimpulse = impulseSlider.getValueAt(0); + MotorFilterPanel.this.filter.setMinimumImpulse(ImpulseClass.values()[minimpulse]); + int maximpulse = impulseSlider.getValueAt(1); + MotorFilterPanel.this.filter.setMaximumImpulse(ImpulseClass.values()[maximpulse]); + onSelectionChanged(); + } + }); + sub.add( impulseSlider, "growx, wrap"); + } + this.add(sub,"grow, wrap"); + + + // Diameter selection + sub = new JPanel(new MigLayout("fill")); + TitledBorder diameterTitleBorder = BorderFactory.createTitledBorder(trans.get("TCMotorSelPan.MotorSize")); + GUIUtil.changeFontStyle(diameterTitleBorder, Font.BOLD); + sub.setBorder(diameterTitleBorder); + + motorMountDimension = new JLabel(); + GUIUtil.changeFontSize(motorMountDimension, -1); + sub.add(motorMountDimension,"growx,wrap"); + { + sub.add( new JLabel("Diameter"), "split 2, wrap"); + diameterSlider = new MultiSlider(MultiSlider.HORIZONTAL,0, diameterValues.length-1, 0, diameterValues.length-1); + diameterSlider.setBounded(true); // thumbs cannot cross + diameterSlider.setMajorTickSpacing(1); + diameterSlider.setPaintTicks(true); + diameterSlider.setLabelTable(diameterLabels); + diameterSlider.setPaintLabels(true); + diameterSlider.addChangeListener( new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + int minDiameter = diameterSlider.getValueAt(0); + MotorFilterPanel.this.filter.setMinimumDiameter(diameterValues[minDiameter]); + int maxDiameter = diameterSlider.getValueAt(1); + if( maxDiameter == diameterValues.length-1 ) { + MotorFilterPanel.this.filter.setMaximumDiameter(null); + } else { + MotorFilterPanel.this.filter.setMaximumDiameter(diameterValues[maxDiameter]); + } + onSelectionChanged(); + } + }); + sub.add( diameterSlider, "growx, wrap"); + } + + { + sub.add( new JLabel(trans.get("TCMotorSelPan.Length")), "split 2, wrap"); + + final DoubleModel minimumLength = new DoubleModel(filter, "MinimumLength", UnitGroup.UNITS_MOTOR_DIMENSIONS, 0); + final DoubleModel maximumLength = new DoubleModel(filter, "MaximumLength", UnitGroup.UNITS_MOTOR_DIMENSIONS, 0); + + JSpinner spin = new JSpinner(minimumLength.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "split 5, growx"); + + sub.add(new UnitSelector(minimumLength), ""); + + lengthSlider = new MultiSlider(MultiSlider.HORIZONTAL,0, 1000, 0, 1000); + lengthSlider.setBounded(true); // thumbs cannot cross + lengthSlider.setMajorTickSpacing(100); + lengthSlider.setPaintTicks(true); + lengthSlider.setLabelTable(diameterLabels); + lengthSlider.addChangeListener( new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + int minLength = lengthSlider.getValueAt(0); + minimumLength.setValue(minLength/1000.0); + int maxLength = lengthSlider.getValueAt(1); + maximumLength.setValue(maxLength/1000.0); + onSelectionChanged(); + } + }); + + minimumLength.addChangeListener( new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + lengthSlider.setValueAt(0, (int)(1000* minimumLength.getValue())); + lengthSlider.setValueAt(1, (int) (1000* maximumLength.getValue())); + } + + }); + + sub.add( lengthSlider, "growx"); + + spin = new JSpinner(maximumLength.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + + sub.add(new UnitSelector(maximumLength), "wrap"); + } + this.add(sub, "grow,wrap"); + + } + + public void setMotorMount( MotorMount mount ) { + filter.setMotorMount(mount); + onSelectionChanged(); + if ( mount == null ) { + // Disable diameter controls? + lengthSlider.setValueAt(1, 1000); + motorMountDimension.setText(""); + } else { + double mountLength = ((RocketComponent)mount).getLength(); + lengthSlider.setValueAt(1, (int) Math.min(1000,Math.round(1000*mountLength))); + + double mountDiameter = mount.getMotorMountDiameter(); + // find the next largest diameter + int i; + for( i =0; i< diameterValues.length; i++ ) { + if ( mountDiameter< diameterValues[i] ) { + break; + } + } + if (i >= diameterValues.length ) { + i--; + } + diameterSlider.setValueAt(1, i-1); + + motorMountDimension.setText( trans.get("TCMotorSelPan.MotorMountDimensions") + " " + + UnitGroup.UNITS_MOTOR_DIMENSIONS.toStringUnit(mountDiameter)+ " x " + UnitGroup.UNITS_MOTOR_DIMENSIONS.toStringUnit(mountLength)); + } + } + + public abstract void onSelectionChanged(); + +} diff --git a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorInformationPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorInformationPanel.java new file mode 100644 index 000000000..8e5fb2ebb --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorInformationPanel.java @@ -0,0 +1,315 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JLayeredPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +class MotorInformationPanel extends JPanel { + + private static final int ZOOM_ICON_POSITION_NEGATIVE_X = 50; + private static final int ZOOM_ICON_POSITION_POSITIVE_Y = 12; + + private static final Color NO_COMMENT_COLOR = Color.GRAY; + private static final Color WITH_COMMENT_COLOR = Color.BLACK; + + private static final Translator trans = Application.getTranslator(); + + // Motors in set + private List selectedMotorSet; + // Selected motor + private ThrustCurveMotor selectedMotor; + + private final JLabel totalImpulseLabel; + private final JLabel classificationLabel; + private final JLabel avgThrustLabel; + private final JLabel maxThrustLabel; + private final JLabel burnTimeLabel; + private final JLabel launchMassLabel; + private final JLabel emptyMassLabel; + private final JLabel dataPointsLabel; + private final JLabel digestLabel; + + private final JTextArea comment; + private final Font noCommentFont; + private final Font withCommentFont; + + private final JFreeChart chart; + private final ChartPanel chartPanel; + private final JLabel zoomIcon; + + public MotorInformationPanel() { + super(new MigLayout("fill")); + + // Thrust curve info + //// Total impulse: + { + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Totalimpulse"))); + totalImpulseLabel = new JLabel(); + this.add(totalImpulseLabel, "split"); + + classificationLabel = new JLabel(); + classificationLabel.setEnabled(false); // Gray out + this.add(classificationLabel, "gapleft unrel, wrap"); + + //// Avg. thrust: + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Avgthrust"))); + avgThrustLabel = new JLabel(); + this.add(avgThrustLabel, "wrap"); + + //// Max. thrust: + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Maxthrust"))); + maxThrustLabel = new JLabel(); + this.add(maxThrustLabel, "wrap"); + + //// Burn time: + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Burntime"))); + burnTimeLabel = new JLabel(); + this.add(burnTimeLabel, "wrap"); + + //// Launch mass: + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Launchmass"))); + launchMassLabel = new JLabel(); + this.add(launchMassLabel, "wrap"); + + //// Empty mass: + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Emptymass"))); + emptyMassLabel = new JLabel(); + this.add(emptyMassLabel, "wrap"); + + //// Data points: + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Datapoints"))); + dataPointsLabel = new JLabel(); + this.add(dataPointsLabel, "wrap para"); + + if (System.getProperty("openrocket.debug.motordigest") != null) { + //// Digest: + this.add(new JLabel(trans.get("TCMotorSelPan.lbl.Digest"))); + digestLabel = new JLabel(); + this.add(digestLabel, "w :300:, wrap para"); + } else { + digestLabel = null; + } + + + comment = new JTextArea(5, 5); + GUIUtil.changeFontSize(comment, -2); + withCommentFont = comment.getFont(); + noCommentFont = withCommentFont.deriveFont(Font.ITALIC); + comment.setLineWrap(true); + comment.setWrapStyleWord(true); + comment.setEditable(false); + JScrollPane scrollpane = new JScrollPane(comment); + this.add(scrollpane, "spanx, growx, wrap para"); + } + + // Thrust curve plot + { + chart = ChartFactory.createXYLineChart( + null, // title + null, // xAxisLabel + null, // yAxisLabel + null, // dataset + PlotOrientation.VERTICAL, + false, // legend + false, // tooltips + false // urls + ); + + // Add the data and formatting to the plot + XYPlot plot = chart.getXYPlot(); + + changeLabelFont(plot.getRangeAxis(), -2); + changeLabelFont(plot.getDomainAxis(), -2); + + //// Thrust curve: + chart.setTitle(new TextTitle(trans.get("TCMotorSelPan.title.Thrustcurve"), this.getFont())); + chart.setBackgroundPaint(this.getBackground()); + plot.setBackgroundPaint(Color.WHITE); + plot.setDomainGridlinePaint(Color.LIGHT_GRAY); + plot.setRangeGridlinePaint(Color.LIGHT_GRAY); + + chartPanel = new ChartPanel(chart, + false, // properties + false, // save + false, // print + false, // zoom + false); // tooltips + chartPanel.setMouseZoomable(false); + chartPanel.setPopupMenu(null); + chartPanel.setMouseWheelEnabled(false); + chartPanel.setRangeZoomable(false); + chartPanel.setDomainZoomable(false); + + chartPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + chartPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (selectedMotor == null || selectedMotorSet == null) + return; + if (e.getButton() == MouseEvent.BUTTON1) { + // Open plot dialog + ThrustCurveMotorPlotDialog plotDialog = new ThrustCurveMotorPlotDialog(selectedMotorSet, + selectedMotorSet.indexOf(selectedMotor), + SwingUtilities.getWindowAncestor(MotorInformationPanel.this)); + plotDialog.setVisible(true); + } + } + }); + + JLayeredPane layer = new CustomLayeredPane(); + + layer.setBorder(BorderFactory.createLineBorder(Color.BLUE)); + + layer.add(chartPanel, (Integer) 0); + + zoomIcon = new JLabel(Icons.ZOOM_IN); + zoomIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + layer.add(zoomIcon, (Integer) 1); + + this.add(layer, "width 300:300:, height 180:180:, grow, spanx"); + } + } + + public void clearData() { + totalImpulseLabel.setText(""); + totalImpulseLabel.setToolTipText(null); + classificationLabel.setText(""); + classificationLabel.setToolTipText(null); + avgThrustLabel.setText(""); + maxThrustLabel.setText(""); + burnTimeLabel.setText(""); + launchMassLabel.setText(""); + emptyMassLabel.setText(""); + dataPointsLabel.setText(""); + if (digestLabel != null) { + digestLabel.setText(""); + } + setComment(""); + chart.getXYPlot().setDataset(new XYSeriesCollection()); + } + + public void updateData( List motors, ThrustCurveMotor selectedMotor ) { + + this.selectedMotorSet = motors; + this.selectedMotor = selectedMotor; + + // Update thrust curve data + double impulse = selectedMotor.getTotalImpulseEstimate(); + MotorClass mc = MotorClass.getMotorClass(impulse); + totalImpulseLabel.setText(UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(impulse)); + classificationLabel.setText("(" + mc.getDescription(impulse) + ")"); + totalImpulseLabel.setToolTipText(mc.getClassDescription()); + classificationLabel.setToolTipText(mc.getClassDescription()); + + avgThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( + selectedMotor.getAverageThrustEstimate())); + maxThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( + selectedMotor.getMaxThrustEstimate())); + burnTimeLabel.setText(UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit( + selectedMotor.getBurnTimeEstimate())); + launchMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( + selectedMotor.getLaunchCG().weight)); + emptyMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( + selectedMotor.getEmptyCG().weight)); + dataPointsLabel.setText("" + (selectedMotor.getTimePoints().length - 1)); + if (digestLabel != null) { + digestLabel.setText(selectedMotor.getDigest()); + } + + setComment(selectedMotor.getDescription()); + + // Update the plot + XYPlot plot = chart.getXYPlot(); + final int index = motors.indexOf(selectedMotor); + + XYSeriesCollection dataset = new XYSeriesCollection(); + for (int i = 0; i < motors.size(); i++) { + ThrustCurveMotor m = motors.get(i); + + //// Thrust + XYSeries series = new XYSeries(trans.get("TCMotorSelPan.title.Thrust") + " (" + i + ")"); + double[] time = m.getTimePoints(); + double[] thrust = m.getThrustPoints(); + + for (int j = 0; j < time.length; j++) { + series.add(time[j], thrust[j]); + } + + dataset.addSeries(series); + + boolean selected = (i == index); + plot.getRenderer().setSeriesStroke(i, new BasicStroke(selected ? 3 : 1)); + plot.getRenderer().setSeriesPaint(i, ThrustCurveMotorSelectionPanel.getColor(i)); + } + + plot.setDataset(dataset); + + } + + private void setComment(String s) { + s = s.trim(); + if (s.length() == 0) { + //// No description available. + comment.setText(trans.get("TCMotorSelPan.noDescription")); + comment.setFont(noCommentFont); + comment.setForeground(NO_COMMENT_COLOR); + } else { + comment.setText(s); + comment.setFont(withCommentFont); + comment.setForeground(WITH_COMMENT_COLOR); + } + comment.setCaretPosition(0); + } + + void changeLabelFont(ValueAxis axis, float size) { + Font font = axis.getTickLabelFont(); + font = font.deriveFont(font.getSize2D() + size); + axis.setTickLabelFont(font); + } + + /** + * Custom layered pane that sets the bounds of the components on every layout. + */ + public class CustomLayeredPane extends JLayeredPane { + @Override + public void doLayout() { + synchronized (getTreeLock()) { + int w = getWidth(); + int h = getHeight(); + chartPanel.setBounds(0, 0, w, h); + zoomIcon.setBounds(w - ZOOM_ICON_POSITION_NEGATIVE_X, ZOOM_ICON_POSITION_POSITIVE_Y, 50, 50); + } + } + } + +} \ No newline at end of file diff --git a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorRowFilter.java b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorRowFilter.java new file mode 100644 index 000000000..b4ac85e9f --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorRowFilter.java @@ -0,0 +1,242 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import javax.swing.RowFilter; +import javax.swing.table.TableModel; + +import net.sf.openrocket.database.motor.ThrustCurveMotorSet; +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.rocketcomponent.MotorConfiguration; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.util.AbstractChangeSource; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.StateChangeListener; + +//////// Row filters + +/** + * Abstract adapter class. + */ +public class MotorRowFilter extends RowFilter implements ChangeSource { + + // configuration data used in the filter process + private final ThrustCurveMotorDatabaseModel model; + private List usedMotors = new ArrayList(); + + private final AbstractChangeSource changeSourceDelegate = new AbstractChangeSource(); + private final Object change = new Object(); + + // things which can be changed to modify filter behavior + + // Limit motors based on length + private double minimumLength = 0; + private double maximumLength = Double.MAX_VALUE; + + // Limit motors based on diameter + private Double minimumDiameter; + private Double maximumDiameter; + + // Collection of strings which match text in the motor + private List searchTerms = Collections. emptyList(); + + // Boolean which hides motors in the usedMotors list + private boolean hideUsedMotors = false; + + // List of manufacturers to exclude. + private List excludedManufacturers = new ArrayList(); + + // Impulse class filtering + private ImpulseClass minimumImpulse; + private ImpulseClass maximumImpulse; + + + public MotorRowFilter(ThrustCurveMotorDatabaseModel model) { + super(); + this.model = model; + } + + public void setMotorMount( MotorMount mount ) { + if (mount != null) { + for (MotorConfiguration m : mount.getMotorConfiguration()) { + this.usedMotors.add((ThrustCurveMotor) m.getMotor()); + } + } + } + + public void setSearchTerms(final List searchTerms) { + this.searchTerms = new ArrayList(); + for (String s : searchTerms) { + s = s.trim().toLowerCase(Locale.getDefault()); + if (s.length() > 0) { + this.searchTerms.add(s); + } + } + } + + public double getMinimumLength() { + return minimumLength; + } + + public void setMinimumLength(double minimumLength) { + if ( this.minimumLength != minimumLength ) { + this.minimumLength = minimumLength; + fireChangeEvent(change); + } + } + + public double getMaximumLength() { + return maximumLength; + } + + public void setMaximumLength(double maximumLength) { + if ( this.maximumLength != maximumLength ) { + this.maximumLength = maximumLength; + fireChangeEvent(change); + } + } + + Double getMinimumDiameter() { + return minimumDiameter; + } + + void setMinimumDiameter(Double minimumDiameter) { + this.minimumDiameter = minimumDiameter; + } + + Double getMaximumDiameter() { + return maximumDiameter; + } + + void setMaximumDiameter(Double maximumDiameter) { + this.maximumDiameter = maximumDiameter; + } + + void setHideUsedMotors(boolean hideUsedMotors) { + this.hideUsedMotors = hideUsedMotors; + } + + List getExcludedManufacturers() { + return excludedManufacturers; + } + + void setExcludedManufacturers(Collection excludedManufacturers) { + this.excludedManufacturers.clear(); + this.excludedManufacturers.addAll(excludedManufacturers); + } + + ImpulseClass getMinimumImpulse() { + return minimumImpulse; + } + + void setMinimumImpulse(ImpulseClass minimumImpulse) { + this.minimumImpulse = minimumImpulse; + } + + ImpulseClass getMaximumImpulse() { + return maximumImpulse; + } + + void setMaximumImpulse(ImpulseClass maximumImpulse) { + this.maximumImpulse = maximumImpulse; + } + + @Override + public boolean include(RowFilter.Entry entry) { + int index = entry.getIdentifier(); + ThrustCurveMotorSet m = model.getMotorSet(index); + return filterManufacturers(m) && filterUsed(m) && filterBySize(m) && filterByString(m) && filterByImpulseClass(m); + } + + private boolean filterManufacturers(ThrustCurveMotorSet m) { + if (excludedManufacturers.contains(m.getManufacturer())) { + return false; + } else { + return true; + } + } + + private boolean filterUsed(ThrustCurveMotorSet m) { + if (!hideUsedMotors) { + return true; + } + for (ThrustCurveMotor motor : usedMotors) { + if (m.matches(motor)) { + return false; + } + } + return true; + } + + private boolean filterBySize(ThrustCurveMotorSet m) { + + if ( minimumDiameter != null ) { + if ( m.getDiameter() <= minimumDiameter - 0.0015 ) { + return false; + } + } + + if ( maximumDiameter != null ) { + if ( m.getDiameter() >= maximumDiameter + 0.0004 ) { + return false; + } + } + + if ( m.getLength() > maximumLength ) { + return false; + } + + if ( m.getLength() < minimumLength ) { + return false; + } + + return true; + } + + + private boolean filterByString(ThrustCurveMotorSet m) { + main: for (String s : searchTerms) { + for (ThrustCurveMotorColumns col : ThrustCurveMotorColumns.values()) { + String str = col.getValue(m).toString().toLowerCase(Locale.getDefault()); + if (str.indexOf(s) >= 0) + continue main; + } + return false; + } + return true; + } + + private boolean filterByImpulseClass(ThrustCurveMotorSet m) { + if ( minimumImpulse != null ) { + if( m.getTotalImpuse() < minimumImpulse.getLow() ) { + return false; + } + } + + if ( maximumImpulse != null ) { + if( m.getTotalImpuse() > maximumImpulse.getHigh() ) { + return false; + } + } + + return true; + } + + public final void addChangeListener(StateChangeListener listener) { + changeSourceDelegate.addChangeListener(listener); + } + + public final void removeChangeListener(StateChangeListener listener) { + changeSourceDelegate.removeChangeListener(listener); + } + + public void fireChangeEvent(Object source) { + changeSourceDelegate.fireChangeEvent(source); + } + +} diff --git a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java index e9781b210..e12b07dd9 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java @@ -1,9 +1,7 @@ package net.sf.openrocket.gui.dialogs.motor.thrustcurve; -import java.awt.BasicStroke; import java.awt.Color; import java.awt.Component; -import java.awt.Cursor; import java.awt.Font; import java.awt.Paint; import java.awt.Rectangle; @@ -11,33 +9,33 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.List; -import java.util.Locale; +import java.util.Set; import java.util.prefs.Preferences; -import javax.swing.BorderFactory; import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JLabel; -import javax.swing.JLayeredPane; import javax.swing.JList; import javax.swing.JPanel; +import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSeparator; +import javax.swing.JTabbedPane; import javax.swing.JTable; -import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; -import javax.swing.RowFilter; import javax.swing.RowSorter; import javax.swing.SortOrder; -import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; @@ -48,110 +46,68 @@ import javax.swing.table.TableRowSorter; import net.miginfocom.swing.MigLayout; import net.sf.openrocket.database.motor.ThrustCurveMotorSet; import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; import net.sf.openrocket.gui.dialogs.motor.CloseableDialog; import net.sf.openrocket.gui.dialogs.motor.MotorSelector; import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.Icons; import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.Markers; +import net.sf.openrocket.motor.Manufacturer; import net.sf.openrocket.motor.Motor; import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.rocketcomponent.MotorConfiguration; +import net.sf.openrocket.rocketcomponent.MotorMount; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.BugException; import net.sf.openrocket.utils.MotorCorrelation; import org.jfree.chart.ChartColor; -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.ValueAxis; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.title.TextTitle; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelector { private static final Logger log = LoggerFactory.getLogger(ThrustCurveMotorSelectionPanel.class); + private static final Translator trans = Application.getTranslator(); - + private static final double MOTOR_SIMILARITY_THRESHOLD = 0.95; - - private static final int SHOW_ALL = 0; - private static final int SHOW_SMALLER = 1; - private static final int SHOW_EXACT = 2; - private static final String[] SHOW_DESCRIPTIONS = { - //// Show all motors - trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc1"), - //// Show motors with diameter less than that of the motor mount - trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc2"), - //// Show motors with diameter equal to that of the motor mount - trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc3") - }; - private static final int SHOW_MAX = 2; - - private static final int ZOOM_ICON_POSITION_NEGATIVE_X = 50; - private static final int ZOOM_ICON_POSITION_POSITIVE_Y = 12; - + private static final Paint[] CURVE_COLORS = ChartColor.createDefaultPaintArray(); - - private static final Color NO_COMMENT_COLOR = Color.GRAY; - private static final Color WITH_COMMENT_COLOR = Color.BLACK; - + private static final ThrustCurveMotorComparator MOTOR_COMPARATOR = new ThrustCurveMotorComparator(); - - - - private final List database; - - private final double diameter; + + private List database; + private CloseableDialog dialog = null; - - + private final ThrustCurveMotorDatabaseModel model; private final JTable table; private final TableRowSorter sorter; - + private final MotorRowFilter rowFilter; + private final JCheckBox hideSimilarBox; - + private final JTextField searchField; - private String[] searchTerms = new String[0]; - - + private final JLabel curveSelectionLabel; private final JComboBox curveSelectionBox; private final DefaultComboBoxModel curveSelectionModel; - - private final JLabel totalImpulseLabel; - private final JLabel classificationLabel; - private final JLabel avgThrustLabel; - private final JLabel maxThrustLabel; - private final JLabel burnTimeLabel; - private final JLabel launchMassLabel; - private final JLabel emptyMassLabel; - private final JLabel dataPointsLabel; - private final JLabel digestLabel; - - private final JTextArea comment; - private final Font noCommentFont; - private final Font withCommentFont; - - private final JFreeChart chart; - private final ChartPanel chartPanel; - private final JLabel zoomIcon; - private final JComboBox delayBox; + private final MotorInformationPanel motorInformationPanel; + private final MotorFilterPanel motorFilterPanel; + private ThrustCurveMotor selectedMotor; private ThrustCurveMotorSet selectedMotorSet; private double selectedDelay; - - + + public ThrustCurveMotorSelectionPanel(MotorMount mount, String currentConfig) { + this(); + setMotorMountAndConfig( mount, currentConfig ); + + } /** * Sole constructor. * @@ -159,448 +115,279 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec * @param delay the currently selected ejection charge delay. * @param diameter the diameter of the motor mount. */ - public ThrustCurveMotorSelectionPanel(ThrustCurveMotor current, double delay, double diameter) { + public ThrustCurveMotorSelectionPanel() { super(new MigLayout("fill", "[grow][]")); - - this.diameter = diameter; - - + // Construct the database (adding the current motor if not in the db already) - List db; - db = Application.getThrustCurveMotorSetDatabase().getMotorSets(); + database = Application.getThrustCurveMotorSetDatabase().getMotorSets(); + + model = new ThrustCurveMotorDatabaseModel(database); + rowFilter = new MotorRowFilter(model); + motorInformationPanel = new MotorInformationPanel(); + + //// MotorFilter + { + // Find all the manufacturers: + Set allManufacturers = new HashSet(); + for (ThrustCurveMotorSet s : database) { + allManufacturers.add(s.getManufacturer()); + } + + motorFilterPanel = new MotorFilterPanel(allManufacturers, rowFilter) { + @Override + public void onSelectionChanged() { + sorter.sort(); + scrollSelectionVisible(); + } + }; + + } + + //// GUI + JPanel panel = new JPanel(new MigLayout("fill","[][grow]")); + this.add(panel, "grow"); + + //// Select thrust curve: + { + curveSelectionLabel = new JLabel(trans.get("TCMotorSelPan.lbl.Selectthrustcurve")); + panel.add(curveSelectionLabel); + + curveSelectionModel = new DefaultComboBoxModel(); + curveSelectionBox = new JComboBox(curveSelectionModel); + curveSelectionBox.setRenderer(new CurveSelectionRenderer(curveSelectionBox.getRenderer())); + curveSelectionBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Object value = curveSelectionBox.getSelectedItem(); + if (value != null) { + select(((MotorHolder) value).getMotor()); + } + } + }); + panel.add(curveSelectionBox, "growx, wrap"); + } + + // Ejection charge delay: + { + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Ejectionchargedelay"))); + + delayBox = new JComboBox(); + delayBox.setEditable(true); + delayBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JComboBox cb = (JComboBox) e.getSource(); + String sel = (String) cb.getSelectedItem(); + //// None + if (sel.equalsIgnoreCase(trans.get("TCMotorSelPan.equalsIgnoreCase.None"))) { + selectedDelay = Motor.PLUGGED; + } else { + try { + selectedDelay = Double.parseDouble(sel); + } catch (NumberFormatException ignore) { + } + } + setDelays(false); + } + }); + panel.add(delayBox, "growx,wrap"); + //// (Number of seconds or \"None\") + panel.add(new StyledLabel(trans.get("TCMotorSelPan.lbl.NumberofsecondsorNone"), -3), "skip, wrap"); + setDelays(false); + } + + //// Hide very similar thrust curves + { + hideSimilarBox = new JCheckBox(trans.get("TCMotorSelPan.checkbox.hideSimilar")); + GUIUtil.changeFontSize(hideSimilarBox, -1); + hideSimilarBox.setSelected(Application.getPreferences().getBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, true)); + hideSimilarBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Application.getPreferences().putBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, hideSimilarBox.isSelected()); + updateData(); + } + }); + panel.add(hideSimilarBox, "gapleft para, spanx, growx, wrap"); + } + + //// Motor selection table + { + table = new JTable(model); + + // Set comparators and widths + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + sorter = new TableRowSorter(model); + for (int i = 0; i < ThrustCurveMotorColumns.values().length; i++) { + ThrustCurveMotorColumns column = ThrustCurveMotorColumns.values()[i]; + sorter.setComparator(i, column.getComparator()); + table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth()); + } + table.setRowSorter(sorter); + // force initial sort order to by diameter, total impulse, manufacturer + { + RowSorter.SortKey[] sortKeys = { + new RowSorter.SortKey(ThrustCurveMotorColumns.DIAMETER.ordinal(), SortOrder.ASCENDING), + new RowSorter.SortKey(ThrustCurveMotorColumns.TOTAL_IMPULSE.ordinal(), SortOrder.ASCENDING), + new RowSorter.SortKey(ThrustCurveMotorColumns.MANUFACTURER.ordinal(), SortOrder.ASCENDING) + }; + sorter.setSortKeys(Arrays.asList(sortKeys)); + } + + sorter.setRowFilter(rowFilter); + + // Set selection and double-click listeners + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + int row = table.getSelectedRow(); + if (row >= 0) { + row = table.convertRowIndexToModel(row); + ThrustCurveMotorSet motorSet = model.getMotorSet(row); + log.info(Markers.USER_MARKER, "Selected table row " + row + ": " + motorSet); + if (motorSet != selectedMotorSet) { + select(selectMotor(motorSet)); + } + } else { + log.info(Markers.USER_MARKER, "Selected table row " + row + ", nothing selected"); + } + } + }); + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { + if (dialog != null) { + dialog.close(true); + } + } + } + }); + + JScrollPane scrollpane = new JScrollPane(); + scrollpane.setViewportView(table); + panel.add(scrollpane, "grow, width :500:, height :300:, spanx, wrap"); + + } + + // Search field + { + //// Search: + StyledLabel label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Search")); + panel.add(label); + searchField = new JTextField(); + searchField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + update(); + } + @Override + public void insertUpdate(DocumentEvent e) { + update(); + } + @Override + public void removeUpdate(DocumentEvent e) { + update(); + } + private void update() { + String text = searchField.getText().trim(); + String[] split = text.split("\\s+"); + rowFilter.setSearchTerms(Arrays.asList(split)); + sorter.sort(); + scrollSelectionVisible(); + } + }); + panel.add(searchField, "span, growx"); + } + this.add(panel, "grow"); + + // Vertical split + this.add(new JSeparator(JSeparator.VERTICAL), "growy, gap para para"); + JTabbedPane rightSide = new JTabbedPane(); + rightSide.add(trans.get("TCMotorSelPan.btn.filter"), motorFilterPanel); + rightSide.add(trans.get("TCMotorSelPan.btn.details"), motorInformationPanel); + + this.add(rightSide); + + // Update the panel data + scrollSelectionVisible(); + updateData(); + setDelays(false); + + } + + public void setMotorMountAndConfig( MotorMount mount, String currentConfig ) { + double diameter = 0; + + if ( mount != null ) { + diameter = mount.getMotorMountDiameter(); + } + + if (currentConfig != null && mount != null) { + MotorConfiguration motorConf = mount.getMotorConfiguration().get(currentConfig); + selectedMotor = (ThrustCurveMotor) motorConf.getMotor(); + selectedDelay = motorConf.getEjectionDelay(); + diameter = mount.getMotorMountDiameter(); + } + // If current motor is not found in db, add a new ThrustCurveMotorSet containing it - if (current != null) { - selectedMotor = current; - for (ThrustCurveMotorSet motorSet : db) { - if (motorSet.getMotors().contains(current)) { + if (selectedMotor != null) { + for (ThrustCurveMotorSet motorSet : database) { + if (motorSet.getMotors().contains(selectedMotor)) { selectedMotorSet = motorSet; break; } } if (selectedMotorSet == null) { - db = new ArrayList(db); + database = new ArrayList(database); ThrustCurveMotorSet extra = new ThrustCurveMotorSet(); - extra.addMotor(current); + extra.addMotor(selectedMotor); selectedMotorSet = extra; - db.add(extra); - Collections.sort(db); + database.add(extra); + Collections.sort(database); } } - database = db; - - - - //// GUI - - JPanel panel; - JLabel label; - - panel = new JPanel(new MigLayout("fill")); - this.add(panel, "grow"); - - - - // Selection label - //// Select rocket motor: - label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Selrocketmotor"), Style.BOLD); - panel.add(label, "spanx, wrap para"); - - // Diameter selection - JComboBox filterComboBox = new JComboBox(SHOW_DESCRIPTIONS); - filterComboBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JComboBox cb = (JComboBox) e.getSource(); - int sel = cb.getSelectedIndex(); - if ((sel < 0) || (sel > SHOW_MAX)) - sel = SHOW_ALL; - switch (sel) { - case SHOW_ALL: - sorter.setRowFilter(new MotorRowFilterAll()); - break; - - case SHOW_SMALLER: - sorter.setRowFilter(new MotorRowFilterSmaller()); - break; - - case SHOW_EXACT: - sorter.setRowFilter(new MotorRowFilterExact()); - break; - - default: - throw new BugException("Invalid selection mode sel=" + sel); - } - Application.getPreferences().putChoice("MotorDiameterMatch", sel); - scrollSelectionVisible(); - } - }); - panel.add(filterComboBox, "spanx, growx, wrap rel"); - - //// Hide very similar thrust curves - hideSimilarBox = new JCheckBox(trans.get("TCMotorSelPan.checkbox.hideSimilar")); - GUIUtil.changeFontSize(hideSimilarBox, -1); - hideSimilarBox.setSelected(Application.getPreferences().getBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, true)); - hideSimilarBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Application.getPreferences().putBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, hideSimilarBox.isSelected()); - updateData(); - } - }); - panel.add(hideSimilarBox, "gapleft para, spanx, growx, wrap para"); - - - // Motor selection table - model = new ThrustCurveMotorDatabaseModel(database); - table = new JTable(model); - - - // Set comparators and widths - table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - sorter = new TableRowSorter(model); - for (int i = 0; i < ThrustCurveMotorColumns.values().length; i++) { - ThrustCurveMotorColumns column = ThrustCurveMotorColumns.values()[i]; - sorter.setComparator(i, column.getComparator()); - table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth()); - } - table.setRowSorter(sorter); - // force initial sort order to by diameter, total impulse, manufacturer - { - RowSorter.SortKey[] sortKeys = { - new RowSorter.SortKey(ThrustCurveMotorColumns.DIAMETER.ordinal(), SortOrder.ASCENDING), - new RowSorter.SortKey(ThrustCurveMotorColumns.TOTAL_IMPULSE.ordinal(), SortOrder.ASCENDING), - new RowSorter.SortKey(ThrustCurveMotorColumns.MANUFACTURER.ordinal(), SortOrder.ASCENDING) - }; - sorter.setSortKeys(Arrays.asList(sortKeys)); - } - - // Set selection and double-click listeners - table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - int row = table.getSelectedRow(); - if (row >= 0) { - row = table.convertRowIndexToModel(row); - ThrustCurveMotorSet motorSet = model.getMotorSet(row); - log.info(Markers.USER_MARKER, "Selected table row " + row + ": " + motorSet); - if (motorSet != selectedMotorSet) { - select(selectMotor(motorSet)); - } - } else { - log.info(Markers.USER_MARKER, "Selected table row " + row + ", nothing selected"); - } - } - }); - table.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { - if (dialog != null) { - dialog.close(true); - } - } - } - }); - - - JScrollPane scrollpane = new JScrollPane(); - scrollpane.setViewportView(table); - panel.add(scrollpane, "grow, width :500:, height :300:, spanx, wrap para"); - - - - - // Motor mount diameter label - //// Motor mount diameter: - label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Motormountdia") + " " + - UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(diameter)); - panel.add(label, "gapright 30lp, spanx, split"); - - - - // Search field - //// Search: - label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Search")); - panel.add(label, ""); - - searchField = new JTextField(); - searchField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void changedUpdate(DocumentEvent e) { - update(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - update(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - update(); - } - - private void update() { - String text = searchField.getText().trim(); - String[] split = text.split("\\s+"); - ArrayList list = new ArrayList(); - for (String s : split) { - s = s.trim().toLowerCase(Locale.getDefault()); - if (s.length() > 0) { - list.add(s); - } - } - searchTerms = list.toArray(new String[0]); - sorter.sort(); - scrollSelectionVisible(); - } - }); - panel.add(searchField, "growx, wrap"); - - - - // Vertical split - this.add(panel, "grow"); - this.add(new JSeparator(JSeparator.VERTICAL), "growy, gap para para"); - panel = new JPanel(new MigLayout("fill")); - - - - // Thrust curve selection - //// Select thrust curve: - curveSelectionLabel = new JLabel(trans.get("TCMotorSelPan.lbl.Selectthrustcurve")); - panel.add(curveSelectionLabel); - - curveSelectionModel = new DefaultComboBoxModel(); - curveSelectionBox = new JComboBox(curveSelectionModel); - curveSelectionBox.setRenderer(new CurveSelectionRenderer(curveSelectionBox.getRenderer())); - curveSelectionBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Object value = curveSelectionBox.getSelectedItem(); - if (value != null) { - select(((MotorHolder) value).getMotor()); - } - } - }); - panel.add(curveSelectionBox, "growx, wrap para"); - - - - - - // Ejection charge delay: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Ejectionchargedelay"))); - - delayBox = new JComboBox(); - delayBox.setEditable(true); - delayBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JComboBox cb = (JComboBox) e.getSource(); - String sel = (String) cb.getSelectedItem(); - //// None - if (sel.equalsIgnoreCase(trans.get("TCMotorSelPan.equalsIgnoreCase.None"))) { - selectedDelay = Motor.PLUGGED; - } else { - try { - selectedDelay = Double.parseDouble(sel); - } catch (NumberFormatException ignore) { - } - } - setDelays(false); - } - }); - panel.add(delayBox, "growx, wrap rel"); - //// (Number of seconds or \"None\") - panel.add(new StyledLabel(trans.get("TCMotorSelPan.lbl.NumberofsecondsorNone"), -3), "skip, wrap para"); - setDelays(false); - - - panel.add(new JSeparator(), "spanx, growx, wrap para"); - - - - // Thrust curve info - //// Total impulse: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Totalimpulse"))); - totalImpulseLabel = new JLabel(); - panel.add(totalImpulseLabel, "split"); - classificationLabel = new JLabel(); - classificationLabel.setEnabled(false); // Gray out - panel.add(classificationLabel, "gapleft unrel, wrap"); - - //// Avg. thrust: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Avgthrust"))); - avgThrustLabel = new JLabel(); - panel.add(avgThrustLabel, "wrap"); - - //// Max. thrust: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Maxthrust"))); - maxThrustLabel = new JLabel(); - panel.add(maxThrustLabel, "wrap"); - - //// Burn time: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Burntime"))); - burnTimeLabel = new JLabel(); - panel.add(burnTimeLabel, "wrap"); - - //// Launch mass: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Launchmass"))); - launchMassLabel = new JLabel(); - panel.add(launchMassLabel, "wrap"); - - //// Empty mass: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Emptymass"))); - emptyMassLabel = new JLabel(); - panel.add(emptyMassLabel, "wrap"); - - //// Data points: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Datapoints"))); - dataPointsLabel = new JLabel(); - panel.add(dataPointsLabel, "wrap para"); - - if (System.getProperty("openrocket.debug.motordigest") != null) { - //// Digest: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Digest"))); - digestLabel = new JLabel(); - panel.add(digestLabel, "w :300:, wrap para"); - } else { - digestLabel = null; - } - - - comment = new JTextArea(5, 5); - GUIUtil.changeFontSize(comment, -2); - withCommentFont = comment.getFont(); - noCommentFont = withCommentFont.deriveFont(Font.ITALIC); - comment.setLineWrap(true); - comment.setWrapStyleWord(true); - comment.setEditable(false); - scrollpane = new JScrollPane(comment); - panel.add(scrollpane, "spanx, growx, wrap para"); - - - - - // Thrust curve plot - chart = ChartFactory.createXYLineChart( - null, // title - null, // xAxisLabel - null, // yAxisLabel - null, // dataset - PlotOrientation.VERTICAL, - false, // legend - false, // tooltips - false // urls - ); - - - // Add the data and formatting to the plot - XYPlot plot = chart.getXYPlot(); - - changeLabelFont(plot.getRangeAxis(), -2); - changeLabelFont(plot.getDomainAxis(), -2); - - //// Thrust curve: - chart.setTitle(new TextTitle(trans.get("TCMotorSelPan.title.Thrustcurve"), this.getFont())); - chart.setBackgroundPaint(this.getBackground()); - plot.setBackgroundPaint(Color.WHITE); - plot.setDomainGridlinePaint(Color.LIGHT_GRAY); - plot.setRangeGridlinePaint(Color.LIGHT_GRAY); - - chartPanel = new ChartPanel(chart, - false, // properties - false, // save - false, // print - false, // zoom - false); // tooltips - chartPanel.setMouseZoomable(false); - chartPanel.setPopupMenu(null); - chartPanel.setMouseWheelEnabled(false); - chartPanel.setRangeZoomable(false); - chartPanel.setDomainZoomable(false); - - chartPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - chartPanel.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (selectedMotor == null || selectedMotorSet == null) - return; - if (e.getButton() == MouseEvent.BUTTON1) { - // Open plot dialog - List motors = getFilteredCurves(); - ThrustCurveMotorPlotDialog plotDialog = new ThrustCurveMotorPlotDialog(motors, - motors.indexOf(selectedMotor), - SwingUtilities.getWindowAncestor(ThrustCurveMotorSelectionPanel.this)); - plotDialog.setVisible(true); - } - } - }); - - JLayeredPane layer = new CustomLayeredPane(); - - layer.setBorder(BorderFactory.createLineBorder(Color.BLUE)); - - layer.add(chartPanel, (Integer) 0); - - zoomIcon = new JLabel(Icons.ZOOM_IN); - zoomIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - layer.add(zoomIcon, (Integer) 1); - - - panel.add(layer, "width 300:300:, height 180:180:, grow, spanx"); - - - - this.add(panel, "grow"); - - - - // Sets the filter: - int showMode = Application.getPreferences().getChoice(net.sf.openrocket.startup.Preferences.MOTOR_DIAMETER_FILTER, SHOW_MAX, SHOW_EXACT); - filterComboBox.setSelectedIndex(showMode); - - - // Update the panel data + updateData(); - selectedDelay = delay; - setDelays(false); - + setDelays(true); + + motorFilterPanel.setMotorMount(mount); + scrollSelectionVisible(); + } - + @Override public Motor getSelectedMotor() { return selectedMotor; } - - + + @Override public double getSelectedDelay() { return selectedDelay; } - - + + @Override public JComponent getDefaultFocus() { return searchField; } - + @Override public void selectedMotor(Motor motorSelection) { if (!(motorSelection instanceof ThrustCurveMotor)) { log.error("Received argument that was not ThrustCurveMotor: " + motorSelection); return; } - + ThrustCurveMotor motor = (ThrustCurveMotor) motorSelection; ThrustCurveMotorSet set = findMotorSet(motor); if (set == null) { log.error("Could not find set for motor:" + motorSelection); return; } - + // Store selected motor in preferences node, set all others to false Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); for (ThrustCurveMotor m : set.getMotors()) { @@ -608,34 +395,27 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec prefs.putBoolean(digest, m == motor); } } - + public void setCloseableDialog(CloseableDialog dialog) { this.dialog = dialog; } - - - - private void changeLabelFont(ValueAxis axis, float size) { - Font font = axis.getTickLabelFont(); - font = font.deriveFont(font.getSize2D() + size); - axis.setTickLabelFont(font); - } - - + + + /** * Called when a different motor is selected from within the panel. */ private void select(ThrustCurveMotor motor) { if (selectedMotor == motor) return; - + ThrustCurveMotorSet set = findMotorSet(motor); if (set == null) { throw new BugException("Could not find motor from database, motor=" + motor); } - + boolean updateDelays = (selectedMotorSet != set); - + selectedMotor = motor; selectedMotorSet = set; updateData(); @@ -643,46 +423,30 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec setDelays(true); } } - - + + private void updateData() { - + if (selectedMotorSet == null) { // No motor selected curveSelectionModel.removeAllElements(); curveSelectionBox.setEnabled(false); curveSelectionLabel.setEnabled(false); - totalImpulseLabel.setText(""); - totalImpulseLabel.setToolTipText(null); - classificationLabel.setText(""); - classificationLabel.setToolTipText(null); - avgThrustLabel.setText(""); - maxThrustLabel.setText(""); - burnTimeLabel.setText(""); - launchMassLabel.setText(""); - emptyMassLabel.setText(""); - dataPointsLabel.setText(""); - if (digestLabel != null) { - digestLabel.setText(""); - } - setComment(""); - chart.getXYPlot().setDataset(new XYSeriesCollection()); + motorInformationPanel.clearData(); return; } - - + // Check which thrust curves to display List motors = getFilteredCurves(); final int index = motors.indexOf(selectedMotor); - - + // Update the thrust curve selection box curveSelectionModel.removeAllElements(); for (int i = 0; i < motors.size(); i++) { curveSelectionModel.addElement(new MotorHolder(motors.get(i), i)); } curveSelectionBox.setSelectedIndex(index); - + if (motors.size() > 1) { curveSelectionBox.setEnabled(true); curveSelectionLabel.setEnabled(true); @@ -691,60 +455,11 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec curveSelectionLabel.setEnabled(false); } - - // Update thrust curve data - double impulse = selectedMotor.getTotalImpulseEstimate(); - MotorClass mc = MotorClass.getMotorClass(impulse); - totalImpulseLabel.setText(UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(impulse)); - classificationLabel.setText("(" + mc.getDescription(impulse) + ")"); - totalImpulseLabel.setToolTipText(mc.getClassDescription()); - classificationLabel.setToolTipText(mc.getClassDescription()); - - avgThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( - selectedMotor.getAverageThrustEstimate())); - maxThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( - selectedMotor.getMaxThrustEstimate())); - burnTimeLabel.setText(UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit( - selectedMotor.getBurnTimeEstimate())); - launchMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( - selectedMotor.getLaunchCG().weight)); - emptyMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( - selectedMotor.getEmptyCG().weight)); - dataPointsLabel.setText("" + (selectedMotor.getTimePoints().length - 1)); - if (digestLabel != null) { - digestLabel.setText(selectedMotor.getDigest()); - } - - setComment(selectedMotor.getDescription()); - - - // Update the plot - XYPlot plot = chart.getXYPlot(); - - XYSeriesCollection dataset = new XYSeriesCollection(); - for (int i = 0; i < motors.size(); i++) { - ThrustCurveMotor m = motors.get(i); - - //// Thrust - XYSeries series = new XYSeries(trans.get("TCMotorSelPan.title.Thrust") + " (" + i + ")"); - double[] time = m.getTimePoints(); - double[] thrust = m.getThrustPoints(); - - for (int j = 0; j < time.length; j++) { - series.add(time[j], thrust[j]); - } - - dataset.addSeries(series); - - boolean selected = (i == index); - plot.getRenderer().setSeriesStroke(i, new BasicStroke(selected ? 3 : 1)); - plot.getRenderer().setSeriesPaint(i, getColor(i)); - } - - plot.setDataset(dataset); + motorInformationPanel.updateData(motors, selectedMotor); + } - - private List getFilteredCurves() { + + List getFilteredCurves() { List motors = selectedMotorSet.getMotors(); if (hideSimilarBox.isSelected()) { List filtered = new ArrayList(motors.size()); @@ -754,7 +469,7 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec filtered.add(m); continue; } - + double similarity = MotorCorrelation.similarity(selectedMotor, m); log.debug("Motor similarity: " + similarity); if (similarity < MOTOR_SIMILARITY_THRESHOLD) { @@ -763,28 +478,13 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec } motors = filtered; } - + Collections.sort(motors, MOTOR_COMPARATOR); - + return motors; } - - - private void setComment(String s) { - s = s.trim(); - if (s.length() == 0) { - //// No description available. - comment.setText(trans.get("TCMotorSelPan.noDescription")); - comment.setFont(noCommentFont); - comment.setForeground(NO_COMMENT_COLOR); - } else { - comment.setText(s); - comment.setFont(withCommentFont); - comment.setForeground(WITH_COMMENT_COLOR); - } - comment.setCaretPosition(0); - } - + + private void scrollSelectionVisible() { if (selectedMotorSet != null) { int index = table.convertRowIndexToView(model.getIndex(selectedMotorSet)); @@ -795,13 +495,13 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec table.scrollRectToVisible(rect); } } - - + + public static Color getColor(int index) { return (Color) CURVE_COLORS[index % CURVE_COLORS.length]; } - - + + /** * Find the ThrustCurveMotorSet that contains a motor. * @@ -814,12 +514,12 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec return set; } } - + return null; } - - - + + + /** * Select the default motor from this ThrustCurveMotorSet. This uses primarily motors * that the user has previously used, and secondarily a heuristic method of selecting which @@ -835,8 +535,8 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec if (set.getMotorCount() == 1) { return set.getMotors().get(0); } - - + + // Find which motor has been used the most recently List list = set.getMotors(); Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); @@ -846,13 +546,13 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec return m; } } - + // No motor has been used Collections.sort(list, MOTOR_COMPARATOR); return list.get(0); } - - + + /** * Set the values in the delay combo box. If reset is true * then sets the selected value as the value closest to selectedDelay, otherwise @@ -860,25 +560,25 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec */ private void setDelays(boolean reset) { if (selectedMotor == null) { - + //// None delayBox.setModel(new DefaultComboBoxModel(new String[] { trans.get("TCMotorSelPan.delayBox.None") })); delayBox.setSelectedIndex(0); - + } else { - + List delays = selectedMotorSet.getDelays(); String[] delayStrings = new String[delays.size()]; double currentDelay = selectedDelay; // Store current setting locally - + for (int i = 0; i < delays.size(); i++) { //// None delayStrings[i] = ThrustCurveMotor.getDelayString(delays.get(i), trans.get("TCMotorSelPan.delayBox.None")); } delayBox.setModel(new DefaultComboBoxModel(delayStrings)); - + if (reset) { - + // Find and set the closest value double closest = Double.NaN; for (int i = 0; i < delays.size(); i++) { @@ -894,120 +594,43 @@ public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelec } else { delayBox.setSelectedItem("None"); } - + } else { - + selectedDelay = currentDelay; //// None delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(currentDelay, trans.get("TCMotorSelPan.delayBox.None"))); - + } - + } } - - - - + ////////////////////// - - + + private class CurveSelectionRenderer implements ListCellRenderer { - + private final ListCellRenderer renderer; - + public CurveSelectionRenderer(ListCellRenderer renderer) { this.renderer = renderer; } - + @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - + Component c = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value instanceof MotorHolder) { MotorHolder m = (MotorHolder) value; c.setForeground(getColor(m.getIndex())); } - + return c; } - - } - - - //////// Row filters - - /** - * Abstract adapter class. - */ - private abstract class MotorRowFilter extends RowFilter { - @Override - public boolean include(RowFilter.Entry entry) { - int index = entry.getIdentifier(); - ThrustCurveMotorSet m = model.getMotorSet(index); - return filterByDiameter(m) && filterByString(m); - } - - public abstract boolean filterByDiameter(ThrustCurveMotorSet m); - - - public boolean filterByString(ThrustCurveMotorSet m) { - main: for (String s : searchTerms) { - for (ThrustCurveMotorColumns col : ThrustCurveMotorColumns.values()) { - String str = col.getValue(m).toString().toLowerCase(Locale.getDefault()); - if (str.indexOf(s) >= 0) - continue main; - } - return false; - } - return true; - } - } - - /** - * Show all motors. - */ - private class MotorRowFilterAll extends MotorRowFilter { - @Override - public boolean filterByDiameter(ThrustCurveMotorSet m) { - return true; - } - } - - /** - * Show motors smaller than the mount. - */ - private class MotorRowFilterSmaller extends MotorRowFilter { - @Override - public boolean filterByDiameter(ThrustCurveMotorSet m) { - return (m.getDiameter() <= diameter + 0.0004); - } - } - - /** - * Show motors that fit the mount. - */ - private class MotorRowFilterExact extends MotorRowFilter { - @Override - public boolean filterByDiameter(ThrustCurveMotorSet m) { - return ((m.getDiameter() <= diameter + 0.0004) && (m.getDiameter() >= diameter - 0.0015)); - } - } - - - /** - * Custom layered pane that sets the bounds of the components on every layout. - */ - public class CustomLayeredPane extends JLayeredPane { - @Override - public void doLayout() { - synchronized (getTreeLock()) { - int w = getWidth(); - int h = getHeight(); - chartPanel.setBounds(0, 0, w, h); - zoomIcon.setBounds(w - ZOOM_ICON_POSITION_NEGATIVE_X, ZOOM_ICON_POSITION_POSITIVE_Y, 50, 50); - } - } + } + + } diff --git a/swing/src/net/sf/openrocket/gui/util/CheckList.java b/swing/src/net/sf/openrocket/gui/util/CheckList.java new file mode 100644 index 000000000..fa7c17948 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/util/CheckList.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2009-2011, EzWare + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer.Redistributions + * in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution.Neither the name of the + * EzWare nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +package net.sf.openrocket.gui.util; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseListener; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.JList; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; + +/** + * The decorator for JList which makes it work like check list + * UI can be designed using JList and which can be later decorated to become a check list + * @author Eugene Ryzhikov + * + * @param list item type + */ +public class CheckList { + + private final JList list; + private static final MouseAdapter checkBoxEditor = new CheckListEditor(); + + public static class Builder { + + private JList list; + + public Builder(JList list) { + this.list = list == null ? new JList() : list; + } + + public Builder() { + this(null); + } + + public CheckList build() { + return new CheckList(list); + } + + } + + + /** + * Wraps the standard JList and makes it work like check list + * @param list + */ + private CheckList(final JList list) { + + if (list == null) + throw new NullPointerException(); + this.list = list; + this.list.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + if (!isEditorAttached()) + list.addMouseListener(checkBoxEditor); + this.list.setCellRenderer(new CheckListRenderer()); + + setupKeyboardActions(list); + + } + + @SuppressWarnings("serial") + private void setupKeyboardActions(final JList list) { + String actionKey = "toggle-check"; + list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0), actionKey); + list.getActionMap().put(actionKey, new AbstractAction() { + + @Override + public void actionPerformed(ActionEvent e) { + toggleIndex(list.getSelectedIndex()); + } + }); + } + + private boolean isEditorAttached() { + + for (MouseListener ml : list.getMouseListeners()) { + if (ml instanceof CheckListEditor) + return true; + } + return false; + + } + + public JList getList() { + return list; + } + + /** + * Sets data to a check list. Simplification for setting new the model + * @param data + */ + public void setData(Collection data) { + setModel(new DefaultCheckListModel(data)); + } + + /** + * Sets the model for check list. + * @param model + */ + public void setModel(DefaultCheckListModel model) { + list.setModel(model); + } + + @SuppressWarnings("unchecked") + public DefaultCheckListModel getModel() { + return (DefaultCheckListModel) list.getModel(); + } + + /** + * Returns a collection of checked items. + * @return collection of checked items. Empty collection if nothing is selected + */ + public Collection getCheckedItems() { + return getModel().getCheckedItems(); + } + + public Collection getUncheckedItems() { + List unchecked = new ArrayList(); + for (int i = getModel().getSize() - 1; i >= 0; i--) { + unchecked.add((T) getModel().getElementAt(i)); + } + unchecked.removeAll(getCheckedItems()); + return unchecked; + } + + public void checkAll() { + getModel().checkAll(); + } + + public void clearAll() { + getModel().clearAll(); + } + + /** + * Resets checked elements + * @param elements + */ + public void setCheckedItems(Collection elements) { + getModel().setCheckedItems(elements); + } + + public void setUncheckedItems( Collection elements ) { + getModel().setUncheckedItems(elements); + } + + public void toggleIndex(int index) { + if (index >= 0 && index < list.getModel().getSize()) { + DefaultCheckListModel model = getModel(); + model.setCheckedIndex(index, !model.isCheckedIndex(index)); + } + } + +} diff --git a/swing/src/net/sf/openrocket/gui/util/CheckListEditor.java b/swing/src/net/sf/openrocket/gui/util/CheckListEditor.java new file mode 100644 index 000000000..68680670f --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/util/CheckListEditor.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2009-2011, EzWare + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer.Redistributions + * in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution.Neither the name of the + * EzWare nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +package net.sf.openrocket.gui.util; + +import java.awt.Rectangle; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Arrays; + +import javax.swing.JList; +import javax.swing.SwingUtilities; + + +/** + * Determines mouse click and + * 1. Toggles the check on selected item if clicked once + * 2. Clears checks and checks selected item if clicked more then once + * + * Created on Feb 4, 2011 + * @author Eugene Ryzhikov + * + */ +final class CheckListEditor extends MouseAdapter { + @Override + public void mouseClicked(MouseEvent e) { + + if (!SwingUtilities.isLeftMouseButton(e)) + return; + + JList list = (JList) e.getSource(); + if (!list.isEnabled() || (!(list.getModel() instanceof DefaultCheckListModel))) + return; + + int index = list.locationToIndex(e.getPoint()); + if (index < 0) + return; + + Rectangle bounds = list.getCellBounds(index, index); + + if (bounds.contains(e.getPoint())) { + + @SuppressWarnings("unchecked") + DefaultCheckListModel model = (DefaultCheckListModel) list.getModel(); + + if (e.getClickCount() > 1) { + // clear all and check selected for more then 1 clicks + // Original implementation had this implementation. I didn't like that behavior. +// model.setCheckedItems(Arrays.asList(model.getElementAt(index))); + } else { + // simple toggle for 1 click + model.setCheckedIndex(index, !model.isCheckedIndex(index)); + } + e.consume(); + } + + } + +} diff --git a/swing/src/net/sf/openrocket/gui/util/CheckListRenderer.java b/swing/src/net/sf/openrocket/gui/util/CheckListRenderer.java new file mode 100644 index 000000000..e0777c4f9 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/util/CheckListRenderer.java @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2009-2011, EzWare + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer.Redistributions + * in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution.Neither the name of the + * EzWare nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +package net.sf.openrocket.gui.util; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Rectangle; +import java.io.Serializable; + +import javax.swing.DefaultListCellRenderer; +import javax.swing.Icon; +import javax.swing.JCheckBox; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import javax.swing.UIManager; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; + +public class CheckListRenderer extends JCheckBox implements ListCellRenderer, Serializable { + + private static final long serialVersionUID = 1L; + + private static final Border NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1); + private static final Border SAFE_NO_FOCUS_BORDER = NO_FOCUS_BORDER; // may change in the feature + + /** + * Constructs a default renderer object for an item in a list. + */ + public CheckListRenderer() { + super(); + setOpaque(true); + setBorder(getNoFocusBorder()); + } + + private static Border getNoFocusBorder() { + if (System.getSecurityManager() != null) { + return SAFE_NO_FOCUS_BORDER; + } else { + return NO_FOCUS_BORDER; + } + } + + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) { + + setComponentOrientation(list.getComponentOrientation()); + + Color bg = null; + Color fg = null; + + JList.DropLocation dropLocation = list.getDropLocation(); + if (dropLocation != null && !dropLocation.isInsert() && dropLocation.getIndex() == index) { + + bg = UIManager.getColor("List.dropCellBackground"); + fg = UIManager.getColor("List.dropCellForeground"); + + isSelected = true; + } + + if (isSelected) { + setBackground(bg == null ? list.getSelectionBackground() : bg); + setForeground(fg == null ? list.getSelectionForeground() : fg); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + + if (value instanceof Icon) { + setIcon((Icon) value); + setText(""); + } else { + setIcon(null); + setText(getObjectAsText(value)); + } + + setSelected(isChecked(list, index)); + + setEnabled(list.isEnabled()); + setFont(list.getFont()); + + Border border = null; + if (cellHasFocus) { + if (isSelected) { + border = UIManager.getBorder("List.focusSelectedCellHighlightBorder"); + } + if (border == null) { + border = UIManager.getBorder("List.focusCellHighlightBorder"); + } + } else { + border = getNoFocusBorder(); + } + setBorder(border); + + return this; + } + + protected String getObjectAsText(Object obj) { + return (obj == null) ? "" : obj.toString(); + } + + private boolean isChecked(JList list, int index) { + + if (list.getModel() instanceof DefaultCheckListModel) { + return ((DefaultCheckListModel) list.getModel()).isCheckedIndex(index); + } else { + return false; + } + + } + + /** + * @return true if the background is opaque and differs from the JList's background; false otherwise + */ + @Override + public boolean isOpaque() { + Color back = getBackground(); + Component p = getParent(); + if (p != null) { + p = p.getParent(); + } + // p should now be the JList. + boolean colorMatch = (back != null) && (p != null) && back.equals(p.getBackground()) && p.isOpaque(); + return !colorMatch && super.isOpaque(); + } + + @Override + protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + + if ("text".equals(propertyName) || + (("font".equals(propertyName) || "foreground".equals(propertyName)) && + oldValue != newValue && getClientProperty(javax.swing.plaf.basic.BasicHTML.propertyKey) != null)) { + + super.firePropertyChange(propertyName, oldValue, newValue); + } + } + + // Methods below are overridden for performance reasons. + + @Override + public void validate() { + } + + @Override + public void invalidate() { + } + + @Override + public void repaint() { + } + + @Override + public void revalidate() { + } + + @Override + public void repaint(long tm, int x, int y, int width, int height) { + } + + @Override + public void repaint(Rectangle r) { + } + + @Override + public void firePropertyChange(String propertyName, byte oldValue, byte newValue) { + } + + @Override + public void firePropertyChange(String propertyName, char oldValue, char newValue) { + } + + @Override + public void firePropertyChange(String propertyName, short oldValue, short newValue) { + } + + @Override + public void firePropertyChange(String propertyName, int oldValue, int newValue) { + } + + @Override + public void firePropertyChange(String propertyName, long oldValue, long newValue) { + } + + @Override + public void firePropertyChange(String propertyName, float oldValue, float newValue) { + } + + @Override + public void firePropertyChange(String propertyName, double oldValue, double newValue) { + } + + @Override + public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { + } + + @SuppressWarnings("serial") + public static class UIResource extends DefaultListCellRenderer implements javax.swing.plaf.UIResource { + } + +} \ No newline at end of file diff --git a/swing/src/net/sf/openrocket/gui/util/DefaultCheckListModel.java b/swing/src/net/sf/openrocket/gui/util/DefaultCheckListModel.java new file mode 100644 index 000000000..cbb4e5919 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/util/DefaultCheckListModel.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2009-2011, EzWare + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer.Redistributions + * in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or + * other materials provided with the distribution.Neither the name of the + * EzWare nor the names of its contributors may be used to endorse or + * promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +package net.sf.openrocket.gui.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.swing.AbstractListModel; + +/** + * Default model for check list. It is based on the list of items + * Implementation of checks is based on HashSet of checked items + * + * @author Eugene Ryzhikov + * + * @param list element type + */ +public class DefaultCheckListModel extends AbstractListModel { + + private static final long serialVersionUID = 1L; + + private final List data = new ArrayList(); + private final Set checks = new HashSet(); + + public DefaultCheckListModel(Collection data) { + + if (data == null) + return; + for (T object : data) { + this.data.add(object); + checks.clear(); + } + } + + public DefaultCheckListModel(T... data) { + this(Arrays.asList(data)); + } + + /* (non-Javadoc) + * @see org.oxbow.swingbits.list.ICheckListModel#getSize() + */ + @Override + public int getSize() { + return data().size(); + } + + private List data() { + return data; + } + + + @Override + public Object getElementAt(int index) { + return data().get(index); + } + + public boolean isCheckedIndex(int index) { + return checks.contains(data().get(index)); + } + + public void setCheckedIndex(int index, boolean value) { + T o = data().get(index); + if (value) + checks.add(o); + else + checks.remove(o); + fireContentsChanged(this, index, index); + } + + public Collection getCheckedItems() { + List items = new ArrayList(checks); + items.retainAll(data); + return Collections.unmodifiableList(items); + } + + public void clearAll() { + checks.clear(); + fireContentsChanged(this, 0, checks.size() - 1); + } + + public void checkAll() { + checks.addAll(data); + fireContentsChanged(this, 0, checks.size() - 1); + } + + public void setCheckedItems(Collection items) { + + // if ( CollectionUtils.isEmpty(items)) return; + + List correctedItems = new ArrayList(items); + correctedItems.retainAll(data); + + checks.clear(); + checks.addAll(correctedItems); + fireContentsChanged(this, 0, checks.size() - 1); + + + } + + public void setUncheckedItems( Collection items ) { + + List correctedItems = new ArrayList(data); + correctedItems.removeAll(items); + + checks.clear(); + checks.addAll(correctedItems); + fireContentsChanged(this, 0, checks.size() - 1); + + } +} diff --git a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java index d752c260a..9bdb5f2e9 100644 --- a/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ b/swing/src/net/sf/openrocket/gui/util/SwingPreferences.java @@ -6,6 +6,7 @@ import java.awt.Point; import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -14,12 +15,10 @@ import java.util.Set; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import net.sf.openrocket.arch.SystemInfo; import net.sf.openrocket.document.Simulation; import net.sf.openrocket.material.Material; +import net.sf.openrocket.motor.Manufacturer; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.simulation.FlightDataType; @@ -30,6 +29,9 @@ import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.BuildProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class SwingPreferences extends net.sf.openrocket.startup.Preferences { private static final Logger log = LoggerFactory.getLogger(SwingPreferences.class); @@ -575,9 +577,9 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { return materials; } - + //// Preset Component Favorites - + @Override public void setComponentFavorite(ComponentPreset preset, ComponentPreset.Type type, boolean favorite) { Preferences prefs = PREFNODE.node("favoritePresets").node(type.name()); @@ -599,35 +601,67 @@ public class SwingPreferences extends net.sf.openrocket.startup.Preferences { } return collection; } - + //// Decal Editor Setting private final static String DECAL_EDITOR_PREFERNCE_NODE = "decalEditorPreference"; private final static String DECAL_EDITOR_USE_SYSTEM_DEFAULT = ""; - public void clearDecalEditorPreference( ) { - putString(DECAL_EDITOR_PREFERNCE_NODE,null); + public void clearDecalEditorPreference() { + putString(DECAL_EDITOR_PREFERNCE_NODE, null); } + public void setDecalEditorPreference(boolean useSystem, String commandLine) { - if ( useSystem ) { - putString(DECAL_EDITOR_PREFERNCE_NODE,DECAL_EDITOR_USE_SYSTEM_DEFAULT); - } else if ( commandLine != null ) { + if (useSystem) { + putString(DECAL_EDITOR_PREFERNCE_NODE, DECAL_EDITOR_USE_SYSTEM_DEFAULT); + } else if (commandLine != null) { putString(DECAL_EDITOR_PREFERNCE_NODE, commandLine); } else { clearDecalEditorPreference(); } } - + public boolean isDecalEditorPreferenceSet() { - String s = getString(DECAL_EDITOR_PREFERNCE_NODE,null); + String s = getString(DECAL_EDITOR_PREFERNCE_NODE, null); return s != null; } public boolean isDecalEditorPreferenceSystem() { - String s = getString(DECAL_EDITOR_PREFERNCE_NODE,null); + String s = getString(DECAL_EDITOR_PREFERNCE_NODE, null); return DECAL_EDITOR_USE_SYSTEM_DEFAULT.equals(s); } + public String getDecalEditorCommandLine() { - return getString(DECAL_EDITOR_PREFERNCE_NODE,null); + return getString(DECAL_EDITOR_PREFERNCE_NODE, null); } + public List getExcludedMotorManufacturers() { + Preferences prefs = PREFNODE.node("excludedMotorManufacturers"); + List collection = new ArrayList(); + try { + String[] manuShortNames = prefs.keys(); + for (String s : manuShortNames) { + Manufacturer m = Manufacturer.getManufacturer(s); + if (m != null) { + collection.add(m); + } + } + } catch (BackingStoreException e) { + } + + return collection; + + } + + public void setExcludedMotorManufacturers(Collection manus) { + Preferences prefs = PREFNODE.node("excludedMotorManufacturers"); + try { + for (String s : prefs.keys()) { + prefs.remove(s); + } + } catch (BackingStoreException e) { + } + for (Manufacturer m : manus) { + prefs.putBoolean(m.getSimpleName(), true); + } + } } diff --git a/swing/src/net/sf/openrocket/gui/widgets/MultiSlider.java b/swing/src/net/sf/openrocket/gui/widgets/MultiSlider.java new file mode 100644 index 000000000..5b956c5a6 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/widgets/MultiSlider.java @@ -0,0 +1,553 @@ +package net.sf.openrocket.gui.widgets; +/* ------------------------------------------------------------------- + * GeoVISTA Center (Penn State, Dept. of Geography) + * + * Java source file for the class MultiSlider + * + * Copyright (c), 1999 - 2002, Masahiro Takatsuka and GeoVISTA Center + * All Rights Researved. + * + * Original Author: Masahiro Takatsuka + * $Author: eytanadar $ + * + * $Date: 2005/10/05 20:19:52 $ + * + * + * Reference: Document no: + * ___ ___ + * + * To Do: + * ___ + * + ------------------------------------------------------------------- */ + +/* --------------------------- Package ---------------------------- */ + +import java.awt.Color; + +import javax.accessibility.AccessibleContext; +import javax.accessibility.AccessibleState; +import javax.swing.BoundedRangeModel; +import javax.swing.DefaultBoundedRangeModel; +import javax.swing.JComponent; +import javax.swing.JSlider; +import javax.swing.plaf.SliderUI; + +/*==================================================================== + Implementation of class MultiSlider + ====================================================================*/ +/*** + * A component that lets the user graphically select values by slding + * multiple thumbs within a bounded interval. MultiSlider inherits all + * fields and methods from javax.swing.JSlider. + *

+ * + * @version $Revision: 1.1 $ + * @author Masahiro Takatsuka (masa@jbeans.net) + * @see JSlider + */ + +public class MultiSlider extends JSlider { + /*** + * @see #getUIClassID + * @see #readObject + */ + private static final String uiClassID = "MultiSliderUI"; + + /*** + * An array of data models that handle the numeric maximum values, + * minimum values, and current-position values for the multi slider. + */ + private BoundedRangeModel[] sliderModels; + + /*** + * If it is true, a thumb is bounded by adjacent thumbs. + */ + private boolean bounded = false; + + /*** + * This is a color to paint the current thumb + */ + private Color currentThumbColor = Color.red; + + transient private int valueBeforeStateChange; + + /*** + * Creates a slider with the specified orientation and the + * specified mimimum, maximum, and initial values. + * + * @exception IllegalArgumentException if orientation is not one of VERTICAL, HORIZONTAL + * + * @see #setOrientation + * @see #setMinimum + * @see #setMaximum + * @see #setValue + */ + public MultiSlider(int orientation, int min, int max, + int val1, int val2) { + checkOrientation(orientation); + this.orientation = orientation; + setNumberOfThumbs(min,max,new int[]{val1,val2}); + } + + /*** + * Creates a slider with the specified orientation and the + * specified mimimum, maximum, and the number of thumbs. + * + * @exception IllegalArgumentException if orientation is not one of VERTICAL, HORIZONTAL + * + * @see #setOrientation + * @see #setMinimum + * @see #setMaximum + * @see #setValue + */ + public MultiSlider(int orientation, int min, int max) { + checkOrientation(orientation); + this.orientation = orientation; + setNumberOfThumbs(min, max, 2); + } + + /*** + * Creates a horizontal slider with the range 0 to 100 and + * an intitial value of 50. + */ + public MultiSlider() { + this(HORIZONTAL, 0, 100); + } + + + /*** + * Creates a slider using the specified orientation with the + * range 0 to 100 and an intitial value of 50. + */ + public MultiSlider(int orientation) { + this(orientation, 0, 100); + } + + + /*** + * Creates a horizontal slider using the specified min and max + * with an intitial value of 50. + */ + public MultiSlider(int min, int max) { + this(HORIZONTAL, min, max); + } + + public void setCurrentThumbColor(Color c) { + this.currentThumbColor = c; + } + + public Color getCurrentThumbColor() { + return this.currentThumbColor; + } + + public int getTrackBuffer() { + return ((MultiSliderUI) this.ui).getTrackBuffer(); + } + + /*** + * Validates the orientation parameter. + */ + private void checkOrientation(int orientation) { + switch (orientation) { + case VERTICAL: + case HORIZONTAL: + break; + default: + throw new IllegalArgumentException("orientation must be one of: VERTICAL, HORIZONTAL"); + } + } + + /*** + * Notification from the UIFactory that the L&F has changed. + * Called to replace the UI with the latest version from the + * default UIFactory. + * + * @see JComponent#updateUI + */ + public void updateUI() { + updateLabelUIs(); + MultiSliderUI ui = new MultiSliderUI(); + if (this.sliderModels != null) { + ui.setThumbCount(this.sliderModels.length); + } + setUI((SliderUI) ui); + } + + /*** + * Returns the number of thumbs in the slider. + */ + public int getNumberOfThumbs() { + return this.sliderModels.length; + } + + /*** + * Sets the number of thumbs with the specified parameters. + */ + private void setNumberOfThumbs(int min, int max, int num, boolean useEndPoints) { + int [] values = createDefaultValues(min, max, num, useEndPoints); + setNumberOfThumbs(min, max, values); + } + + /*** + * Sets the number of thumbs with the specified parameters. + */ + private void setNumberOfThumbs(int min, int max, int num) { + setNumberOfThumbs(min, max, num, false); + } + + /*** + * Sets the number of thumbs with the specified parameters. + */ + private void setNumberOfThumbs(int min, int max, int[] values) { + if (values == null || values.length < 1) { + values = new int[] {50}; + } + int num = values.length; + this.sliderModels = new BoundedRangeModel[num]; + for (int i = 0; i < num; i++) { + this.sliderModels[i] = new DefaultBoundedRangeModel(values[i], 0, min, max); + this.sliderModels[i].addChangeListener(changeListener); + } + updateUI(); + } + + /*** + * Sets the number of thumbs. + */ + private void setNumberOfThumbs(int num) { + setNumberOfThumbs(num, false); + } + + /*** + * Sets the number of thumbs. + */ + private void setNumberOfThumbs(int num, boolean useEndPoints) { + if (getNumberOfThumbs() != num) { + setNumberOfThumbs(getMinimum(), getMaximum(), num, useEndPoints); + } + } + + /*** + * Sets the number of thumbs by specifying the initial values. + */ + private void setNumberOfThumbs(int[] values) { + setNumberOfThumbs(getMinimum(), getMaximum(), values); + } + + /*** + * creates evenly spaced values for thumbs. + */ + private int[] createDefaultValues(int min, int max, int num_of_values, boolean useEndPoints) { + int[] values = new int[num_of_values]; + int range = max - min; + + if (!useEndPoints) { + int step = range / (num_of_values + 1); + for (int i = 0; i < num_of_values; i++) { + values[i] = min + (i + 1) * step; + } + } else { + if (num_of_values < 1) { + return new int[0]; + } + values[0] = getMinimum(); + values[num_of_values - 1] = getMaximum(); + int[] def = createDefaultValues(getMinimum(), getMaximum(), num_of_values - 2, false); + for (int i = 0; i < def.length; i++) { + values[i + 1] = def[i]; + } + } + return values; + } + + /*** + * Returns the index number of currently operated thumb. + */ + public int getCurrentThumbIndex() { + return ((MultiSliderUI)ui).getCurrentIndex(); + } + + /*** + * Returns data model that handles the sliders three + * fundamental properties: minimum, maximum, value. + * + * @see #setModel + */ + public BoundedRangeModel getModel() { + return getModelAt(getCurrentThumbIndex()); + } + + /*** + * Returns data model that handles the sliders three + * fundamental properties: minimum, maximum, value. + * + * @see #setModel + */ + public BoundedRangeModel getModelAt(int index) { + if (this.sliderModels == null || index >= this.sliderModels.length) { + return null; + } + return this.sliderModels[index]; + } + + /*** + * Returns data model that handles the sliders three + * fundamental properties: minimum, maximum, value. + * + * @see #setModel + */ + public BoundedRangeModel[] getModels() { + return this.sliderModels; + } + + /*** + * Sets the model that handles the sliders three + * fundamental properties: minimum, maximum, value. + * + * @see #getModel + * @beaninfo + * bound: true + * description: The sliders BoundedRangeModel. + */ + public void setModel(BoundedRangeModel newModel) { + setModelAt(getCurrentThumbIndex(), newModel); + } + + /*** + * Sets the model that handles the sliders three + * fundamental properties: minimum, maximum, value. + * + * @see #getModel + * @beaninfo + * bound: true + * description: The sliders BoundedRangeModel. + */ + public void setModelAt(int index, BoundedRangeModel newModel) { + BoundedRangeModel oldModel = getModelAt(index); + + if (oldModel != null) { + oldModel.removeChangeListener(changeListener); + } + + this.sliderModels[index] = newModel; + + if (newModel != null) { + newModel.addChangeListener(changeListener); + + if (accessibleContext != null) { + accessibleContext.firePropertyChange( + AccessibleContext.ACCESSIBLE_VALUE_PROPERTY, + (oldModel == null + ? null : new Integer(oldModel.getValue())), + (newModel == null + ? null : new Integer(newModel.getValue()))); + } + } + + firePropertyChange("model", oldModel, this.sliderModels[index]); + } + + /*** + * Sets the models minimum property. + * + * @see #getMinimum + * @see BoundedRangeModel#setMinimum + * @beaninfo + * bound: true + * preferred: true + * description: The sliders minimum value. + */ + public void setMinimum(int minimum) { + int count = getNumberOfThumbs(); + int oldMin = getModel().getMinimum(); + for (int i = 0; i < count; i++) { + getModelAt(i).setMinimum(minimum); + } + firePropertyChange( "minimum", new Integer( oldMin ), new Integer( minimum ) ); + } + + /*** + * Sets the models maximum property. + * + * @see #getMaximum + * @see BoundedRangeModel#setMaximum + * @beaninfo + * bound: true + * preferred: true + * description: The sliders maximum value. + */ + public void setMaximum(int maximum) { + int count = getNumberOfThumbs(); + int oldMax = getModel().getMaximum(); + for (int i = 0; i < count; i++) { + getModelAt(i).setMaximum(maximum); + } + firePropertyChange( "maximum", new Integer( oldMax ), new Integer( maximum ) ); + } + + /*** + * Returns the sliders value. + * @return the models value property + * @see #setValue + */ + public int getValue() { + return getValueAt(getCurrentThumbIndex()); + } + + /*** + * Returns the sliders value. + * @return the models value property + * @see #setValue + */ + public int getValueAt(int index) { + return getModelAt(index).getValue(); + } + + /*** + * Sets the sliders current value. This method just forwards + * the value to the model. + * + * @see #getValue + * @beaninfo + * preferred: true + * description: The sliders current value. + */ + public void setValue(int n) { + setValueAt(getCurrentThumbIndex(), n); + } + + /*** + * Sets the sliders current value. This method just forwards + * the value to the model. + * + * @see #getValue + * @beaninfo + * preferred: true + * description: The sliders current value. + */ + public void setValueAt(int index, int n) { + BoundedRangeModel m = getModelAt(index); + int oldValue = m.getValue(); + m.setValue(n); + + if (accessibleContext != null) { + accessibleContext.firePropertyChange( + AccessibleContext.ACCESSIBLE_VALUE_PROPERTY, + new Integer(oldValue), + new Integer(m.getValue())); + } + } + + /*** + * True if the slider knob is being dragged. + * + * @return the value of the models valueIsAdjusting property + * @see #setValueIsAdjusting + */ + public boolean getValueIsAdjusting() { + boolean result = false; + int count = getNumberOfThumbs(); + for (int i = 0; i < count; i++) { + result = (result || getValueIsAdjustingAt(i)); + } + return result; + } + + /*** + * True if the slider knob is being dragged. + */ + public boolean getValueIsAdjustingAt(int index) { + return getModelAt(index).getValueIsAdjusting(); + } + + /*** + * Sets the models valueIsAdjusting property. Slider look and + * feel implementations should set this property to true when + * a knob drag begins, and to false when the drag ends. The + * slider model will not generate ChangeEvents while + * valueIsAdjusting is true. + * + * @see #getValueIsAdjusting + * @see BoundedRangeModel#setValueIsAdjusting + * @beaninfo + * expert: true + * description: True if the slider knob is being dragged. + */ + public void setValueIsAdjusting(boolean b) { + setValueIsAdjustingAt(getCurrentThumbIndex(), b); + } + + /*** + * Sets the models valueIsAdjusting property. Slider look and + * feel implementations should set this property to true when + * a knob drag begins, and to false when the drag ends. The + * slider model will not generate ChangeEvents while + * valueIsAdjusting is true. + */ + public void setValueIsAdjustingAt(int index, boolean b) { + BoundedRangeModel m = getModelAt(index); + boolean oldValue = m.getValueIsAdjusting(); + m.setValueIsAdjusting(b); + + if ((oldValue != b) && (accessibleContext != null)) { + accessibleContext.firePropertyChange( + AccessibleContext.ACCESSIBLE_STATE_PROPERTY, + ((oldValue) ? AccessibleState.BUSY : null), + ((b) ? AccessibleState.BUSY : null)); + } + } + + /*** + * Sets the size of the range "covered" by the knob. Most look + * and feel implementations will change the value by this amount + * if the user clicks on either side of the knob. + * + * @see #getExtent + * @see BoundedRangeModel#setExtent + * @beaninfo + * expert: true + * description: Size of the range covered by the knob. + */ + public void setExtent(int extent) { + int count = getNumberOfThumbs(); + for (int i = 0; i < count; i++) { + getModelAt(i).setExtent(extent); + } + } + + + /*** + * Sets a bounded attribute of a slider thumb. + *

+     * 
+ * + * @param b + * @return void + */ + public void setBounded(boolean b) { + this.bounded = b; + } + + /*** + * Returns a bounded attribute of a slider thumb. + *
+     * 
+ * + * @return boolean + */ + public boolean isBounded() { + return this.bounded; + } + + public int getValueBeforeStateChange() { + return this.valueBeforeStateChange; + } + + void setValueBeforeStateChange(int v) { + this.valueBeforeStateChange = v; + } +} + + + diff --git a/swing/src/net/sf/openrocket/gui/widgets/MultiSliderUI.java b/swing/src/net/sf/openrocket/gui/widgets/MultiSliderUI.java new file mode 100644 index 000000000..5bbfa31d4 --- /dev/null +++ b/swing/src/net/sf/openrocket/gui/widgets/MultiSliderUI.java @@ -0,0 +1,612 @@ +package net.sf.openrocket.gui.widgets; +/* ------------------------------------------------------------------- + * GeoVISTA Center (Penn State, Dept. of Geography) + * + * Java source file for the class MultiSliderUI + * + * Copyright (c), 1999 - 2002, Masahiro Takatsuka and GeoVISTA Center + * All Rights Researved. + * + * Original Author: Masahiro Takatsuka + * $Author: eytanadar $ + * + * $Date: 2005/10/05 20:19:52 $ + * + * + * Reference: Document no: + * ___ ___ + * + * To Do: + * ___ + * +------------------------------------------------------------------- */ + +/* --------------------------- Package ---------------------------- */ +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; + +import javax.swing.AbstractAction; +import javax.swing.BoundedRangeModel; +import javax.swing.JComponent; +import javax.swing.JSlider; +import javax.swing.plaf.ComponentUI; +import javax.swing.plaf.basic.BasicSliderUI; +import javax.swing.plaf.metal.MetalSliderUI; + +/*==================================================================== + Implementation of class MultiSliderUI + ====================================================================*/ +/*** + * A Basic L&F implementation of SliderUI. + * + * @version $Revision: 1.1 $ + * @author Masahiro Takatsuka (masa@jbeans.net) + * @see MetalSliderUI + */ + +class MultiSliderUI extends BasicSliderUI { + private Rectangle[] thumbRects = null; + + private int thumbCount; + transient private int currentIndex = 0; + + transient private boolean isDragging; + transient private int[] minmaxIndices = new int[2]; + + /*** + * ComponentUI Interface Implementation methods + */ + public static ComponentUI createUI(JComponent b) { + return new MultiSliderUI(); + } + + /*** + * Construct a new MultiSliderUI object. + */ + public MultiSliderUI() { + super(null); + } + + int getTrackBuffer() { + return this.trackBuffer; + } + + /*** + * Sets the number of Thumbs. + */ + public void setThumbCount(int count) { + this.thumbCount = count; + } + + /*** + * Returns the index number of the thumb currently operated. + */ + protected int getCurrentIndex() { + return this.currentIndex; + } + + public void installUI(JComponent c) { + this.thumbRects = new Rectangle[this.thumbCount]; + for (int i = 0; i < this.thumbCount; i++) { + this.thumbRects[i] = new Rectangle(); + } + this.currentIndex = 0; + if (this.thumbCount > 0) { + thumbRect = this.thumbRects[this.currentIndex]; + } + super.installUI(c); + } + + public void uninstallUI(JComponent c) { + super.uninstallUI(c); + for (int i = 0; i < this.thumbCount; i++) { + this.thumbRects[i] = null; + } + this.thumbRects = null; + } + + protected void installListeners( JSlider slider ) { + slider.addMouseListener(trackListener); + slider.addMouseMotionListener(trackListener); + slider.addFocusListener(focusListener); + slider.addComponentListener(componentListener); + slider.addPropertyChangeListener( propertyChangeListener ); + for (int i = 0; i < this.thumbCount; i++) { + ((MultiSlider)slider).getModelAt(i).addChangeListener(changeListener); + } + } + + protected void uninstallListeners( JSlider slider ) { + slider.removeMouseListener(trackListener); + slider.removeMouseMotionListener(trackListener); + slider.removeFocusListener(focusListener); + slider.removeComponentListener(componentListener); + slider.removePropertyChangeListener( propertyChangeListener ); + for (int i = 0; i < this.thumbCount; i++) { + BoundedRangeModel model = ((MultiSlider)slider).getModelAt(i); + if (model != null) { + model.removeChangeListener(changeListener); + } + } + } + + protected void calculateThumbSize() { + Dimension size = getThumbSize(); + for (int i = 0; i < this.thumbCount; i++) { + this.thumbRects[i].setSize(size.width, size.height); + } + thumbRect.setSize(size.width, size.height); + } + + protected void calculateThumbLocation() { + MultiSlider slider = (MultiSlider) this.slider; + int majorTickSpacing = slider.getMajorTickSpacing(); + int minorTickSpacing = slider.getMinorTickSpacing(); + int tickSpacing = 0; + + if (minorTickSpacing > 0) { + tickSpacing = minorTickSpacing; + } else if (majorTickSpacing > 0) { + tickSpacing = majorTickSpacing; + } + for (int i = 0; i < this.thumbCount; i++) { + if (slider.getSnapToTicks()) { + int sliderValue = slider.getValueAt(i); + int snappedValue = sliderValue; + if (tickSpacing != 0) { + // If it's not on a tick, change the value + if ((sliderValue - slider.getMinimum()) % tickSpacing != 0 ) { + float temp = (float)(sliderValue - slider.getMinimum()) / (float)tickSpacing; + int whichTick = Math.round(temp); + snappedValue = slider.getMinimum() + (whichTick * tickSpacing); + } + + if( snappedValue != sliderValue ) { + slider.setValueAt(i, snappedValue); + } + } + } + + if (slider.getOrientation() == JSlider.HORIZONTAL) { + int valuePosition = xPositionForValue(slider.getValueAt(i)); + this.thumbRects[i].x = valuePosition - (this.thumbRects[i].width / 2); + this.thumbRects[i].y = trackRect.y; + } else { + int valuePosition = yPositionForValue(slider.getValueAt(i)); + this.thumbRects[i].x = trackRect.x; + this.thumbRects[i].y = valuePosition - (this.thumbRects[i].height / 2); + } + } + } + + public void paint(Graphics g, JComponent c) { + recalculateIfInsetsChanged(); + recalculateIfOrientationChanged(); + Rectangle clip = g.getClipBounds(); + + if (slider.getPaintTrack() && clip.intersects(trackRect)) { + paintTrack( g ); + } + if (slider.getPaintTicks() && clip.intersects(tickRect)) { + paintTicks( g ); + } + if (slider.getPaintLabels() && clip.intersects(labelRect)) { + paintLabels( g ); + } + if (slider.hasFocus() && clip.intersects(focusRect)) { + paintFocus( g ); + } + + // first paint unfocused thumbs. + for (int i = 0; i < this.thumbCount; i++) { + if (i != this.currentIndex) { + if (clip.intersects(this.thumbRects[i])) { + thumbRect = this.thumbRects[i]; + paintThumb(g); + } + } + } + // then paint currently focused thumb. + if (clip.intersects(this.thumbRects[this.currentIndex])) { + thumbRect = this.thumbRects[this.currentIndex]; + paintThumb(g); + } + } + + public void paintThumb(Graphics g) { + super.paintThumb(g); + } + + public void paintTrack(Graphics g) { + super.paintTrack(g); + } + + public void scrollByBlock(int direction) { + synchronized(slider) { + int oldValue = ((MultiSlider)slider).getValueAt(this.currentIndex); + int blockIncrement = slider.getMaximum() / 10; + int delta = blockIncrement * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL); + ((MultiSlider)slider).setValueAt(this.currentIndex, oldValue + delta); + } + } + + public void scrollByUnit(int direction) { + synchronized(slider) { + int oldValue = ((MultiSlider)slider).getValueAt(this.currentIndex); + int delta = 1 * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL); + ((MultiSlider)slider).setValueAt(this.currentIndex, oldValue + delta); + } + } + + protected TrackListener createTrackListener( JSlider slider ) { + return new MultiTrackListener(); + } + + /*** + * Track Listener Class tracks mouse movements. + */ + class MultiTrackListener extends BasicSliderUI.TrackListener { + int _trackTop; + int _trackBottom; + int _trackLeft; + int _trackRight; + transient private int[] firstXY = new int[2]; + + /*** + * If the mouse is pressed above the "thumb" component + * then reduce the scrollbars value by one page ("page up"), + * otherwise increase it by one page. If there is no + * thumb then page up if the mouse is in the upper half + * of the track. + */ + public void mousePressed(MouseEvent e) { + int[] neighbours = new int[2]; + boolean bounded = ((MultiSlider)slider).isBounded(); + if (!slider.isEnabled()) { + return; + } + + currentMouseX = e.getX(); + currentMouseY = e.getY(); + firstXY[0] = currentMouseX; + firstXY[1] = currentMouseY; + + slider.requestFocus(); + // Clicked in the Thumb area? + minmaxIndices[0] = -1; + minmaxIndices[1] = -1; + for (int i = 0; i < MultiSliderUI.this.thumbCount; i++) { + if (MultiSliderUI.this.thumbRects[i].contains(currentMouseX, currentMouseY)) { + if (minmaxIndices[0] == -1) { + minmaxIndices[0] = i; + MultiSliderUI.this.currentIndex = i; + } + if (minmaxIndices[1] < i) { + minmaxIndices[1] = i; + } + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + offset = currentMouseY - MultiSliderUI.this.thumbRects[i].y; + break; + case JSlider.HORIZONTAL: + offset = currentMouseX - MultiSliderUI.this.thumbRects[i].x; + break; + } + MultiSliderUI.this.isDragging = true; + thumbRect = MultiSliderUI.this.thumbRects[i]; + if (bounded) { + neighbours[0] = ((i - 1) < 0) ? -1 : (i - 1); + neighbours[1] = ((i + 1) >= MultiSliderUI.this.thumbCount) ? -1 : (i + 1); + //findClosest(currentMouseX, currentMouseY, neighbours, i); + } else { + MultiSliderUI.this.currentIndex = i; + ((MultiSlider)slider).setValueIsAdjustingAt(i, true); + neighbours[0] = -1; + neighbours[1] = -1; + } + setThumbBounds(neighbours); + //return; + } + } + if (minmaxIndices[0] > -1) { + return; + } + + MultiSliderUI.this.currentIndex = findClosest(currentMouseX, currentMouseY, neighbours, -1); + thumbRect = MultiSliderUI.this.thumbRects[MultiSliderUI.this.currentIndex]; + MultiSliderUI.this.isDragging = false; + ((MultiSlider)slider).setValueIsAdjustingAt(MultiSliderUI.this.currentIndex, true); + + Dimension sbSize = slider.getSize(); + int direction = POSITIVE_SCROLL; + + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + if (thumbRect.isEmpty()) { + int scrollbarCenter = sbSize.height / 2; + if (!drawInverted()) { + direction = (currentMouseY < scrollbarCenter) ? POSITIVE_SCROLL : NEGATIVE_SCROLL; + } else { + direction = (currentMouseY < scrollbarCenter) ? NEGATIVE_SCROLL : POSITIVE_SCROLL; + } + } else { + int thumbY = thumbRect.y; + if (!drawInverted()) { + direction = (currentMouseY < thumbY) ? POSITIVE_SCROLL : NEGATIVE_SCROLL; + } else { + direction = (currentMouseY < thumbY) ? NEGATIVE_SCROLL : POSITIVE_SCROLL; + } + } + break; + case JSlider.HORIZONTAL: + if (thumbRect.isEmpty() ) { + int scrollbarCenter = sbSize.width / 2; + if (!drawInverted()) { + direction = (currentMouseX < scrollbarCenter) ? NEGATIVE_SCROLL : POSITIVE_SCROLL; + } else { + direction = (currentMouseX < scrollbarCenter) ? POSITIVE_SCROLL : NEGATIVE_SCROLL; + } + } else { + int thumbX = thumbRect.x; + if (!drawInverted()) { + direction = (currentMouseX < thumbX) ? NEGATIVE_SCROLL : POSITIVE_SCROLL; + } else { + direction = (currentMouseX < thumbX) ? POSITIVE_SCROLL : NEGATIVE_SCROLL; + } + } + break; + } + scrollDueToClickInTrack(direction); + Rectangle r = thumbRect; + if ( !r.contains(currentMouseX, currentMouseY) ) { + if (shouldScroll(direction) ) { + scrollTimer.stop(); + scrollListener.setDirection(direction); + scrollTimer.start(); + } + } + } + + /*** + * Sets a track bound for th thumb currently operated. + */ + private void setThumbBounds(int[] neighbours) { + int halfThumbWidth = thumbRect.width / 2; + int halfThumbHeight = thumbRect.height / 2; + + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + _trackTop = (neighbours[1] == -1) ? trackRect.y : MultiSliderUI.this.thumbRects[neighbours[1]].y + halfThumbHeight; + _trackBottom = (neighbours[0] == -1) ? trackRect.y + (trackRect.height - 1) : MultiSliderUI.this.thumbRects[neighbours[0]].y + halfThumbHeight; + break; + case JSlider.HORIZONTAL: + _trackLeft = (neighbours[0] == -1) ? trackRect.x : MultiSliderUI.this.thumbRects[neighbours[0]].x + halfThumbWidth; + _trackRight = (neighbours[1] == -1) ? trackRect.x + (trackRect.width - 1) : MultiSliderUI.this.thumbRects[neighbours[1]].x + halfThumbWidth; + break; + } + } + + /* + * this is a very lazy way to find the closest. One might want to + * implement a much faster algorithm. + */ + private int findClosest(int x, int y, int[] neighbours, int excluded) { + int orientation = slider.getOrientation(); + int rightmin = Integer.MAX_VALUE; // for dxw, dy + int leftmin = -Integer.MAX_VALUE; // for dx, dyh + int dx = 0; + int dxw = 0; + int dy = 0; + int dyh = 0; + neighbours[0] = -1; // left + neighbours[1] = -1; // right + for (int i = 0; i < MultiSliderUI.this.thumbCount; i++) { + if (i == excluded) { + continue; + } + switch (orientation) { + case JSlider.VERTICAL: + dy = MultiSliderUI.this.thumbRects[i].y - y; + dyh = (MultiSliderUI.this.thumbRects[i].y + MultiSliderUI.this.thumbRects[i].height) - y; + if (dyh <= 0) { + if (dyh > leftmin) { // has to be > and not >= + leftmin = dyh; + neighbours[0] = i; + } + } + if (dy >= 0) { + if (dy <= rightmin) { + rightmin = dy; + neighbours[1] = i; + } + } + break; + case JSlider.HORIZONTAL: + dx = MultiSliderUI.this.thumbRects[i].x - x; + dxw = (MultiSliderUI.this.thumbRects[i].x + MultiSliderUI.this.thumbRects[i].width) - x; + if (dxw <= 0) { + if (dxw >= leftmin) { + leftmin = dxw; + neighbours[0] = i; + } + } + if (dx >= 0) { + if (dx < rightmin) { // has to be < and not <= + rightmin = dx; + neighbours[1] = i; + } + } + break; + } + } + //System.out.println("neighbours = " + neighbours[0] + ", " + neighbours[1]); + int closest = (Math.abs(leftmin) <= Math.abs(rightmin)) ? neighbours[0] : neighbours[1]; + return (closest == -1) ? 0 : closest; + } + + /*** + * Set the models value to the position of the top/left + * of the thumb relative to the origin of the track. + */ + public void mouseDragged( MouseEvent e ) { + ((MultiSlider) MultiSliderUI.this.slider).setValueBeforeStateChange(((MultiSlider) MultiSliderUI.this.slider).getValueAt(MultiSliderUI.this.currentIndex)); + int thumbMiddle = 0; + boolean bounded = ((MultiSlider)slider).isBounded(); + + if (!slider.isEnabled()) { + return; + } + + currentMouseX = e.getX(); + currentMouseY = e.getY(); + + if (! MultiSliderUI.this.isDragging) { + return; + } + + switch (slider.getOrientation()) { + case JSlider.VERTICAL: + int halfThumbHeight = thumbRect.height / 2; + int thumbTop = e.getY() - offset; + if (bounded) { + int[] neighbours = new int[2]; + int idx = -1; + int diff = e.getY() - firstXY[1]; + //System.out.println("diff = " + diff); + if (e.getY() - firstXY[1] > 0) { + idx = minmaxIndices[0]; + } else { + idx = minmaxIndices[1]; + } + minmaxIndices[0] = minmaxIndices[1] = idx; + //System.out.println("idx = " + idx); + if (idx == -1) { + break; + } + + //System.out.println("thumbTop = " + thumbTop); + neighbours[0] = ((idx - 1) < 0) ? -1 : (idx - 1); + neighbours[1] = ((idx + 1) >= MultiSliderUI.this.thumbCount) ? -1 : (idx + 1); + thumbRect = MultiSliderUI.this.thumbRects[idx]; + MultiSliderUI.this.currentIndex = idx; + ((MultiSlider)slider).setValueIsAdjustingAt(idx, true); + setThumbBounds(neighbours); + } + + thumbTop = Math.max(thumbTop, _trackTop - halfThumbHeight); + thumbTop = Math.min(thumbTop, _trackBottom - halfThumbHeight); + + setThumbLocation(thumbRect.x, thumbTop); + + thumbMiddle = thumbTop + halfThumbHeight; + ((MultiSlider)slider).setValueAt(MultiSliderUI.this.currentIndex, valueForYPosition(thumbMiddle) ); + break; + case JSlider.HORIZONTAL: + int halfThumbWidth = thumbRect.width / 2; + int thumbLeft = e.getX() - offset; + if (bounded) { + int[] neighbours = new int[2]; + int idx = -1; + if (e.getX() - firstXY[0] <= 0) { + idx = minmaxIndices[0]; + } else { + idx = minmaxIndices[1]; + } + minmaxIndices[0] = minmaxIndices[1] = idx; + //System.out.println("idx = " + idx); + if (idx == -1) { + break; + } + //System.out.println("thumbLeft = " + thumbLeft); + neighbours[0] = ((idx - 1) < 0) ? -1 : (idx - 1); + neighbours[1] = ((idx + 1) >= MultiSliderUI.this.thumbCount) ? -1 : (idx + 1); + thumbRect = MultiSliderUI.this.thumbRects[idx]; + MultiSliderUI.this.currentIndex = idx; + ((MultiSlider)slider).setValueIsAdjustingAt(idx, true); + setThumbBounds(neighbours); + } + + thumbLeft = Math.max(thumbLeft, _trackLeft - halfThumbWidth); + thumbLeft = Math.min(thumbLeft, _trackRight - halfThumbWidth); + + setThumbLocation(thumbLeft, thumbRect.y); + + thumbMiddle = thumbLeft + halfThumbWidth; + + ((MultiSlider)slider).setValueAt(MultiSliderUI.this.currentIndex, valueForXPosition(thumbMiddle)); + break; + default: + return; + } + } + + public void mouseReleased(MouseEvent e) { + if (!slider.isEnabled()) { + return; + } + + offset = 0; + scrollTimer.stop(); + + if (slider.getSnapToTicks()) { + MultiSliderUI.this.isDragging = false; + ((MultiSlider)slider).setValueIsAdjustingAt(MultiSliderUI.this.currentIndex, false); + } else { + ((MultiSlider)slider).setValueIsAdjustingAt(MultiSliderUI.this.currentIndex, false); + MultiSliderUI.this.isDragging = false; + } + + slider.repaint(); + } + } + + /*** + * A static version of the above. + */ + static class SharedActionScroller extends AbstractAction { + int _dir; + boolean _block; + + public SharedActionScroller(int dir, boolean block) { + _dir = dir; + _block = block; + } + + public void actionPerformed(ActionEvent e) { + JSlider slider = (JSlider)e.getSource(); + MultiSliderUI ui = (MultiSliderUI)slider.getUI(); + if ( _dir == NEGATIVE_SCROLL || _dir == POSITIVE_SCROLL ) { + int realDir = _dir; + if (slider.getInverted()) { + realDir = _dir == NEGATIVE_SCROLL ? POSITIVE_SCROLL : NEGATIVE_SCROLL; + } + if (_block) { + ui.scrollByBlock(realDir); + } else { + ui.scrollByUnit(realDir); + } + } else { + if (slider.getInverted()) { + if (_dir == MIN_SCROLL) { + ((MultiSlider)slider).setValueAt(ui.currentIndex, + slider.getMaximum()); + } else if (_dir == MAX_SCROLL) { + ((MultiSlider)slider).setValueAt(ui.currentIndex, + slider.getMinimum()); + } + } else { + if (_dir == MIN_SCROLL) { + ((MultiSlider)slider).setValueAt(ui.currentIndex, + slider.getMinimum()); + } else if (_dir == MAX_SCROLL) { + ((MultiSlider)slider).setValueAt(ui.currentIndex, + slider.getMaximum()); + } + } + } + } + } +}