diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 842f0fdc7..57645d9c8 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -949,8 +949,12 @@ CenteringRingCfg.tab.Generalproperties = General properties !ComponentConfigDialog ComponentCfgDlg.configuration = configuration -ComponentCfgDlg.configuration1 = +ComponentCfgDlg.MultiComponent = Multi-component +ComponentCfgDlg.MultiComponentConfig = Multi-component configuration +ComponentCfgDlg.MultiComponentEdit = Multi-component edit +ComponentCfgDlg.MultiComponentEdit.ttip = You are editing the following components:
ComponentCfgDlg.Modify = Modify +ComponentCfgDlg.ModifyComponents = Modify components !StageConfig StageConfig.tab.Separation = Separation diff --git a/core/resources/l10n/messages_cs.properties b/core/resources/l10n/messages_cs.properties index c6354d4a8..f5b5933f7 100644 --- a/core/resources/l10n/messages_cs.properties +++ b/core/resources/l10n/messages_cs.properties @@ -678,7 +678,6 @@ CenteringRingCfg.tab.Generalproperties = Obecn !ComponentConfigDialog ComponentCfgDlg.configuration = konfigurace -ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = Uprav !StageConfig diff --git a/core/resources/l10n/messages_de.properties b/core/resources/l10n/messages_de.properties index 442081510..45f91eda5 100644 --- a/core/resources/l10n/messages_de.properties +++ b/core/resources/l10n/messages_de.properties @@ -734,7 +734,6 @@ CenteringRingCfg.tab.Generalproperties = Allgemeine Eigenschaften !ComponentConfigDialog ComponentCfgDlg.configuration = Konfiguration -ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = Verändern !StageConfig diff --git a/core/resources/l10n/messages_es.properties b/core/resources/l10n/messages_es.properties index 9d0e5b2c3..255e26369 100644 --- a/core/resources/l10n/messages_es.properties +++ b/core/resources/l10n/messages_es.properties @@ -130,8 +130,7 @@ CompassSelectionButton.lbl.W = O ComponentCfgDlg.Modify = Modificar !ComponentConfigDialog -ComponentCfgDlg.configuration = -ComponentCfgDlg.configuration1 = Configuraci\u00f3n +ComponentCfgDlg.configuration = Configuraci\u00f3n ComponentIcons.Bodytube = Cuerpo tubular ComponentIcons.Bulkhead = Disco de enganche diff --git a/core/resources/l10n/messages_fr.properties b/core/resources/l10n/messages_fr.properties index ab7fb5544..d4cee80d0 100644 --- a/core/resources/l10n/messages_fr.properties +++ b/core/resources/l10n/messages_fr.properties @@ -121,7 +121,6 @@ CompassSelectionButton.lbl.W = O ComponentCfgDlg.Modify = Modifier !ComponentConfigDialog ComponentCfgDlg.configuration = configuration -ComponentCfgDlg.configuration1 = configuration ComponentIcons.Bodytube = Tube ComponentIcons.Bulkhead = Cloison diff --git a/core/resources/l10n/messages_it.properties b/core/resources/l10n/messages_it.properties index ef7a5d226..f99e43eb3 100644 --- a/core/resources/l10n/messages_it.properties +++ b/core/resources/l10n/messages_it.properties @@ -736,7 +736,6 @@ CenteringRingCfg.tab.Generalproperties = Proprieta' generali !ComponentConfigDialog ComponentCfgDlg.configuration = (configurazione) -ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = Modifica !StageConfig diff --git a/core/resources/l10n/messages_ja.properties b/core/resources/l10n/messages_ja.properties index 8cf8b6b03..2417dd0c0 100644 --- a/core/resources/l10n/messages_ja.properties +++ b/core/resources/l10n/messages_ja.properties @@ -766,7 +766,6 @@ CenteringRingCfg.tab.Generalproperties = \u4E00\u822C !ComponentConfigDialog ComponentCfgDlg.configuration = \u30B3\u30F3\u30D5\u30A3\u30AE\u30E5\u30EC\u30FC\u30B7\u30E7\u30F3 -ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = \u5909\u66F4 !StageConfig diff --git a/core/resources/l10n/messages_nl.properties b/core/resources/l10n/messages_nl.properties index 0b7e783f8..b32a8d0a5 100644 --- a/core/resources/l10n/messages_nl.properties +++ b/core/resources/l10n/messages_nl.properties @@ -889,7 +889,6 @@ CenteringRingCfg.tab.Generalproperties = Algemene eigenschappen !ComponentConfigDialog ComponentCfgDlg.configuration = configuratie -ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = Wijzigen !StageConfig diff --git a/core/resources/l10n/messages_pl.properties b/core/resources/l10n/messages_pl.properties index 05127de1c..08bcaec77 100644 --- a/core/resources/l10n/messages_pl.properties +++ b/core/resources/l10n/messages_pl.properties @@ -680,7 +680,6 @@ update.dlg.latestVersion = Korzystasz z najnowszej wersji OpenRocket: %s. !ComponentConfigDialog ComponentCfgDlg.configuration = konfiguracja - ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = Zmodyfikuj !StageConfig diff --git a/core/resources/l10n/messages_ru.properties b/core/resources/l10n/messages_ru.properties index 90883b5c1..cc5d08328 100644 --- a/core/resources/l10n/messages_ru.properties +++ b/core/resources/l10n/messages_ru.properties @@ -951,7 +951,6 @@ CenteringRingCfg.tab.Generalproperties = \u041E\u0441\u043D\u043E\u0432\u043D\u0 !ComponentConfigDialog ComponentCfgDlg.configuration = \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B -ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C !StageConfig diff --git a/core/resources/l10n/messages_uk_UA.properties b/core/resources/l10n/messages_uk_UA.properties index 8a06a77ef..5d7147134 100644 --- a/core/resources/l10n/messages_uk_UA.properties +++ b/core/resources/l10n/messages_uk_UA.properties @@ -838,7 +838,6 @@ CenteringRingCfg.tab.Generalproperties = General properties !ComponentConfigDialog ComponentCfgDlg.configuration = configuration -ComponentCfgDlg.configuration1 = ComponentCfgDlg.Modify = Modify !StageConfig diff --git a/core/src/net/sf/openrocket/appearance/AppearanceBuilder.java b/core/src/net/sf/openrocket/appearance/AppearanceBuilder.java index 230c80ebd..3763eae0e 100644 --- a/core/src/net/sf/openrocket/appearance/AppearanceBuilder.java +++ b/core/src/net/sf/openrocket/appearance/AppearanceBuilder.java @@ -1,10 +1,14 @@ package net.sf.openrocket.appearance; import net.sf.openrocket.appearance.Decal.EdgeMode; +import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.util.AbstractChangeSource; import net.sf.openrocket.util.Color; import net.sf.openrocket.util.Coordinate; +import java.util.LinkedHashMap; +import java.util.Map; + /** * Use this class to build an immutable Appearance object in a friendly way. Set * the various values one at a time with the setter methods and then call @@ -28,6 +32,13 @@ public class AppearanceBuilder extends AbstractChangeSource { private Decal.EdgeMode edgeMode; private boolean batch; + + /** + * List of appearance builders that will set their appearance properties to the same as the current appearance + */ + private final Map configListeners = new LinkedHashMap<>(); + // If true, appearance change events will not be fired + private boolean bypassAppearanceChangeEvent = false; /** * Default constructor @@ -59,7 +70,9 @@ public class AppearanceBuilder extends AbstractChangeSource { rotation = 0; image = null; edgeMode = EdgeMode.REPEAT; - fireChangeEvent();//shouldn't this fire change event? + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -88,6 +101,9 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param d The decal */ public void setDecal(Decal d){ + for (AppearanceBuilder listener : configListeners.values()) { + listener.setDecal(d); + } if (d != null) { setOffset(d.getOffset().x, d.getOffset().y); setCenter(d.getCenter().x, d.getCenter().y); @@ -96,7 +112,9 @@ public class AppearanceBuilder extends AbstractChangeSource { setEdgeMode(d.getEdgeMode()); setImage(d.getImage()); } - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -137,9 +155,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param paint the new color */ public void setPaint(Color paint) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setPaint(paint); + } this.paint = paint; - fireChangeEvent(); - + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -158,8 +180,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param shine the new shine for template */ public void setShine(double shine) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setShine(shine); + } this.shine = shine; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -179,6 +206,9 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param opacity new opacity value expressed in a percentage, where 0 is fully transparent and 1 is fully opaque */ public void setOpacity(double opacity) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setOpacity(opacity); + } if (this.paint == null) { return; } @@ -186,8 +216,10 @@ public class AppearanceBuilder extends AbstractChangeSource { // Clamp opacity between 0 and 1 opacity = Math.max(0, Math.min(1, opacity)); - this.paint.setAlpha((int) (opacity * 255)); - fireChangeEvent(); + // Instead of simply setting the alpha, we need to create a new color with the new alpha value, otherwise undoing + // the setOpacity will not work correctly. (don't ask me why) + Color c = new Color(paint.getRed(), paint.getGreen(), paint.getBlue(), (int) (opacity * 255)); + setPaint(c); } /** @@ -207,8 +239,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param offsetU the new offset to be used */ public void setOffsetU(double offsetU) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setOffsetU(offsetU); + } this.offsetU = offsetU; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -227,8 +264,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param offsetV the new offset to be used */ public void setOffsetV(double offsetV) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setOffsetV(offsetV); + } this.offsetV = offsetV; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -259,8 +301,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param centerU value of axis U for center */ public void setCenterU(double centerU) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setCenterU(centerU); + } this.centerU = centerU; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -276,11 +323,16 @@ public class AppearanceBuilder extends AbstractChangeSource { * set a new value for axis V for center in template * fires change event * - * @param centerU value of axis V for center + * @return value of axis V for center */ public void setCenterV(double centerV) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setCenterV(centerV); + } this.centerV = centerV; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -311,8 +363,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param scaleU new value of scalling in axis U */ public void setScaleU(double scaleU) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setScaleU(scaleU); + } this.scaleU = scaleU; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -331,8 +388,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param scaleV new value of scalling in axis V */ public void setScaleV(double scaleV) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setScaleV(scaleV); + } this.scaleV = scaleV; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -379,7 +441,7 @@ public class AppearanceBuilder extends AbstractChangeSource { * sets a new value of axis Y for scalling in template * fires change event * - * @param scaleX the new value for axis Y + * @param scaleY the new value for axis Y */ public void setScaleY(double scaleY) { setScaleV(1.0 / scaleY); @@ -401,14 +463,19 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param rotation the new value for rotation in template */ public void setRotation(double rotation) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setRotation(rotation); + } this.rotation = rotation; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** * gets the current image in template * - * @param the current image in template + * @return the current image in template */ public DecalImage getImage() { return image; @@ -421,8 +488,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param image the new image to be used as template */ public void setImage(DecalImage image) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setImage(image); + } this.image = image; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -441,8 +513,13 @@ public class AppearanceBuilder extends AbstractChangeSource { * @param edgeMode the new edgeMode to be used */ public void setEdgeMode(Decal.EdgeMode edgeMode) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setEdgeMode(edgeMode); + } this.edgeMode = edgeMode; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } } /** @@ -455,15 +532,55 @@ public class AppearanceBuilder extends AbstractChangeSource { } /** - * function that garantees that chenges event only occurs after all changes are made + * function that guarantees that changes event only occurs after all changes are made * * param r the functor to be executed */ public void batch(Runnable r) { + for (AppearanceBuilder listener : configListeners.values()) { + listener.batch(r); + } batch = true; r.run(); batch = false; - fireChangeEvent(); + if (!bypassAppearanceChangeEvent) { + fireChangeEvent(); + } + } + + /** + * Add a new config listener that will undergo the same configuration changes as this AppearanceBuilder. + * @param component the component to add as a config listener + * @param ab new AppearanceBuilder config listener + * @return true if listener was successfully added, false if not + */ + public boolean addConfigListener(RocketComponent component, AppearanceBuilder ab) { + if (component == null || ab == null) { + return false; + } + configListeners.put(component, ab); + ab.setBypassChangeEvent(true); + return true; + } + + public void removeConfigListener(RocketComponent listener) { + configListeners.remove(listener); + listener.setBypassChangeEvent(false); + } + + public void clearConfigListeners() { + for (AppearanceBuilder listener : configListeners.values()) { + listener.setBypassChangeEvent(false); + } + configListeners.clear(); + } + + public Map getConfigListeners() { + return configListeners; + } + + public void setBypassChangeEvent(boolean newValue) { + this.bypassAppearanceChangeEvent = newValue; } } diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java index f03862f0a..4971e1525 100644 --- a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -123,7 +123,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab private Appearance appearance = null; // If true, component change events will not be fired - private boolean ignoreComponentChange = false; + private boolean bypassComponentChangeEvent = false; /** @@ -464,10 +464,6 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab * @param appearance */ public void setAppearance(Appearance appearance) { - for (RocketComponent listener : configListeners) { - listener.setAppearance(appearance); - } - this.appearance = appearance; if (this.appearance != null) { Decal d = this.appearance.getTexture(); @@ -581,9 +577,9 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab */ public final void setMassOverridden(boolean o) { for (RocketComponent listener : configListeners) { - listener.setIgnoreComponentChange(false); + listener.setBypassChangeEvent(false); listener.setMassOverridden(o); - listener.setIgnoreComponentChange(false); + listener.setBypassChangeEvent(false); } if (massOverridden == o) { @@ -655,9 +651,9 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab */ public final void setCGOverridden(boolean o) { for (RocketComponent listener : configListeners) { - listener.setIgnoreComponentChange(false); + listener.setBypassChangeEvent(false); listener.setCGOverridden(o); - listener.setIgnoreComponentChange(true); + listener.setBypassChangeEvent(true); } if (cgOverridden == o) { @@ -806,9 +802,9 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab */ public final void setName(String name) { for (RocketComponent listener : configListeners) { - listener.setIgnoreComponentChange(false); + listener.setBypassChangeEvent(false); listener.setName(name); - listener.setIgnoreComponentChange(true); + listener.setBypassChangeEvent(true); } if (this.name.equals(name)) { @@ -1671,6 +1667,24 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab } return false; } + + /** + * Checks whether all components in the list have the same class as this component. + * @param components list to check + * @return true if all components are of the same class, false if not + */ + public boolean checkAllClassesEqual(List components) { + if (components == null || components.size() == 0) { + return true; + } + Class myClass = this.getClass(); + for (RocketComponent c : components) { + if (!c.getClass().equals(myClass)) { + return false; + } + } + return true; + } /** * Get the root component of the component tree. @@ -1936,7 +1950,7 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab */ protected void fireComponentChangeEvent(ComponentChangeEvent e) { checkState(); - if (parent == null || ignoreComponentChange) { + if (parent == null || bypassComponentChangeEvent) { /* Ignore if root invalid. */ return; } @@ -1955,37 +1969,36 @@ public abstract class RocketComponent implements ChangeSource, Cloneable, Iterab fireComponentChangeEvent(new ComponentChangeEvent(this, type)); } - public void setIgnoreComponentChange(boolean newValue) { - this.ignoreComponentChange = newValue; + public void setBypassChangeEvent(boolean newValue) { + this.bypassComponentChangeEvent = newValue; } - public boolean getIgnoreComponentChange() { - return this.ignoreComponentChange; + public boolean getBypassComponentChangeEvent() { + return this.bypassComponentChangeEvent; } /** - * Add a new config listener that will undergo the same configuration changes as this.component. Listener must be - * of the same class as this.component. + * Add a new config listener that will undergo the same configuration changes as this.component. * @param listener new config listener * @return true if listener was successfully added, false if not */ public boolean addConfigListener(RocketComponent listener) { - if (listener == null || !this.getClass().equals(listener.getClass())) { + if (listener == null) { return false; } configListeners.add(listener); - listener.setIgnoreComponentChange(true); + listener.setBypassChangeEvent(true); return true; } public void removeConfigListener(RocketComponent listener) { configListeners.remove(listener); - listener.setIgnoreComponentChange(false); + listener.setBypassChangeEvent(false); } public void clearConfigListeners() { for (RocketComponent listener : configListeners) { - listener.setIgnoreComponentChange(false); + listener.setBypassChangeEvent(false); } configListeners.clear(); } diff --git a/swing/src/net/sf/openrocket/gui/components/StyledLabel.java b/swing/src/net/sf/openrocket/gui/components/StyledLabel.java index 70328411b..bf2a29314 100644 --- a/swing/src/net/sf/openrocket/gui/components/StyledLabel.java +++ b/swing/src/net/sf/openrocket/gui/components/StyledLabel.java @@ -1,5 +1,6 @@ package net.sf.openrocket.gui.components; +import java.awt.Color; import java.awt.Font; import javax.swing.JLabel; @@ -87,11 +88,9 @@ public class StyledLabel extends JLabel { private void checkPreferredSize(float size, Style style) { String str = this.getText(); - if (str.startsWith("") && str.indexOf("") && !str.contains(" textureDropDown = new JComboBox(decalModel); + // We need to add this action listener that triggers a decalModel update when the same item is selected, because + // for multi-comp edits, the listeners' decals may not be updated otherwise + textureDropDown.addActionListener(new ActionListener() { + private DecalImage previousSelection = (DecalImage) decalModel.getSelectedItem(); + @Override + public void actionPerformed(ActionEvent e) { + DecalImage decal = (DecalImage) textureDropDown.getSelectedItem(); + if (decal == previousSelection) { + decalModel.setSelectedItem(decal); + } + previousSelection = decal; + } + }); + JButton colorButton = new SelectColorButton(new ColorIcon(builder.getPaint())); colorButton.addActionListener(new ColorActionListener(builder, "Paint")); @@ -464,13 +508,31 @@ public class AppearancePanel extends JPanel { previousUserSelectedInsideAppearance = (builder == null) ? null : builder.getAppearance(); } + + // Set the listeners' appearance to the default appearance + for (RocketComponent listener : builder.getConfigListeners().keySet()) { + builder.getConfigListeners().get(listener).setAppearance(defaultAppearance); + listener.setAppearance(null); + } + + // Set this component's appearance to the default appearance builder.setAppearance(defaultAppearance); c.setAppearance(null); } else { - if (!insideBuilder) + if (!insideBuilder) { + // Set the listeners' appearance to the previous user selected appearance + for (AppearanceBuilder listener : builder.getConfigListeners().values()) { + listener.setAppearance(previousUserSelectedAppearance); + } builder.setAppearance(previousUserSelectedAppearance); - else + } + else { + // Set the listeners' inside appearance to the previous user selected appearance + for (AppearanceBuilder listener : builder.getConfigListeners().values()) { + listener.setAppearance(previousUserSelectedInsideAppearance); + } builder.setAppearance(previousUserSelectedInsideAppearance); + } } } }); @@ -551,9 +613,9 @@ public class AppearancePanel extends JPanel { mDefault.addEnableComponent(spinShine, false); mDefault.addEnableComponent(unitShine, false); - panel.add(spinShine, "split 3, w 50"); + panel.add(spinShine, "split 3, w 60"); panel.add(unitShine); - panel.add(slideShine, "w 50"); + panel.add(slideShine, "w 50, growx"); // Offset panel.add(new JLabel(trans.get("AppearanceCfg.lbl.texture.offset"))); @@ -585,9 +647,9 @@ public class AppearancePanel extends JPanel { mDefault.addEnableComponent(spinOpacity, false); mDefault.addEnableComponent(unitOpacity, false); - panel.add(spinOpacity, "split 3, w 50"); + panel.add(spinOpacity, "split 3, w 60"); panel.add(unitOpacity); - panel.add(slideOpacity, "w 50"); + panel.add(slideOpacity, "w 50, growx"); // Rotation panel.add(new JLabel(trans.get("AppearanceCfg.lbl.texture.rotation"))); @@ -622,10 +684,24 @@ public class AppearancePanel extends JPanel { opacityModel.stateChanged(null); lastOpacity = builder.getOpacity(); } - if (!insideBuilder) + if (!insideBuilder) { + // Set the listeners' outside appearance + for (RocketComponent listener : builder.getConfigListeners().keySet()) { + listener.setAppearance(builder.getConfigListeners().get(listener).getAppearance()); + } + // Set this component's outside appearance c.setAppearance(builder.getAppearance()); - else - ((InsideColorComponent)c).getInsideColorComponentHandler().setInsideAppearance(builder.getAppearance()); + } + else { + // Set the listeners' inside appearance + for (RocketComponent listener : builder.getConfigListeners().keySet()) { + if (!(listener instanceof InsideColorComponent)) continue; + ((InsideColorComponent) listener).getInsideColorComponentHandler() + .setInsideAppearance(builder.getConfigListeners().get(listener).getAppearance()); + } + // Set this component's inside appearance + ((InsideColorComponent) c).getInsideColorComponentHandler().setInsideAppearance(builder.getAppearance()); + } decalModel.refresh(); } }); diff --git a/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java b/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java index fbf92a046..7defaca26 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java @@ -14,8 +14,10 @@ import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.gui.util.GUIUtil; import net.sf.openrocket.gui.util.SwingPreferences; import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.AxialStage; import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.BugException; @@ -25,7 +27,7 @@ import net.sf.openrocket.util.Reflection; * A dialog that contains the configuration elements of one component. * The contents of the dialog are instantiated from CONFIGDIALOGPACKAGE according * to the current component. - * + * * @author Sampo Niskanen */ @@ -33,26 +35,26 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis private static final long serialVersionUID = 1L; private static final String CONFIGDIALOGPACKAGE = "net.sf.openrocket.gui.configdialog"; private static final String CONFIGDIALOGPOSTFIX = "Config"; - + // Static Value -- This is a singleton value, and we should only have zero or one active at any time private static ComponentConfigDialog dialog = null; - - private OpenRocketDocument document = null; - private RocketComponent component = null; - private RocketComponentConfig configurator = null; + private OpenRocketDocument document = null; + protected RocketComponent component = null; + private RocketComponentConfig configurator = null; + protected static boolean clearConfigListeners = true; private static String previousSelectedTab = null; // Name of the previous selected tab - + + private final Window parent; private static final Translator trans = Application.getTranslator(); - - private ComponentConfigDialog(Window parent, OpenRocketDocument document, RocketComponent component, - List listeners) { + + private ComponentConfigDialog(Window parent, OpenRocketDocument document, RocketComponent component) { super(parent); this.parent = parent; - + setComponent(document, component); - + GUIUtil.setDisposableDialogOptions(this, null); GUIUtil.rememberWindowPosition(this); @@ -63,33 +65,28 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis * In fact, it should trigger for any method of closing the dialog. */ public void windowClosed(WindowEvent e){ + configurator.clearConfigListeners(); configurator.invalidate(); document.getRocket().removeComponentChangeListener(ComponentConfigDialog.this); ComponentConfigDialog.this.dispose(); - component.clearConfigListeners(); + if (clearConfigListeners) { + component.clearConfigListeners(); + } } - - public void windowClosing(WindowEvent e){} @Override public void windowOpened(WindowEvent e) { super.windowOpened(e); - // Add config listeners - component.clearConfigListeners(); - if (listeners != null) { - for (RocketComponent listener : listeners) { - component.addConfigListener(listener); - } - } + clearConfigListeners = true; } }); } - - + + /** * Set the component being configured. The listening connections of the old configurator * will be removed and the new ones created. - * + * * @param component Component to configure. */ private void setComponent(OpenRocketDocument document, RocketComponent component) { @@ -97,24 +94,43 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis // Remove listeners by setting all applicable models to null GUIUtil.setNullModels(configurator); // null-safe } - + this.document = document; this.component = component; this.document.getRocket().addComponentChangeListener(this); - + configurator = getDialogContents(); this.setContentPane(configurator); configurator.updateFields(); - // Set the selected tab - configurator.setSelectedTab(previousSelectedTab); - + List listeners = component.getConfigListeners(); + + // Set the default tab to 'Appearance' for a different-type multi-comp dialog (this is the most prominent use case) + if (listeners != null && listeners.size() > 0 && !component.checkAllClassesEqual(listeners)) { + configurator.setSelectedTabIndex(1); + } else { + configurator.setSelectedTab(previousSelectedTab); + } + //// configuration - setTitle(trans.get("ComponentCfgDlg.configuration1") + " " + component.getComponentName() + " " + trans.get("ComponentCfgDlg.configuration")); - + if (component.checkAllClassesEqual(listeners)) { + if (listeners != null && listeners.size() > 0) { + setTitle("(" + trans.get("ComponentCfgDlg.MultiComponent") + ") " + + component.getComponentName() + " " + trans.get("ComponentCfgDlg.configuration")); + } else { + setTitle(component.getComponentName() + " " + trans.get("ComponentCfgDlg.configuration")); + } + } else { + setTitle(trans.get("ComponentCfgDlg.MultiComponentConfig")); + } + this.pack(); } + public RocketComponent getComponent() { + return component; + } + public static ComponentConfigDialog getDialog() { return dialog; } @@ -124,32 +140,36 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis * Return the configurator panel of the current component. */ private RocketComponentConfig getDialogContents() { - Constructor c = + List listeners = component.getConfigListeners(); + boolean isSameClass = component.checkAllClassesEqual(listeners); + if (!isSameClass) { + return new RocketComponentConfig(document, component); + } + + Constructor constructor = findDialogContentsConstructor(component); - if (c != null) { + if (constructor != null) { try { - return c.newInstance(document, component); - } catch (InstantiationException e) { - throw new BugException("BUG in constructor reflection", e); - } catch (IllegalAccessException e) { + return constructor.newInstance(document, component); + } catch (InstantiationException | IllegalAccessException e) { throw new BugException("BUG in constructor reflection", e); } catch (InvocationTargetException e) { throw Reflection.handleWrappedException(e); } } - + // Should never be reached, since RocketComponentConfig should catch all // components without their own configurator. throw new BugException("Unable to find any configurator for " + component); } - + @Override public void componentChanged(ComponentChangeEvent e) { if (e.isTreeChange() || e.isUndoChange()) { - + // Hide dialog in case of tree or undo change disposeDialog(); - + } else { /* * TODO: HIGH: The line below has caused a NullPointerException (without null check) @@ -161,10 +181,10 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis configurator.updateFields(); } } - - + + /** - * Finds the Constructor of the given component's config dialog panel in + * Finds the Constructor of the given component's config dialog panel in * CONFIGDIALOGPACKAGE. */ @SuppressWarnings("unchecked") @@ -172,10 +192,10 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis Class currentclass; String currentclassname; String configclassname; - + Class configclass; Constructor c; - + currentclass = component.getClass(); while ((currentclass != null) && (currentclass != Object.class)) { currentclassname = currentclass.getCanonicalName(); @@ -184,7 +204,7 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis currentclassname = currentclassname.substring(index + 1); configclassname = CONFIGDIALOGPACKAGE + "." + currentclassname + CONFIGDIALOGPOSTFIX; - + try { configclass = Class.forName(configclassname); c = (Constructor) @@ -192,69 +212,57 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis return c; } catch (Exception ignore) { } - + currentclass = currentclass.getSuperclass(); } return null; } - - + + ////////// Static dialog ///////// - + /** - * A singleton configuration dialog. Will create and show a new dialog if one has not + * A singleton configuration dialog. Will create and show a new dialog if one has not * previously been used, or update the dialog and show it if a previous one exists. - * + * * @param document the document to configure. * @param component the component to configure. - * @param listeners config listeners for the component * @param rememberPreviousTab if true, the previous tab will be remembered and used for the new dialog */ - public static void showDialog(Window parent, OpenRocketDocument document, - RocketComponent component, List listeners, boolean rememberPreviousTab) { + public static void showDialog(Window parent, OpenRocketDocument document, RocketComponent component, boolean rememberPreviousTab) { if (dialog != null) { - previousSelectedTab = dialog.getSelectedTabName(); + // Don't remember the previous tab for rockets or stages, because this will leave you in the override tab for + // the next component, which is generally not what you want. + if (dialog.getComponent() instanceof Rocket || + (dialog.getComponent() instanceof AxialStage && !(component instanceof AxialStage))) { + previousSelectedTab = null; + } else { + previousSelectedTab = dialog.getSelectedTabName(); + } + // If the component is the same as the ComponentConfigDialog component, and the dialog is still visible, + // that means that the user did a ctr/cmd click on a new component => don't remove the config listeners of component + if (component == dialog.getComponent()) { + ComponentConfigDialog.clearConfigListeners = false; + } dialog.dispose(); } + final SwingPreferences preferences = (SwingPreferences) Application.getPreferences(); if (preferences.isAlwaysOpenLeftmostTab() || !rememberPreviousTab) { previousSelectedTab = null; } - dialog = new ComponentConfigDialog(parent, document, component, listeners); + dialog = new ComponentConfigDialog(parent, document, component); dialog.setVisible(true); - + ////Modify - document.addUndoPosition(trans.get("ComponentCfgDlg.Modify") + " " + component.getComponentName()); - } - - /** - * A singleton configuration dialog. Will create and show a new dialog if one has not - * previously been used, or update the dialog and show it if a previous one exists. - * By default, the previous tab is remembered. - * - * @param document the document to configure. - * @param component the component to configure. - * @param listeners config listeners for the component - */ - public static void showDialog(Window parent, OpenRocketDocument document, - RocketComponent component, List listeners) { - ComponentConfigDialog.showDialog(parent, document, component, listeners, true); - } - - /** - * A singleton configuration dialog. Will create and show a new dialog if one has not - * previously been used, or update the dialog and show it if a previous one exists. - * - * @param document the document to configure. - * @param component the component to configure. - * @param rememberPreviousTab if true, the previous tab will be remembered and used for the new dialog - */ - public static void showDialog(Window parent, OpenRocketDocument document, - RocketComponent component, boolean rememberPreviousTab) { - ComponentConfigDialog.showDialog(parent, document, component, null, rememberPreviousTab); + if (component.getConfigListeners().size() == 0) { + document.addUndoPosition(trans.get("ComponentCfgDlg.Modify") + " " + component.getComponentName()); + } else { + document.addUndoPosition(trans.get("ComponentCfgDlg.ModifyComponents")); + } } /** @@ -265,26 +273,20 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis * @param document the document to configure. * @param component the component to configure. */ - public static void showDialog(Window parent, OpenRocketDocument document, - RocketComponent component) { - ComponentConfigDialog.showDialog(parent, document, component, null, true); + public static void showDialog(Window parent, OpenRocketDocument document, RocketComponent component) { + ComponentConfigDialog.showDialog(parent, document, component, true); } - static void showDialog(RocketComponent component, List listeners, boolean rememberPreviousTab) { - showDialog(dialog.parent, dialog.document, component, listeners, rememberPreviousTab); - } - - - /* package */ - static void showDialog(RocketComponent component, List listeners) { - showDialog(dialog.parent, dialog.document, component, listeners, true); + static void showDialog(RocketComponent component, boolean rememberPreviousTab) { + showDialog(dialog.parent, dialog.document, component, rememberPreviousTab); } + /* package */ static void showDialog(RocketComponent component) { - ComponentConfigDialog.showDialog(component, null); + showDialog(dialog.parent, dialog.document, component, true); } - + /** * Disposes the configuration dialog. May be used even if not currently visible. */ @@ -293,8 +295,8 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis dialog.dispose(); } } - - + + /** * Returns whether the singleton configuration dialog is currently visible or not. */ @@ -302,6 +304,10 @@ public class ComponentConfigDialog extends JDialog implements ComponentChangeLis return (dialog != null) && (dialog.isVisible()); } + public int getSelectedTabIndex() { + return configurator.getSelectedTabIndex(); + } + public String getSelectedTabName() { if (configurator != null) { return configurator.getSelectedTabName(); diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java index 06dee6dc5..e5470185e 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java @@ -82,17 +82,10 @@ public abstract class FinSetConfig extends RocketComponentConfig { //// Convert fin set document.addUndoPosition(trans.get("FinSetConfig.Convertfinset")); - List listeners = new ArrayList<>(); - for (RocketComponent listener : component.getConfigListeners()) { - if (listener instanceof FinSet) { - listeners.add(FreeformFinSet.convertFinSet((FinSet) listener)); - } - } - RocketComponent freeform = FreeformFinSet.convertFinSet((FinSet) component); - ComponentConfigDialog.showDialog(freeform, listeners); + ComponentConfigDialog.showDialog(freeform); } }); diff --git a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java index f49741d82..283cdc52b 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java @@ -1,6 +1,7 @@ package net.sf.openrocket.gui.configdialog; +import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.event.*; @@ -39,7 +40,6 @@ import net.sf.openrocket.gui.widgets.SelectColorButton; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.material.Material; import net.sf.openrocket.preset.ComponentPreset; -import net.sf.openrocket.rocketcomponent.ComponentAssembly; import net.sf.openrocket.rocketcomponent.*; import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; import net.sf.openrocket.rocketcomponent.position.AxialMethod; @@ -66,22 +66,42 @@ public class RocketComponentConfig extends JPanel { private final TextFieldListener textFieldListener; private JPanel buttonPanel; + private AppearancePanel appearancePanel = null; private JLabel infoLabel; - - + private StyledLabel multiCompEditLabel; + + private boolean allSameType; // Checks whether all listener components are of the same type as + private boolean allMassive; // Checks whether all listener components, and this component, are massive + public RocketComponentConfig(OpenRocketDocument document, RocketComponent component) { setLayout(new MigLayout("fill, gap 4!, ins panel", "[]:5[]", "[growprio 5]5![fill, grow, growprio 500]5![growprio 5]")); this.document = document; this.component = component; - + + // Check the listeners for the same type and massive status + allSameType = true; + allMassive = component.isMassive(); + List listeners = component.getConfigListeners(); + if (listeners != null && listeners.size() > 0) { + allSameType = component.checkAllClassesEqual(listeners); + if (allMassive) { // Only check if is already massive + for (RocketComponent listener : listeners) { + if (!listener.isMassive()) { + allMassive = false; + break; + } + } + } + } + //// Component name: JLabel label = new JLabel(trans.get("RocketCompCfg.lbl.Componentname")); //// The component name. label.setToolTipText(trans.get("RocketCompCfg.ttip.Thecomponentname")); this.add(label, "spanx, height 32!, split"); - + componentNameField = new JTextField(15); textFieldListener = new TextFieldListener(); componentNameField.addActionListener(textFieldListener); @@ -89,34 +109,34 @@ public class RocketComponentConfig extends JPanel { //// The component name. componentNameField.setToolTipText(trans.get("RocketCompCfg.ttip.Thecomponentname")); this.add(componentNameField, "growx"); - - if (component.getPresetType() != null) { + + if (allSameType && component.getPresetType() != null) { // If the component supports a preset, show the preset selection box. presetModel = new PresetModel(this, document, component); presetComboBox = new JComboBox(presetModel); presetComboBox.setEditable(false); this.add(presetComboBox, ""); } - - + tabbedPane = new JTabbedPane(); this.add(tabbedPane, "newline, span, growx, growy 100, wrap"); - + //// Override and Mass and CG override options tabbedPane.addTab(trans.get("RocketCompCfg.tab.Override"), null, overrideTab(), trans.get("RocketCompCfg.tab.MassandCGoverride")); - if (component.isMassive()) { + if (allMassive) { //// Appearance options - tabbedPane.addTab(trans.get("RocketCompCfg.tab.Appearance"), null, new AppearancePanel(document, component), + appearancePanel = new AppearancePanel(document, component); + tabbedPane.addTab(trans.get("RocketCompCfg.tab.Appearance"), null, appearancePanel, "Appearance Tool Tip"); } - + //// Comment and Specify a comment for the component tabbedPane.addTab(trans.get("RocketCompCfg.tab.Comment"), null, commentTab(), trans.get("RocketCompCfg.tab.Specifyacomment")); - + addButtons(); - + updateFields(); } @@ -128,6 +148,12 @@ public class RocketComponentConfig extends JPanel { buttonPanel = new JPanel(new MigLayout("fillx, ins 5")); + //// Multi-comp edit label + multiCompEditLabel = new StyledLabel(" ", -1, Style.BOLD); + //multiCompEditLabel.setFontColor(new Color(0, 0, 239)); + multiCompEditLabel.setFontColor(new Color(170, 0, 100)); + buttonPanel.add(multiCompEditLabel, "split 2"); + //// Mass: infoLabel = new StyledLabel(" ", -1); buttonPanel.add(infoLabel, "growx"); @@ -159,16 +185,17 @@ public class RocketComponentConfig extends JPanel { public void updateFields() { // Component name componentNameField.setText(component.getName()); - + // Info label StringBuilder sb = new StringBuilder(); - - if (component.getPresetComponent() != null) { + + if (allSameType && component.getPresetComponent() != null) { ComponentPreset preset = component.getPresetComponent(); sb.append(preset.getManufacturer() + " " + preset.getPartNo() + " "); } - - if (component.isMassive()) { + + List listeners = component.getConfigListeners(); + if (allMassive && (listeners == null || listeners.size() == 0)) { // TODO: support aggregate mass display for current component and listeners? sb.append(trans.get("RocketCompCfg.lbl.Componentmass") + " "); sb.append(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( component.getComponentMass())); @@ -193,8 +220,31 @@ public class RocketComponentConfig extends JPanel { } else { infoLabel.setText(""); } + + // Multi-comp edit label + if (listeners != null && listeners.size() > 0) { + multiCompEditLabel.setText(trans.get("ComponentCfgDlg.MultiComponentEdit")); + + StringBuilder components = new StringBuilder(trans.get("ComponentCfgDlg.MultiComponentEdit.ttip")); + components.append(component.getName()).append(", "); + for (int i = 0; i < listeners.size(); i++) { + if (i < listeners.size() - 1) { + components.append(listeners.get(i).getName()).append(", "); + } else { + components.append(listeners.get(i).getName()); + } + } + multiCompEditLabel.setToolTipText(components.toString()); + } else { + multiCompEditLabel.setText(""); + } + } + + public void clearConfigListeners() { + if (appearancePanel != null) { + appearancePanel.clearConfigListeners(); + } } - protected JPanel materialPanel(Material.Type type) { ////Component material: and Component finish: @@ -269,6 +319,10 @@ public class RocketComponentConfig extends JPanel { return subPanel; } + public int getSelectedTabIndex() { + return tabbedPane.getSelectedIndex(); + } + public String getSelectedTabName() { if (tabbedPane != null) { return tabbedPane.getTitleAt(tabbedPane.getSelectedIndex()); @@ -277,6 +331,12 @@ public class RocketComponentConfig extends JPanel { } } + public void setSelectedTabIndex(int index) { + if (tabbedPane != null) { + tabbedPane.setSelectedIndex(index); + } + } + public void setSelectedTab(String tabName) { if (tabbedPane != null) { for (int i = 0; i < tabbedPane.getTabCount(); i++) { diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index 54ea0c45d..3a435b329 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -338,16 +338,16 @@ public class BasicFrame extends JFrame { if (!ComponentConfigDialog.isDialogVisible()) return; + else + ComponentConfigDialog.disposeDialog(); + RocketComponent c = (RocketComponent) paths[0].getLastPathComponent(); - List listeners = new ArrayList<>(); + c.clearConfigListeners(); for (int i = 1; i < paths.length; i++) { RocketComponent listener = (RocketComponent) paths[i].getLastPathComponent(); - if (listener.getClass().equals(c.getClass())) { - listeners.add((RocketComponent) paths[i].getLastPathComponent()); - } + c.addConfigListener(listener); } - ComponentConfigDialog.showDialog(BasicFrame.this, - BasicFrame.this.document, c, listeners); + ComponentConfigDialog.showDialog(BasicFrame.this, BasicFrame.this.document, c); } }); @@ -1332,7 +1332,6 @@ public class BasicFrame extends JFrame { * * @param worker the OpenFileWorker that loads the file. * @param displayName the file name to display in dialogs. - * @param file the File to set the document to (may be null). * @param parent * @param openRocketConfigDialog if true, will open the configuration dialog of the rocket. This is useful for examples. * @return diff --git a/swing/src/net/sf/openrocket/gui/main/RocketActions.java b/swing/src/net/sf/openrocket/gui/main/RocketActions.java index e966061ec..3292af843 100644 --- a/swing/src/net/sf/openrocket/gui/main/RocketActions.java +++ b/swing/src/net/sf/openrocket/gui/main/RocketActions.java @@ -5,7 +5,6 @@ import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; @@ -879,16 +878,17 @@ public class RocketActions { List components = selectionModel.getSelectedComponents(); Simulation[] sims = selectionModel.getSelectedSimulations(); - if ((components != null) && (components.size() > 0) && checkAllClassesEqual(components)) { - // Do nothing if the config dialog is already visible + if ((components != null) && (components.size() > 0)) { if (ComponentConfigDialog.isDialogVisible()) - return; + ComponentConfigDialog.disposeDialog(); - List listeners = null; + RocketComponent component = components.get(0); if (components.size() > 1) { - listeners = components.subList(1, components.size()); + for (int i = 1; i < components.size(); i++) { + component.addConfigListener(components.get(i)); + } } - ComponentConfigDialog.showDialog(parentFrame, document, components.get(0), listeners); + ComponentConfigDialog.showDialog(parentFrame, document, component); } else if (sims != null && sims.length > 0 && (simulationPanel != null)) { simulationPanel.editSimulation(); } @@ -898,25 +898,7 @@ public class RocketActions { public void clipboardChanged() { List components = selectionModel.getSelectedComponents(); - this.setEnabled(checkAllClassesEqual(components) || isSimulationSelected()); - } - - /** - * Checks whether all components in the list have the same class - * @param components list to check - * @return true if all components are of the same class, false if not - */ - private boolean checkAllClassesEqual(List components) { - if (components == null || components.size() == 0) { - return false; - } - Class myClass = components.get(0).getClass(); - for (int i = 1; i < components.size(); i++) { - if (!components.get(i).getClass().equals(myClass)) { - return false; - } - } - return true; + this.setEnabled((components != null && components.size() > 0) || isSimulationSelected()); } }