diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 7aa9fd2f7..d45d6bb50 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -20,9 +20,9 @@ body:
label: To Reproduce
description: "Steps to reproduce the behavior:"
value: |
- 1.
- 2.
- 3.
+ 1.
+ 2.
+ 3.
...
validations:
required: true
@@ -32,11 +32,11 @@ body:
label: Screenshots / .ork file
description: Provide screenshots for clarification and/or the .ork file that caused the issue.
value: |
- ## Screenshot(s):
+ #### Screenshot(s):
*(drag-and-drop the screenshot(s) here)*
- ## .ork file:
+ #### .ork file:
*(drag-and-drop the file here as a .zip file)*
diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties
index a33d04389..73d919e16 100644
--- a/core/resources/l10n/messages.properties
+++ b/core/resources/l10n/messages.properties
@@ -323,6 +323,8 @@ pref.dlg.checkbox.AlwaysOpenLeftmost = Always open leftmost tab when opening a c
pref.dlg.checkbox.AlwaysOpenLeftmost.ttip = If checked, a component edit dialog will always pop up with the first tab selected.
If unchecked, the previous selected tab will be used.
pref.dlg.checkbox.ShowDiscardConfirmation = Show confirmation dialog for discarding component changes
pref.dlg.checkbox.ShowDiscardConfirmation.ttip = If checked, you will be asked if you want really want to discard component configuration configuration changes.
+pref.dlg.checkbox.ShowDiscardSimulationConfirmation = Show confirmation dialog for discarding simulation changes
+pref.dlg.checkbox.ShowDiscardSimulationConfirmation.ttip = If checked, you will be asked if you want really want to discard simulation configuration configuration changes.
pref.dlg.lbl.User-definedthrust = User-defined thrust curves:
pref.dlg.lbl.Windspeed = Wind speed
pref.dlg.Allthrustcurvefiles = All thrust curve files (*.eng; *.rse; *.zip; directories)
@@ -422,6 +424,8 @@ simedtdlg.but.savedefault = Save as default
simedtdlg.but.add = Add
simedtdlg.but.delete = Delete
simedtdlg.title.Editsim = Edit simulation
+simedtdlg.title.MultiSimEdit = Multi-simulation edit
+simedtdlg.title.MultiSimEdit.ttip = You are editing the following simulations:
simedtdlg.lbl.Simname = Simulation name:
simedtdlg.tab.Launchcond = Launch conditions
simedtdlg.tab.Simopt = Simulation options
@@ -531,6 +535,12 @@ SimulationEditDialog.btn.export = Export
SimulationEditDialog.btn.edit = Edit
SimulationEditDialog.btn.simulate = Simulate
SimulationEditDialog.btn.simulateAndPlot = Simulate & Plot
+SimulationEditDialog.btn.OK.ttip = Keep changes and close the dialog
+SimulationEditDialog.btn.Cancel.ttip = Discard changes and close the dialog
+SimulationEditDialog.CancelOperation.msg.discardChanges = Are you sure you want to discard your changes to this simulation?
+SimulationEditDialog.CancelOperation.msg.undoAdd = Are you sure you want to undo adding this simulation?
+SimulationEditDialog.CancelOperation.title = Cancel operation
+SimulationEditDialog.CancelOperation.checkbox.dontAskAgain = Don't ask me again
GeodeticComputationStrategy.flat.name = Flat Earth
GeodeticComputationStrategy.flat.desc = Perform computations with a flat Earth approximation. Sufficient for low-altitude flights.
@@ -2349,6 +2359,7 @@ PhotoSettingsConfig.lbl.lightAz = Light Azimuth
PhotoSettingsConfig.lbl.lightAlt = Light Altitude
PhotoSettingsConfig.lbl.sky = Sky
PhotoSettingsConfig.lbl.skyColor = Sky Color
+PhotoSettingsConfig.lbl.skyColorOpacity = Sky Color Opacity
PhotoSettingsConfig.lbl.skyImage = Sky Image
PhotoSettingsConfig.lbl.skyCredit = Image Credit
diff --git a/core/src/net/sf/openrocket/document/Simulation.java b/core/src/net/sf/openrocket/document/Simulation.java
index 068fc91c8..2eca7fd54 100644
--- a/core/src/net/sf/openrocket/document/Simulation.java
+++ b/core/src/net/sf/openrocket/document/Simulation.java
@@ -268,6 +268,18 @@ public class Simulation implements ChangeSource, Cloneable {
mutex.verify();
return simulationExtensions;
}
+
+ /**
+ * Applies the simulation extensions to the simulation.
+ * @param extensions the simulation extensions to apply.
+ */
+ public void copyExtensionsFrom(List extensions) {
+ if (extensions == null) {
+ return;
+ }
+ this.simulationExtensions.clear();
+ this.simulationExtensions.addAll(extensions);
+ }
/**
@@ -349,6 +361,7 @@ public class Simulation implements ChangeSource, Cloneable {
*/
public void syncModID() {
this.simulatedConfigurationID = getActiveConfiguration().getModID();
+ fireChangeEvent();
}
@@ -522,6 +535,67 @@ public class Simulation implements ChangeSource, Cloneable {
mutex.unlock("copy");
}
}
+
+ public Simulation clone() {
+ mutex.lock("clone");
+ try {
+ Simulation clone = (Simulation) super.clone();
+
+ clone.mutex = SafetyMutex.newInstance();
+ clone.name = this.name;
+ clone.configId = this.configId;
+ clone.simulatedConfigurationDescription = this.simulatedConfigurationDescription;
+ clone.simulatedConfigurationID = this.simulatedConfigurationID;
+ clone.options = this.options.clone();
+ clone.listeners = new ArrayList<>();
+ if (this.simulatedConditions != null) {
+ clone.simulatedConditions = this.simulatedConditions.clone();
+ } else {
+ clone.simulatedConditions = null;
+ }
+ clone.simulationExtensions = new ArrayList<>();
+ for (SimulationExtension c : this.simulationExtensions) {
+ clone.simulationExtensions.add(c.clone());
+ }
+ clone.status = this.status;
+ clone.simulatedData = this.simulatedData != null ? this.simulatedData.clone() : this.simulatedData;
+ clone.simulationStepperClass = this.simulationStepperClass;
+ clone.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass;
+
+ return clone;
+ } catch (CloneNotSupportedException e) {
+ throw new BugException("Clone not supported, BUG", e);
+ } finally {
+ mutex.unlock("clone");
+ }
+ }
+
+ /**
+ * Load the data from the specified simulation into this simulation.
+ * @param simulation the simulation to load from.
+ */
+ public void loadFrom(Simulation simulation) {
+ mutex.lock("loadFrom");
+ try {
+ this.name = simulation.name;
+ this.configId = simulation.configId;
+ this.simulatedConfigurationDescription = simulation.simulatedConfigurationDescription;
+ this.simulatedConfigurationID = simulation.simulatedConfigurationID;
+ this.options.copyConditionsFrom(simulation.options);
+ if (simulation.simulatedConditions == null) {
+ this.simulatedConditions = null;
+ } else {
+ this.simulatedConditions.copyConditionsFrom(simulation.simulatedConditions);
+ }
+ copyExtensionsFrom(simulation.getSimulationExtensions());
+ this.status = simulation.status;
+ this.simulatedData = simulation.simulatedData;
+ this.simulationStepperClass = simulation.simulationStepperClass;
+ this.aerodynamicCalculatorClass = simulation.aerodynamicCalculatorClass;
+ } finally {
+ mutex.unlock("loadFrom");
+ }
+ }
/**
@@ -541,7 +615,7 @@ public class Simulation implements ChangeSource, Cloneable {
final Simulation newSim = new Simulation(this.document, newRocket);
newSim.name = this.name;
newSim.configId = this.configId;
- newSim.options.copyFrom(this.options);
+ newSim.options.copyConditionsFrom(this.options);
newSim.simulatedConfigurationDescription = this.simulatedConfigurationDescription;
for (SimulationExtension c : this.simulationExtensions) {
newSim.simulationExtensions.add(c.clone());
@@ -584,15 +658,9 @@ public class Simulation implements ChangeSource, Cloneable {
private class ConditionListener implements StateChangeListener {
-
- private Status oldStatus = null;
-
@Override
public void stateChanged(EventObject e) {
- if (getStatus() != oldStatus) {
- oldStatus = getStatus();
- fireChangeEvent();
- }
+ fireChangeEvent();
}
}
diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java
index 3e9c82389..108eb5667 100644
--- a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java
+++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java
@@ -488,7 +488,6 @@ public class OpenRocketSaver extends RocketSaver {
for (int i = 0; i < types.length; i++) {
data.add(branch.get(types[i]));
}
- List timeData = branch.get(FlightDataType.TYPE_TIME);
// Build the tag
StringBuilder sb = new StringBuilder();
diff --git a/core/src/net/sf/openrocket/simulation/FlightData.java b/core/src/net/sf/openrocket/simulation/FlightData.java
index 27f6c1318..0bc41ae76 100644
--- a/core/src/net/sf/openrocket/simulation/FlightData.java
+++ b/core/src/net/sf/openrocket/simulation/FlightData.java
@@ -273,7 +273,23 @@ public class FlightData {
return mutable.isMutable();
}
-
+ public FlightData clone() {
+ FlightData clone = new FlightData();
+ clone.warnings.addAll(warnings);
+ for (FlightDataBranch b : branches) {
+ clone.branches.add(b.clone());
+ }
+ clone.maxAltitude = maxAltitude;
+ clone.maxVelocity = maxVelocity;
+ clone.maxAcceleration = maxAcceleration;
+ clone.maxMachNumber = maxMachNumber;
+ clone.timeToApogee = timeToApogee;
+ clone.flightTime = flightTime;
+ clone.groundHitVelocity = groundHitVelocity;
+ clone.launchRodVelocity = launchRodVelocity;
+ clone.deploymentVelocity = deploymentVelocity;
+ return clone;
+ }
/**
* Find the maximum acceleration before apogee.
diff --git a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java
index 77bc60f56..e2547e4c4 100644
--- a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java
+++ b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java
@@ -363,5 +363,20 @@ public class FlightDataBranch implements Monitorable {
public int getModID() {
return modID;
}
+
+ public FlightDataBranch clone() {
+ FlightDataType[] types = getTypes();
+ FlightDataBranch clone = new FlightDataBranch(branchName, types);
+ for (FlightDataType type : values.keySet()) {
+ clone.values.put(type, values.get(type).clone());
+ }
+ clone.minValues.putAll(minValues);
+ clone.maxValues.putAll(maxValues);
+ clone.events.addAll(events);
+ clone.timeToOptimumAltitude = timeToOptimumAltitude;
+ clone.optimumAltitude = optimumAltitude;
+ clone.modID = modID;
+ return clone;
+ }
}
diff --git a/core/src/net/sf/openrocket/simulation/SimulationOptions.java b/core/src/net/sf/openrocket/simulation/SimulationOptions.java
index 84c20313e..19c769903 100644
--- a/core/src/net/sf/openrocket/simulation/SimulationOptions.java
+++ b/core/src/net/sf/openrocket/simulation/SimulationOptions.java
@@ -96,6 +96,7 @@ public class SimulationOptions implements ChangeSource, Cloneable {
if (MathUtil.equals(this.launchRodLength, launchRodLength))
return;
this.launchRodLength = launchRodLength;
+ fireChangeEvent();
}
@@ -104,7 +105,10 @@ public class SimulationOptions implements ChangeSource, Cloneable {
}
public void setLaunchIntoWind(boolean i) {
+ if (launchIntoWind == i)
+ return;
launchIntoWind = i;
+ fireChangeEvent();
}
public double getLaunchRodAngle() {
@@ -404,28 +408,6 @@ public class SimulationOptions implements ChangeSource, Cloneable {
}
}
-
- public void copyFrom(SimulationOptions src) {
-
- this.launchAltitude = src.launchAltitude;
- this.launchLatitude = src.launchLatitude;
- this.launchLongitude = src.launchLongitude;
- this.launchPressure = src.launchPressure;
- this.launchRodAngle = src.launchRodAngle;
- this.launchRodDirection = src.launchRodDirection;
- this.launchRodLength = src.launchRodLength;
- this.launchTemperature = src.launchTemperature;
- this.maximumAngle = src.maximumAngle;
- this.timeStep = src.timeStep;
- this.windAverage = src.windAverage;
- this.windTurbulence = src.windTurbulence;
- this.windDirection = src.windDirection;
- this.calculateExtras = src.calculateExtras;
- this.randomSeed = src.randomSeed;
-
- fireChangeEvent();
- }
-
public void copyConditionsFrom(SimulationOptions src) {
// Be a little smart about triggering the change event.
// only do it if one of the "important" (user specified) parameters has really changed.
@@ -550,6 +532,10 @@ public class SimulationOptions implements ChangeSource, Cloneable {
public void removeChangeListener(StateChangeListener listener) {
listeners.remove(listener);
}
+
+ public List getChangeListeners() {
+ return listeners;
+ }
private final EventObject event = new EventObject(this);
diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java
index a3dfedc3f..8cf591ea6 100644
--- a/core/src/net/sf/openrocket/startup/Preferences.java
+++ b/core/src/net/sf/openrocket/startup/Preferences.java
@@ -54,7 +54,7 @@ public abstract class Preferences implements ChangeSource {
public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter";
public static final String USER_LOCAL = "locale";
public static final String DEFAULT_DIRECTORY = "defaultDirectory";
-
+
public static final String PLOT_SHOW_POINTS = "ShowPlotPoints";
private static final String IGNORE_WELCOME = "IgnoreWelcome";
@@ -78,6 +78,7 @@ public abstract class Preferences implements ChangeSource {
private static final String AUTO_OPEN_LAST_DESIGN = "AutoOpenLastDesign";
private static final String OPEN_LEFTMOST_DESIGN_TAB = "OpenLeftmostDesignTab";
private static final String SHOW_DISCARD_CONFIRMATION = "IgnoreDiscardEditingWarning";
+ private static final String SHOW_DISCARD_SIMULATION_CONFIRMATION = "IgnoreDiscardSimulationEditingWarning";
public static final String MARKER_STYLE_ICON = "MarkerStyleIcon";
private static final String SHOW_MARKERS = "ShowMarkers";
private static final String SHOW_ROCKSIM_FORMAT_WARNING = "ShowRocksimFormatWarning";
diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preferences/DesignPreferencesPanel.java b/swing/src/net/sf/openrocket/gui/dialogs/preferences/DesignPreferencesPanel.java
index c91962d7c..74dcf1580 100644
--- a/swing/src/net/sf/openrocket/gui/dialogs/preferences/DesignPreferencesPanel.java
+++ b/swing/src/net/sf/openrocket/gui/dialogs/preferences/DesignPreferencesPanel.java
@@ -115,6 +115,19 @@ public class DesignPreferencesPanel extends PreferencesPanel {
});
this.add(showDiscardConfirmation, "wrap, growx, spanx");
+ // // Show confirmation dialog for discarding simulation configuration changes
+ final JCheckBox showDiscardSimulationConfirmation = new JCheckBox(
+ trans.get("pref.dlg.checkbox.ShowDiscardSimulationConfirmation"));
+ showDiscardSimulationConfirmation.setSelected(preferences.isShowDiscardSimulationConfirmation());
+ showDiscardSimulationConfirmation.setToolTipText(trans.get("pref.dlg.checkbox.ShowDiscardSimulationConfirmation.ttip"));
+ showDiscardSimulationConfirmation.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ preferences.setShowDiscardSimulationConfirmation(e.getStateChange() == ItemEvent.SELECTED);
+ }
+ });
+ this.add(showDiscardSimulationConfirmation, "wrap, growx, spanx");
+
// // Update flight estimates in the design window
final JCheckBox updateEstimates = new JCheckBox(
trans.get("pref.dlg.checkbox.Updateestimates"));
diff --git a/swing/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java
index 5b1065f9b..4dadc0f14 100644
--- a/swing/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java
+++ b/swing/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java
@@ -101,6 +101,7 @@ public class PreferencesDialog extends JDialog {
if (parent != null) {
parent.getRocketPanel().updateExtras();
parent.getRocketPanel().updateFigures();
+ parent.getRocketPanel().updateRulers();
}
}
});
diff --git a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoPanel.java b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoPanel.java
index 4a8461a9e..628d34bd9 100644
--- a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoPanel.java
+++ b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoPanel.java
@@ -156,6 +156,7 @@ public class PhotoPanel extends JPanel implements GLEventListener {
final GLProfile glp = GLProfile.get(GLProfile.GL2);
final GLCapabilities caps = new GLCapabilities(glp);
+ caps.setBackgroundOpaque(false);
if (Application.getPreferences().getBoolean(
Preferences.OPENGL_ENABLE_AA, true)) {
@@ -169,10 +170,12 @@ public class PhotoPanel extends JPanel implements GLEventListener {
Preferences.OPENGL_USE_FBO, false)) {
log.trace("GL - Creating GLJPanel");
canvas = new GLJPanel(caps);
+ ((GLJPanel) canvas).setOpaque(false);
} else {
log.trace("GL - Creating GLCanvas");
canvas = new GLCanvas(caps);
}
+ canvas.setBackground(new java.awt.Color(0, 0, 0, 0));
((GLAutoDrawable) canvas).addGLEventListener(this);
this.add(canvas, BorderLayout.CENTER);
@@ -277,7 +280,7 @@ public class PhotoPanel extends JPanel implements GLEventListener {
if (!imageCallbacks.isEmpty()) {
BufferedImage i = (new AWTGLReadBufferUtil(
- GLProfile.get(GLProfile.GL2), false))
+ GLProfile.get(GLProfile.GL2), true)) // Set the second parameter to true
.readPixelsToBufferedImage(drawable.getGL(), 0, 0,
drawable.getSurfaceWidth(), drawable.getSurfaceHeight(), true);
final Vector cbs = new Vector(
@@ -298,18 +301,41 @@ public class PhotoPanel extends JPanel implements GLEventListener {
out[0] = 1;
out[1] = 1;
out[2] = 0;
+ out[3] = 1;
} else {
out[0] = (float) color.getRed() / 255f;
out[1] = (float) color.getGreen() / 255f;
out[2] = (float) color.getBlue() / 255f;
+ out[3] = (float) color.getAlpha() / 255f;
}
}
+ /**
+ * Blend two colors
+ * @param color1 first color to blend
+ * @param color2 second color to blend
+ * @param ratio blend ratio. 0 = full color 1, 0.5 = mid-blend, 1 = full color 2
+ * @return blended color
+ */
+ private static Color blendColors(Color color1, Color color2, double ratio) {
+ if (ratio < 0 || ratio > 1) {
+ throw new IllegalArgumentException("Blend ratio must be between 0 and 1");
+ }
+
+ double inverseRatio = 1 - ratio;
+
+ int r = (int) ((color1.getRed() * inverseRatio) + (color2.getRed() * ratio));
+ int g = (int) ((color1.getGreen() * inverseRatio) + (color2.getGreen() * ratio));
+ int b = (int) ((color1.getBlue() * inverseRatio) + (color2.getBlue() * ratio));
+
+ return new Color(r, g, b);
+ }
+
private void draw(final GLAutoDrawable drawable, float dx) {
GL2 gl = drawable.getGL().getGL2();
GLU glu = new GLU();
- float[] color = new float[3];
+ float[] color = new float[4];
gl.glEnable(GL.GL_MULTISAMPLE);
@@ -333,8 +359,16 @@ public class PhotoPanel extends JPanel implements GLEventListener {
new float[] { spc * color[0], spc * color[1], spc * color[2], 1 },
0);
- convertColor(p.getSkyColor(), color);
- gl.glClearColor(color[0], color[1], color[2], 1);
+ // Machines that don't use off-screen rendering can't render transparent background, so we create it
+ // artificially by blending the sky color with white (= color that is rendered as transparent background)
+ if (!Application.getPreferences().getBoolean(
+ Preferences.OPENGL_USE_FBO, false)) {
+ convertColor(blendColors(p.getSkyColor(), new Color(255, 255, 255), 1-p.getSkyColorOpacity()),
+ color);
+ } else {
+ convertColor(p.getSkyColor(), color);
+ }
+ gl.glClearColor(color[0], color[1], color[2], color[3]);
gl.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT);
gl.glMatrixMode(GLMatrixFunc.GL_PROJECTION);
diff --git a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettings.java b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettings.java
index 2aecbe434..493cae0f0 100644
--- a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettings.java
+++ b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettings.java
@@ -24,6 +24,7 @@ public class PhotoSettings extends AbstractChangeSource implements FlameSettings
private double ambiance = .3f;
private Color skyColor = new Color(55, 95, 155);
+ private double skyColorOpacity = 1.0;
private boolean motionBlurred = false;
@@ -186,9 +187,20 @@ public class PhotoSettings extends AbstractChangeSource implements FlameSettings
public void setSkyColor(Color skyColor) {
this.skyColor = skyColor;
+ this.skyColorOpacity = skyColor.getAlpha() / 255f;
fireChangeEvent();
}
-
+
+ public double getSkyColorOpacity() {
+ return skyColorOpacity;
+ }
+
+ public void setSkyColorOpacity(double skyColorOpacity) {
+ this.skyColorOpacity = skyColorOpacity;
+ skyColor.setAlpha((int) (skyColorOpacity * 255));
+ fireChangeEvent();
+ }
+
public Color getFlameColor() {
return flameColor;
}
diff --git a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettingsConfig.java b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettingsConfig.java
index a758cc320..e66251414 100644
--- a/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettingsConfig.java
+++ b/swing/src/net/sf/openrocket/gui/figure3d/photo/PhotoSettingsConfig.java
@@ -256,10 +256,10 @@ public class PhotoSettingsConfig extends JTabbedPane {
/// Light altitude
add(new JLabel(trans.get("PhotoSettingsConfig.lbl.lightAlt")));
- DoubleModel lightAltModle = new DoubleModel(p, "LightAlt", UnitGroup.UNITS_ANGLE, -Math.PI / 2, Math.PI / 2);
- add(new EditableSpinner(lightAltModle.getSpinnerModel()), "growx, split 2");
- add(new UnitSelector(lightAltModle));
- add(new BasicSlider(lightAltModle.getSliderModel(-Math.PI / 2, Math.PI / 2)), "wrap");
+ DoubleModel lightAltModel = new DoubleModel(p, "LightAlt", UnitGroup.UNITS_ANGLE, -Math.PI / 2, Math.PI / 2);
+ add(new EditableSpinner(lightAltModel.getSpinnerModel()), "growx, split 2");
+ add(new UnitSelector(lightAltModel));
+ add(new BasicSlider(lightAltModel.getSliderModel(-Math.PI / 2, Math.PI / 2)), "wrap");
// Sky
add(new StyledLabel(trans.get("PhotoSettingsConfig.lbl.sky"), Style.BOLD), "split, span, gapright para");
@@ -269,6 +269,17 @@ public class PhotoSettingsConfig extends JTabbedPane {
add(new JLabel(trans.get("PhotoSettingsConfig.lbl.skyColor")));
add(skyColorButton, "wrap");
+ /// Sky color opacity
+ add(new JLabel(trans.get("PhotoSettingsConfig.lbl.skyColorOpacity")));
+ DoubleModel skyColorOpacityModel = new DoubleModel(p, "SkyColorOpacity", UnitGroup.UNITS_RELATIVE, 0, 1);
+ EditableSpinner skyColorOpacitySpinner = new EditableSpinner(skyColorOpacityModel.getSpinnerModel());
+ add(skyColorOpacitySpinner, "growx, split 2");
+ UnitSelector skyColorOpacityUnitSelector = new UnitSelector(skyColorOpacityModel);
+ add(skyColorOpacityUnitSelector);
+ BasicSlider skyColorOpacitySlider = new BasicSlider(skyColorOpacityModel.getSliderModel());
+ add(skyColorOpacitySlider, "wrap");
+ p.addChangeListener(skyColorOpacityModel);
+
/// Sky image
add(new JLabel(trans.get("PhotoSettingsConfig.lbl.skyImage")));
@@ -293,9 +304,15 @@ public class PhotoSettingsConfig extends JTabbedPane {
if (s instanceof Sky && s != noSky) {
p.setSky((Sky) s);
skyColorButton.setEnabled(false);
+ skyColorOpacitySpinner.setEnabled(false);
+ skyColorOpacityUnitSelector.setEnabled(false);
+ skyColorOpacitySlider.setEnabled(false);
} else if (s == noSky) {
p.setSky(null);
skyColorButton.setEnabled(true);
+ skyColorOpacitySpinner.setEnabled(true);
+ skyColorOpacityUnitSelector.setEnabled(true);
+ skyColorOpacitySlider.setEnabled(true);
}
}
});
diff --git a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
index b1a31377b..a22987392 100644
--- a/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
+++ b/swing/src/net/sf/openrocket/gui/main/SimulationPanel.java
@@ -297,7 +297,7 @@ public class SimulationPanel extends JPanel {
simulationTable.addRowSelectionInterval(n, n);
updatePreviousSelection();
- openDialog(false, sim);
+ openDialog(false, true, sim);
}
private void plotSimulation() {
@@ -595,8 +595,8 @@ public class SimulationPanel extends JPanel {
return simulationTable.getSelectionModel();
}
- private void openDialog(boolean plotMode, final Simulation... sims) {
- SimulationEditDialog d = new SimulationEditDialog(SwingUtilities.getWindowAncestor(this), document, sims);
+ private void openDialog(boolean plotMode, boolean isNewSimulation, final Simulation... sims) {
+ SimulationEditDialog d = new SimulationEditDialog(SwingUtilities.getWindowAncestor(this), document, isNewSimulation, sims);
if (plotMode) {
d.setPlotMode();
}
@@ -605,6 +605,10 @@ public class SimulationPanel extends JPanel {
takeTheSpotlight();
}
+ private void openDialog(boolean plotMode, final Simulation... sims) {
+ openDialog(plotMode, false, sims);
+ }
+
private void openDialog(final Simulation sim) {
boolean plotMode = false;
if (sim.hasSimulationData() && Simulation.isStatusUpToDate(sim.getStatus())) {
diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java
index bc9ebf44e..fda3e62e2 100644
--- a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java
+++ b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java
@@ -271,6 +271,15 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change
figure3d.updateFigure();
}
+ /**
+ * Updates the rulers of the rocket panel to the currently selected default unit.
+ */
+ public void updateRulers() {
+ scrollPane.updateRulerUnit();
+ scrollPane.revalidate();
+ scrollPane.repaint();
+ }
+
private void go3D() {
if (is3d)
return;
diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java b/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java
index a8a38b648..db7ebad82 100644
--- a/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java
+++ b/swing/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java
@@ -15,6 +15,7 @@ import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
+import java.util.EventObject;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
@@ -56,6 +57,7 @@ public class ScaleScrollPane extends JScrollPane
private final AbstractScaleFigure figure;
private DoubleModel rulerUnit;
+ private UnitSelector unitSelector;
private Ruler horizontalRuler;
private Ruler verticalRuler;
@@ -83,7 +85,7 @@ public class ScaleScrollPane extends JScrollPane
this.component = component;
this.figure = (AbstractScaleFigure) component;
-
+
rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH);
rulerUnit.addChangeListener(new ChangeListener() {
@Override
@@ -95,10 +97,10 @@ public class ScaleScrollPane extends JScrollPane
verticalRuler = new Ruler(Ruler.VERTICAL);
this.setColumnHeaderView(horizontalRuler);
this.setRowHeaderView(verticalRuler);
-
- UnitSelector selector = new UnitSelector(rulerUnit);
- selector.setFont(new Font("SansSerif", Font.PLAIN, 8));
- this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector);
+
+ unitSelector = new UnitSelector(rulerUnit);
+ unitSelector.setFont(new Font("SansSerif", Font.PLAIN, 8));
+ this.setCorner(JScrollPane.UPPER_LEFT_CORNER, unitSelector);
this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
@@ -176,6 +178,14 @@ public class ScaleScrollPane extends JScrollPane
public Unit getCurrentUnit() {
return rulerUnit.getCurrentUnit();
}
+
+ /**
+ * Updates the units of the ruler to the default units of the current unit group.
+ */
+ public void updateRulerUnit() {
+ rulerUnit.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit());
+ unitSelector.stateChanged(new EventObject(this));
+ }
public String toViewportString(){
Rectangle view = this.getViewport().getViewRect();
diff --git a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java
index 32effec96..096bfc576 100644
--- a/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java
+++ b/swing/src/net/sf/openrocket/gui/simulation/SimulationEditDialog.java
@@ -2,17 +2,22 @@ package net.sf.openrocket.gui.simulation;
import java.awt.CardLayout;
+import java.awt.Color;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
+import java.util.EventObject;
import javax.swing.JButton;
-import javax.swing.JComboBox;
+import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
+import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.JTextField;
@@ -24,7 +29,9 @@ import javax.swing.event.DocumentListener;
import net.miginfocom.swing.MigLayout;
import net.sf.openrocket.document.OpenRocketDocument;
import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.document.events.DocumentChangeEvent;
import net.sf.openrocket.gui.components.ConfigurationComboBox;
+import net.sf.openrocket.gui.components.StyledLabel;
import net.sf.openrocket.gui.util.GUIUtil;
import net.sf.openrocket.gui.widgets.SelectColorButton;
import net.sf.openrocket.l10n.Translator;
@@ -33,6 +40,8 @@ import net.sf.openrocket.rocketcomponent.FlightConfigurationId;
import net.sf.openrocket.rocketcomponent.Rocket;
import net.sf.openrocket.simulation.extension.SimulationExtension;
import net.sf.openrocket.startup.Application;
+import net.sf.openrocket.startup.Preferences;
+import net.sf.openrocket.util.StateChangeListener;
public class SimulationEditDialog extends JDialog {
@@ -41,19 +50,37 @@ public class SimulationEditDialog extends JDialog {
private final Simulation[] simulationList;
private final OpenRocketDocument document;
private static final Translator trans = Application.getTranslator();
+ private static final Preferences preferences = Application.getPreferences();
JPanel cards;
private final static String EDITMODE = "EDIT";
private final static String PLOTMODE = "PLOT";
- private WindowListener applyChangesToSimsListener;
+ private final WindowListener applyChangesToSimsListener;
+ private final Simulation initialSim; // A copy of the first selected simulation before it was modified
+ private final boolean initialIsSaved; // Whether the document was saved before the dialog was opened
+ private boolean isModified = false; // Whether the simulation has been modified
+ private final boolean isNewSimulation; // Whether you are editing a new simulation, or an existing one
- public SimulationEditDialog(Window parent, final OpenRocketDocument document, Simulation... sims) {
+ public SimulationEditDialog(Window parent, final OpenRocketDocument document, boolean isNewSimulation, Simulation... sims) {
//// Edit simulation
- super(parent, trans.get("simedtdlg.title.Editsim"), JDialog.ModalityType.DOCUMENT_MODAL);
+ super(parent, sims.length == 1 ? trans.get("simedtdlg.title.Editsim") : trans.get("simedtdlg.title.MultiSimEdit"),
+ JDialog.ModalityType.DOCUMENT_MODAL);
this.document = document;
this.parentWindow = parent;
this.simulationList = sims;
+ this.initialSim = simulationList[0].clone();
+ this.initialIsSaved = document.isSaved();
+ this.isNewSimulation = isNewSimulation;
+
+ simulationList[0].addChangeListener(new StateChangeListener() {
+ @Override
+ public void stateChanged(EventObject e) {
+ isModified = true;
+ setTitle("* " + getTitle()); // Add component changed indicator to the title
+ simulationList[0].removeChangeListener(this);
+ }
+ });
this.cards = new JPanel(new CardLayout());
this.add(cards);
@@ -85,6 +112,9 @@ public class SimulationEditDialog extends JDialog {
}
public void setEditMode() {
+ String baseTitle = simulationList.length == 1 ?
+ trans.get("simedtdlg.title.Editsim") : trans.get("simedtdlg.title.MultiSimEdit");
+ setTitle((isModified ? "* " : "") + baseTitle);
CardLayout cl = (CardLayout) (cards.getLayout());
cl.show(cards, EDITMODE);
cards.validate();
@@ -96,7 +126,7 @@ public class SimulationEditDialog extends JDialog {
return;
}
this.removeWindowListener(applyChangesToSimsListener);
- setTitle(trans.get("simplotpanel.title.Plotsim"));
+ setTitle((isModified ? "* " : "") + trans.get("simplotpanel.title.Plotsim"));
CardLayout cl = (CardLayout) (cards.getLayout());
cl.show(cards, PLOTMODE);
cards.validate();
@@ -122,7 +152,7 @@ public class SimulationEditDialog extends JDialog {
}
private void buildEditCard() {
- JPanel simEditPanel = new JPanel(new MigLayout("fill"));
+ JPanel simEditPanel = new JPanel(new MigLayout("fill, hidemode 1"));
if (isSingleEdit()) {
JPanel panel = new JPanel(new MigLayout("fill, ins 0"));
@@ -150,7 +180,6 @@ public class SimulationEditDialog extends JDialog {
String name = field.getText();
if (name == null || name.equals(""))
return;
- //System.out.println("Setting name:" + name);
simulationList[0].setName(name);
}
@@ -208,12 +237,29 @@ public class SimulationEditDialog extends JDialog {
}
});
- simEditPanel.add(button, "spanx, split 3, align left");
+ simEditPanel.add(button, "spanx, split 5, align left");
if (allowsPlotMode()) {
button.setVisible(true);
} else {
button.setVisible(false);
}
+
+ //// Multi-simulation edit
+ if (simulationList.length > 1) {
+ StyledLabel multiSimEditLabel = new StyledLabel("", -1, StyledLabel.Style.BOLD);
+ multiSimEditLabel.setFontColor(new Color(170, 0, 100));
+ multiSimEditLabel.setText(trans.get("simedtdlg.title.MultiSimEdit"));
+ StringBuilder components = new StringBuilder(trans.get("simedtdlg.title.MultiSimEdit.ttip"));
+ for (int i = 0; i < simulationList.length; i++) {
+ if (i < simulationList.length - 1) {
+ components.append(simulationList[i].getName()).append(", ");
+ } else {
+ components.append(simulationList[i].getName());
+ }
+ }
+ multiSimEditLabel.setToolTipText(components.toString());
+ simEditPanel.add(multiSimEditLabel, "align left");
+ }
//// Run simulation button
button = new SelectColorButton(trans.get("SimulationEditDialog.btn.simulateAndPlot"));
@@ -231,17 +277,49 @@ public class SimulationEditDialog extends JDialog {
}
}
});
- simEditPanel.add(button, " align right, tag ok");
-
- //// Close button
- JButton close = new SelectColorButton(trans.get("dlg.but.close"));
- close.addActionListener(new ActionListener() {
+ simEditPanel.add(button, "align right, gapright 10lp, tag ok");
+
+ //// Cancel button
+ JButton cancelButton = new SelectColorButton(trans.get("dlg.but.cancel"));
+ cancelButton.setToolTipText(trans.get("SimulationEditDialog.btn.Cancel.ttip"));
+ cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
+ // Don't do anything on cancel if you are editing an existing simulation, and it is not modified
+ if (!isNewSimulation && !isModified) {
+ SimulationEditDialog.this.removeWindowListener(applyChangesToSimsListener);
+ SimulationEditDialog.this.dispose();
+ return;
+ }
+
+ // Apply the cancel operation if set to auto discard in preferences
+ if (!preferences.isShowDiscardSimulationConfirmation()) {
+ discardChanges();
+ return;
+ }
+
+ // Yes/No dialog: Are you sure you want to discard your changes?
+ JPanel msg = createCancelOperationContent();
+ int resultYesNo = JOptionPane.showConfirmDialog(SimulationEditDialog.this, msg,
+ trans.get("SimulationEditDialog.CancelOperation.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
+ if (resultYesNo == JOptionPane.YES_OPTION) {
+ discardChanges();
+ }
+ }
+ });
+ simEditPanel.add(cancelButton, "tag ok");
+
+ //// Ok button
+ JButton okButton = new SelectColorButton(trans.get("dlg.but.ok"));
+ okButton.setToolTipText(trans.get("SimulationEditDialog.btn.OK.ttip"));
+ okButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ copyChangesToAllSims();
SimulationEditDialog.this.dispose();
}
});
- simEditPanel.add(close, "tag ok");
+ simEditPanel.add(okButton, "tag ok");
cards.add(simEditPanel, EDITMODE);
}
@@ -336,4 +414,47 @@ public class SimulationEditDialog extends JDialog {
}
}
+
+ private JPanel createCancelOperationContent() {
+ JPanel panel = new JPanel(new MigLayout());
+ String msg = isNewSimulation ? trans.get("SimulationEditDialog.CancelOperation.msg.undoAdd") :
+ trans.get("SimulationEditDialog.CancelOperation.msg.discardChanges");
+ JLabel msgLabel = new JLabel(msg);
+ JCheckBox dontAskAgain = new JCheckBox(trans.get("SimulationEditDialog.CancelOperation.checkbox.dontAskAgain"));
+ dontAskAgain.setSelected(false);
+ dontAskAgain.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (e.getStateChange() == ItemEvent.SELECTED) {
+ preferences.setShowDiscardSimulationConfirmation(false);
+ }
+ // Unselected state should be not be possible and thus not be handled
+ }
+ });
+
+ panel.add(msgLabel, "left, wrap");
+ panel.add(dontAskAgain, "left, gaptop para");
+
+ return panel;
+ }
+
+ private void discardChanges() {
+ if (isNewSimulation) {
+ document.removeSimulation(simulationList[0]);
+ } else {
+ undoSimulationChanges();
+ }
+ document.setSaved(this.initialIsSaved); // Restore the saved state of the document
+ document.fireDocumentChangeEvent(new DocumentChangeEvent(this));
+
+ SimulationEditDialog.this.removeWindowListener(applyChangesToSimsListener);
+ SimulationEditDialog.this.dispose();
+ }
+
+ private void undoSimulationChanges() {
+ if (simulationList == null || simulationList.length == 0) {
+ return;
+ }
+ simulationList[0].loadFrom(initialSim);
+ }
}