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); + } }