diff --git a/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java b/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java index df2285c37..504969e27 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java +++ b/core/src/net/sf/openrocket/rocketcomponent/AxialStage.java @@ -241,6 +241,7 @@ public class AxialStage extends ComponentAssembly implements FlightConfigurableC @Override public void clearConfigListeners() { super.clearConfigListeners(); + // StageSeparationConfiguration also has config listeners, so clear them as well StageSeparationConfiguration thisConfig = getSeparationConfiguration(); thisConfig.clearConfigListeners(); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java index 212aadd48..40b292044 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java +++ b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java @@ -549,6 +549,7 @@ public class BodyTube extends SymmetricComponent implements BoxBounded, MotorMou @Override public void clearConfigListeners() { super.clearConfigListeners(); + // The motor config also has listeners, so clear them as well getDefaultMotorConfig().clearConfigListeners(); } } diff --git a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java index 272de11ac..581d8583c 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java +++ b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java @@ -482,6 +482,7 @@ public class InnerTube extends ThicknessRingComponent implements AxialPositionab @Override public void clearConfigListeners() { super.clearConfigListeners(); + // The motor config also has listeners, so clear them as well getDefaultMotorConfig().clearConfigListeners(); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java b/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java index 312eb9dad..d35f1928d 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java +++ b/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java @@ -1,14 +1,9 @@ package net.sf.openrocket.rocketcomponent; -import net.sf.openrocket.appearance.Appearance; -import net.sf.openrocket.appearance.Decal; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.preset.ComponentPreset.Type; import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.StateChangeListener; - -import java.util.EventObject; /** * Rocket nose cones of various types. Implemented as a transition with the @@ -205,7 +200,7 @@ public class NoseCone extends Transition implements InsideColorComponent { return; } - boolean previousByPass = getBypassComponentChangeEvent(); + boolean previousByPass = isBypassComponentChangeEvent(); setBypassChangeEvent(true); if (flipped) { setForeRadius(getAftRadiusNoAutomatic()); diff --git a/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java b/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java index 3b0c0bbde..9fc15a8e4 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java @@ -177,6 +177,7 @@ public abstract class RecoveryDevice extends MassObject implements FlightConfigu @Override public void clearConfigListeners() { super.clearConfigListeners(); + // The DeploymentConfiguration also has listeners, so clear them as well DeploymentConfiguration thisConfig = getDeploymentConfigurations().getDefault(); thisConfig.clearConfigListeners(); } diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index df7e618c6..e3f1cdcd4 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -448,6 +448,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab } // Make sure the config listeners aren't cloned clone.configListeners = new LinkedList<>(); + clone.bypassComponentChangeEvent = false; return clone; } @@ -603,7 +604,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab for (RocketComponent listener : configListeners) { listener.setBypassChangeEvent(false); listener.setMassOverridden(o); - listener.setBypassChangeEvent(false); + listener.setBypassChangeEvent(true); } if (massOverridden == o) { @@ -2277,7 +2278,11 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab this.bypassComponentChangeEvent = newValue; } - public boolean getBypassComponentChangeEvent() { + /** + * Returns whether the current component if ignoring ComponentChangeEvents. + * @return true if the component is ignoring ComponentChangeEvents. + */ + public boolean isBypassComponentChangeEvent() { return this.bypassComponentChangeEvent; } @@ -2287,7 +2292,18 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab * @return true if listener was successfully added, false if not */ public boolean addConfigListener(RocketComponent listener) { - if (listener == null || configListeners.contains(listener) || listener == this) { + if (isBypassComponentChangeEvent()) { + // This is a precaution. If you are multi-comp editing and the current component is bypassing events, + // the editing will be REALLY weird, see GitHub issue #1956. + throw new IllegalStateException("Cannot add config listener while bypassing events"); + } + if (listener == null) { + return false; + } + if (listener.getConfigListeners().size() > 0) { + throw new IllegalArgumentException("Listener already has config listeners"); + } + if (configListeners.contains(listener) || listener == this) { return false; } configListeners.add(listener); @@ -2555,6 +2571,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab this.displayOrder_side = src.displayOrder_side; this.displayOrder_back = src.displayOrder_back; this.configListeners = new LinkedList<>(); + this.bypassComponentChangeEvent = false; if (this instanceof InsideColorComponent && src instanceof InsideColorComponent) { InsideColorComponentHandler icch = new InsideColorComponentHandler(this); icch.copyFrom(((InsideColorComponent) src).getInsideColorComponentHandler()); diff --git a/swing/src/net/sf/openrocket/gui/main/DesignPanel.java b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java index dcb300261..b30a605ce 100644 --- a/swing/src/net/sf/openrocket/gui/main/DesignPanel.java +++ b/swing/src/net/sf/openrocket/gui/main/DesignPanel.java @@ -105,6 +105,7 @@ public class DesignPanel extends JSplitPane { // Double-click if ((e.getButton() == MouseEvent.BUTTON1) && (e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { RocketComponent component = (RocketComponent) selPath.getLastPathComponent(); + component.clearConfigListeners(); // Multi-component edit if shift/meta key is pressed if ((e.isShiftDown() || e.isMetaDown()) && tree.getSelectionPaths() != null) { diff --git a/swing/src/net/sf/openrocket/gui/main/RocketActions.java b/swing/src/net/sf/openrocket/gui/main/RocketActions.java index 6bce6fbd7..1d91ece32 100644 --- a/swing/src/net/sf/openrocket/gui/main/RocketActions.java +++ b/swing/src/net/sf/openrocket/gui/main/RocketActions.java @@ -985,6 +985,7 @@ public class RocketActions { ComponentConfigDialog.disposeDialog(); RocketComponent component = components.get(0); + component.clearConfigListeners(); if (components.size() > 1) { for (int i = 1; i < components.size(); i++) { RocketComponent listener = components.get(i); diff --git a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java index 386aa4de6..aa7ef052e 100644 --- a/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java +++ b/swing/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java @@ -636,6 +636,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change if (event.isShiftDown() || event.isMetaDown()) { List paths = new ArrayList<>(Arrays.asList(selectionModel.getSelectionPaths())); RocketComponent component = selectedComponents.get(selectedComponents.size() - 1); + component.clearConfigListeners(); // Make sure the clicked component is selected for (RocketComponent c : clicked) {